1. 项目起因和需求拆解老人用药场景与智能药盒要解决的六个问题1.1 一个真实高频场景慢性病用药为什么容易“漏”外婆的降压药和降糖药分三四种剂量我妈白天上班老人家一忙就漏吃等发现时往往已经过了两个小时。这个基于 STM32 的智能药盒项目就是从这个场景里长出来的——整套系统包含完整工程代码、可出图的原理图和 Proteus 仿真源文件定位是开源的老龄用药管理方案。它做的事可以概括为一句话到了设定时间就响铃、亮灯、转动药盘老人完成服药后按一个键确认。我调研了一圈市面上的药盒发现传统药盒普遍只解决“分装”问题不解决“按时提醒”问题。普通定时闹钟虽然能响但不会提示这顿该吃哪一格不少智能药盒价格又不低操作界面复杂老人根本不愿意用。真正的痛点在于慢性病用药往往不是每天一次而是分散在早、中、晚、睡前多个时间点每种药的剂量还要区分。老人记不住时间表子女不在身边又无法监督漏服和重复服用的情况相当常见。1.2 六条核心需求清单做硬件项目最忌讳一上来就堆功能我先把需求压缩成六条硬性指标到点提醒时间到后蜂鸣器响、LED 闪烁声音要足够大卧室和客厅都能听到。仓位提示每顿药对应转盘上的一个独立仓位系统需要给出明确的当前仓位提示不能用“第三格”这种模糊表达。简单确认老人按一个键就能完成服药确认最多允许长按切换设置没有二级菜单。断电记忆设备被拔掉电源再插上时间不能丢。这里需要独立实时时钟芯片加备份电池。低成本可复现整套物料成本控制在 60 元以内方便学生和爱好者自己复刻。可仿真验证工程必须能在 Proteus 里跑通避免没有硬件的人无法学习。这六条看似简单实际做下来会发现每条都牵动软硬件联调。比如“断电记忆”就必须放弃用单片机内部 RTC 的方案改用外部 DS3231“到点提醒不能漏”就需要明确的调度状态机否则同一分钟会反复触发。1.3 项目定位学习向与实用向兼顾我把这个项目定位成“拿来就能学的完整小系统”而不是纯玩具。对刚接触 STM32 的开发者可以从最小系统设计、外设驱动到状态机编程整体走一遍对正在做课程设计或者电子设计竞赛的人这套代码和原理图可以作为基础框架二次开发对家里真有老人需要照顾的读者跟着文章把实物搭起来它也确实能解决一部分现实问题。开发环境我选的是 Keil MDK 加 STM32CubeMX 生成 HAL 库工程这是目前资料最多、对新手最友好的组合。仿真部分用 Proteus 8 以上版本器件模型和真实行为比较接近尤其是 I2C 时序和定时器 PWM 波形都能看到这对理解代码运行逻辑帮助很大。2. 硬件选型与原理图设计F103C8T6 如何驱动整个系统2.1 核心器件选型的理由主控没有悬念用了 STM32F103C8T6。这是 Cortex-M3 内核主频 72MHzFlash 64KBRAM 20KB虽然看起来参数朴实但在这个项目里绰绰有余。更关键的是它在 Proteus 里有成熟仿真模型网上资料和例程也最丰富基本不会踩“没资料可查”的坑。实时时钟芯片选了 DS3231而不是内部 RTC 或者 DS1302。原因很直接内部 RTC 依赖 VBAT 供电断电后时间维持能力其实不差但校准很麻烦DS1302 需要额外三线操作精度也一般DS3231 带温补晶振走时精度高I2C 接口只要两根线模块自带电池座和充电管理断电时间不丢省去很多麻烦。显示部分用 0.96 寸 OLEDSSD1306 驱动I2C 接口。OLED 对比度高、可视角度大老人从侧面看也能看清而且 128x64 分辨率可以同时显示时间、日期、下一顿提醒比 LCD1602 信息量更足。舵机选了 SG90扭矩足够带动轻质药盘价格也便宜。蜂鸣器用有源蜂鸣器程序里只要给高低电平就能响不占用定时器资源。2.2 最小系统电路与电源设计说明画原理图时先把 F103C8T6 的最小系统搭好这部分最容易抄错。8MHz 外部晶振接 OSC_IN/OSC_OUT即 PD0/PD1 引脚两个引脚分别对地接 20pF 负载电容。BOOT0 通过 10k 电阻下拉到 GND确保从 Flash 启动。PB2 的 BOOT1 同样下拉虽然常规运行不影响但两个启动引脚状态明确可以避免调试时的玄学问题。电源拓扑是这样的外部 5V 输入经过 AMS1117-3.3 稳压成 3.3V给 MCU、OLED、DS3231 供电。舵机不经过 AMS1117直接接 5V。这里有一个新手常犯的错误觉得 3.3V 就能驱动 SG90实际上舵机堵转电流能到 500mA 以上AMS1117 根本扛不住而且供电不足会导致舵机抖动、MCU 复位。舵机在 Gy_From 前记得和 STM32 共地。F103C8T6 的电源引脚有多组 VDD/VSS每一组 VDD 旁边都放一个 104 去耦电容Bulk 电容用 10uF 钽电容这部分是稳定性的基础不能省。SWD 调试接口留出 PA13/PA14加上 GND 和 3.3V一共四个引脚方便 ST-Link 烧录调试。2.3 外设引脚分配表这是我最终定稿的引脚分配全部避开功能冲突外设引脚说明I2C1 SCLPB6OLED 与 DS3231 共用I2C1 SDAPB7OLED 与 DS3231 共用舵机 PWMPA1TIM2_CH250Hz 信号蜂鸣器PA4GPIO 推挽输出KEY1 确认PB3内部上拉输入按下为低KEY2 切换显示PB4内部上拉输入KEY3 进入设置PB5内部上拉输入LED1 提醒灯PB12提醒时闪烁LED2 状态灯PB13上电常亮OLED 和 DS3231 挂在同一路 I2C 总线上两者的芯片地址一个 0x3C一个 0x68不冲突。总线上需要 4.7k 上拉电阻到 3.3V如果用现成模块部分模块已经自带确认一下别重复加。TIM2_CH2 在 PA1 上这个引脚没有和其他外设冲突用起来很稳。2.4 机械结构舵机带动转盘出药的方案对比机械部分我比较了三种方案简单说下取舍第一种是抽屉式舵机拉绳子把药抽屉拖出来结构直观但滑轨摩擦大SG90 扭矩不够容易卡死。第二种是指示灯加手动取药不做机械动作药盒上每个仓位装 LED提醒时对应 LED 闪烁老人自己打开对应格子。这个方案成本低、可靠性高但自动化和“智能感”弱一点。第三种就是我采用的转盘式圆盘分成四个扇形仓位中心轴连接舵机舵机转到对应角度后该仓位对准出药口药丸自动滑落到接药槽。SG90 扭矩对轻质塑料或纸板做的转盘完全够用转角度更数也简单只需要把 0°、90°、180° 映射到仓位索引。最终机械件我用 3D 打印做了一个直径约 12cm 的转盘没有打印机的读者可以用硬纸板加热熔胶做原型效果一样只要保证中心轴固定牢固就行。3. 软件框架与状态机设计让逻辑不乱的关键3.1 开发环境与 CubeMX 配置要点软件工程用 STM32CubeMX 生成基础框架版本差距不大F1 系列固件包装好后按以下配置勾选即可。RCC 时钟选择 Crystal/Ceramic Resonator启用 HSESYS 调试口选择 Serial Wire对应 SWD 引脚I2C1 开启速率设为 100kHz Standard ModeTIM2 选择 Channel2 PWM Generation分频和周期在代码里给到PSC71ARR19999这样 PWM 频率正好 50Hz和舵机匹配。GPIO 方面PA4、PB12、PB13 设为推挽输出PB3、PB4、PB5 设为输入模式并开启内部上拉。时钟树要把 HCLK 配置到 72MHzCubeMX 会自动设置 PLL 倍频系数一般默认是 PLL 9 倍频到 72MHz。这个步骤容易忽略的是 APB1 总线分频它会影响定时器时钟频率我习惯在初始化后打印 SystemCoreClock 确认避免后面算 PWM 周期时差了倍数。3.2 分层结构与模块划分工程按标准目录组织分三层驱动层、应用层、调度层。驱动层负责具体的寄存器操作和 HAL 库封装包括 bsp_i2c.c、bsp_tim.c、ds3231.c、ssd1306.c、servo.c。应用层实现业务逻辑包括 schedule.c 提醒调度、key.c 按键扫描、beep.c 蜂鸣器控制、led.c 状态指示。调度层就是 main 函数里的主循环和 SysTick 中断负责周期性调用各模块的轮询函数。这个分层的意义在于后面要替换显示模块或者增加一个温度传感器只需要改驱动层对应文件应用层的状态机完全不用动。新手一开始可能不习惯但做类似项目时一定要坚持这种结构否则后面代码乱到不敢改。3.3 状态机IDLE、REMIND、UNLOAD、SET#include stdio.h系统的核心是一个四状态状态机IDLE 空闲态系统正常运行随时检查提醒表。REMIND 提醒态时间到达触发条件蜂鸣器响、LED 闪烁等待老人按键。UNLOAD 出药态老人确认后舵机转到目标仓位保持 1 秒后归位。SET 设置态长按 KEY3 进入用于调整时间和日期。IDLE 到 REMIND 的触发条件不是“当前时间等于提醒时间”这一条还要加一个“分钟变化”的信号。主循环每一秒读一次 DS3231如果当前分钟数和上一次不同才去扫描提醒表。这个机制防止同一分钟里多次触发。REMIND 状态下如果 5 分钟没有按键确认蜂鸣器停止但 LED 继续闪避免半夜一直响吵到邻居。老人按 KEY1 后进入 UNLOAD舵机转到仓位1 秒后自动回 IDLE。SET 状态通过长按 KEY3 进入短按 KEY1 增加值短按 KEY2 切换设置项再次长按 KEY3 保存退出超时 30 秒也会自动保存退出。在 Proteus 里观察状态迁移很直观第一次跑通状态机时我明显感觉到“逻辑复杂度靠状态机收敛”这句话的分量。没有状态机的裸循环也能实现提示功能但状态一旦多起来边界条件覆盖不全会反复出现灵异 bug。3.4 提醒表结构与防重复触发机制提醒表用结构体数组定义方便从代码里直接维护typedef struct { uint8_t hour; // 小时 uint8_t min; // 分钟 uint8_t slot; // 仓位索引 0-3 } RemindItem; const RemindItem remind_table[] { {7, 0, 0}, // 早上降压药 {12, 0, 1}, // 中午降糖药 {18, 30, 2}, // 晚上抗生素 {21, 0, 3} // 睡前他汀 }; #define REMIND_NUM (sizeof(remind_table) / sizeof(RemindItem))这里的“slot”对应转盘的四格仓位舵机转动角度由 slot 乘以一个系数得到。防重复触发通过标志位实现每一条提醒项在触发后把对应标志置 1等到系统时间跨过当天这条提醒的“完成点”或者到达次日零点标志才清 0。这个设计比直接比较时间戳更保险。我最初的做法是每分钟比较一遍时间结果一分钟内蜂鸣器响三轮老人被烦到直接把电源拔了。改成标志位后同一分钟只触发一次除非老人主动按键确认否则系统不会二次打扰。4. 核心代码模块精讲驱动、显示、出药、按键4.1 DS3231 读取与 BCD 转换DS3231 的时间寄存器以 BCD 格式存储读出来不能直接用。HAL 库的 I2C 读函数封装得很简单uint8_t bcd2dec(uint8_t val) { return (val 4) * 10 (val 0x0F); } uint8_t dec2bcd(uint8_t val) { return ((val / 10) 4) | (val % 10); } uint8_t ds3231_read_time(DS3231_Time_t *tm) { uint8_t buf[7]; if (HAL_I2C_Mem_Read(hi2c1, 0x68 1, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 7, 100) ! HAL_OK) { return 1; } tm-sec bcd2dec(buf[0]); tm-min bcd2dec(buf[1]); tm-hour bcd2dec(buf[2]); tm-date bcd2dec(buf[4]); tm-month bcd2dec(buf[5]); tm-year bcd2dec(buf[6]); return 0; }读写 DS3231 时有个细节I2C 地址要左移一位0x68 变成 0xD0 写地址和 0xD1 读地址HAL 库期望的是 7 位地址左移后的结果写错地址不会报错但数据全是 FF。第一次调的时候我被这个问题卡了半小时所以这里特别提醒一下。4.2 OLED 显示与中文字库处理SSD1306 的初始化序列网上很多我直接用了开源的 ssd1306.c 并做了一层封装。显示内容核心是三行第一行大字显示当前时间第二行显示日期第三行显示“下一顿 HH:MM 仓位 X”。中文字库是整个显示部分最占资源的。SSD1306 是图形显示ASCII 可以直接用取模数组但完整汉字库太大放不下 64KB Flash。我的处理方案是用 PCtoLCD2002 软件把项目里需要的几个汉字早、中、晚、睡、药、服逐个取模硬编码成 16x16 点阵调用时按索引绘制。这个办法适合固定文案的场景如果要做任意文本显示就得外挂 SPI Flash 字库了。OLED 通过 I2C 刷新 128x64 全屏大约需要几十毫秒如果每帧都全屏刷新会明显干扰主循环时序。我的做法是只在时间变化时刷新局部分区分钟刷新一次时间区域状态切换时刷新提醒区域这样 I2C 总线的占用率很低。4.3 SG90 舵机 PWM 生成与角度映射舵机控制本质是 20ms 周期中高电平脉冲宽度决定角度。SG90 在 0.5ms 对应 0 度1.5ms 对应 90 度2.5ms 对应 180 度。TIM2 配置为 PSC71、ARR19999 后计数器时钟 1MHz一个计数单位是 1us所以直接把脉冲宽度映射到 CCR 值#define PWM_PERIOD 20000 // 20ms #define SERVO_0 500 // 0.5ms - 0度 #define SERVO_90 1500 // 1.5ms - 90度 #define SERVO_180 2500 // 2.5ms - 180度 void servo_set_angle(uint16_t ccr) { __HAL_TIM_SET_COMPARE(htim2, TIM_CHANNEL_2, ccr); }初始化后先让舵机归零位然后根据提醒表 slot 计算目标角度。跨 90 度大幅转动时把角度变化拆成每 20ms 增 5 度的渐进式转动避免舵机猛甩带动药盘震动。我在仿真里观察过直接跳转和渐进转动的 PWM 波形渐进式虽然响应慢一点但机械结构明显更稳定。4.4 按键消抖与状态迁移代码按键扫描放在主循环里每 10ms 调用一次。状态机每秒扫描一次两次扫描时间去抖已经够用但在实际硬件上机械抖动时间 5-20ms 波动很大我用带上边界检测的扫描函数uint8_t key1_short_press(void) { static uint8_t last 1; uint8_t now HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin); if (now 0 last 1) { HAL_Delay(20); if (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin) 0) { last 0; return 1; } } if (now 1) { last 1; } return 0; }这里的关键是“边沿触发”而不是“电平触发”。如果只看电平按下期间主循环每 10ms 就会返回一次按键有效状态机会被连续触发三四次加了 last 变量后只有从松开到按下这个边沿才返回 1。配合消抖延时的同时按流程走到状态迁移整体按键手感稳定很多。5. Proteus 仿真环境的搭建与排坑5.1 仿真工程搭建步骤Proteus 里新建工程后从元器件库拖出 STM32F103C8T6、OLED SSD1306仿真库脚本可能有差异没有 SSD1306 时可以用 LCD1602 加 PCF8574 替代、DS3231、SERVO 电机模型、LED、按键和蜂鸣器。连线时注意 MCU 的 VDD 全部接 3.3VVSSA/VSS 接地VCAP 引脚虽然有时候悬空也能跑但仿真环境下不接会被扣分建议按参考设计补上。OLED 和 DS3231 的 SDA/SCL 并联上拉到 3.3V。舵机信号线接 PA1电源接 5V。Proteus 的 STM32 模型不像 ATMEGA 那样从库面板拿就能直接工作需要在 MCU 属性里指定 Program File 为 Keil 生成的 HEX 文件还要把 Crystal Frequency 填成 8MHz。这两个不填对仿真启动时要么白屏要么直接报错。5.2 HEX 文件生成与时钟配置Keil 默认不会生成 HEX 文件需要到 Options for Target 的 Output 页勾选 Create HEX File。生成后在工程目录的 MDK-ARM 文件夹里能找到 project.hex。Proteus 加载后点运行仿真器启动是瞬时的但 I2C 设备的初始化可能比较慢OLED 白屏时先等一下我这里大约 1-2 秒后才正常显示。如果 OLED 一直白屏大概率是 I2C 时序仿真太慢导致的初始化失败。把 ssd1306 初始化后的延时从 100ms 加大到 300ms再试一次。Proteus 里的 I2C 通信速度比真实硬件慢很多有时驱动里 50ms 的延时在仿真里要 200ms 才够。5.3 常见仿真问题排查我在仿真阶段遇到并解决了这几个典型问题整理成表格方便对照现象原因解决办法程序停在 HAL_Delay 循环不运行MCU 时钟配置与实际晶振不符检查 Crystal Frequency 设为 8MHSE 配置正确OLED 白屏无反应I2C 上拉电阻缺失或初始化地址错误加 4.7k 上拉扫描 0x3C/0x3D 两个地址DS3231 时间读出全是 FFI2C 设备地址左移位写错HAL 库需要传 0x68 1舵机不动PWM 频率不对确认 TIM2 的 PSC71ARR19999按键按下无响应仿真里按键接地方式不对按键一端接 PA 脚另一端接 GND并在软件内开上拉仿真跑通后我心里才有底去焊实物。如果没有仿真环境直接做板子状态机逻辑有 bug 时排查成本会高很多。6. 实物联调、烧录与运行稳定性检查6.1 烧录环境搭建和最常见的“找不到目标”烧录用 ST-Link V2 或者 DAP-Link接四根线SWDIO 到 PA13SWCLK 到 PA14GND 到 GND3.3V 到 3.3V。如果用的是带自动下载电路的核心板直接插 USB 就行如果是最小系统板飞线的注意 ST-Link 和目标板必须共地。很多人第一次连 STM32CubeProgrammer 时报错error: no stm32 target found! if your product embeds debug authentication这个报错 80% 出在以下三处接线接触不良尤其是杜邦线松垮SWD 这种高速信号对接触电阻很敏感。先看 ST-Link 的 LED 状态红绿闪烁说明连线异常。BOOT0 被意外拉高。BOOT0 高电平时芯片从系统存储器启动调试口默认不工作用万用表量一下 BOOT0 是不是低电平。复位引脚被外部电路锁住。解决方法是 CubeProgrammer 连接设置里勾选 Hot Plug再把连接模式改成 Under Reset。我从一次“无论拔插多少次都连接不上”的状态中排查出来的原因是电源线太细压降过大芯片根本没正常上电。所以接线顺序建议先把 ST-Link 的 GND 和 3.3V 接好再插 SWDIO/SWCLK最后上电这样成功率最高。6.2 按点故障I2C 上拉、舵机电源、按键抖动实物联调阶段仿真没问题不等于硬件没问题。我实际遇到的几类故障非常有代表性I2C 总线问题。裸模块上的 OLED 和 DS3231 多数自带 4.7k 上拉电阻直接并联可用。如果自己打板用的裸屏和裸芯片一定要在 PCB 上补上拉。我见过最低级的错误是把上拉电阻接到了 5V 而不是 3.3V导致 SCL/SDA 高电平超过 MCU IO 耐压STM32 虽然没有马上烧但通信极不稳定。舵机电源问题。SG90 接在 5V 上没问题但如果 5V 是由 USB 供电电流稍微不够就会在舵机启动瞬间拉低电压OLED 直接花屏重启。解决方案是单独用 5V 2A 适配器给舵机供电或者至少把舵机的 VCC 和驱动电路其他部分分开只在 GND 处汇合。按键抖动问题。理论上内部上拉加 20ms 去抖够了但老化按键的抖动时间会变长。我把去抖逻辑改成连续三次读取电平一致才有效效果更好。另外内部上拉阻值约 30-50k在电磁环境较差的场合容易误触发给按键外部焊一个 10k 上拉电阻是更稳妥的做法。6.3 我实际测试下来的稳定性结论拿到实物后我连续测了 48 小时中间故意断电两次。DS3231 走时偏差在一周内不到 3 秒断电两小时后再开机时间依然准确。提醒触发的 4 个时间点全部准时蜂鸣器声音在 30 平米的客厅里能清晰听到卧室关上门也能听到达到预期。不过我也观察到两个不够理想的地方。第一蜂鸣器响铃时如果老人正好在药盒旁边耳朵会觉得刺耳后续可以考虑加一个音量挡位。第二转盘在食物残渣或者灰尘较多时转动会有轻微卡顿需要定期清理轨道。这些在仿真阶段是体验不到的只有原型机跑起来才能发现。7. 开源文件使用说明与后续扩展思路7.1 代码目录说明如果你拿到了开源压缩包整个目录结构是这样的smart_medicine_box/ ├── Hardware/ │ ├── 智能药盒.SchDoc // 原理图工程 │ └── 智能药盒.PcbDoc // PCB 参考 ├── Firmware/ │ ├── MDK-ARM/ // Keil 工程 │ ├── Drivers/ │ │ ├── BSP/ // bsp_i2c.c, bsp_tim.c │ │ ├── ds3231.c │ │ ├── ssd1306.c │ │ └── servo.c │ └── App/ │ ├── schedule.c // 提醒调度 │ ├── key.c // 按键扫描 │ └── beep.c // 蜂鸣器控制 ├── Simulation/ │ └── smart_medicine_box.pdsprj // Proteus 仿真 └── README.md拿到压缩包后建议先打开 README 看硬件版本和固件版本的对应关系。我先在 Proteus 里跑一遍仿真确认逻辑没问题再拿真实硬件。硬件接线和引脚分配表要一致否则舵机角度、按键扫描位置全都会乱。7.2 许可与使用提醒项目采用 MIT 开源协议你可以在保留版权声明的前提下自由修改、商用。原理图和 PCB 文件给的是可编辑原始文件我用的是嘉立创 EDA直接导入或者重画都方便。这里想多提醒一句如果只是学习直接看代码就能跑通大部分逻辑但如果要做成产品给医院或者养老机构试用机械部分的强度和阻燃材料需要重新设计不能直接用 3D 打印件。开源硬件的意义在于降低起步门槛不代表原件可以直接量产。7.3 扩展短信、语音、云端这个项目预留了几个明确的扩展方向都是我在使用中觉得值得做的加语音播报。蜂鸣器只能提示“该吃药了”但不能告诉老人吃哪一格。如果接一个 SYN6288 语音合成模块提醒时直接播报“早上好请服用降压药打开第一格”对视力不好的老人帮助很大。加短信通知。通过 SIM800C 或者 A7670 模块在提醒发出后 30 分钟仍未确认时给子女手机发一条短信形成一个简单的远程监护闭环。这个扩展的硬件改动很小串口透传即可主要是软件上增加一条“超时未确认”分支。加云端记录。用 ESP8266 连接局域网把每次服药确认时间点上报到服务器或物联网平台进一步可以生成周报让家庭医生了解老人的用药依从性。这几个方向实际上都比现有功能更“聪明”但现有系统是它们的地基。没有稳定的时钟、状态机和出药机构上层功能再多也没有意义这也是我把基础版本开源出来的原因——先让有需要的人能跑起来再一起在上面做增量。这次开发下来我印象最深的反而是一个小细节STM32 单片机本身并不难难的是把时间管理、机械执行、人机交互这些写在代码之外的因素都考虑进去。老人按按键的力度、药盘的摩擦、断电保护这些才是项目真正花时间的地方。希望大家复刻的时候也留出这部分耐心。