☰
STM32智能药盒开源项目:提醒-出药-确认服药闭环设计
2026/10/29 23:33:03 网站建设 项目流程

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纽扣电池就能保持走时。当然它也有精度一般的弱点,日误差大概一两秒,不过对用药提醒来说完全够用。如果追求更高精度可以换DS3231,I2C接口,带温补晶振,年误差控制在几分钟内,代码改动也不大,后面会提到。

显示部分用0.96寸OLED,SSD1306驱动,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针SWD:SWDIO、SWCLK、GND、3.3V。比起20针JTAG,SWD只用四根线,体积小,刷写速度快,日常调试完全够用。

2.2 外设接口电路:OLED、RTC、蜂鸣器、按键、舵机

OLED是I2C接口,SCL接PB6,SDA接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_CH1,PWM输出。SG90的电源线接5V,地线要跟MCU共地。舵机是个很容易引入干扰的部件,负载变化时电源纹波很大,所以PCB上舵机电源走线和信号线要分开走,最好在舵机电源输入处加一个100uF电解电容,效果立竿见影。

2.3 联网扩展:ESP8266与远程通知

ESP8266-01S通过串口跟STM32通信,我用的USART1:PA9做TX,PA10做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.3V,VCC1接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、LM016L(LCD1602)、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接PB6
OLED显示正常但仿真空屏Proteus中LCD/OLED共用一个显示引脚冲突给OLED单独分配I2C引脚,仿真中用LCD1602替代
蜂鸣器响声太小GPIO直接驱动蜂鸣器电流不足改用三极管开关驱动,从5V取电
舵机角度不准、抖动舵机电源电压塌陷、PWM占空比漂移独立舵机电源并加大电容;开机做角度自校准
按键偶尔无效或误触消抖时间过短、按键引脚悬空软件消抖10ms以上,GPIO使能内部上拉,按键并100nF电容
按键在Proteus仿真中频繁误触仿真按键模型与真实抖动特征不同仿真中把消抖时间临时调到50ms
ESP8266收不到AT返回模块供电不足、串口波特率不对模块单独用3.3V电源,波特率固定115200或9600并检查串口配对

这些问题的排查思路其实可以归纳成一句话:先查电源,再查时序,然后查配置。电源不稳,后面所有问题都会被放大;时序不对,外设就像神志不清似的不理你;配置不对,逻辑再对也跑不出预期结果。

5.2 几个能救命的小习惯

第一,每次改完代码必须编译到零警告再烧录。STM32的一些警告比如“implicit declaration of function”看着无害,实际可能让你调试几小时。第二,串口打印要舍得用。我在每个状态切换、按键事件、提醒触发点都加了一条printf,实物调试时接个TTL转USB在电脑上看日志,能瞬间定位问题。第三,多读芯片手册。DS1302时序、ESP8266的AT指令集、SG90的PWM脉宽这些资料网上都能找到,但最权威的永远是官方数据手册。很多“玄学”问题,翻完手册就豁然开朗了。

最后再分享一个小技巧:如果你的药盒后续想让老人用起来更顺手,建议在出药口加一个光电传感器,药盘到位后检测是否有药丸落下,再配合确认按键,形成更完整的逻辑闭环。我目前这套开源方案已经把主流程跑通了,这个传感器升级算是下一步的规划。整个项目从设计到开源,最让我感慨的一点是:嵌入式开发的价值不在于用多高端的技术,而在于把一个朴素的需求做得可靠、稳定、可维护。这套药盒的技术难度并不算高,但它的使用场景决定了它必须足够可靠,而“可靠”恰恰是嵌入式最迷人的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询