2026 ADI CodeFusion 竞赛 - 基于 MAX32650 设计智能服药提醒药盒
该项目使用了MAX32650、AMOLED、RTC、蓝牙通信及舵机控制,实现了智能服药提醒药盒的设计,它的主要功能为:支持定时服药提醒、手机 App 连接、红绿灯状态提示、按键确认服药以及舵机药仓转动控制。。
标签
嵌入式系统
智能药盒
MAX32650
niconiconi
更新2026-10-08
厦门大学
63

1. 所选任务

所选任务:基于 MAX32650 的智能服药提醒药盒设计【入门级题目1:智能药盒提醒器】

本项目围绕 MAX32650 开展智能服药提醒药盒的设计与实现,完成了 AMOLED/LVGL 界面显示、RTC 定时提醒、蓝牙 App 连接、LED 状态提示、按键确认及舵机药仓控制等功能,并使用 CodeFusion Studio 完成工程开发、编译与调试配置。

2. 项目概述

本项目基于 ADI MAX32650 构建一套智能服药提醒药盒原型系统,主要面向日常服药过程中容易忘记时间、提醒方式单一以及缺少状态反馈等问题。系统以“定时提醒—用户确认—状态记录—药仓动作”为主线,将时间管理、图形界面、蓝牙通信、声光提醒以及执行机构整合到同一嵌入式平台中,完成一个较完整的服药辅助流程。

系统采用 RTC 作为独立时间基准,用于持续维护当前时间并与预设服药计划进行比较。AMOLED 配合 LVGL 负责显示当前时间、蓝牙连接状态、服药提醒信息以及系统运行状态;蓝牙模块用于与手机 App 建立通信,使用户可以在手机端完成时间设置、提醒计划配置、设备状态查询以及开盒控制。提醒输出部分由红绿 LED 和蜂鸣器共同完成,其中绿色 LED 用于表示正常待机状态,进入提醒状态后切换为红色 LED 提示,并配合蜂鸣器进行声音提醒。

当前系统保留三条服药提醒计划,默认时间分别为 08:00、12:30 和 20:00。到达计划时间后,提醒模块会根据当前 RTC 时间和计划表进行匹配,并切换到提醒状态。用户完成服药后,可通过开发板上的 SW1 按键进行确认,系统随后停止当前提醒、恢复正常状态,并记录本次确认结果。药仓部分通过舵机执行机构实现位置切换,同时也可以通过手机端发送开盒指令触发动作。

软件结构上,项目将 RTC、AMOLED、触摸、蓝牙、蜂鸣器、舵机以及提醒逻辑分别封装为独立模块,由主程序统一调度。这样可以减少不同功能之间的直接耦合,例如 UI 只负责显示状态,蓝牙协议只负责解析命令,而底层 LED、蜂鸣器和舵机动作由对应接口统一完成。该结构既方便当前功能调试,也为后续增加更多提醒计划、历史记录保存或药仓数量扩展预留了空间。

3. 系统总体设计

系统整体以 MAX32650 为核心控制器,由主程序统一协调时间管理、显示刷新、蓝牙通信、提醒状态以及药仓执行等功能。软件采用周期轮询和模块化服务相结合的方式运行,主循环以 1 ms 为基础时间节拍推进 system_elapsed_ms,并在此基础上分别调度舵机、蜂鸣器、蓝牙通信和 LVGL 等模块。

RTC 不需要在每一次主循环中读取,而是按照固定周期进行更新。当前程序每 200 ms 读取一次 RTC 时间,在读取成功后,将当前时间同步给手机协议模块和界面模块,同时调用提醒服务进行计划匹配。通过这种方式既可以保证时间显示和提醒判断足够及时,又避免频繁访问 RTC 带来不必要的开销。

提醒模块内部维护多条服药计划,并记录每条计划的触发状态、当前活动计划以及已确认次数。当当前 RTC 的小时和分钟与某条计划匹配,并且该计划在当前时间段内尚未触发时,系统进入提醒状态。通过触发标志可以避免程序在同一分钟内反复执行同一条提醒,从而保证提醒逻辑稳定。

进入提醒状态后,系统会同时联动多个输出模块。红色 LED 按约 500 ms 的节奏进行闪烁,绿色 LED 熄灭;蜂鸣器按照软件节拍周期性短鸣;LVGL 界面中的提醒状态也同步更新,使用户可以同时通过视觉、声音和界面信息感知当前提醒。蓝牙处于连接状态时,系统还可以向手机端发送对应的提醒消息。

用户确认流程由 SW1 按键完成。主程序对按键输入进行边沿检测和简单去抖处理,避免一次按键被重复识别。当检测到有效确认动作后,提醒模块记录本次确认状态并结束当前提醒,随后关闭红色 LED 和蜂鸣器,重新点亮绿色 LED,使系统回到正常待机状态。

药仓动作由独立的舵机接口负责。主程序和手机协议模块只负责发出“执行药仓动作”的请求,具体脉冲输出和执行过程由舵机模块内部完成。这样可以避免 UI、蓝牙或提醒模块直接操作底层 GPIO,使上层逻辑和执行机构之间保持相对独立。

