智能药盒提醒器 项目总结报告
一、所选任务
所选任务:2026 ADI CodeFusion Studio 设计竞赛 · 任务5(赛道五)· 入门级题目1 —— 智能药盒提醒器。
二、项目描述
本项目使用 ADI CodeFusion Studio 开发环境和 C 语言,实现了基于 MAX32655FTHR 的智能药盒提醒器。它面向"老人 / 慢病患者容易忘记按时服药"这一日常问题,用一块开发板把"定时提醒、声光报警、按键确认、记录保存、手机推送"整合在一起。
设备以 MAX32655 内置 RTC 作为时间基准,支持单次设定每日多个服药时间点(默认 08:00、12:30、18:00、21:30,最多 4 个)。到点后,蜂鸣器和板载红色 LED 进行声光报警,0.96 寸 128×64 OLED 屏幕显示提醒内容和剩余确认时间,同时通过 BLE 把提醒信息推送到手机。服用者按一下板载按键即可确认服药,系统自动记录为 TAKEN;若 20 秒内未确认,则自动记录为 MISSED,并继续调度下一次提醒。手机端使用 nRF Connect 即可远程配置服药时间表、校准当前时间、查询状态,无需专门开发 App。服药时间表、当日记录和墙钟时间均存入内部 Flash 并带校验,断电重启后自动恢复,时间和记录都不丢失。此外还扩展了 AHT30 温湿度传感器,在 OLED 上显示环境数据。
三、硬件介绍
主控:MAX32655FTHR 开发板,集成 Cortex-M4 内核、BLE 5.2 和 RTC,如图3.1所示。
图3.1 MAX32655FTHR 开发板
显示:0.96 寸 128×64 SSD1306 OLED,I2C 接口,地址 0x3C;SCL 接 P0.30(J7.11)、SDA 接 P0.31(J7.12),如图3.2所示。

图3.2 OLED图
- 报警:有源蜂鸣器,高电平触发,信号接 P2.0,如图3.3所示。

图3.3 有源蜂鸣器
- 输入:板载两个按键——SW2(P0.2)确认服药,SW3(P0.3)翻页 / 长按清空记录。
- 状态指示:板载 RGB LED——红色提醒、绿色已服药、蓝色 BLE 已连接。
- 环境检测:AHT30 温湿度传感器,I2C 地址 0x38,与 OLED 共用同一组 SCL/SDA,如图3.4所示。

图3.4 AHT30
- 全部外设使用 3.3V 供电,连接在开发板 J7 排针上,采用杜邦线搭建,无自绘 PCB。
四、方案框图与设计思路
整体以 MAX32655FTHR 为中心:左侧为输入——内置 RTC 提供时间基准、两个按键用于交互、AHT30 采集温湿度;中间为主控,集成 Cortex-M4、BLE 5.2 和 RTC;右侧为输出——OLED 显示、蜂鸣器声音报警、RGB LED 状态指示;上方通过 BLE 与手机连接,实现提醒推送和远程配置;下方为内部 Flash,负责时间表、服药记录和墙钟时间的掉电保存。
设计上采用**"外设中断 + 主循环轮询"的事件驱动模型**:RTC 闹钟中断只设置一个标志位,不在中断里做耗时操作;100ms 轮询任务检测到标志后再执行提醒、显示、推送,从而不阻塞 BLE 协议栈,保证手机连接稳定。方案框图如图4.1所示。

