基于Arduino UNO的智能药箱:闭环拿药确认与状态机设计 📅 发布时间:2026/9/16 22:42:06 👁 浏览次数: 简介一套基于 Arduino 的智能药箱完整项目资源面向电子信息、自动化、计算机等专业的课程设计、毕业设计或 Arduino 进阶学习者重点解决定时服药提醒与药品防潮防高温问题。系统支持 LCD 显示时间与温湿度、通过蓝牙设定服药时间、到达时间自动打开舱门若未取药则触发报警与警示灯取药后警报停止放回药瓶后舱门自动关闭手机端也可切换键盘模式手动开关舱门提升容错性。资源共 38 个文件以 .ino 主程序、.cpp/.h 驱动库、.md 说明文档为主体另含 .txt、.json 等辅助配置压缩包仅 48KB结构清晰。目前已有 110 人学习下载。包内 Arduino 源码完整涵盖温湿度采集、实时时钟、蓝牙通信与舱门控制等模块代码经测试运行成功可直接烧录调试或基于现有逻辑二次开发说明文档与依赖库一并提供遇到环境或运行问题还可向作者咨询远程教学便于入门者快速上手。1. 为什么智能药箱的关键在“拿药确认”而不在“定时开门”机械式的药箱只能做到“到点响铃”响完老人拿没拿、吃没吃设备完全不知道。这套基于 Arduino UNO 的智能药箱最值得拆的不是蓝牙、不是舵机而是它把“提醒”升级成了“闭环”到点自动开舱门同时用红外遮挡检测判断药瓶是否被拿走。没拿就蜂鸣器响、警示灯亮、LCD 上反复提示“请拿药”直到检测到药瓶离位才解除报警放回药瓶后舱门再自动关闭。这套逻辑用状态机实现比单纯堆模块更有代码参考价值也正适合计算机、自动化、电子信息方向的课设和毕设。整个系统由 DS1307RTC 硬件时钟提供时间基准DHT11 采集温湿度SG90 舵机带动舱门HC-06 蓝牙接收手机指令LCD1602 加 I2C 转接板显示状态。手机端蓝牙名称 LMH、配对密码 310支持“命令行模式”校时和“键盘模式”手动开关舱门。接下来从硬件接线讲起逐步把状态机、蓝牙协议和调参陷阱拆开。2. 硬件接线、I2C 地址扫描与 Arduino 库环境准备2.1 模块选型与引脚规划项目用到的外设不多但接口类型混在一起DHT11 是单总线协议DS1307 走 I2CLCD1602 通过 PCF8574 转接板也走 I2CHC-06 蓝牙走串口红外遮挡模块输出数字电平SG90 舵机需要 PWM 信号。把这些接口归纳清楚接线就不会乱。模块接口类型连接 Arduino UNO 引脚说明DHT11单总线D2数据脚需 4.7kΩ 上拉部分模块已内置DS1307RTCI2CA4(SDA)、A5(SCL)板载电池座断电后保持计时LCD1602 I2C 转接板I2CA4(SDA)、A5(SCL)与 DS1307 并联在同一条 I2C 总线上SG90 舵机PWMD9信号线接 D9电源建议外接 5V红外遮挡传感器数字输出D3检测药瓶是否在位拿走后输出电平翻转蜂鸣器数字输出D6有源蜂鸣器高电平触发警示灯 LED数字输出D5限流电阻 220Ω 串接HC-06 蓝牙串口D0(RX)、D1(TX)模块 TX 接 D0、RX 接 D1注意电平匹配接线时有两点容易踩坑。第一SG90 舵机堵转电流可达 500mA 以上直接从 UNO 的 5V 引脚取电负载大时会导致复位常见做法是给舵机单独接 5V 电源且共地第二HC-06 的 RX 引脚电平逻辑是 3.3VUNO 的 D1 输出是 5V长期直连有风险稳妥做法是加一个 1kΩ 串联电阻分压。这两处改好后系统稳定性会明显提升尤其在校验“舱门在设定时间自动打开”这个动作时舵机抖动导致的复位问题大大减少。2.2 用 I2C 扫描确认设备地址DS1307 的默认 I2C 地址是 0x68而 LCD1602 的 PCF8574 转接板常见地址是 0x27 或 0x3F不同批次模块可能不同。如果 LCD 显示乱码或者 DS1307 读取失败先不急着调代码先扫描总线上的实际地址。#include Wire.h void setup() { Serial.begin(9600); while (!Serial) { delay(10); } Wire.begin(); Serial.println(I2C Scanner); byte count 0; for (byte addr 1; addr 127; addr) { Wire.beginTransmission(addr); if (Wire.endTransmission() 0) { Serial.print(Found I2C device at 0x); if (addr 16) Serial.print(0); Serial.println(addr, HEX); count; } } Serial.print(Total devices: ); Serial.println(count); } void loop() { delay(5000); }这段扫描代码遍历 1127 的 I2C 地址逐个发起传输并检查是否有设备应答。Wire.beginTransmission(addr)会向设备发出起始信号加地址帧Wire.endTransmission()返回 0 表示有设备 ACK。如果扫描结果只有一个 0x68 而没有 0x27 或 0x3F优先怀疑 LCD 转接板供电是否正常或者模块焊接的地址电阻被改过。拿到真实地址后再修改LiquidCrystal_I2C构造函数的第一个参数。2.3 库的安装与本地缓存位置调整工程依赖四个库DHT、LiquidCrystal_I2C、Time 和 DS1307RTC。前三个在 Arduino IDE 的库管理器里直接搜索安装DS1307RTC 也在库管理器可查。但有些环境下默认库目录磁盘空间不足或者多个 Arduino 版本共用库目录导致版本冲突需要把库安装位置改到自定义目录。Arduino IDE 的库目录在文件菜单的“首选项”里设置中文版路径是“文件 - 首选项 - 项目文件夹位置”英文版是“File - Preferences - Sketchbook location”。把 Sketchbook 路径改到 D 盘或其它空间充足的目录后库管理器安装的库会自动落到新目录的libraries子目录下。注意一点修改 Sketchbook 位置后之前安装在旧目录的库不会自动迁移建议重新在库管理器里安装一次或者手动把旧libraries目录里的库文件夹复制过来。项目里如果同时存在Time-master和DS1307RTC-master的压缩包解压后注意目录名不能带-master后缀否则 IDE 会报“无效库”错误需要把文件夹重命名为Time和DS1307RTC。3. 状态机拆解定时开舱、拿药检测与蓝牙指令的实现3.1 时间字符串解析与闹钟设定手机蓝牙端进入命令行模式后发送14:33这样的一条消息Arduino 收到后解析出小时和分钟存成目标开舱时间。这个动作看似只是一个sscanf实际要处理好字符串末尾的换行符和回车符否则解析会失败。bool parseAlarmTime(String input, int *hour, int *minute) { input.trim(); // 去掉首尾空格、\r、\n int colonIndex input.indexOf(:); if (colonIndex 0) return false; int h input.substring(0, colonIndex).toInt(); int m input.substring(colonIndex 1).toInt(); if (h 0 || h 23 || m 0 || m 59) return false; *hour h; *minute m; return true; }parseAlarmTime先调用trim()把 HC-06 蓝牙透传时附加的\r\n清理掉再用indexOf(:)定位分隔符。substring截取后转整数并做边界校验。很多人在手机上通过串口调试工具发送时间时工具会自动追加换行符如果不trim()toInt()其实只解析数字部分后面粘着的换行不影响但如果手机端发送的是14:33\n且没有做裁剪indexOf(:)定位后substring(colonIndex 1)得到33\ntoInt()会停在\n前依然能得到 33。真正的坑在于部分调试工具会发送 Unicode 字符或全角冒号这时indexOf(:)返回 -1解析失败。所以手机端建议使用支持 ASCII 发送的蓝牙串口工具。3.2 主状态机的核心循环这里的代码逻辑不是简单的if (当前时间 设定时间)就开门而是要处理开舱后“没拿药 - 报警 - 拿药 - 放回 - 关门”这整条链路。用枚举定义状态配合一个主循环状态机代码清晰且方便日后扩充。enum MedicineState { STATE_IDLE, // 待机等待开舱时间到达 STATE_DOOR_OPEN, // 舱门已打开等待拿药 STATE_MEDICINE_OUT, // 检测到药瓶已被拿走 STATE_MEDICINE_BACK // 药瓶已放回准备关门 }; int state STATE_IDLE; bool doorOpen false; unsigned long lastMotionCheck 0; void updateStateMachine(int nowHour, int nowMinute, int alarmHour, int alarmMinute, bool medicinePresent) { switch (state) { case STATE_IDLE: if (nowHour alarmHour nowMinute alarmMinute) { openDoor(); // 舵机转到开舱角度 state STATE_DOOR_OPEN; } break; case STATE_DOOR_OPEN: if (!medicinePresent) { // 药瓶被拿起解除报警 stopAlarm(); state STATE_MEDICINE_OUT; } else { startAlarm(); // 时间到但药瓶还在持续报警 } break; case STATE_MEDICINE_OUT: if (medicinePresent) { closeDoor(); // 药瓶放回自动关门 state STATE_MEDICINE_BACK; } break; case STATE_MEDICINE_BACK: stopAlarm(); digitalWrite(LED_PIN, LOW); state STATE_IDLE; break; } }状态机的关键在于STATE_DOOR_OPEN阶段只要红外传感器还检测到药瓶在位报警器就一直响。这里借用了常闭逻辑即红外遮挡传感器被药瓶挡住时输出低电平取走后变高电平。medicinePresent是经过反相处理后的布尔值。报警循环要避免阻塞每次进入STATE_DOOR_OPEN时执行一次startAlarm()让蜂鸣器响而不是在循环里反复调用否则蜂鸣器会发出刺耳的断续声。3.3 蓝牙串口指令命令行模式与键盘模式HC-06 蓝牙模块的角色是串口透传手机端连接后发送的数据会原样到达 Arduino 的串口接收缓冲区。项目里设计了两种控制模式命令行模式负责校时和设定闹钟键盘模式负责手动开关舱门。void handleBluetoothCommand() { static String cmdBuffer ; while (Serial.available()) { char c Serial.read(); if (c \n) { if (cmdBuffer.startsWith(TIME:)) { int h, m; if (parseAlarmTime(cmdBuffer.substring(5), h, m)) { setAlarmTime(h, m); Serial.println(Alarm set OK); } } else if (cmdBuffer.equals(OPEN)) { manualOpenDoor(); } else if (cmdBuffer.equals(CLOSE)) { manualCloseDoor(); } cmdBuffer ; } else { cmdBuffer c; } } }示例代码里做了轻微变动原始设计的手机端直接发送14:33就设定时间因为设备只负责定时开舱可以直接把收到的内容当成闹钟时间如果想支持闹钟设定、手动开关门等多个操作推荐用一个两字符前缀区分命令类型如T14:33表示设定时间、OPEN表示开门、CLOSE表示关门。注意 HC-06 默认波特率一般是 9600代码里Serial.begin(9600)要和模块一致同时 HC-06 的 AT 指令配置也必须工作在同一波特率下常见的 HC-06 进入 AT 模式需要在上电前按住模块上的按键。4. 舵机角度标定、I2C 地址冲突与常见运行故障排查4.1 舵机角度与机械结构标定SG90 舵机的 PWM 控制周期是 20ms高电平时间 0.5ms2.5ms 对应 0°180°。项目里舱门打开和关闭的角度不是固定的 0° 和 90°而是取决于你安装舵机臂和舱门的机械结构。直接套用代码里的角度往往会出现门开到一半或者关不严的情况。#include Servo.h Servo doorServo; int OPEN_ANGLE 95; int CLOSE_ANGLE 8; void openDoor() { doorServo.write(OPEN_ANGLE); doorOpen true; } void closeDoor() { doorServo.write(CLOSE_ANGLE); doorOpen false; }标定方法是这样给舵机接上电源先doorServo.write(90)让舵机转到中位把舵机臂手动拧到垂直方向再锁紧螺丝这样两侧行程余量对称。然后逐步调整OPEN_ANGLE和CLOSE_ANGLE每次增大或减小 5°观察舱门是否完全打开且不卡死。机械结构上药箱的舱门最好做成翻盖式舵机安装在侧壁用一根长舵机臂连接门板边缘比直接顶推门板更顺滑。如果开门时舵机发出嗡嗡声但门不动通常是舵机扭矩不够或者供电不足换 MG90S 金属齿轮舵机是低成本解决方案但要注意 MG90S 堵转电流更大供电更要独立。4.2 I2C 地址冲突的两种表现DS1307 和 PCF8574 在同一条 I2C 总线上没有物理冲突因为它们地址不同但代码里如果初始化顺序错误会出现一种隐蔽故障LCD 显示正常但RTC.get()返回的时间永远是 2000-01-01。原因是某些 DS1307 模块在 I2C 总线繁忙时会复位失败尤其是在 LCD 初始化完成后立刻读取 RTC总线时序不稳定。解决方法是初始化时给 RTC 一个延迟或者先把 DS1307 的begin()放在最前面。#include Wire.h #include LiquidCrystal_I2C.h #include DS1307RTC.h #include TimeLib.h LiquidCrystal_I2C lcd(0x27, 16, 2); void setup() { Wire.begin(); lcd.init(); lcd.backlight(); delay(500); setSyncProvider(RTC.get); if (timeStatus() timeNotSet) { lcd.setCursor(0, 0); lcd.print(RTC Error!); } }setSyncProvider(RTC.get)的作用是告诉 TimeLib 库每次调用now()时从 RTC 芯片同步时间。timeStatus()返回timeNotSet说明 RTC 没工作可能原因是 DS1307 板载电池没电或者晶振没起振。一个典型误用是在没有 DS1307 的情况下直接跑这套代码setSyncProvider(RTC.get)会返回 0TimeLib 会一直显示 1970 年。若你只是想先在无硬件环境下验证逻辑可以用 PC 的时间做时间源编译时把setSyncProvider(RTC.get)替换成setSyncProvider(getPCFTime)其中函数体直接return now()但这样断电后会丢失时间只适用于开发调试。4.3 故障排查速查表故障表现可能原因检查方法与解决LCD 只显示一排方块I2C 地址错误或对比度失调先用扫描程序确认地址LCD 背面蓝色电位器逆时针微调时间始终不走DS1307 电池没电换 CR2032 电池或重新用蓝牙校时蜂鸣器一直响拿药也不停红外遮挡传感器安装位置偏移药瓶离位仍被误判为在位检查红外对射管是否被舱门边框遮挡调整安装角度舵机开舱后不停抖动电源供电不足独立 5V 电源给舵机供电并共地蓝牙连不上HC-06 进入 AT 模式失败名称或密码不是 LMH/310断开电源按住模块按键重新上电用 USB 转 TTL 发送 AT 指令恢复手机发送时间没反应波特率不匹配确认 HC-06 与串口初始化都是 96004.4 Arduino 上传超时的常见根因很多人把 HC-06 的 TX/RX 接到 D0/D1 后正常跑通但下次再往 UNO 烧录程序时IDE 报avrdude: stk500_recv(): programmer is not responding或上传超时。这是因为 HC-06 占用了串口虽然蓝牙空闲时是高阻态但部分模块在上电瞬间会干扰复位时序。最直接的解法是烧录前拔掉 HC-06 的 TX/RX 跳线或者拔掉整个模块烧录完成后再接回去。另一个隐藏因素是 DS1307 的 I2C 引脚 A4/A5 如果被误接成上拉电阻会拉低 SDA/SCL 电平间接影响 USB 转串口芯片的 DTR 信号。遇到上传超时优先断开蓝牙模块其次断开 I2C 外设再不行按住 UNO 的复位键不放等 IDE 提示上传开始瞬间松开这是一线工程师常用的土办法。5. 用 Wokwi 仿真先行与 EEPROM 服药记录扩展在硬件到手之前先用 Wokwi 仿真平台把状态机和蓝牙指令逻辑跑通能省下大量调试时间。Wokwi 支持 Arduino UNO、LCD1602、DHT11、舵机等常见元件而且可以直接在网页里模拟 HC-06 蓝牙串口输入。做法是把智能药箱_310v1.ino的代码复制到 Wokwi 的项目里在代码编辑区添加diagram.json配置把 DHT11 接到 D2、舵机接到 D9、LCD 的 I2C 地址设为 0x27。void saveMedicineLog() { EEPROM.write(0, lastHour); EEPROM.write(1, lastMinute); byte count EEPROM.read(2); count; EEPROM.write(2, count); Serial.print(Times taken today: ); Serial.println(count); }这段代码把最近一次拿药时间写入 EEPROM同时累加当天拿药次数。ATmega328P 的 EEPROM 寿命约 10 万次写入不需要频繁擦写只在每次检测到STATE_MEDICINE_OUT时调用一次。如果需要记录“哪一天吃过药”可以把年月日编码成三个字节连续写入例如地址 35 分别存年偏移、月、日读出来时再组合成日期。要注意 EEPROM 写入耗时约 3ms期间如果来了蓝牙中断建议先关中断再写入或者接受这 3ms 的微小延迟因为蓝牙指令丢一个字节不是什么严重问题。实际使用中你还可以把蓝牙校时的频率降低如果 DS1307 的温漂在每天 ±2 秒左右一个月就会差 1 分钟药箱这种场景每周校时一次就够了不必每次开机都校时。用手机上任意一款蓝牙串口工具连接 LMH 后输入T14:33设备会在 14:33 准点开舱开舱后 30 秒内不拿药蜂鸣器会以 1Hz 频率间歇报警这组参数在delay或非阻塞定时里都能调建议根据老人的听觉习惯把报警改成 0.5Hz 两短一长三段式比连续响更容易被辨认。最后还有一个小技巧蜂鸣器正极不要直接接 D6用一个 S8050 三极管做开关基极串 1kΩ 电阻接 D6集电极接蜂鸣器负极发射极接地这样蜂鸣器音量更大也不会把 D6 引脚拉垮。本文还有配套的精品资源点击获取