蓝牙模块通过 UART 与手机 App 通信,主循环持续轮询接收数据并交给协议层处理。协议层负责解析手机端发送的时间设置、计划配置、状态读取和开盒等命令,并将执行结果反馈给手机端。与此同时,系统还会根据蓝牙连接状态更新 AMOLED 上的 Disconnected 或 Connected 显示。

整体软件可以概括为四个主要层次:底层驱动负责 RTC、AMOLED、触摸、UART、LED、蜂鸣器和舵机等硬件;功能模块负责提醒逻辑、蓝牙接口和手机协议;UI 层负责 LVGL 界面和状态显示;主程序负责各模块的初始化和周期调度。通过这种分层方式,系统的时间管理、提醒判断、用户交互和执行机构之间形成较清晰的功能边界,便于后续调试和扩展。

4. 硬件组成

本项目主要由 MAX32650FTHR 开发板、CO5300 AMOLED、CST820 触摸控制器、RTC 时钟源、DX-BT36 蓝牙模块、LED、按键、蜂鸣器和舵机组成,各部分功能如下表所示。

图 4-1 智能药盒硬件整体实物图

硬件模块

接口/方式

主要作用

MAX32650FTHR 开发板

主控制器

完成系统初始化、任务调度、提醒逻辑及各外设控制

CO5300 AMOLED

QSPI

显示时间、蓝牙连接状态、服药提醒及 LVGL 界面

CST820 触摸控制器

I2C1,0x15,400 kHz

提供 LVGL 触摸输入

32.768 kHz RTC 时钟源

RTC

提供系统时间基准,用于服药计划定时判断

DX-BT36 蓝牙模块

UART1,9600 8N1

与手机 App 通信,接收设置指令并发送设备状态

红色 LED

GPIO

表示当前处于服药提醒状态

绿色 LED

GPIO

表示系统处于正常待机状态

SW1 按键

GPIO

用户完成服药后的确认输入

蜂鸣器

GPIO

在服药时间到达后进行声音提醒

舵机

GPIO 脉冲控制

执行药仓转动动作

系统控制核心采用 MAX32650FTHR 开发板,工程基于 FTHR_APPS_A 板级支持包进行开发。MAX32650 负责 RTC 时间读取、提醒逻辑判断、蓝牙通信、LVGL 界面刷新、按键检测以及蜂鸣器和舵机等外设的统一调度,是整个智能药盒的主控制器。

显示部分采用 CO5300 AMOLED 屏幕,显示区域为 410×502,使用 RGB565 像素格式。MAX32650 通过 QSPI GPIO 驱动完成 CO5300 控制器命令和像素数据传输,LVGL 采用 partial buffer 的方式进行界面刷新,不需要为整块屏幕分配完整帧缓存,从而减少 SRAM 占用。AMOLED 主要用于显示当前时间、服药提醒状态、蓝牙连接状态以及其他交互信息。

图 4-2 AMOLED 显示模块实物安装

触摸部分使用 CST820 触摸控制器,通过 I2C1 与 MAX32650 通信,器件地址为 0x15,总线速率配置为 400 kHz。驱动读取触摸坐标后,将结果作为 LVGL 的指针输入,实现屏幕界面的触摸交互。RTC 时间基准由开发板上的 32.768 kHz 时钟源提供,用于维持系统时间并完成服药计划的定时判断。

蓝牙通信使用 DX-BT36 蓝牙模块,通过 UART1 与 MAX32650 连接,串口采用 9600 8N1 配置。蓝牙模块用于连接手机 App,实现服药时间设置、提醒计划配置、设备状态读取以及相关控制指令的传输。系统根据蓝牙模块返回的信息维护当前连接状态,并同步更新 AMOLED 上的 Disconnected / Connected 状态。

声音提醒部分采用独立 蜂鸣器模块。当系统检测到当前 RTC 时间到达预设服药时间后,蜂鸣器按照软件设定的节拍进行短鸣,与红色 LED 和界面提醒状态同时工作;当用户确认完成服药后,蜂鸣器停止工作,系统恢复正常待机状态。

图 4-3 蓝牙通信与蜂鸣器模块实物安装

状态指示部分使用红色和绿色 LED。正常待机时绿色 LED 保持亮起,用于表示设备处于正常运行状态;进入服药提醒状态后,绿色 LED 熄灭,红色 LED 按设定节奏闪烁,使用户可以直观判断当前设备状态。板载 SW1 按键作为服药确认输入,用户完成服药后按下 SW1,系统结束当前提醒并恢复正常状态。

药仓执行机构采用 360°连续旋转舵机。舵机由 MAX32650 输出控制脉冲进行驱动,并与药盒内部的旋转药仓结构连接。当系统收到开盒指令或执行药仓动作时,控制程序驱动舵机转动,使圆形药仓机构移动到下一个位置,从而完成药仓位置切换。舵机控制被封装为独立的软件接口,界面和蓝牙协议只需要发送动作请求,不直接控制底层 GPIO,有利于将药仓机械动作与上层逻辑分离。

