简介:一套结合微信小程序与STM32的智能药盒管理系统完整方案,以小程序作为交互入口、STM32作为控制核心,构成端云联动的物联网用药管理方案。主要面向嵌入式开发者、物联网爱好者及医疗健康产品设计人员,解决传统用药管理依赖人工提醒、易漏服错服,以及监护人无法实时掌握服药情况的问题。压缩包共8个文件,大小约71.02MB,主要包含硬件PCB设计、STM32固件源码、小程序前端源码和项目结构说明,另附README与许可证文件,rar工程分模块打包,便于按需选用和二次开发。已有76人下载学习。资源完整覆盖从硬件电路、嵌入式逻辑到移动端交互的全链路,实现药盒定时开启精确控制、用药数据采集上传与远程监护,可作为毕业设计、课程项目或智能医疗产品原型参考。 做这个项目的起因,是去年回老家时看到奶奶的药盒上贴满胶布,早中晚的药还是经常吃重或漏吃。市面上号称“智能药盒”的产品不少,但要么贵得离谱,要么必须装专用App,老人根本用不来。后来我决定自己动手做一套,用微信小程序作为交互端,用STM32作为药盒主控,做成一套基于微信小程序和STM32的智能药盒管理系统。核心目标就三个:用药计划可管理、智能硬件可连接、监护人能远程监护。这套方案我已经跑通了完整链路,从硬件选型到小程序联调都有可复现的细节,这次一次性整理出来。
1. 需求拆解:药盒为什么要拆成“小程序+STM32”两级架构
1.1 真正需要解决的四个用药场景
先说场景,不然容易做成“为了智能而智能”。我在社区里看到过不少同类项目,最后都变成蓝牙控制舵机转一圈的花架子。真实的用药痛点,其实集中在四件事上:
- 漏服:老人独居,或者子女白天上班,没有人到点提醒。药盒需要主动发声、发光,而不是靠老人自己看。
- 重复服药:不少老人会“觉得今天好像没吃”,然后补吃一次,危险系数很高。药盒需要记录每次实际取出药品的动作。
- 子女不知道服药情况:很多老人不愿意承认自己忘了吃,电话里都说“吃了吃了”。子女需要一份客观的、可查询的服药记录。
- 药品种类和时间经常变:慢性病老人一个月要调整几次药量,今天加一片,明天减一片。药盒设置必须简单到子女远程就能改。
所以这个系统不是“硬件遥控器”,而是一套完整的用药管理闭环:计划由小程序维护,到点由硬件执行,结果自动回传,监护人随时查看。这个定位决定了后面所有技术选型。
1.2 为什么是微信小程序 + STM32,而不是纯App或纯云方案
我在项目中常被问到:为什么不用手机App直接连蓝牙?为什么不上一个带WiFi的板子,直接把数据传到云端?我的答案是:要看这个设备的使用者是谁。
微信小程序的核心优势是“免安装”。给老人用的东西,无论是iOS还是Android,打开微信扫一下就能用,子女在异地也能帮老人设置计划。对老人来说,不需要教他们怎么装App、怎么注册账号,点开小程序直接看到今天的药格子就行。这个体验是普通App给不了的。
STM32这边则负责“真正动起来”的部分。药盒需要控制舵机开锁、驱动蜂鸣器、读取门控状态,这些操作对时序要求高,而且不能因为手机蓝牙断连就罢工。用STM32做主控,一是实时性和稳定性有保障,二是功耗低、成本可控,三是整个方案里你能完全掌控硬件逻辑,不会像某些家用路由器刷开源固件那样受限。
为什么不走“纯云+WiFi模块”方案?我其实试验过:如果药盒直接依赖WiFi模块接收云端指令,一旦家里断网或者路由器重启,整个药盒就处于“哑巴”状态,连本地出药都做不了。而用蓝牙直连方案,手机是临时网关,药盒本体有独立工作能力,断网时只要手机蓝牙还连着,照样能按时出药。系统真正需要的云端能力是“远程查看记录”,而不是“远程实时控制”的强依赖。这个取舍在老人场景下非常重要。
2. 硬件方案:从药格到主控板的实操选型
2.1 主控与外设清单
我的硬件清单如下,都是从常用元件库里能直接买到的型号,不搞稀缺料:
| 模块 | 型号/规格 | 作用 |
|---|---|---|
| 主控MCU | STM32F103C8T6 | 处理蓝牙指令、控制舵机和传感器 |
| 蓝牙透传模块 | JDY-23 BLE 4.2 / 5.0 | 与微信小程序建立BLE连接 |
| 舵机 | MG90S 金属齿轮舵机 × 6 | 控制6个药格锁扣 |
| 药格状态检测 | 微动开关 × 6 | 检测药格是否被打开/关回 |
| 蜂鸣器 | 有源蜂鸣器 5V | 到点提醒、异常报警 |
| 显示屏 | 0.96寸OLED I2C | 显示时间、今日服药状态 |
| 电源管理 | AMS1117-3.3 + 7.4V锂电池 | 给MCU和舵机供电 |
为什么用MG90S小舵机而不是步进电机?我对比过。步进电机优点是定位精确,但驱动需要驱动芯片,药盒这场景本质上不是“分度定位”,而是“把锁扣拉开一个角度”,舵机最合适。MG90S堵转电流约200-300mA,六路不会同时动作,一个独立电源完全扛得住。如果你用大扭力舵机,比如MG995,就需要单独考虑电源了。
2.2 电路连接与供电细节
硬件连接上容易翻车的点,主要是供电和电平匹配。
STM32的GPIO是3.3V电平,JDY-23蓝牙模块的串口电平通常兼容3.3V,可以直接连,但我不建议直接共地后把模块VCC接到板子的5V上就不管了。稳妥做法是:BLE模块VCC接3.3V,TXD接STM32的PA10(USART1_RX),RXD接PA9(USART1_TX),GND共地。这样收发不会出现电平“半高不高”的问题。
舵机供电要单独给。MG90S的堵转瞬间可能把5V电源拉低,如果和MCU共用同一个电源,轻则屏幕闪烁,重则单片机直接复位。我的方案是:7.4V锂电池直接给舵机电源端,再通过AMS1117降压到3.3V给STM32供电;舵机VCC接5V的独立DCDC模块,信号线接MCU的PA0、PA1等PWM输出引脚。实际接线时,每个舵机的电源线最好加一个100uF电容滤波,能明显减少瞬时压降。
药格打开状态的检测,我用的是微动开关,安装在药格门锁位置,门关上时开关被压住,门弹开后开关释放。STM32的GPIO配置为输入上拉:开关闭合时读到低电平,打开时读到高电平。如果用的是常开型传感器,就要在代码里反过来处理。这里有个顺序问题:安装时先把微动开关固定在盒体上,再装舵机锁扣,否则容易出现“门关上了但锁扣压不住开关”的物理错位。
3. 小程序端:用药计划、BLE通信与远程监护闭环
3.1 页面结构和云开发数据模型
小程序整体结构很简单,一共四个主要页面:
- 首页:显示设备连接状态、今日用药计划的卡片列表。
- 计划编辑:添加/修改药品名称、剂量、提醒时间。
- 记录页:按天查看服药记录,区分“已确认”“未确认”“已漏服”。
- 监护端:绑定家庭成员后,查看被监护人的实时服药记录和剩余药量。
数据存储我用了微信云开发,省去了自建服务器。云数据库里建三个集合就够了:
users:用户信息,包含openid、家庭绑定关系。devices:设备信息,包含deviceId、绑定人ownerId、当前药格数量。plans:用药计划,包含userId、deviceId、drugName、dosage、reminderTimes、enabled。
records我不建议单独建集合,数据量大了之后查询很麻烦。我直接把每日服药记录嵌套在plans文档里,以数组形式保存,每次取当天记录时只查一次plans集合,性能足够,逻辑也清晰。
3.2 微信小程序蓝牙通信的几个关键封装
微信小程序连BLE模块,核心流程是:
wx.openBluetoothAdapter({}) // 初始化蓝牙适配器 wx.startBluetoothDevicesDiscovery({ allowDuplicatesKey: true }) // 过滤名称包含 'MedBox_' 的设备 wx.createBLEConnection({ deviceId }) wx.getBLEDeviceServices({ deviceId }) wx.getBLEDeviceCharacteristics({ deviceId, serviceId }) // 然后通过 writeBLECharacteristicValue 下发指令这里有三个实际会遇到的问题,值得单独提醒。
第一,发现设备后不要马上就createBLEConnection,要过滤设备名,否则会把周围十几台蓝牙设备都尝试连一遍。我的做法是:在onBluetoothDeviceFound回调里判断device.name是否包含MedBox_,匹配后才执行连接。
第二,iOS和Android对蓝牙权限的处理不同。iOS上如果用户没有打开“定位服务”,蓝牙扫描可能拿不到任何设备,这个坑不实际踩一次很难想到。小程序初始化蓝牙前,最好先用接口判断一下系统版本,如果检测到iOS,就先引导用户打开定位权限,再走蓝牙流程。
第三,写数据时要考虑分包。微信BLE接口一次写入的最大长度通常在20字节左右,我的协议帧只有6个字节,完全没问题。如果你要扩展功能,比如下一条较长的配置指令,就必须把数据拆成多段,每次写完后等待writeBLECharacteristicValue成功回调,再写下一段。同时,写操作要加防抖,不能在极短时间内连续写,否则手机会提示“连接不稳定”。
3.3 监护人远程监护的数据流
远程监护的数据流是这样的:被监护人的手机小程序和药盒保持BLE连接,当药盒检测到某个药格被打开并关回后,会通过BLE回传一条确认帧,小程序收到后调用云函数写入服药记录。监护人打开小程序时,后台根据家庭绑定关系查对应被监护人的plans和records,页面展示当日服药状态。
这里有一个权限问题要提前设计:微信云开发默认只允许用户读写自己的数据,监护人怎么读取被监护人的数据?我的方案是,在用户表里维护一个family字段:
{ "_openid": "被监护人的openid", "ownerId": "被监护人openid", "family": [ { "openid": "监护人openid", "role": "child" } ] }云函数里根据当前登录用户的openid,查找ownerId等于该用户或family数组中包含该用户的设备,再返回对应的计划与记录。这样权限关系依然清晰,不会出现任何人能查别人数据的问题。
如果监护人想主动提醒,不使用蓝牙直连(因为他在异地),我用了微信订阅消息:被监护人在小程序里授权“服药提醒”消息模板,监护端点击“提醒服药”时,通过云函数调用微信推送接口,向被监护人发送一条订阅消息。注意订阅消息一次授权只能推送一次,所以我做了一个“连续授权”引导,在用户首次进入计划页时弹窗申请多次授权。这个设计在真机测试中有效,但不要指望它像短信一样强制,只能作为提醒增强。
4. STM32端状态机与自定义BLE协议:一条指令怎样变成一颗药
4.1 自定义BLE指令帧格式与解析
BLE透传模块传输的是裸串口字节,直接传字符串也不是不能做,但项目一旦复杂就会很痛苦。我自定义了一套极简二进制协议,每个指令固定6字节:
| 字节 | 定义 | 说明 |
|---|---|---|
| 第1字节 | 帧头 | 固定0xAA |
| 第2字节 | 命令字 | 0x01出药、0x02查询状态、0x03复位 |
| 第3字节 | 参数 | 药格编号,0~5 |
| 第4字节 | 保留 | 固定0x00 |
| 第5字节 | 校验和低8位 | 前4字节累加和的低8位 |
| 第6字节 | 帧尾 | 固定0x55 |
举个例子,下发“打开第2格药”指令就是:
AA 01 02 00 AD 55因为前四个字节0xAA + 0x01 + 0x02 + 0x00 = 0xAD,所以校验码是0xAD。STM32端收到后先判断帧头和帧尾,再验证累加和,校验通过才执行。这个协议看起来简单,但好处是不依赖字符串解析,不会出现换行符干扰、大小写混淆的问题,也方便以后扩展命令字。
4.2 STM32主状态机与出药逻辑
STM32端的核心不是怎么写串口中断,而是怎么把“收到一条指令”变成“一次稳定的出药动作”。我这里用了一个简单的状态机,四个状态:
IDLE:空闲,等待BLE指令。POPPING:舵机正在转动,等待到达指定角度。WAIT_CONFIRM:药格已弹开,等待药格被取走并关闭。MISSED:超时未关闭,执行提醒并上报。
核心逻辑大概是这样的:
switch (state) { case IDLE: if (ble_rx.cmd == CMD_POP) { pop_drug(ble_rx.arg); // 控制对应舵机转动 state = WAIT_CONFIRM; } break; case WAIT_CONFIRM: if (door_sensor_closed(ble_rx.arg)) { send_status(STATUS_TAKEN); state = IDLE; } else if (tick >= 15000) { buzzer_beep(3); send_status(STATUS_MISSED); state = IDLE; } break; }这里有两个细节。第一个是舵机转动的时间:MG90S从0度转到90度约120ms,我在pop_drug里先让蜂鸣器短响一声,再控制舵机转到对应角度,防止老人被突然弹出的药格吓到。第二个是状态机里不要阻塞式等待,用定时器中断维护一个毫秒级的tick,主循环只做状态判断。如果直接写HAL_Delay(120),在延时期间蓝牙指令来了也处理不了,体验会差很多。
4.3 掉电保护与误弹出设计
药盒最怕的就是掉电后状态丢失,或者误触发导致药格弹开。针对这两个问题,我做了三个处理。
第一,在STM32的Flash里保存一个“当前已弹开药格”的状态位。每次上电初始化时读取这个状态,如果发现某个药格在掉电前是“已弹开未确认”,蜂鸣器会持续鸣叫并上报一条待确认记录,提示用户检查。这里用STM32F103的FLASH操作,其实只需要擦写一个扇区,不存在什么难度,注意不要在运行时频繁写Flash就行。
第二,BLE指令执行前加一个“二次校验”逻辑。出药这种动作不能收到一帧就执行,万一飘来一个CRC碰巧校验通过呢?实际项目中我设置了同一个指令3秒内最多执行一次,并且指令必须连续收到两次且内容一致才真正触发。用户在小程序端点一次“出药”,小程序会自动发两帧相同的指令,间隔200ms。这样即使BLE偶尔丢包,也不会造成误操作。
第三,药格的状态检测要消抖。微动开关在机械动作瞬间会有一连串的电平抖动,如果直接读GPIO判断,可能会把一个开合动作识别成多次。我在代码里加了一个简单的软件消抖:GPIO状态变化后,延时20ms再读一次,一致才确认变化。如果用定时器中断做轮询,效果更好,但要注意别在中断里做延时,否则会拖慢主循环。
5. 联调实测与踩坑记录:从点击“出药”到药格弹开
5.1 实测链路延迟与稳定性
整个链路跑通之后,我做了一轮比较完整的实测,数据如下:
| 测试项目 | 结果 |
|---|---|
| 打开小程序到蓝牙连接成功平均耗时 | 2.3秒(iOS),1.8秒(Android) |
| 下发“出药”指令到药格弹开平均耗时 | 180ms |
| 药格状态回传到小程序显示平均耗时 | 480ms |
| 连续操作10次,指令丢失次数 | 0次(同一楼层环境) |
| 蓝牙距离大于8米后丢包率 | 明显上升 |
实测下来,用户点击“出药”后的实际体感是非常快的,几乎感觉不到延迟。真正影响体验的反而是“打开小程序连蓝牙”这个过程。后来我在小程序首页加了一个自动重连逻辑:如果检测到上次连接的设备ID,就跳过设备扫描,直接用createBLEConnection去连,连接速度从2秒多降到1秒以内。
5.2 我踩过的三个坑
这里必须把几个印象深刻的坑单独拿出来说,因为不踩一次,很多问题看教程根本发现不了。
坑一:舵机供电不足导致STM32频繁复位。最初我把五块面包板接到同一个5V降压模块上,按下出药按键的瞬间,屏幕直接白屏,串口打印全是乱码。后来用万用表量舵机电源端,瞬间跌落到了3.8V,而STM32的3.3V供电又是从同一个5V降压出来的,在低压瞬间直接复位。解决方案就是前面说的:舵机独立供电,STM32通过AMS1117单独降压。这个教训让我明白,在机电一体项目里,供电规划比代码优先级更高。
坑二:微信小程序拿到的BLE服务UUID是“动态”的。JDY-23模块出厂默认有一套Service UUID和Characteristic UUID,但不同固件版本返回的UUID并不一致,甚至在重新上电后可能变化。我开始按照说明书硬编码UUID,结果换了一块模块就连不上。后来改成了动态获取:连接设备后先调用getBLEDeviceServices拿到所有服务,再从中筛选出包含“写入”和“通知”权限的特征值,不再依赖固定值。这样无论模块固件怎么变,都能找到可用的通道。
坑三:微动开关抖动导致药格状态误报。一开始我直接按GPIO电平跳变判断“门被打开”,结果发现老人轻轻关门时,开关会连续抖动,记录上显示“打开-关闭-打开-关闭”了好几次。我折腾半天后加了软件消抖,还在小程序端增加了“状态变化后500ms内不再接受反向变化”的辅助逻辑,最终记录稳定很多。这种事情调试时很难复现,但装上电池放在家里,第二天数据统计就开始出问题。所以凡是读取机械传感器的地方,都别省消抖逻辑。
5.3 可以继续扩展的方向
这套系统的硬件和软件底座都搭好了,后续扩展其实非常自然。我目前已经验证过几个方向:
- 离线语音播报:加一个SYN6288语音合成模块,到点直接播报“请服用阿司匹林一片”,比蜂鸣器好用太多。语音模块通过串口和STM32通信,协议类似AT指令,非常成熟。
- 用药数据导出:云函数定期把每周服药依从性汇总成一个PDF,通过小程序让用户下载或分享给家庭医生。这个我已经用云开发的
generateUrl做了简单版,能生成临时链接,后续可以接文件存储。 - 多设备家庭管理:把
devices集合改成数组,支持一位用户绑定多个药盒,比如老家一台、自己家一台。小程序首页按设备分组展示计划,监护人端查看不同设备时也能按家庭维度过滤。
最后分享一个我实际用下来最管用的小技巧:给药盒的每颗药格编号,在盒体上刻数字,然后在微信小程序计划编辑页里,让用户拍照绑定“第3格对应哪种药”。这个功能看着不起眼,但老人实际使用时,眼睛看着盒体编号再扫一眼手机,几乎不会放错药。很多同类项目败在功能复杂但“不好用”上,如果把交互细节做到这种程度,整个系统才真正有了长期被使用的价值。
本文还有配套的精品资源,点击获取