图4.1 方案框图
五、软件流程、调试软件说明与关键代码说明
软件流程:上电后依次初始化 WSF 内存池与串口日志、BLE 控制器与 32kHz 晶体校准、蜂鸣器、OLED、BLE 协议栈和药盒应用,最后进入 WSF 事件循环。系统通过一个 100ms 定时器驱动 pill_reminder_poll():处理 RTC 闹钟标志、跨天清零、按键(确认 / 翻页 / 长按)、提醒超时、温湿度采样、Flash 持久化,并刷新蜂鸣器、LED 和 OLED。服药状态在 PENDING → TAKEN / MISSED 之间迁移,漏服后继续调度下一次提醒。
图5.1 软件流程图
调试软件说明:开发全程使用 CodeFusion Studio 编写、编译和烧录(OpenOCD),并通过串口日志([main]、[pill]、[oled]、[store] 等标签)定位问题;BLE 侧使用 nRF Connect 连接 PillBox、订阅通知并写入命令验证。
关键代码说明:
1. RTC 闹钟:中断只置标志,重装先关中断
void RTC_IRQHandler(void) /* 中断里只记事件 */
{
if (MXC_RTC_GetFlags() & MXC_F_RTC_CTRL_TOD_ALARM) {
MXC_RTC_ClearFlags(MXC_F_RTC_CTRL_TOD_ALARM);
pill_rtc_alarm_pending = true; /* 不做任何耗时操作 */
}
}
static int pill_rtc_set_alarm(uint32_t ras) /* 重装下一次闹钟 */
{
/* 关键:先关中断、清标志、写新值,最后重新使能 */
while (MXC_RTC_DisableInt(MXC_RTC_INT_EN_LONG) == E_BUSY) { /* 重试 */ }
MXC_RTC_ClearFlags(MXC_F_RTC_CTRL_TOD_ALARM);
while (MXC_RTC_SetTimeofdayAlarm(ras) == E_BUSY) { /* 重试 */ }
while (MXC_RTC_EnableInt(MXC_RTC_INT_EN_LONG) == E_BUSY) { /* 重试 */ }
}
说明:中断只置标志,声光报警、OLED 刷新和 BLE 推送全部交给 100ms 主循环处理,避免阻塞蓝牙射频事件。重装闹钟必须先关中断再写值——调试中发现直接写新值会导致第二次及之后的闹钟被静默丢弃(表现为"只有第一次提醒能响")。
2. 服药状态机:PENDING → TAKEN / MISSED
static void confirm_reminder(void) /* 按键确认 → TAKEN */
{
if (active_slot < 0) return; /* 只在提醒进行中有效 */
slot_status[active_slot] = PILL_STATUS_TAKEN;
last_taken_slot = active_slot;
active_slot = -1;
store_dirty = true; /* 标记待写入 Flash */
buzzer_set(false); /* 停声光、亮绿灯 */
LED_Off(LED_RED);
LED_On(LED_GREEN);
snprintf(message, sizeof(message), "PillReminder: %s TAKEN", time_str);
send_ble_message(message); /* 推送手机 */
}
static void miss_reminder(void) /* 20 秒未确认 → MISSED */
{
if (active_slot < 0) return;
slot_status[active_slot] = PILL_STATUS_MISSED;
active_slot = -1;
store_dirty = true;
buzzer_set(false);
LED_Off(LED_RED);
snprintf(message, sizeof(message), "PillReminder: %s MISSED", time_str);
send_ble_message(message); /* 下一次提醒继续调度 */
}
说明:这是"按键确认并记录服药状态"的核心。状态变化只置 store_dirty,真正的 Flash 写入留到主循环,避免在按键路径里插入几十毫秒的擦写阻塞;漏服只记录状态,不中断后续提醒。
3. 100ms 主循环调度
void pill_reminder_poll(void) /* 每 100ms 执行 */
{
tick_count++;
if (pill_rtc_alarm_pending) { /* ① 闹钟到点 */
pill_rtc_alarm_pending = false;
if (active_slot >= 0) miss_reminder();
start_reminder(pending_slot);
arm_next_alarm(); /* 立即装载下一次 */
}
if (rel_day > last_relative_day) { /* ② 跨天:当日记录清零 */
last_relative_day = rel_day;
/* 记录复位为 PENDING、重装闹钟、标记持久化 */
arm_next_alarm();
}
if ((wall_now / 60) != (last_saved_wall_seconds / 60)) {
store_dirty = true; /* ③ 每分钟保存墙钟时间 */
}
btn1 = PB_Get(BUTTON1) != 0; /* ④ SW2:短按确认服药 */
btn2 = PB_Get(BUTTON2) != 0; /* SW3:短按翻页、长按清空 */
if (btn1 && !btn1_last) {
confirm_reminder();
}
if (btn2_held && ((tick_count - btn2_press_tick) >= SW2_LONG_PRESS_TICKS)) {
clear_today_log();
}
if ((tick_count - active_start_tick) >=
((uint32_t)REMINDER_TIMEOUT_SEC * 1000U) / POLL_PERIOD_MS) {
miss_reminder(); /* ⑤ 20 秒超时 → 漏服 */
}
persist_records(); /* ⑥ 需要时写入 Flash */
/* ⑦ 刷新蜂鸣器节奏、RGB LED、OLED 页面,并非阻塞采样温湿度 */
}
说明:整个应用只有这一个调度点,闹钟、跨天、按键、超时、持久化、显示刷新全部收敛在此,顺序即优先级。按键用"边沿检测 + 按下计时"区分短按与长按,规避机械抖动。
4. BLE 文本命令协议与通知推送
static uint8_t pillWpWriteCback(dmConnId_t connId, uint16_t handle, ...)
{
memcpy(cmd, pValue, len);
cmd[len] = '\0'; /* 取出手机写入的文本命令 */
if (strcmp(cmd, "GET") == 0) { /* 查询时间表 */
pill_reminder_get_schedule_text(times, sizeof(times));
snprintf(reply, sizeof(reply), "Schedule: %s", times);
pillSendData(connId, reply);
} else if (strncmp(cmd, "TIME ", 5) == 0) {/* 校准当前时间 */
if (pillParseTime(&cmd[5], &wall_seconds)) {
pill_reminder_set_base_time(wall_seconds);
pillSendData(connId, "Time set: ...");
} else {
pillSendData(connId, "ERR: bad time");
}
} else if (strncmp(cmd, "SET ", 4) == 0) {/* 设定每日时间表 */
count = pillParseSchedule(&cmd[4], minutes, PILL_SCHEDULE_MAX);
if ((count > 0) && pill_reminder_set_schedule(minutes, count)) {
pillSendData(connId, "Schedule set: ...");
} else {
pillSendData(connId, "ERR: bad schedule");
}
}
return ATT_SUCCESS;
}
static void pillSendData(dmConnId_t connId, const char *msg)
{
if (AttsCccEnabled(connId, PILL_WP_DAT_CCC_IDX)) { /* 手机已订阅 */
AttsHandleValueNtf(connId, WP_DAT_HDL, strlen(msg), (uint8_t *)msg);
}
}
说明:复用 Cordio 的 WP 服务(数据特征本身支持 Write 与 Notify),因此不需要自定义 UUID、也不需要配对,手机用 nRF Connect 即可操作。命令集为 TIME / SET / GET / STATUS / BEEP / RESET;提醒、确认、漏服时固件主动推送 PillReminder: … take medicine / TAKEN / MISSED。
5. Flash 持久化:时间表、记录与墙钟时间
typedef struct {
uint32_t magic, version, checksum; /* 有效性校验(魔数/版本/校验和) */
uint32_t day_key; /* 跨天计数 */
uint32_t base_wall_seconds; /* 墙钟时间,断电后由此恢复 */
uint16_t schedule_minutes[4]; /* 每日提醒时间表 */
uint8_t status[4]; /* 当日各时段服药状态 */
uint8_t count, reserved[3];
} pill_store_t;
bool pill_store_save(const pill_store_t *store)
{
data.checksum = pill_store_checksum(&data);
__disable_irq(); /* 擦写期间必须关中断 */
MXC_FLC_PageErase(PILL_STORE_ADDR); /* 整页擦除 */
for (i = 0; i < sizeof(pill_store_t) / 4; i++) {
MXC_FLC_Write32(PILL_STORE_ADDR + i * 4, words[i]); /* 逐 32 位编程 */
}
__enable_irq();
MXC_FLC_Read(PILL_STORE_ADDR, verify, sizeof(verify));
return memcmp(verify, &data, sizeof(data)) == 0; /* 回读校验 */
}
说明:数据存放在内部 Flash 最后一页(0x1007E000),该页已在链接脚本中从代码区排除。结构带魔数、版本和校验和,上电校验不通过就回退默认配置;墙钟时间每 60 秒保存一次,因此断电重启后时间从断电前继续走,服药记录同样不丢失。
六、实物演示及说明
上电后 OLED 显示时间页;手机连接 PillBox 并订阅通知;写入 TIME 校准时间、写入 SET 设定时间表;到点后蜂鸣器响、红灯闪、OLED 显示 ALARM TAKE,手机收到 take medicine;按 SW2 确认后绿灯亮、状态变 TAKEN、手机收到 TAKEN,如图6.3所示;SW3 翻页查看服药记录页和环境页;不按键等待 20 秒则自动记录 MISSED 并继续等待下一次提醒;断电重启后时间和记录仍保留。
图6.1 开发板实物图