当前药仓结构采用 3D 打印方式制作,舵机安装在旋转机构中心附近,通过连续旋转带动药仓结构完成位置变化。这种结构便于快速完成智能药盒原型验证,也方便后续继续调整药仓尺寸、数量以及机械限位方式。

图 4-4 360°舵机及旋转药仓机构实物图

为了配合舵机完成药仓切换,本项目使用 Fusion 360 对药盒主体、旋转药仓、出药通道以及舵机安装位置进行了三维建模。机械结构主要由圆形旋转药仓、固定外壳、出药导向区域和底部舵机安装机构组成。

旋转药仓位于主体上方,通过中心位置与舵机传动结构连接。舵机转动时带动药仓结构旋转,使不同药仓依次移动到出药位置。外侧固定结构用于限制旋转组件的位置,并为显示模块、电路板和连接线预留安装空间。

在结构设计时还考虑了 3D 打印加工和后期装配,因此整体采用较规则的壁厚和支撑结构,并为舵机、转轴以及药仓之间预留必要间隙。通过三维建模可以提前检查药仓尺寸、旋转空间以及舵机安装位置,减少实际打印后反复修改结构的情况。

图 4-5 智能药盒整体机械结构三维模型

图 4-6 旋转药仓与出药通道结构设计

图 4-7 舵机安装及药仓传动结构



5. 方案框图与设计思路

本系统在结构上采用“主控集中调度、功能模块独立封装、界面与硬件解耦”的设计思路。MAX32650 作为系统核心,不直接把所有功能堆叠在主循环中,而是通过不同的软件模块分别管理时间、通信、提醒、界面和执行机构,再由主程序负责统一调度。

系统中的信息流可以分为两类。一类是状态信息,例如 RTC 时间、蓝牙连接状态、当前提醒状态和服药确认结果,这些数据由底层模块产生,再传递给 UI 或手机端进行显示;另一类是控制信息,例如手机 App 发送的时间设置、提醒计划和开盒请求,这些指令由通信模块接收并解析后,再交给对应的功能模块执行。

这种设计避免了 UI、蓝牙协议和底层 GPIO 之间直接耦合。例如界面只接收“当前是否提醒”“蓝牙是否连接”等状态,不直接控制 LED 或舵机;蓝牙模块只负责收发和命令解析,不直接处理提醒逻辑;舵机、蜂鸣器等执行器也分别封装成独立接口。这样即使后续修改某个模块,也不会对其他部分造成较大影响。

从整体结构上看,系统可以划分为四个层次:

  • 硬件与驱动层:负责 RTC、显示、触摸、串口、LED、按键、蜂鸣器和舵机等基础资源;
  • 功能模块层:负责提醒状态机、蓝牙通信、手机命令协议以及执行器控制;
  • 界面层:负责 LVGL 页面、状态显示和触摸事件;
  • 应用调度层:由主程序完成各模块初始化和周期调用,并维护系统运行节奏。

这种分层结构的主要目的不是增加复杂度,而是让各部分职责更加清晰。对于当前智能药盒项目来说,可以方便地分别调试提醒逻辑、蓝牙通信或显示界面;如果后续增加更多提醒计划、扩展药仓数量,或者修改手机端协议,也不需要重新组织整个工程。

图 5-1 智能服药提醒药盒系统总体框图

6. 软件设计与关键代码说明

软件部分采用模块化结构组织。main.c 作为整个系统的软件入口,主要负责各硬件模块的初始化、周期任务调度以及不同功能之间的协调。RTC、显示、触摸、蓝牙、蜂鸣器和舵机等功能并没有全部直接写在 main.c 中,而是分别封装在不同的驱动和功能模块里,主程序只负责按照一定顺序初始化并周期调用对应接口。

这种结构的好处是,当某个功能出现问题时,可以比较容易判断问题属于显示、通信、提醒逻辑还是执行机构,而不需要在一个很大的主函数中逐行排查。

6.1 系统初始化流程

程序进入 main() 后,首先定义当前时间、按键状态、蓝牙连接状态以及若干软件计时变量。随后开始进行板级硬件初始化。

第一步调用 Board_Init() 完成 MAX32650FTHR 开发板的基础资源初始化。如果板级初始化失败,程序会进入 fatal(),通过串口输出错误信息并停止继续运行。这样可以避免在基础硬件尚未正确工作的情况下继续初始化其他模块。

完成板级初始化后,程序继续初始化 RTC。RTC 是服药提醒功能的时间基础,因此需要在显示和提醒模块之前完成初始化。初始化成功后,程序通过串口打印 RTC 启动信息,便于调试时确认时间模块已经正常工作。

随后初始化蓝牙模块。蓝牙被安排在触摸模块之前启动,是因为触摸属于辅助输入设备,即使触摸初始化失败,系统仍希望保留手机通信、显示和板载按键等基本功能。这样的启动顺序可以提高系统在部分外设异常时的可用性。

之后程序依次初始化 QSPI GPIO、CO5300 AMOLED 和 LVGL。QSPI GPIO 用于向显示控制器发送命令和像素数据,CO5300 初始化完成后,LVGL 才能建立实际的显示输出通道。

