简介:一套基于 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 走 I2C,LCD1602 通过 PCF8574 转接板也走 I2C,HC-06 蓝牙走串口,红外遮挡模块输出数字电平,SG90 舵机需要 PWM 信号。把这些接口归纳清楚,接线就不会乱。
| 模块 | 接口类型 | 连接 Arduino UNO 引脚 | 说明 |
|---|---|---|---|
| DHT11 | 单总线 | D2 | 数据脚,需 4.7kΩ 上拉,部分模块已内置 |
| DS1307RTC | I2C | A4(SDA)、A5(SCL) | 板载电池座,断电后保持计时 |
| LCD1602 + I2C 转接板 | I2C | A4(SDA)、A5(SCL) | 与 DS1307 并联在同一条 I2C 总线上 |
| SG90 舵机 | PWM | D9 | 信号线接 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.3V,UNO 的 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); }这段扫描代码遍历 1~127 的 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\n,toInt()会停在\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.5ms~2.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)会返回 0,TimeLib 会一直显示 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 与串口初始化都是 9600 |
4.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时调用一次。如果需要记录“哪一天吃过药”,可以把年月日编码成三个字节连续写入,例如地址 3~5 分别存年偏移、月、日,读出来时再组合成日期。要注意 EEPROM 写入耗时约 3ms,期间如果来了蓝牙中断,建议先关中断再写入,或者接受这 3ms 的微小延迟,因为蓝牙指令丢一个字节不是什么严重问题。
实际使用中,你还可以把蓝牙校时的频率降低:如果 DS1307 的温漂在每天 ±2 秒左右,一个月就会差 1 分钟,药箱这种场景每周校时一次就够了,不必每次开机都校时。用手机上任意一款蓝牙串口工具,连接 LMH 后输入T14:33,设备会在 14:33 准点开舱,开舱后 30 秒内不拿药,蜂鸣器会以 1Hz 频率间歇报警,这组参数在delay或非阻塞定时里都能调,建议根据老人的听觉习惯把报警改成 0.5Hz 两短一长三段式,比连续响更容易被辨认。最后还有一个小技巧:蜂鸣器正极不要直接接 D6,用一个 S8050 三极管做开关,基极串 1kΩ 电阻接 D6,集电极接蜂鸣器负极,发射极接地,这样蜂鸣器音量更大,也不会把 D6 引脚拉垮。
本文还有配套的精品资源,点击获取