基于STM32的智能药盒项目详解:硬件设计、状态机与仿真调试
做嵌入式这些年陆陆续续折腾过不少小项目但要说家里人真正能每天用上的还是这套给家里老人做的智能药盒。一开始只是想着老人总忘吃药降压药一天两顿经常漏后来做着做着就收不住了——从一颗STM32F103C8T6最小系统板到把原理图、代码、上位机、仿真全折腾齐断断续续改了三版才敢说这是个能稳定跑起来的“用药管理系统”。这篇文章就把这个项目完整地拆开讲包括整体方案为什么这么定、硬件每个模块怎么选型、代码的状态机怎么写才不容易出逻辑漏洞、Proteus仿真和实物调试各自踩过哪些坑以及老人实际使用反馈倒逼出来的几处设计修正。无论你是准备拿STM32做课设、电赛还是真想给家里长辈做一套能用的东西这项目的代码结构、原理图思路、仿真方法都能直接参考整套东西我也开源出来了。1. 项目整体设计与思路拆解我先把这套系统到底干了什么事说清楚它本质上是一个“定时提醒 用药记录 异常上报”的小型嵌入式系统。主机固件跑在STM32F103C8T6上外接了一个0.96寸OLED屏、三个按键、一个语音播报模块、一个DS1307或内部RTC时钟源另外通过串口和ESP8266模块跟手机端/服务端通信。药盒本身是3D打印的分成早、中、晚三个格子每个格子配一个光电开关检测“药有没有被拿走”。1.1 需求拆解与功能定位这个项目最开始的需求其实就两条到点提醒以及提醒之后家里人能知道老人到底吃没吃。但真做起来就会发现这两条需求背后牵扯出的细节非常多到点提醒一天三次还是四次周末和平时用药方案不一样怎么办提醒之后老人没反应要不要二次提醒吃药确认怎么知道老人真的把药拿出来了光检测到药盒被打开不够还得判断格子里的药是不是减少了。漏服通知老人没吃药怎么让远在几公里外的子女知道短信、微信、还是App推送断电保护药盒是常年插电还是用电池断电之后时钟会不会乱重新上电要不要重新校准时间所以最后的功能清单定了这样一版功能模块具体实现说明定时提醒每天最多6组服药计划每组对应一个格子早中晚三次配置周末可单独配置语音播报通过SYN6288或DFPlayer播放语音提示明确提示“该吃哪个格子的药”吃药检测每组药格底部放一对红外对射管检测药品是否被取走通过GPIO电平判断漏服上报超过设定时间30分钟未取药向服务端推送消息通过ESP8266的HTTP协议上报显示交互OLED显示当前时间、下一次服药倒计时、历史记录三个物理按键负责翻页和确认低功耗待机无操作1分钟后进入STOP模式GPIO唤醒电池供电下的续航优化这套功能组合下来基本把“家用医疗辅助设备”该有的东西都覆盖了既没有盲目堆功能也不会让老人觉得操作复杂。1.2 为什么选STM32F103C8T6而不是更便宜的51或ESP32这块可能是很多新手一上来就问的问题做一个药盒而已51单片机不香吗ESP32还能直接连WiFi为什么非用STM32我先说结论这个项目用STM32F103C8T6不是因为它性能有多强而是因为它卡在一个非常舒服的“中间位置”。相比51F103C8T6的主频72MHz、64KB Flash、20KB RAM跑个轻量级RTOS或者裸机状态机都绰绰有余。51在处理DS1307的I2C时序、OLED的SPI刷新时往往需要大量的延时凑时序代码写起来很别扭。STM32的硬件I2C和SPI外设能省掉这些事。相比ESP32药盒这项目本身对WiFi的需求很轻就是定时上报一下状态ESP8266作为协处理器完全够用。主控独立出来逻辑更清晰调试时也不用一直盯着无线连接的状态。另外STM32的生态资料实在太多了出了问题随便一搜就是答案对新手极其友好。成本考虑当时F103C8T6的性价比极高几块钱一片外围电路简单甚至直接买最小系统板也就十来块做坏了不心疼。实际做下来我的体会是选型不能只看“谁更强”得看“谁让你更快把功能跑起来”。STM32F103C8T6刚好就是这颗能让项目快速落地的芯片。1.3 系统架构与数据流设计整个系统的数据流是这样的DS1307或内部RTC产生秒脉冲和当前时间主控每秒钟读一次时间刷新OLED显示同时主控内部维护一个服药计划表计划表里记录了每个时段的提醒时间、对应格子编号、提醒状态。当系统时间到达计划时间时状态机触发提醒流程语音播报播报对应内容OLED高亮显示“请取药”药格底部的红外对射管输出电平信号取药后光路被遮挡电平变化主控判断为“已取药”记录当前时间戳如果到计划时间30分钟后依然没有检测到取药动作则通过ESP8266向服务端上报漏服信息。多说的就是吃药检测这部分的逻辑。一开始我用的方案是光电开关检测但后来发现一个问题药被拿出来再放回去或者药盒倾斜都可能导致误判。后来改成了“取走检测”就是药格底部放一对红外对射管有药片时挡住了光路拿走药片后光路恢复。加上延时去抖和连续采样判断实测误报率低了很多。2. 硬件原理图设计与模块选型解析原理图这块是整个项目里最绕不开的部分。我画的是四层板的思路虽然是两层板打样主要模块包括STM32F103C8T6最小系统、电源管理、时钟模块、显示模块、语音模块、药格检测模块、通信模块和调试接口。2.1 电源管理与功耗分配电源是整个系统的根基没做好的话后面所有模块都跟着遭殃。我的方案是这样的外部用5V/2A的USB适配器供电经过一颗AMS1117-3.3转出3.3V给主控、OLED和语音模块供电DS1307的备用电池用一颗CR1220纽扣电池保证主电源断电后时钟依然在跑。这里有个细节是很多人容易忽略的——AMS1117的压差。它的最小压差大约是1.1V也就是说输入5V时输出3.3V没问题但如果输入掉到4.5V以下输出就会开始跌落。所以我选了5V/2A适配器而不是5V/1A目的就是留足余量。另外在电源输入口加了一个SS34肖特基二极管做反接保护实测即使正负极接错也不至于烧板子。整个系统在正常运行时的电流大约在80mA到120mA之间主要功耗大头是OLED背光和语音模块。为了延长电池续航如果脱离USB供电的话我在固件里加了一个空闲1分钟自动熄灭OLED背光的功能仅保留一个呼吸LED指示心跳。2.2 STM32F103C8T6最小系统细节最小系统这部分网上教程多得是但我还是踩了一个坑复位电路。最初我用的是一颗10uF电解电容加10K电阻的标准复位电路但在低温环境下偶尔出现复位不彻底的情况表现为开机后OLED白屏。后来查到是电容太大导致复位时间过长改成100nF贴片电容加10K电阻的组合之后问题彻底消失。另一个细节是BOOT0引脚的处理。我的板子把BOOT0通过10K电阻下拉到地并且预留了一个跳线帽焊盘。这样平时正常从Flash启动需要ISP下载程序时把跳线短接重新上电就能进入系统存储器引导模式。开发调试阶段这个设计非常管用不用频繁插拔USB转串口模块。晶振方面我用的8MHz无源晶振配合两颗20pF负载电容。STM32F103的HSE电路比较标准按照数据手册推荐参数来就行不需要过多纠结。不过要注意晶振底下尽量走地线避免干扰导致起振不稳定。2.3 药格检测模块的红外对射管设计药格检测模块是在研制过程中改版最多的部分。最初方案是用微动开关但药片重量太轻经常压不下去后来换成了红外对射管才算稳定下来。红外对射管的工作方式很简单发射管持续发射红外光接收管接收有物体经过时遮断光路接收管的输出电平翻转。我在每个药格底部对角位置分别焊一颗红外发射管和接收管中间留出空间让药片通过。接收管输出经过一个LM393比较器整形后接到STM32的GPIO上。这里有两个关键参数需要注意发射管限流电阻我用的是5mm红外对射管Forward Voltage大约1.2V工作电流10mA到20mA限流电阻在3.3V供电下用100欧姆到150欧姆比较合适。电流太小会导致接收端信号弱。LM393的滞回比较为了防止光线反射和环境光引起的误触发我在比较器正反馈端加了一个100K电阻做成滞回比较实测抗干扰能力大幅提升。实际测试下来这个方案的误判率低于1%但要注意药盒外壳的材料——如果是黑色不透光材质还好如果用透明亚克力外壳环境光干扰会比较明显需要在结构上做遮光处理。2.4 OLED显示与按键电路显示模块用的是0.96寸I2C接口OLEDSSD1306驱动4个引脚VCC、GND、SCL、SDA接在STM32的PB6和PB7上也就是I2C1的SCL和SDA。这屏功耗低、显示清晰在室内环境下完全够用而且不需要背光控制代码也简单。唯一的遗憾是尺寸小一屏显示不了太多信息所以我分了三个界面主界面显示时间和下次服药倒计时按KEY1切换到服药计划列表按KEY2查看历史记录。按键我直接用三个轻触开关一端接地另一端接GPIO内部启用上拉电阻按下时读到低电平。这里要注意的是按键消抖我用的10ms软件消抖配合50ms的连续采样确认实测非常稳定。有的人喜欢用外部RC硬件消抖但在这个应用场景下软件消抖已经足够。3. 代码结构、状态机设计与关键实现代码是整套系统的灵魂。我的代码基础架构是裸机前后台架构主循环里跑任务调度定时器中断做时基状态机负责处理提醒与确认交互。工程用Keil MDK开发代码按照模块划分主要包括main.c、rtc.c、oled.c、voice.c、detect.c、wifi.c、plan.c总共大约2000多行。3.1 主循环与任务调度STM32F103没有操作系统所以我用手写的前后台调度来处理多任务。核心是维护一个tick计数器SysTick定时器每1ms中断一次tick主循环里判断各个任务的执行周期volatile uint32_t tick_ms 0; void SysTick_Handler(void) { tick_ms; } void main_loop(void) { uint32_t last_1s 0; uint32_t last_50ms 0; uint32_t last_200ms 0; while (1) { if (tick_ms - last_1s 1000) { last_1s tick_ms; task_1s(); // 读RTC、刷新时间显示、检查服药计划 } if (tick_ms - last_50ms 50) { last_50ms tick_ms; task_50ms(); // 按键扫描 } if (tick_ms - last_200ms 200) { last_200ms tick_ms; task_200ms(); // 药格检测采样 } } }这种写法比把所有的延时任务塞进一个超级循环里要清晰得多每个任务周期固定不会出现某次长时间阻塞导致其他任务受影响的情况。当然如果后面要扩展更多功能直接上一颗FreeRTOS也行但对于这个项目来说裸机完全够用。3.2 服药计划与状态机设计服药提醒的核心逻辑是一个有限状态机。每个服药计划有四个状态IDLE未到时间、PENDING时间到等待取药、CONFIRMED已取药、MISSED超时未取。状态机每次tick检查当前时间和计划时间的关系触发迁移。typedef enum { STATUS_IDLE 0, STATUS_PENDING, STATUS_CONFIRMED, STATUS_MISSED } PlanStatus; typedef struct { uint8_t hour; uint8_t min; uint8_t box_id; PlanStatus status; } MedPlan;状态迁移逻辑是这样的主循环每秒读取RTC时间遍历所有计划项如果当前时间的小时和分钟等于计划时间且状态是IDLE就置为PENDING同时触发提醒动作语音播报OLED高亮如果状态为PENDING且药格检测到取药动作置为CONFIRMED记录时间戳语音播报“确认完成”如果状态为PENDING超过30分钟依然没有取药动作置为MISSED通过ESP8266上报漏服。这里有一个细节同一秒可能命中多个计划吗不会因为计划时间都会错开设计但代码里仍然做了循环遍历所有计划项的处理将来要加新计划也很方便。3.3 吃药检测的软件去抖实现红外对射管输出的信号是GPIO电平直接读很容易受到瞬时干扰尤其是有物体快速经过的时候。我的做法是采样200ms每次采样间隔20ms取10次如果10次里超过8次都是“药被拿走了”的状态才判定为取药动作。uint8_t detect_box_taken(uint8_t box_id) { uint8_t count 0; for (uint8_t i 0; i 10; i) { if (HAL_GPIO_ReadPin(BOX_DETECT_PORT, box_id)) { count; } HAL_Delay(20); } return (count 8); }这个简单的多数投票逻辑有效滤除了绝大部分环境干扰。我测试过人为快速抖动手掌、阳光斜射、旁边经过车辆时都不会误触发。3.4 语音播报模块与播放控制语音播报用了SYN6288中文语音合成模块通过串口USART2PA2/PA3发送GBK编码的文本命令模块就能合成播放。比如发送“请取早间药格中的药物”对应的GBK字节流模块就会播报出来。SYN6288的控制命令格式比较固定我在voice.c里封装了几个函数void voice_play_text(const char* text) { uint16_t len strlen(text); // 构造SYN6288帧格式 uint8_t frame[128]; frame[0] 0xFD; frame[1] (len 3) 8; frame[2] (len 3) 0xFF; frame[3] 0x01; frame[4] 0x00; memcpy(frame[5], text, len); HAL_UART_Transmit(huart2, frame, len 5, 1000); }注意SYN6288只支持GBK编码所以字符串数组在定义时要确保是GBK否则播报出来是乱码。我在Keil里直接把源文件编码改成GB2312中文常量就都能正确编译。如果手头没有SYN6288用DFPlayer Mini播放预制MP3文件也是可以的只不过要提前把语音内容录音好灵活性略差。3.5 ESP8266通信与漏服上报通信模块用的是ESP8266-01S工作在透传模式下通过USART3PB10/PB11与主控通信。主控用AT指令集来控制模块连接WiFi和发送HTTP请求。因为只是定时上报少量数据不需要长连接所以代码里每次上报前动态建立TCP连接发送完就关闭。void wifi_upload_status(uint8_t event_code, uint32_t timestamp) { // 设置AP和密码实际项目中建议存储在Flash中 // ATCWJAPSSID,PASSWORD // ATCIPSTARTTCP,your.server.com,8080 // ATCIPSENDlength // 发送 HTTP GET /api/report?eventxxtsxxxx // ATCIPCLOSE }这里踩过一个坑ESP8266的串口波特率默认是115200但如果模块固件版本较老可能默认是9600。上电后建议先发几个“AT”测试一下确保通信正常再往下走。还有ESP8266的供电问题它启动瞬间电流能达到300mA以上如果直接从AMS1117的3.3V取电电压会被拉低导致模块反复复位。我是单独加了一颗ME6211 LDO给ESP8266供电并且并联了一个100uF电解电容缓冲这才稳下来。3.6 OLED显示界面的实现思路OLED我移植了常见的SSD1306驱动库提供了基本的画点、画线、显示字符串接口。主界面每秒钟刷新一次显示当前时间、当前日期、下一组服药倒计时。由于OLED是I2C接口刷新率不高所以不要频繁全屏刷新否则会看到明显的闪烁。我的做法是只在数据变化时更新局部区域比如时间变化时只更新时间区域的8行像素倒计时变化时只更新倒计时区域这样看起来非常流畅。主界面 2025-02-20 周六 08:32:15 下次服药09:00 早间药格待取 按KEY1进入计划列表可以查看今天所有服药计划的执行状态。按KEY2进入历史记录显示最近10次取药时间戳。这两个界面都支持长按返回。4. Proteus仿真搭建与实物调试验证仿真这部分是很多人关心的。用Proteus搭建STM32的仿真工程并不难但由于Proteus对STM32的支持不如51那么完善需要注意几个点。4.1 Proteus仿真工程搭建步骤我用的Proteus 8.11版本支持STM32F103C8T6的模型。搭建步骤如下新建工程选择“No firmware”或直接选择STM32F103C8T6芯片。在元件库中搜索并放置STM32F103C8T6、OLED屏用Proteus自带的128x64 OLED模型代替、按键、LED、电阻等元件。连接VDDA、VSSA等引脚到电源轨注意STM32F103C8T6的引脚多务必检查每个引脚的功能映射。双击芯片加载HEX文件HEX文件由Keil在编译后生成勾选Create HEX File。点击运行观察OLED显示和按键响应。有两点特别提示Proteus里的STM32模型分两种带完整外设模拟的和只支持GPIO的。OLED这类外设可能没有直接的模型对应我用的替代方案是Proteus的“Virtual Terminal”来显示串口调试信息OLED部分用逻辑分析仪/虚拟示波器观察I2C波形。如果仿真中点击运行后提示“logic contention”之类的警告多半是引脚冲突或者漏接了上拉/下拉电阻检查一遍电路再跑。4.2 仿真中遇到的典型问题在仿真阶段我遇到的最典型问题是程序在Proteus中跑不起来OLED完全没反应。排查了很久最后发现是Proteus的I2C模拟太“严格”了。SSD1306库的I2C起始信号如果时序不标准Proteus模型会直接忽略。解决办法是改用了软件模拟I2CGPIO翻转而不是硬件I2C在仿真里跑得非常稳定。这也提醒了一个事硬件I2C虽然高效但在兼容性和移植性上不如软件模拟I2C灵活尤其在不同型号的模拟器或硬件之间迁移时。另一个问题是仿真的时间尺度。Proteus的仿真速度受电脑性能影响很大如果整个系统跑得很慢RTC的秒跳变可能要好几分钟才能看到一次。解决办法是把“Animation”选项卡里的定时器速度调到最快并适当减小晶振频率比如仿真中降到1MHz这样能看到秒级变化。4.3 实物调试中出现的问题与解决仿真跑通之后真正做实物又是一轮新的磨炼。我遇到的几个问题非常有代表性首次上电OLED花屏原因是最小系统的复位时间不够导致SSD1306初始化失败。解决方式是在代码里加了一个300ms上电延时并在初始化OLED前多做几次I2C复位脉冲花屏问题彻底解决。语音播报偶尔卡顿SYN6288在播报时串口会占用如果此时ESP8266也在发数据USART2的数据会偶尔丢失。解决方案是调整任务优先级——语音播报期间关闭ESP8266接收中断播完再打开。同时将串口缓冲区开大加环形队列缓冲数据。取药检测偶尔漏报我最初把红外对射管装在药格底部但药片是圆形的滚到角落时可能不经过光路。后来在药格内部做了一个V形导流槽药片落下时必然经过对射管位置漏报问题彻底解决。这就是结构设计和硬件电路必须一起考虑的例子。ESP8266连接路由器不稳定家里的WiFi环境比较复杂2.4G信道拥堵。后来我把ESP8266的AT固件升级到最新版并且把串口波特率调到9600更稳定重连逻辑加了指数退避才算稳定下来。4.4 关键调试工具与技巧调试STM32项目我强烈建议准备好这几样东西ST-Link V2调试器、USB转TTL串口模块、逻辑分析仪便宜的二三十块那种就够了。ST-Link用来在线调试和烧录串口用来打印日志逻辑分析仪用来抓I2C、SPI、串口的时序波形。特别是调OLED和语音模块的时候逻辑分析仪几乎救命。SSD1306的初始化时序不对屏幕就是一坨白或一坨黑靠肉眼完全看不出哪里错了但把I2C波形抓出来对比数据手册很快就能定位是起始位、设备地址还是寄存器命令的问题。另一个非常实用的小技巧是在代码里预留一个调试串口所有关键时刻都往调试串口打印一行日志。比如“RTC updated”、“DETECT TAKEN”、“WIFI SEND OK”、“MISSED EVENT”。这样在实物调试时用串口助手盯着日志就能知道系统内部到底在干什么比猜盲盒高效一万倍。5. 常见问题与排查技巧实录这部分是把我在整个开发过程中积累的问题排查经验整理成速查表方便后来者直接对照。现象可能原因排查与解决OLED花屏或白屏I2C时序不标准上电复位时序不够软件模拟I2C替代硬件I2C初始化前加300ms延时多做I2C复位脉冲RTC时间不准确晶振精度不够或外部干扰用32.768KHz晶振并尽量靠近MCU引脚走线远离电源和信号线通过串口校准语音播报乱码GBK编码问题源码文件编码改为GB2312中文常量确保是GBK字节ESP8266连接不上供电不足或波特率不匹配单独LDO供电并加200uF以上电容先用AT测试模块取药检测误报环境光干扰或药片未经过光路LM393滞回比较V形导流槽设计系统频繁复位电源跌落或看门狗误触发检查电源输入、AMS1117压差延长看门狗喂狗间隔程序烧录失败BOOT0配置错误或驱动问题BOOT0拉低确认ST-Link驱动安装检查SWDIO/SWCLK接线5.1 系统时钟配置的经验STM32F103C8T6的时钟配置是个绕不开的环节。我的做法是直接用标准库RCC_Configuration外部8MHz晶振PLL倍频到72MHz。时钟配置好之后SysTick定时器、USART波特率、I2C速率都对得上如果配错了会导致所有外设行为异常。建议刚开始做时先在main函数里把时钟配好然后用GPIO翻转测试一下比如让一个LED以1Hz闪烁如果LED闪烁正常说明时钟基本没问题再继续往下写其他外设。5.2 如何调试硬件I2C和软件I2C的选择在STM32F103上硬件I2C有时会卡在BUSY状态这在社区里被吐槽过很多次。我的做法是直接放弃硬件I2C用两个GPIO口软件模拟I2C只占用两个引脚代码简单兼容性好。实测速度比硬件I2C慢一些但驱动OLED这种小屏完全够用。如果你要用硬件I2C我建议学会一个复位技巧SCL翻转9次同时SDA保持高电平可以解除I2C总线死锁状态。这在很多国产芯片上都有这个问题移植代码时最好加上这个复位逻辑。5.3 代码调试与日志系统一个良好的日志系统能省掉一半的调试时间。我建议在代码里封装一个debug_printf函数通过USART1输出到电脑串口助手。关键变量、状态迁移、外设初始化结果都可以打出来。上线前的最后一步再把日志关闭或改为条件编译不影响最终产品运行。注意串口的波特率最好固定我用的115200调试助手里设置好即可。如果数据量较大可以加一个简单的环形缓冲区把日志放入缓冲区由中断或主循环慢慢发送这样不会阻塞主流程。6. 项目扩展方向与建议这套系统做到现在这个程度已经可以满足日常家用了。但如果你想进一步扩展有几个方向值得尝试数据上云与微信小程序将服药记录定时上报到云服务器通过微信小程序或支付宝小程序查看这样子女可以随时了解老人的服药情况。服务端可以用Node-RED或Flask快速搭建简单够用。多用户支持如果家里有两位老人可以扩展为两套药盒数据在云端区分用户。语音交互升级把SYN6288换成更智能的离线语音识别模块如离线语音唤醒词方案支持老人用语音确认“我吃药了”体验会好很多。心率/血压监测集成如果后续加一个心率传感器模块药盒可以变身为一个基础健康监测站服药数据结合生理数据给子女更完整的健康视图。4G Cat.1通信不依赖家庭WiFi用4G模块直接上云适合家中没有稳定网络的情况但成本会上升。不管怎么扩展核心始终是“让老人规律用药、让家人安心”。这也是我当时做这个项目的初衷。根据我个人这几个月的调试经验最大的收获其实是嵌入式项目不是写完代码烧进去就完事的硬件、结构、交互每一个环节都会反过来逼你改软件。药格的V形导流槽、语音播报的优先级、ESP8266的供电方案这些都是从一次次“翻车”里学出来的。如果你也打算做类似的康复类辅助设备项目我特别建议你从这套系统的状态机逻辑和硬件模块划分入手先把核心流程跑通再慢慢优化细节。那套代码和原理图我都分门别类整理在仓库里了需要的朋友直接拿走有问题也可以随时交流。