触摸部分采用 CST820。如果 CST820 初始化成功,则继续将触摸接口注册到 LVGL;如果初始化失败,程序不会直接停止,而是通过串口打印提示信息,并继续保留通过 SW1 按键进行基本操作的能力。

最后初始化舵机、蜂鸣器、提醒模块以及手机协议模块,并设置蓝牙接收回调。等这些模块全部完成以后,创建主界面,整个系统才正式进入主循环。

int main(void)
{
rtc_time_t current_time = { 0 };
bool previous_button = false;
bool previous_ble_connected = false;
uint32_t last_rtc_read_ms = 0U;
uint32_t last_button_ms = 0U;

if (Board_Init() != E_NO_ERROR)
fatal("BOARD INIT");

if (rtc_init() != E_NO_ERROR)
fatal("RTC INIT");

if (ble_interface_init() != E_NO_ERROR)
fatal("DX-BT36 INIT");

if (qspi_gpio_init() != E_NO_ERROR)
fatal("QSPI GPIO INIT");

co5300_reset();

if (co5300_init() != E_NO_ERROR)
fatal("CO5300 INIT");

if (lvgl_port_init() != E_NO_ERROR)
fatal("LVGL PORT");

if (touch_cst820_init() == E_NO_ERROR) {
lvgl_port_register_touch();
}
}

代码 6-1 系统主要外设初始化


这一段代码主要说明了系统启动时各模块之间的先后依赖关系。


6.2 提醒状态控制

服药提醒部分由 reminder_alert_service() 负责。这个函数并不负责判断“现在是不是到了服药时间”,而是负责在提醒已经被触发以后,控制 LED 和蜂鸣器的具体表现。

函数首先判断 reminder_active。

如果当前没有提醒任务,程序会检查系统之前是否处于提醒状态。如果刚刚从提醒状态退出,就执行以下操作:

  • 关闭红色 LED;
  • 点亮绿色 LED;
  • 停止蜂鸣器;
  • 输出提醒结束信息。

这样可以保证系统从提醒状态恢复到正常待机状态时,各个输出设备能够同步恢复,而不是出现红灯已经关闭但蜂鸣器仍在响的情况。

如果当前处于提醒状态,程序会根据 elapsed_ms 计算红色 LED 的状态:

next_led_state = ((elapsed_ms / 500U) & 1U) != 0U;

这里将系统运行时间按照 500 ms 分段,从而实现红色 LED 周期性亮灭。也就是说,提醒状态下红灯不会持续亮起,而是通过闪烁让提醒效果更加明显。

蜂鸣器采用类似的非阻塞方式控制。当距离上一次蜂鸣已经超过 1000 ms 时,调用:

buzzer_beep(120U);

让蜂鸣器短鸣一次,并更新 last_beep_ms。

这种写法没有直接使用很长的延时函数,因此在蜂鸣器发出提醒的同时,蓝牙、RTC 和 LVGL 等其他任务仍然可以继续运行。

static void reminder_alert_service(bool reminder_active,
uint32_t elapsed_ms)
{
static bool was_active;
static bool led_is_on;
static uint32_t last_beep_ms;
bool next_led_state;

if (!reminder_active) {
if (was_active) {
LED_Off(LED_RED);
LED_On(LED_GREEN);
buzzer_stop();
}

was_active = false;
return;
}

next_led_state = ((elapsed_ms / 500U) & 1U) != 0U;

if (!was_active || (next_led_state != led_is_on)) {
if (next_led_state)
LED_On(LED_RED);
else
LED_Off(LED_RED);

LED_Off(LED_GREEN);
led_is_on = next_led_state;
}

if (!was_active ||
((elapsed_ms - last_beep_ms) >= 1000U)) {
buzzer_beep(120U);
last_beep_ms = elapsed_ms;
}

was_active = true;
}

代码 6-2 LED 与蜂鸣器提醒控制

这一段代码体现了本项目中一个比较重要的设计思路:提醒效果通过软件状态和时间节拍控制,而不是通过阻塞式延时实现。


6.3 主循环任务调度

完成所有初始化后,程序进入:

while (1)

主循环。

主循环首先执行 1 ms 延时,并递增:

system_elapsed_ms

这个变量可以看作整个软件的基础时间节拍。舵机、蓝牙提醒、LED 闪烁以及其他周期任务都可以基于这个变量判断当前是否应该执行。

随后依次调用:

servo_interface_service();
buzzer_interface_service();
ble_interface_service();
ble_interface_poll();

分别处理舵机状态、蜂鸣器状态以及蓝牙收发。

蓝牙模块如果检测到设备刚刚从未连接状态切换为已连接状态,会调用:

reminder_notify_now();

这样如果手机是在提醒已经触发以后才连接,仍然可以立即获得当前提醒,而不需要等待下一次提醒重新出现。

RTC 时间并不是每次主循环都读取。程序通过:

(system_elapsed_ms - last_rtc_read_ms) >= 200U

控制读取周期,大约每 200 ms 更新一次当前时间。

