1. 项目概述与方案设计先说一句去年我家里的长辈因为慢性病需要长期服药药多了就难免记混、漏吃。我在网上搜了一圈市售智能药盒功能要么过于简单要么App绑定、云服务一关就变砖几乎没有一款能把“提醒—出药—确认服药”这一整条闭环做扎实。后来我决定自己动手用STM32从零做了一套智能药盒/老人用药管理系统并且把代码、原理图、仿真工程全部开源出来。这篇文章就是这套开源项目的完整复盘从方案选型到原理图设计从核心代码到Proteus仿真再到实物调试中的各种坑一次性讲透。如果你是想做毕业设计、电子课程设计或者家里正好有需要长期用药的老人那么这个项目可以直接参考复现。整套系统基于STM32F103C8T6核心功能包括多时段用药提醒、声光提示、OLED信息显示、舵机自动出药、按键确认服药以及可选联网通知。硬件成本压到50元以内就能跑起来代码用标准库编写逻辑清晰扩展性也不错。下面我从设计思路开始逐步拆解各个关键环节。1.1 为什么做智能药盒痛点与需求很多年轻人觉得“按时吃药”是件很简单的事但放到老年人身上事情就没那么轻松了。慢性病老人一天可能要服用三四种药每种药的服用时间不同有的饭前、有的饭后一旦记忆衰退或当天事情一多漏服、错服非常常见。更棘手的是重复服药——老人想不起来自己吃过没有顺手又吃了一顿这对某些药来说是相当危险的。所以这个项目的核心需求不是“定时响铃”这么简单而是一整套用药管理闭环按时提醒你该吃药了告诉你吃哪一格你取药之后需要主动确认“我吃了”系统记录下来。只有完成确认当天这个时间点才算结束。如果超时未确认系统会再次提醒或者通过联网模块告知家属。我把这些需求拆成了几个功能模块时间管理模块负责记录当前时间和用药计划提醒模块负责声音、灯光、屏幕等多维提示出药模块用舵机带动药盘把对应药仓转到取药口交互模块用按键完成服药确认日志模块记录每一次用药情况。整机做成一个可以放在床头柜的小盒子老人不用学太多操作听声音、看灯、按键三步就够。1.2 功能规划从提醒到闭环记录先给大家一个总的功能清单方便后面对照原理图和代码看8个药仓每个药仓对应一种药品可以配置独立时间表每日最多可配置6组提醒时间每组可关联多个药仓到达服药时间OLED显示“吃药时间 药仓编号 药品名”蜂鸣器滴滴响对应LED灯闪烁舵机自动旋转药盘到指定药仓老人按压绿色按键确认取药按键确认后记录服药日志当天该组提醒不再重复30分钟内未确认蜂鸣器再次鸣响通过ESP8266发送提醒消息到手机配有独立RTC时钟断电后时间不丢失低功耗待机思路无操作时背光关闭减少耗电这中间最讲究的其实是“确认服药”这个动作。市面上一大堆药盒只会定时响铃响完就完了老人到底吃没吃系统完全不知道。我们这个系统把“服药”设计成一个完整的状态机从“待提醒”到“提醒中”再到“出药中”最后“等待确认”确认完回到“待提醒”。每一步都有明确状态表示这也为后面的日志和远程通知打下基础。1.3 技术选型STM32F103C8T6为中心的方案权衡主控芯片我选了STM32F103C8T6也就是大家常说的“C8T6”。这颗芯片虽然不是什么新料但在这种场景下非常合适72MHz主频足够跑完所有逻辑20KB RAM、64KB Flash完全装得下价格便宜资料铺天盖地市面上任何一款仿真器、开发板都能兼容。关键是它足够“普通”普通到所有坑都被前人踩过这对于开源项目来说是巨大的优势——复现成本越低项目价值越高。时钟方案我选了DS1302。DS1302是经典串行RTC芯片三线接口价格便宜带备用电池引脚断开主电源后靠CR2032纽扣电池就能保持走时。当然它也有精度一般的弱点日误差大概一两秒不过对用药提醒来说完全够用。如果追求更高精度可以换DS3231I2C接口带温补晶振年误差控制在几分钟内代码改动也不大后面会提到。显示部分用0.96寸OLEDSSD1306驱动I2C接口四根线就搞定。OLED的好处是自发光、对比度高老人看起来清楚在Proteus里也比较好模拟。提醒用有源蜂鸣器高电平驱动即可。出药用SG90舵机PWM控制便宜且够用。可选联网模块是ESP8266-01S串口透传用来把未确认的提醒推送给家属手机。到这里硬件方案基本定型。2. 硬件原理图拆解与设计细节原理图是整个项目的地基。很多新手喜欢一上来就写代码等要接硬件了才发现引脚冲突、供电不足、驱动能力不够回头再改原理图非常痛苦。我这份原理图用立创EDA画的下载开源仓库里的工程文件就能直接看下面把几个关键电路拆开讲。2.1 最小系统电路晶振、复位、电源、下载口STM32F103C8T6要跑起来最少需要四块电路电源、晶振、复位、下载接口。先说电源。系统统一用5V USB供电板上用AMS1117-3.3稳压到3.3V给MCU、OLED、RTC这些3.3V器件供电。AMS1117前面加一个10uF电解电容滤波输出端加一个10uF和0.1uF去耦电容这是基本盘不能省。舵机建议单独从5V取电因为SG90堵转电流可能到几百毫安如果和MCU共用一路容易把3.3V电压拉垮。晶振电路很多人画错。STM32F103C8T6的HSE主晶振用8MHz两个负载电容取值在18pF到22pF之间都可以我用的20pF。注意负载电容不是越大越好也不是越小越好它的计算公式是等效负载电容CL (C1 * C2) / (C1 C2) 寄生电容Cstray。PCB上走线、焊盘会引入大约3~6pF的寄生电容所以如果用20pF电容实际等效负载大约是10 5 15pF跟8MHz晶振标称的负载电容比较匹配。换句话讲如果你非要用30pF也能起振但频率偏差会变大RTC这种对时间敏感的场景就不太行了。复位电路就是10k电阻上拉NRST到3.3V再并一个0.1uF电容到地。这个RC电路能让复位引脚在上电瞬间保持低电平约1ms确保MCU稳定启动。下载口我留了一个4针SWDSWDIO、SWCLK、GND、3.3V。比起20针JTAGSWD只用四根线体积小刷写速度快日常调试完全够用。2.2 外设接口电路OLED、RTC、蜂鸣器、按键、舵机OLED是I2C接口SCL接PB6SDA接PB7这是STM32F103的I2C1引脚。I2C总线上必须接上拉电阻我放了4.7k。很多新手直接拿杜邦线连OLED不加上拉也能显示那是因为OLED模块板载了上拉自己画PCB时一定要注意这个细节否则总线时序不稳定屏幕会花。DS1302的三个信号线分别是SCLK、I/O、CE我接到PA0、PA1、PA2。它的数据线需要外部上拉吗看手册I/O脚内部带弱上拉但手册也建议外部加一个10k上拉到3.3V实际测试加了更稳。DS1302的32.768kHz晶振两个负载电容选6pF到12.5pF之间我用的是6pF。这个地方很多人容易抄错把20pF主晶振电容直接套过来导致RTC停振或者走时偏快后面会详细说。蜂鸣器驱动不能直接接GPIO。STM32的GPIO最大输出电流只有20mA左右工作电压3.3V带动3.3V有源蜂鸣器勉强可以但声音小而且一旦并联其他负载容易复位。我用了S8050三极管做开关驱动MCU引脚通过1k电阻接到三极管基极蜂鸣器接在5V和集电极之间发射极接地再反并一个1N4148二极管做续流保护。这样GPIO只输出毫安级控制信号蜂鸣器从5V取电声音洪亮稳定。按键电路最基础也最容易忽略。每个按键一端接GPIO另一端接地GPIO内部配置为输入上拉模式。这样按下时引脚被拉低松开时回到高电平。软件上必须做消抖硬件上可以加一个100nF电容并联到按键两端进一步滤除抖动。药盒上的按键是老人每天要按的手感很重要我选的是12x12轻触开关按键帽大比较适合老年人。舵机接口信号线接PB0也就是TIM2_CH1PWM输出。SG90的电源线接5V地线要跟MCU共地。舵机是个很容易引入干扰的部件负载变化时电源纹波很大所以PCB上舵机电源走线和信号线要分开走最好在舵机电源输入处加一个100uF电解电容效果立竿见影。2.3 联网扩展ESP8266与远程通知ESP8266-01S通过串口跟STM32通信我用的USART1PA9做TXPA10做RX。ESP8266模块是3.3V供电注意它的峰值电流可以达到300mA以上AMS1117-3.3勉强带得动但最好单独用一个3.3V稳压模块否则WiFi发射瞬间会把电压拖低导致STM32复位。通信协议很简单STM32通过串口发送AT指令直接操作ESP8266的透传模式。这里我没有搞复杂的MQTT或TCP协议栈而是用了一个更轻的方案——通过AT指令把数据POST到一个低代码平台的Webhook接口再由平台推送到微信。这样MCU端代码量很小也不用自己维护服务器个人项目完全够用。当然如果只是想先把裸机功能跑通ESP8266可以先不焊电路上预留4个引脚的位置即可。仿真阶段本来也模拟不了真实WiFi不影响核心功能验证。2.4 原理图设计中的几个避坑点原理图这块我踩过的坑不少集中说几个关键的。第一电源地要分清楚。模拟地、数字地、功率地在单面或双层板上一开始不用分得太细但舵机和蜂鸣器这类大电流器件的地不要直接穿过MCU晶振下方分开走线最后在电源输入端汇合。第二所有IC的电源引脚旁边都要放0.1uF去耦电容而且要尽量靠近引脚这是保证系统稳定的基本操作。第三预留测试点。SWD接口旁边我放了一排GND、3.3V、PB0、PB6等测试点调试时示波器、逻辑分析仪夹起来方便这个习惯能帮你省下大量排查时间。最后DS1302备用电池电路VCC2接主电源3.3VVCC1接CR2032电池正极两个电源之间用一个1N4148二极管隔离防止电池给主板反向供电。电池插座选带开关的发货运输时电池不会一直耗电。3. 软件代码实现与核心逻辑硬件只占这个项目一半的分量另一半是软件。我的代码全部基于标准外设库编写工程在Keil MDK下编译目标芯片是STM32F103C8T6。之所以用标准库而不是HAL库是因为标准库代码直观寄存器操作一目了然做逻辑复杂的项目更容易掌控每一个细节。3.1 工程结构与代码分层整个工程我做了清晰的分层避免所有功能塞进main.c变成“屎山”。目录结构如下User/ main.c // 主函数、状态机调度 med_config.c/h // 用药计划配置与存储 ui_display.c/h // OLED显示逻辑 Hardware/ bsp_oled.c/h // OLED底层驱动(SSD1306) bsp_ds1302.c/h // RTC芯片驱动 bsp_key.c/h // 按键扫描与消抖 bsp_buzzer.c/h // 蜂鸣器控制 bsp_servo.c/h // 舵机PWM控制 bsp_esp8266.c/h // ESP8266通信main.c里的主循环是一个超级循环加上时间片调度用SysTick产生1ms系统时基再在此基础上构建100ms、1s这样的逻辑节拍。RTC时间读取放在1s节拍里按键扫描放在10ms节拍里状态机调度放在主循环里。这样逻辑清晰也方便后续移植到RTOS。3.2 用药数据结构与时间管理用药计划用结构体数组管理。我定义了一个MedReminder结构体字段很简单typedef struct { uint8_t slot; // 药仓编号0~7 uint8_t enabled; // 该条配置是否启用 uint8_t hour; // 服药时间小时 uint8_t minute; // 服药时间分钟 uint8_t taken; // 当日是否已确认服药 char medicine_name[16]; // 药品名称用于显示 } MedReminder;系统里最多维护24条记录8个药仓每天最多3次这些配置写死在程序里也可以通过串口命令修改。每个新的一天开始时系统时间跨过0点遍历所有配置把taken标志清零。这个“跨天清零”的逻辑不要放在RTC中断里而是每秒读取时间后做一次日期比较日期变了就执行清零简单可靠。RTC驱动方面DS1302的难点在于时序。它每一位数据的读写都是由SCLK上升沿/下降沿控制的代码里需要做微秒级延时。我用的是GPIO模拟时序波特率不敏感代码通用性好缺点是稍微占用CPU。驱动里提供了读取时间的接口void DS1302_GetTime(RTC_TimeTypeDef *time);拿到的time结构体里有year、month、date、hour、minute、second。主循环每秒调用一次存入全局变量g_rtc_time其他模块直接读取。3.3 状态机设计提醒→出药→确认服药这是整个软件里最重要的一环。我定义了一个枚举类型表示药盒当前状态typedef enum { STATE_IDLE, // 空闲等待提醒时间 STATE_REMINDING, // 正在提醒声光提示中 STATE_DISPENSING, // 舵机出药中 STATE_WAIT_CONFIRM, // 等待老人按键确认 STATE_SNOOZE // 超时未确认进入二次提醒 } SysState;默认状态下处于STATE_IDLE每秒读取RTC时间遍历用药计划表。如果发现某条计划的小时、分钟与当前时间一致并且taken标志为0就进入STATE_REMINDING。在STATE_REMINDING状态下OLED会显示提醒信息蜂鸣器发出“滴—滴—滴”的规律响声LED闪烁。持续10秒后自动进入STATE_DISPENSING舵机转到对应药仓角度等待出药完成。这里我加了一个舵机到位检测不是简单的延时而是通过PWM输出角度后等待2秒让舵机稳定再读取药盘的霍尔传感器可选确认到位。没有传感器的话就直接用延时低成本方案下能接受。出药完成后进入STATE_WAIT_CONFIRM。屏幕上显示“请按确认键”绿色按键开始轮询。老人按下后系统将该条记录的taken置1蜂鸣器短鸣一声表示确认成功回到STATE_IDLE。如果进入STATE_WAIT_CONFIRM后30分钟内没有收到按键系统进入STATE_SNOOZE蜂鸣器再次间歇鸣响同时通过ESP8266发送一条“某某老人未按时服药”的消息到手机。这个30分钟就是给老人留的缓冲时间也方便家属及时介入。核心判断代码大致长这样void MedTask_Process(void) { switch (g_sys_state) { case STATE_IDLE: CheckReminderTime(); // 遍历计划表判断是否需要提醒 break; case STATE_REMINDING: RemindUi_Update(); // 声光提醒 break; case STATE_DISPENSING: Servo_DispenseMed(); // 控制舵机转动到位 break; case STATE_WAIT_CONFIRM: ConfirmKey_Scan(); // 扫描确认按键 break; case STATE_SNOOZE: Snooze_Alert(); // 二次提醒 远程消息 break; } }状态机的好处是每个状态职责单一不会出现“提醒到一半突然被另一个任务打断”的混乱情况而且出问题很好定位。3.4 OLED显示与多级提醒策略OLED显示分为待机界面和提醒界面。待机界面显示当前时间、日期和今日服药完成情况比如“已服 2/8”。这个界面每秒刷新一次数字显示大字号字体方便老人远距离看清。提醒界面是重中之重。我用三行显示第一行“吃药时间”第二行“仓号 药名”第三行“按确认键”。文字尽量大字号不够就用两屏切换显示。OLED驱动用的SSD1306常见库改了一下中文取模方式把“吃药时间”这类关键词做成字库表。提醒声音也别从头到尾一个频率。我用SysTick做了三档提醒策略第一档是急促短鸣8kHz方波驱动蜂鸣器0.1秒、停0.2秒第二档是低频长鸣适合夜间提醒不刺耳第三档是语音播报如果有语音模块直接播“该吃降血压药了”。每一档对应不同状态比如初次提醒用第一档二次提醒用第二档。实际使用下来急促短鸣的存在感最强低频长鸣更适合夜间。4. Proteus仿真与实物调试实录做这类项目仿真和实物缺一不可。仿真用来验证逻辑特别是状态机和RTC时间判断这些纯逻辑部分实物用来验证硬件交互比如蜂鸣器声音、舵机力度、按键手感。我习惯先仿真后实物这能省下大量烧芯片、排查接线的时间。4.1 仿真环境搭建与器件选择Proteus仿真我用的8.11版本自带的STM32F103C8T6模型支持大部分外设。新建工程后在元件库里搜索STM32F103C8放到画布上。元件库里也有DS1302、LM016LLCD1602、BUTTON、BUZZER、POT等常用元件。OLED的Proteus模型不太好找所以仿真时我用LCD1602替代OLED显示逻辑层的串口一样可以打印调试信息。等仿真通过后再把显示驱动换成OLED的函数即可上层逻辑几乎不用改。关键一步是给STM32配置时钟。双击MCU在Program File里加载编译出来的hex文件同时要设置晶振频率为8MHz并把Crystal Frequency选项设为72MHz不对这里要注意Proteus里的Crystal Frequency指的是MCU外部晶振输入仿真时直接填8MHz然后代码里通过PLL倍频到72MHz。如果你不想在仿真里处理PLL配置也可以在Proteus里把Crystal Frequency直接设为72MHz同时代码里把SystemInit里的PLL设置跳过让HCLK直接等于外部时钟这样仿真能跑但会和实物逻辑不一致我不推荐。仿真的调试反馈主要靠两类手段一是虚拟终端把USART1的输出接到VIRTUAL TERMINAL上这样printf的信息一目了然二是逻辑分析仪用来观察PWM波形的占空比和频率验证舵机控制是否正常。实测下来虚拟终端看RTC时间跳动、状态切换、按键事件最方便。4.2 仿真踩坑记录时钟、I2C模型与断点仿真阶段我遇到最典型的坑是程序下载进去之后“没反应”。排查下来发现是时钟配置时的启动文件选错了——我用的是startup_stm32f10x_hd.s但C8T6属于中等容量md应该用startup_stm32f10x_md.s。这个不匹配在实物上不一定立刻出问题但仿真环境会强制暴露非常恶心。后来我在Keil的Manage Project Items里反复确认器件型号是STM32F103C8启动文件和FLASH大小都对应上了问题解决。第二个坑是按键抖动在仿真中被过度放大。Proteus里的按键模型是理想开关点击一次产生的抖动波形和实物差异很大有时主循环里明明做了10ms消抖仿真里还是会误触两三次。解决办法是在仿真里人为把消抖时间提高到50ms等实物调试再降回10ms。我一开始没意识到这点花了很久查按键逻辑最后发现是仿真模型的问题。第三个坑是DS1302在仿真里读回的时间不刷新。原因是DS1302模型默认需要较长的时序延时我的驱动里延时函数在仿真环境下执行时间被放大导致读到的是寄存器残留值。后来我在驱动里对读操作加了while循环等待确保每个字节都完整读取问题才消失。4.3 从仿真到实物差异与调试经验仿真通过后打样焊接实物调试又是另一番体验。第一印象就是舵机的实际表现和仿真差异巨大。仿真里给个PWM占空比舵机角度立刻到位实物SG90在不同负载下的角度偏移能差10度以上而且转动过程中有抖动。我用的药盘是3D打印的8格转盘如果舵机角度不准药仓对不齐取药口现场会很难看。解决办法是引入舵机角度校准。开机时做一次自检把舵机转到0度和180度两个极限位置记录对应PWM脉宽然后据此计算中间角度的脉宽值。后来实测发现一只舵机的0度对应0.5ms脉宽180度对应2.5ms这是标准值但不同批次舵机会有微小差异校准逻辑能把这个差异抹平。实物调试中另一个重点是电源稳定性。第一次上电测试只要一驱动舵机OLED就闪一下像是瞬间断电。用万用表一量3.3V在舵机启动瞬间被拉到了2.9V以下。这就是前面提到的供电隔离问题。之后我把舵机电源独立出来从USB 5V直接走一条粗线给舵机MCU和OLED单独由AMS1117供电再在舵机电源端并联470uF电解电容这个问题彻底解决。仿真和实物的最大区别在于仿真验证的是“逻辑对不对”实物验证的是“逻辑在真实物理世界里还对不对”。电平跳变时序、供电纹波、元件离散性这些在仿真里统统看不到但恰恰是实物调试的主角。所以我的建议是仿真阶段大胆跑逻辑实物阶段老老实实测电源、测波形、测时序。5. 常见问题与排查技巧速查把项目放到开源社区后陆续有网友复现反馈了许多问题。我整理了一份高频问题对照表覆盖仿真、实物、代码三个层面方便后来者少走弯路。5.1 高频问题与解决方案对照表问题现象可能原因解决办法Keil下载报“Error: No STM32 Target Found”仿真器没识别到芯片、SWD接线错误、芯片被读保护检查SWD四线接线按住复位键再尝试连接用ST-Link Utility执行整片擦除以解除读保护晶振电容值不对导致RTC走时偏差大32.768kHz晶振负载电容选成了20pF换成6~12.5pF负载电容实测6pF走时最稳OLED显示花屏或不显示I2C地址不对、未加上拉电阻、SCL/SDA接反常见地址是0x3C检查模块上拉的A0引脚焊盘确认SDA接PB7、SCL接PB6OLED显示正常但仿真空屏Proteus中LCD/OLED共用一个显示引脚冲突给OLED单独分配I2C引脚仿真中用LCD1602替代蜂鸣器响声太小GPIO直接驱动蜂鸣器电流不足改用三极管开关驱动从5V取电舵机角度不准、抖动舵机电源电压塌陷、PWM占空比漂移独立舵机电源并加大电容开机做角度自校准按键偶尔无效或误触消抖时间过短、按键引脚悬空软件消抖10ms以上GPIO使能内部上拉按键并100nF电容按键在Proteus仿真中频繁误触仿真按键模型与真实抖动特征不同仿真中把消抖时间临时调到50msESP8266收不到AT返回模块供电不足、串口波特率不对模块单独用3.3V电源波特率固定115200或9600并检查串口配对这些问题的排查思路其实可以归纳成一句话先查电源再查时序然后查配置。电源不稳后面所有问题都会被放大时序不对外设就像神志不清似的不理你配置不对逻辑再对也跑不出预期结果。5.2 几个能救命的小习惯第一每次改完代码必须编译到零警告再烧录。STM32的一些警告比如“implicit declaration of function”看着无害实际可能让你调试几小时。第二串口打印要舍得用。我在每个状态切换、按键事件、提醒触发点都加了一条printf实物调试时接个TTL转USB在电脑上看日志能瞬间定位问题。第三多读芯片手册。DS1302时序、ESP8266的AT指令集、SG90的PWM脉宽这些资料网上都能找到但最权威的永远是官方数据手册。很多“玄学”问题翻完手册就豁然开朗了。最后再分享一个小技巧如果你的药盒后续想让老人用起来更顺手建议在出药口加一个光电传感器药盘到位后检测是否有药丸落下再配合确认按键形成更完整的逻辑闭环。我目前这套开源方案已经把主流程跑通了这个传感器升级算是下一步的规划。整个项目从设计到开源最让我感慨的一点是嵌入式开发的价值不在于用多高端的技术而在于把一个朴素的需求做得可靠、稳定、可维护。这套药盒的技术难度并不算高但它的使用场景决定了它必须足够可靠而“可靠”恰恰是嵌入式最迷人的地方。