图6.2 OLED 时间页 / 记录页 / 环境页

图6.3 nRF Connect 收到提醒 TAKEN 通知
图6.4 CodeFusion Studio 软件截图
七、遇到的难点及解决方法
- OLED 一直不亮:排查发现 MAX32655FTHR 的 GPIO 默认供电域 VDDIO 只有 1.8V,而 SSD1306 在 3.3V 供电下要求高电平 ≥2.3V,导致从机不应答。改用 GPIO 位操作软 I2C,并把引脚切换到 VDDIOH(3.3V)后解决。
- 第二次及后续提醒不触发:RTC time-of-day 闹钟第一次触发后,直接写新闹钟值会被静默丢弃。改为"先关中断 → 写新值 → 再开中断"后问题消失。
- BLE 连接后提醒停摆:
DM_ADV_STOP事件会误停 UI 轮询定时器,导致连接建立后不再轮询。修正后跨广播/连接状态始终保持 100ms 定时器运行。 - 漏服状态被覆盖:下一个提醒触发时会把上一条 MISSED 立即改写。为状态机增加保护后,漏服状态得以保留显示。
- 读取 RTC 卡死:CFS 生成的外设初始化可能未启动 RTC,
MXC_RTC_GetSeconds会死等 RDY 标志。增加 RTC Init/Start 后解决。 - 断电时间归零:板子无后备电池,RTC 断电清零。通过把墙钟时间每 60 秒写入 Flash、上电恢复的方式,实现断电后时间从断电前继续走。
八、对本次活动的心得体会
本次竞赛让我完整经历了一次从硬件接线、底层驱动、BLE 协议栈到数据持久化的嵌入式开发过程。最大的收获是学会用"中断只置标志、主循环统一处理"的思路保证实时外设和无线协议栈的稳定;同时体会到了排查硬件供电域、中断时序这类"看似软件、实为底层"问题的重要性。CodeFusion Studio 与 MSDK 提供了很好的工程化支持,建议后续能在示例中补充 FTHR 排针与 GPIO 供电域的说明。感谢主办方和评审提供的学习机会。