RTC 读取成功以后,会连续完成几个操作:

  1. 将当前时间更新到手机协议模块;
  2. 调用 reminder_service() 判断是否达到提醒时间;
  3. 更新 LVGL 界面中的当前时间;
  4. 更新提醒药物、提醒时间和已确认次数。

这样,RTC、提醒逻辑、手机状态和屏幕显示都基于同一份当前时间数据进行更新,可以避免不同模块之间出现时间不一致。

while (1) {
MXC_Delay(MXC_DELAY_MSEC(1));
system_elapsed_ms++;

servo_interface_service(system_elapsed_ms);
buzzer_interface_service();

ble_interface_service(system_elapsed_ms);
ble_interface_poll();

if (ble_interface_is_connected() &&
!previous_ble_connected) {
reminder_notify_now();
}

previous_ble_connected =
ble_interface_is_connected();

if ((system_elapsed_ms - last_rtc_read_ms) >= 200U) {
last_rtc_read_ms = system_elapsed_ms;

if (rtc_get_time(&current_time) == E_NO_ERROR) {
mobile_protocol_update_time(&current_time);

reminder_service(&current_time,
system_elapsed_ms);

ui_main_update_time(current_time.hour,
current_time.minute);

ui_main_set_reminder(
reminder_is_active(),
reminder_medicine(),
reminder_hour(),
reminder_minute(),
reminder_taken_count());
}
}
}

代码 6-3 主循环中的 RTC、蓝牙及提醒任务调度


6.4 按键确认与界面刷新

在主循环的后半部分,程序读取板载 SW1 状态:

bool button_pressed = PB_Get(0U) != 0;

程序不仅判断当前按键是否被按下,还将当前状态与 previous_button 进行比较,从而只响应“刚刚按下”这一瞬间,而不是在用户持续按住按键时反复执行确认操作。

同时通过:

(system_elapsed_ms - last_button_ms) >= 50U

增加简单的时间去抖处理。只有距离上一次有效按键超过一定时间,才会再次接受新的确认操作。

当按键满足条件以后,调用:

reminder_confirm(&current_time);

完成当前服药任务确认。

随后,程序继续调用:

reminder_alert_service();
ui_main_set_ble_connected();
ui_main_set_reminder_flash();

分别更新声光提醒、蓝牙连接状态以及界面中的提醒闪烁状态。

最后:

lvgl_port_tick_inc(1U);
lvgl_port_task_handler();

持续驱动 LVGL 的内部时间和任务处理,使界面动画、状态刷新和触摸输入能够正常运行。

bool button_pressed = PB_Get(0U) != 0;

if (button_pressed &&
!previous_button &&
((system_elapsed_ms - last_button_ms) >= 50U)) {

last_button_ms = system_elapsed_ms;
reminder_confirm(&current_time);
}

previous_button = button_pressed;

reminder_alert_service(
reminder_is_active(),
system_elapsed_ms);

ui_main_set_ble_connected(
ble_interface_is_connected());

ui_main_set_reminder_flash(
reminder_is_active() &&
(((system_elapsed_ms / 500U) & 1U) != 0U));

lvgl_port_tick_inc(1U);
lvgl_port_task_handler();

代码 6-4 SW1 服药确认及 LVGL 状态更新

通过以上几个部分可以看到,主程序本身并不直接实现所有功能,而是负责在固定节拍下组织各模块运行。提醒判断、蓝牙通信、执行器控制和界面显示分别由对应模块完成,从而使软件结构更加清晰。

7. 软件流程

软件运行过程可以分为“系统初始化”和“循环运行”两个阶段。

设备上电后,程序首先进行 MAX32650 的板级初始化,然后启动 RTC。RTC 初始化成功以后,系统继续初始化蓝牙、AMOLED 和 LVGL。显示部分准备完成后,再初始化 CST820 触摸、舵机、蜂鸣器以及提醒模块。

所有模块初始化完成后,系统进入主循环,此后正常运行过程中不会重新执行初始化。

进入主循环以后,程序持续完成四类任务:

  • 读取 RTC 当前时间;
  • 接收并处理蓝牙数据;
  • 更新 LVGL 界面;
  • 检测用户按键输入。

其中 RTC 当前时间会与提醒计划中的时间进行比较。

如果当前时间尚未到达任何服药计划,系统继续维持正常待机状态。绿色 LED 保持正常状态,程序重新回到主循环,继续读取时间和处理其他任务。

如果 RTC 当前时间与某条提醒计划匹配,提醒模块将系统切换到提醒状态。

进入提醒状态以后:

  • 红色 LED 开始闪烁;
  • 绿色 LED 熄灭;
  • 蜂鸣器周期性短鸣;
  • AMOLED 界面更新提醒状态;
  • 蓝牙已连接时,可以向手机端同步当前提醒信息。

此后程序进入“等待用户确认”的状态。

如果用户没有按下 SW1,则提醒状态继续保持,程序仍然可以继续执行蓝牙和界面刷新任务。

当用户按下 SW1 后,程序调用提醒确认接口,结束当前提醒。随后停止蜂鸣器、关闭红灯、恢复绿色 LED,并根据当前控制逻辑执行药仓动作。

完成这一轮提醒后,系统重新返回主循环,继续等待下一条服药计划。

因此整个程序并不是“执行一次后结束”,而是形成一个持续运行的闭环:

初始化 → 正常监测 → 触发提醒 → 等待确认 → 恢复正常 → 再次监测

图 7-1 智能服药提醒药盒软件主流程图


8. CodeFusion Studio 开发与调试

本项目的软件开发、工程构建和在线调试均围绕 CodeFusion Studio 展开。工程采用 MAX32650 的 MSDK Makefile 结构,源代码仍然按照普通 C 工程方式组织,但可以通过 CodeFusion Studio 对工程进行统一管理。

8.1 工程编译

在 CodeFusion Studio 左侧的 Workspace 中选择当前 MAX32650 工程后,可以在 Actions 中直接找到 Build 操作。

点击 Build 后,CodeFusion Studio 会调用工程中的 Makefile,再由 Arm GNU GCC 完成 C 源文件编译、链接以及最终固件生成。

正常构建时,下方终端可以看到编译和链接过程,并最终生成:

  • .elf
  • .hex
  • .bin
  • .map

等文件。

当前工程已经完成完整构建验证。

从编译结果可以看到:

  • text 约为 384076 B;
  • data 约为 2364 B;
  • bss 约为 44184 B。

根据最终固件占用计算,Flash 使用量约为 386 KB,占 3 MB Flash 的 12.28%;SRAM 使用量约为 46.5 KB,占 1 MB SRAM 的 4.44%。

这些数据可以帮助判断当前程序是否仍有足够资源用于后续功能扩展。

8.2 ELF 文件分析

编译成功以后,可以直接在 CodeFusion Studio 中打开生成的 ELF 文件。

ELF 文件不仅包含最终程序,还包含段信息、符号信息以及调试相关数据。

在 ELF File Editor 中,可以看到:

  • .text
  • .data
  • .bss
  • .heap
  • .stack_dummy
  • 调试信息
  • 符号表

等内容。

例如:

.text 主要对应程序代码;

.data 用于保存需要初始值的全局或静态变量;

.bss 主要保存未初始化或初始化为零的全局变量。

通过 ELF Statistics 页面,还可以进一步查看哪些符号占用了较大的空间,对后续程序优化比较有帮助。

图 8-1 CodeFusion Studio 工程构建与 ELF 文件分析


8.3 在线调试配置

除了编译以外,本项目还配置了 CMSIS-DAP SWD 在线调试。

开发板连接以后,CodeFusion Studio 可以通过 OpenOCD 与 MAX32650 建立调试连接,再由 ARM GDB 对程序进行控制。

本项目使用的调试链路可以理解为:

CodeFusion Studio
↓
ARM GDB
↓
OpenOCD
↓
CMSIS-DAP / SWD
↓
MAX32650

启动 Debug 后,OpenOCD 首先通过 SWD 检测目标处理器。

在调试输出中可以看到类似:

Cortex-M4 processor detected
target has 6 breakpoints, 4 watchpoints
Examination succeeded

等信息,说明调试器已经能够识别 MAX32650 的 Cortex-M4 内核。

随后 GDB 载入工程生成的 ELF 文件,并根据其中包含的调试符号建立源代码与程序地址之间的对应关系。

因此调试时不仅能够看到 CPU 地址,还可以直接回到 main.c 等实际源码中进行断点和单步调试。

8.4 main() 入口调试

为了验证整个软件是否能够正常进入应用程序,本项目在调试配置中设置程序启动后自动运行到 main()。

启动在线调试后,程序会先经过复位和启动流程,然后暂停在 main() 入口附近。

此时可以在 CodeFusion Studio 中查看:

  • 当前正在执行的源文件;
  • 程序停止位置;
  • Call Stack;
  • 局部变量;
  • Cortex-M4 运行状态;
  • GDB / OpenOCD 调试输出。

相比单纯观察串口输出,在线调试可以直接确认程序执行到哪一步,对于排查外设初始化、状态机和通信问题更加方便。

例如,当 RTC、蓝牙或触摸初始化出现异常时,可以在初始化函数前后设置断点,逐步检查返回值,而不需要反复修改代码加入 printf()。

图 8-2 CodeFusion Studio 在线调试 MAX32650 工程


8.5 CodeFusion Studio 在本项目中的作用

在整个开发过程中,CodeFusion Studio 并不仅仅作为代码编辑器使用,而是承担了完整的软件开发流程。

包括:

  1. 浏览和编辑 MAX32650 工程源码;
  2. 调用 MSDK Makefile 完成 Build 和 Clean;
  3. 查看编译生成的 ELF 文件;
  4. 分析 Flash、SRAM、Section 和 Symbol;
  5. 通过 OpenOCD 与 CMSIS-DAP 建立 SWD 调试连接;
  6. 使用 GDB 对 MAX32650 进行断点和在线调试。

因此,从代码编写、工程构建到最终运行调试,整个项目的软件开发过程都可以在同一套环境中完成。

9. 实物功能演示及说明

本项目的实物验证主要围绕智能药盒的完整使用流程展开,包括手机端设置提醒、设备本地定时判断、到点后的声光提醒、用户确认以及舵机药仓动作。测试过程中,MAX32650 作为主控制器持续运行 RTC、蓝牙、LVGL、按键检测和执行机构服务,各模块在同一主循环中协同工作。

为了便于观察系统状态,实物演示中同时关注手机 App、AMOLED 显示、红绿 LED、蜂鸣器以及舵机机构的变化。测试重点不是单独验证某一个外设,而是确认从“设置计划”到“提醒触发”再到“确认结束”的整个流程能够连续运行。

9.1 手机端连接与提醒计划设置

系统启动后,手机 App 首先通过蓝牙扫描附近设备,并搜索名为 SmartMedBox 的药盒。手机与设备建立连接后,可以通过蓝牙向 MAX32650 发送设置和控制命令。

App 中设置了服药计划相关参数,包括药物名称和提醒时间。例如测试时可将药物设置为 VitaminC,提醒时间设置为 08:00。完成参数输入后,通过“保存计划”按钮将新的计划发送到药盒端。

除计划设置外,手机端还提供多个控制入口:

  • “同步手机时间”用于将手机当前时间同步给设备;
  • OPEN 用于发送药仓动作请求;
  • “确认已服药”用于远程发送服药确认;
  • “刷新药盒状态”用于读取当前提醒、连接和记录状态。

药盒端接收到蓝牙数据后,由通信模块完成数据接收,再交给手机协议模块解析。协议层根据具体命令调用提醒、时间或舵机接口,上层 App 不直接操作底层 GPIO。

这种方式使手机主要承担“配置和控制入口”的作用,而真正的时间判断仍由药盒本地 RTC 完成。因此即使手机端暂时不操作,已经保存的提醒计划仍然可以由设备独立执行。


图 9-1 手机 App 与智能药盒交互界面



9.2 RTC 到点判断与提醒触发

完成提醒计划设置后,系统持续读取 RTC 当前时间,并周期性将时间与已保存的提醒计划进行比较。

当当前时间没有与任何计划匹配时,药盒保持正常待机状态。此时绿色 LED 表示系统正常运行,AMOLED 继续显示时间和设备状态,蓝牙通信和触摸功能也保持可用。

当 RTC 的小时和分钟与某条提醒计划匹配后,提醒模块将对应计划设置为当前活动提醒,并进入提醒状态。

进入提醒状态后,系统会产生多种联动响应:

  1. 红色 LED 开始周期性闪烁;
  2. 绿色 LED 熄灭;
  3. 蜂鸣器按照设定周期进行短鸣;
  4. AMOLED 中的提醒状态同步发生变化;
  5. 如果蓝牙处于连接状态,可向手机端发送当前提醒信息。

红色 LED 的闪烁和蜂鸣器的响声均由软件计时控制。程序没有使用长时间阻塞延时,因此在提醒过程中,主循环仍然能够继续处理 RTC、蓝牙以及 LVGL 任务。

这一点对于实际运行比较重要。如果在提醒期间直接使用较长的阻塞式延时,可能导致蓝牙数据无法及时接收,或者界面刷新出现卡顿。当前实现通过统一的软件时间节拍,让提醒动作与其他任务并行推进。

另外,提醒模块还维护当前计划的触发状态,避免同一个提醒时间在同一分钟内被连续重复触发。

图 9-2 智能药盒到点提醒状态


9.3 用户确认服药

当用户完成服药后,可以通过板载 SW1 按键进行确认。

主程序持续读取按键输入,并保存前一次按键状态。当检测到新的有效按下动作时,程序调用提醒确认接口,而不是在按键持续按住期间反复执行确认。

同时,程序还加入了简单的时间去抖条件,用于降低机械按键抖动造成重复触发的可能性。

确认操作完成后,系统退出当前提醒状态,并产生以下变化:

  • 停止蜂鸣器;
  • 红色 LED 熄灭;
  • 绿色 LED 恢复;
  • 当前活动提醒被清除;
  • 服药确认次数更新;
  • 界面恢复到正常显示状态。

确认后系统不会停止运行,而是重新进入正常监测过程,继续等待下一条提醒计划。

因此整个服药提醒过程实际上构成了一个完整的状态循环:

正常待机 → 到点提醒 → 等待确认 → 确认完成 → 返回待机

如果手机仍然处于连接状态,设备端也可以将新的确认状态同步到手机端,使手机端和药盒本地状态保持一致。


9.4 舵机药仓动作

药仓执行机构采用 360°连续旋转舵机,并与旋转药仓结构连接。

舵机控制模块被单独封装,上层提醒逻辑和手机协议只负责发送动作请求,具体的舵机输出由舵机接口内部完成。

当用户在手机 App 中点击 OPEN,或者系统执行对应的药仓控制流程时,MAX32650 会调用舵机控制接口,驱动舵机转动,使药仓机构移动到下一位置。

这种设计将“是否需要开盒”和“舵机具体怎么转”分开处理。后续即使需要调整舵机转动时间、机械结构或药仓位置,也只需要修改执行层,不需要重新修改手机协议和提醒状态机。

实物测试中可以直接观察舵机转动以及旋转药仓的位置变化,从而确认软件控制命令能够正确传递到机械执行机构。


图 9-3 舵机及旋转药仓动作实物图


9.5 完整功能流程验证

通过上述测试,系统从提醒计划设置到服药确认与药仓动作已经形成完整闭环,具体流程如下。

阶段

系统行为

1. 提醒计划设置

用户通过手机 App 设置药物名称和提醒时间,并将计划发送至 MAX32650

2. 计划保存与等待

MAX32650 保存提醒信息,RTC 持续提供当前时间,系统保持正常待机状态

3. 到点判断

当 RTC 当前时间与提醒计划匹配时,提醒模块进入活动状态

4. 声光提醒

红色 LED 闪烁、蜂鸣器短鸣,同时 AMOLED 更新提醒状态

5. 用户确认

用户完成服药后按下 SW1,系统记录本次确认结果

6. 状态恢复

蜂鸣器停止、红灯熄灭、绿灯恢复,系统退出当前提醒状态

7. 药仓动作

根据当前控制请求调用舵机接口,使旋转药仓完成一次位置切换

8. 返回待机

系统重新进入正常监测状态,继续等待下一条提醒计划

在整个过程中,蓝牙、RTC、界面和执行机构并不是分别独立运行,而是由 MAX32650 统一调度。手机端主要负责提醒计划配置和状态交互,RTC 负责时间基准,提醒模块负责状态判断,LED、蜂鸣器和 AMOLED 负责向用户提供提示,舵机则负责完成最终的药仓动作。

当前测试结果表明,手机端计划设置、RTC 到点判断、声光提醒、按键确认以及舵机动作均能够按照预期流程连续执行,完成了从“设置—提醒—确认—执行—恢复”的完整功能验证。

10. 遇到的问题及解决方法

开发过程中遇到的问题主要集中在开发环境、显示、触摸和蜂鸣器这几个部分。

一开始 CodeFusion Studio 里的 MSDK 工程无法直接编译,终端提示找不到 make。检查后发现并不是工程本身的问题,而是 CFS 工作区的环境变量中没有包含对应的工具链目录,导致系统无法找到 make.exe。最后只在当前工程的 Workspace 范围补充了 MSDK、GCC、OpenOCD 和 PATH 配置,没有修改 Windows 的全局环境。调整后,CFS 的 Actions Build、Ctrl+Shift+B 和 Clean Build 均可以正常使用。

AMOLED 显示部分主要遇到了 CO5300 控制器初始化和坐标配置的问题。调试过程中发现,QSPI 命令格式、复位时序以及显示窗口偏移都需要根据实际屏幕参数调整。最后分别处理命令发送和像素数据写入,并修正窗口的 X 方向偏移后,RGB565 图像能够正常显示。

触摸部分使用 CST820,前期调试时出现过 I2C 读取异常。之后重新检查了总线配置和器件地址,并通过读取芯片 ID 确认通信正常。LVGL 触摸输入采用轮询方式读取,同时限制读取频率,避免触摸读取过于频繁影响界面刷新。

蜂鸣器部分也遇到过实际模块电平逻辑和最初判断不一致的问题。经过实测确认有效电平后,程序在初始化时保持静音状态,只在需要提醒时驱动蜂鸣器发声,避免设备上电后出现误响。

11. 项目总结与活动心得

通过这次项目,我把之前比较零散接触过的嵌入式知识真正串到了一起。从 MAX32650 的外设初始化,到 RTC 定时、AMOLED 显示、触摸输入、蓝牙通信,再到舵机和状态灯控制,很多功能单独看并不复杂,但真正放进一个完整系统里以后,就需要考虑各个模块之间的配合、状态切换以及实际运行中的稳定性。

开发过程中也遇到了不少实际问题,比如编译环境配置、屏幕显示偏移、触摸通信异常以及蜂鸣器有效电平判断等。相比单纯把功能做出来,我觉得这些调试过程反而让我收获更大,也让我更熟悉遇到问题时应该从软件、硬件还是开发环境哪一层去排查。

这次活动也让我比较完整地使用了一遍 CodeFusion Studio 和 MSDK,从工程构建、编译,到 ELF 查看和在线调试,基本走了一遍实际的嵌入式开发流程。项目目前已经能够完成基本的服药提醒、手机交互和药仓控制,后续还可以继续完善记录保存、手机端界面以及药仓结构等功能。

也感谢电子森林和 ADI 提供这次实践机会,让我能够围绕真实开发板和完整工具链完成一次从方案设计、代码实现到实物联调的完整项目。相比单纯学习单个外设,这种完整项目的开发过程让我对嵌入式系统设计有了更实际的认识,也让我更清楚后续需要继续补足和深入的方向。

附件下载
MAX32650.zip
完整源码
团队介绍
本项目由个人独立完成,主要负责 MAX32650 嵌入式软件开发、AMOLED/LVGL 界面设计、RTC 定时逻辑、蓝牙通信、舵机控制以及整体功能联调与测试。开发过程中完成了从方案设计、代码实现到实物调试和演示验证的完整流程。
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号