简介:面向电子、计算机等相关专业学生与开发者的Arduino智能药箱项目,解决传统药箱无法按时提醒服药和药品储存环境监测的问题,实现定时提醒、温湿度监测、蓝牙配置及舱门自动开关等完整功能,可作为课程设计、毕业设计或入门实践项目。资源共38个文件,以Arduino主控程序(ino)、驱动库(h/cpp)、文档说明(md/txt)及环境配置为主,代码经过测试运行成功,包体仅48KB,结构紧凑,便于阅读与二次开发。目前已有110人学习下载。项目覆盖LCD显示当前时间与箱内温湿度、手机蓝牙连接后同步时间、定时触发舱门开启与拿药检测联动报警、紧急手动控制舱门等核心逻辑,并附有项目说明文档,可帮助理解传感器读取、实时时钟、I2C显示及蓝牙串口通信等关键模块,适合在此基础上自行扩展改造。
1. 智能药箱先把需求立住,再用 Arduino 把误差压到分钟级
家里老人一天要吃的药可能有三四种,早上空腹、饭后、睡前各不一样。真正常见的场景不是老人不愿意吃,而是吃完没记录、放药的位置不固定、家人问起来谁也说不准今天到底吃没吃。智能药箱要解决的不是“提醒”这一个动作,而是把「该吃药了」「吃没吃」「吃的哪一格」「有没有漏」这四件事串成一个闭环。用 Arduino 做这件事,成本很低,但难点不在电路,在于把状态管理做对。
如果你只想做一个会响的闹钟,那是买一个成品的事。真正值得动手的,是让药箱能感知“药被取走”,并且在屏幕上留下记录。整篇会按硬件搭建、核心代码、源码整理、调试验证的顺序讲,最后落在连续运行可靠性和文档化上。适合已经跑通过 Arduino Blink,但想做一个完整项目的开发者。
2. 从零搭 Arduino 智能药箱:硬件选型、引脚分配和最小电路
2.1 主控选型:Arduino Uno、Nano 与 ESP32 在药箱场景的取舍
智能药箱对主控的要求并不高。一个定时器、几个 IO、一路 I2C 就够用。Arduino Uno 胜在扩展板多、调试资料多;Arduino Nano 体积小,能直接插在面包板上,适合塞进药箱外壳。ESP32 的优势在 WiFi,可以把吃药记录同步到手机或者家里的小程序,但代价是功耗高、引脚电平是 3.3V,驱动传统 5V 舵机要多加一路电平转换。
我的建议是:如果你只是做本地原型,选 Arduino Nano 或者 Uno;如果你本来就熟悉 ESP32 环境搭建,干脆一步到位用 ESP32,后续接腾讯连连、OneNET 这类物联网平台不用换板子。药箱本身没有复杂计算,别为性能选型,为外设的驱动电压和开发效率选型更实际。
2.2 核心外设与引脚规划(含最小电路表)
| 外设模块 | 引脚(Arduino Nano/Uno) | 说明 |
|---|---|---|
| DS3231 RTC 时钟模块 | A4(SDA)、A5(SCL)、VCC、GND | 走 I2C,自带温度补偿 |
| SG90 舵机 | D9(PWM)、VCC、GND | 控制药格抽屉或挡板 |
| 蜂鸣器 | D8 | 提醒输出,低电平触发 |
| 独立按键 | D2 | 确认吃药,内部上拉 |
| 状态 LED(绿/红) | D6 / D7 | 绿色=已吃,红色=未吃 |
| 门磁/微动开关 | D3 | 检测抽屉是否真正打开 |
接线表里有三个容易踩坑的地方。第一,DS3231 一般是 5V 供电,但 SDA/SCL 引脚是开漏结构,必须接上拉电阻,Arduino 内部已经有上拉,不需要额外加 4.7K。第二,SG90 舵机的 VCC 不要直接接在 Arduino 的 5V 引脚上,药箱抽屉如果卡住,舵机堵转电流会到 500mA 以上,直接从板载 5V 取电可能导致主控复位。第三,门磁不是必须的,但它是“状态闭环”的关键,后面代码会讲。
2.2.1 舵机方案和机械结构怎么配合
常见的做法是舵机控制一个旋转挡板:服药时间到,挡板从“锁定”转到“释放”,用户拉开抽屉,微动开关状态翻转。SG90 扭矩只有 1.2kg·cm,适合轻质亚克力或纸板结构。如果药箱是金属外壳、抽屉有阻尼,换成 MG996R 金属舵机,但供电就得配独立 5V/2A 电源。舵机复位问题后面排错章节会详细说,这里先记住一个原则:挡板转动角度要留 5 度余量,避免舵机扫到物理限位后持续抖动。
2.2.2 传感器与提示灯的作用边界
红外避障模块、称重传感器、光电门都能检测“药是否被拿走”,但都不如微动开关可靠。称重方案在药粒数量少的时候容易误判,红外方案又会被药瓶颜色干扰。我一般会保留两种信号:微动开关判断抽屉被打开过,按键确认吃药完成。这两个信号合在一起,才算一次有效的服药行为。
2.3 面包板接线与供电注意
// pins.h #define PIN_SERVO 9 #define PIN_BUZZER 8 #define PIN_BUTTON 2 #define PIN_LED_GREEN 6 #define PIN_LED_RED 7 #define PIN_DOOR 3接线顺序建议先接电源,再接 I2C,最后接输出器件。舵机单独供电的接地端必须和 Arduino GND 连在一起,否则 PWM 信号没有参考地,舵机会乱跳。蜂鸣器如果是无源蜂鸣器,要用tone()驱动,不能直接digitalWrite。有源蜂鸣器可以直接拉高,但声音频率固定,提醒效果不如无源的好。LED 记得串 220Ω 限流电阻。
3. 智能药箱核心代码:吃药提醒、电机控制与按键反馈
3.1 时间管理:RTC 模块与 millis() 的两个可选方案
吃药提醒的基准时间是系统的命脉。只有两个选择:用 RTC 硬件时钟,或者用millis()做相对时间轮询。RTC 掉电后时间不丢,适合药箱这种长期通电的设备。millis()适合做“上次提醒后 12 小时再提醒”这种周期逻辑,但掉电就清零,必须配合 RTC 才能确定今天日期。
#include "RTClib.h" RTC_DS3231 rtc; void setup() { if (!rtc.begin()) { while (1) delay(10); // I2C 找不到模块,停在这里 } if (rtc.lostPower()) { rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // 用编译时间兜底 } } void loop() { DateTime now = rtc.now(); checkSchedule(now.hour(), now.minute()); delay(1000); }这段代码的逻辑链是:先确认 RTC 在线,再检查是否掉过电,掉电就用编译时的电脑时间做一次粗同步。lostPower()很关键,很多人的药箱换完电池后时间错乱,就是少了这一步。F(__DATE__)把字符串放到 flash,Arduino Nano 只有 2KB RAM,能省就省。如果你的板子有网络,直接从 NTP 同步更好,RTC 只做断电保持。
3.2 吃药逻辑状态机:从“到点提醒”到“确认完成”
药箱最容易出的问题是状态没分清。提醒响了、用户没拿药,和提醒响了、用户拿了药但没按确认,这是两种完全不同的状态。用状态机而不是散落的 if 判断,能让逻辑一眼看穿。
typedef enum { STATE_IDLE, STATE_REMIND, STATE_DRAWER_OPENED, STATE_WAIT_CONFIRM, STATE_DONE } MedicineState; MedicineState state = STATE_IDLE; unsigned long remindStartTime = 0; bool todayTaken[3] = {false, false, false}; // 早中晚 void updateState(DateTime now, bool drawerOpen, bool btnPressed) { switch (state) { case STATE_IDLE: if (isMedicineTime(now) && !todayTaken[slotIndex(now)]) { state = STATE_REMIND; remindStartTime = millis(); digitalWrite(PIN_LED_RED, HIGH); } break; case STATE_REMIND: if (drawerOpen) { state = STATE_DRAWER_OPENED; tone(PIN_BUZZER, 2000, 500); } else if (millis() - remindStartTime > 5UL * 60 * 1000) { // 连续提醒 5 分钟,避免半夜响个没完 state = STATE_IDLE; } break; case STATE_DRAWER_OPENED: if (btnPressed) { state = STATE_DONE; todayTaken[slotIndex(rtc.now())] = true; digitalWrite(PIN_LED_RED, LOW); digitalWrite(PIN_LED_GREEN, HIGH); } break; } }isMedicineTime里默认每格药是“一天一次”,如果你要把早中晚三格分开记,就得用todayTaken[3]这个数组按时段标记。注意第 18 行的超时判断,这个参数是给“半夜药”准备的,不能省略。连续响 5 分钟把家人吵醒,比漏一次药的体验更差。millis()溢出这个问题在 Uno 上约 49 天后发生,用millis() - remindStartTime的无符号减法可以规避,不需要手动清零。
3.3 按键去抖和舵机联动:如何判断“确定”不是误触
机械按键必须去抖。直接读引脚判断按下,大概率会在按下瞬间读到多次抖动,一次吃药记录被记成三次。常见做法是 50ms 延时确认。
void checkButton() { if (digitalRead(PIN_BUTTON) == LOW) { delay(50); if (digitalRead(PIN_BUTTON) == LOW) { while (digitalRead(PIN_BUTTON) == LOW); buttonPressed = true; } } }这个代码在按下后一直阻塞,直到松开才返回。放在loop()里没问题,但如果你的程序里还有屏幕刷新,阻塞会导致显示卡顿。更保险的做法是记录按键变化沿:上次读到高电平、这次读到低电平才视为一次按下,配合 50ms 去抖窗口。状态机里的btnPressed只在该 cycle 内使用,用完后要立刻复位为 false,否则下一次循环会重复触发STATE_DONE。
3.4 舵机控制:开锁、复位和防抖
void relaySwitch(bool openLock) { if (openLock) { servo.write(95); // 挡板从 20 度转 95 度 digitalWrite(PIN_LED_RED, HIGH); } else { servo.write(20); } delay(300); // 等舵机到位再往下执行 }舵机写入角度后不是瞬间到达,SG90 在 5V 下转 60 度大约需要 100ms。delay(300)给足余量,但注意这个 delay 会阻塞状态机。更优做法是把舵机目标角度存成全局变量,在loop()里每 20ms 调用一次servo.write,让舵机自然过渡。上电时舵机默认角度是 90 度,如果挡板在锁定位置,一上电就可能把抽屉弹开。解决方法是 setup 里先servo.attach后立即servo.write(20),让挡板先归位,再初始化状态为 IDLE。
4. 项目源码整理与文档说明:从文件结构到可复现的 README
4.1 源码目录结构怎么摆,才能让别人 5 分钟看懂
Arduino 项目的 .ino 文件可以拆成多个 tab,但很多初学者把几千行塞在一个文件里。等你需要加新功能时,光是滚动就够受的。推荐的结构是按硬件模块拆文件,而不是按步骤拆。
smart_medicine_box/ ├── smart_medicine_box.ino // 主循环和状态机 ├── pins.h // 所有引脚定义 ├── config.h // 吃药时间、提醒时长等参数 ├── scheduler.h/.cpp // 时间表判断 ├── drawer.h/.cpp // 舵机控制 ├── ui.h/.cpp // 按键和 LED/蜂鸣器 ├── README.md ├── docs/ │ └── wiring.md // 接线说明 └── hardware/ └── box.stl // 3D 打印结构图pins.h和config.h分离是必须的。引脚定义是硬件层面的,吃药时间 8:00/12:00/20:00是业务参数,两者耦合会让换板子时改错东西。.cpp和.h成对出现时,注意在.ino文件里要把自定义.h放在最上方,否则编译顺序会出问题。如果只用.h文件,里面一定要写 include guard,防止多个文件重复包含同一个头。
4.2 把接线信息写进 README 的常见做法
文档说明不是把代码注释抄一遍,而是要回答三个问题:板子选什么、线怎么接、参数改了在哪生效。README 的第一屏就该给出接线表,不要放在文末。我一般会直接在 README 里放一张精简版引脚图,再放完整版表格。代码块用 Markdown 表格会比图片更好维护,改引脚时只需改一行内容。
| 外设 | 信号引脚 | 电源 | 地线 | | --- | --- | --- | --- | | DS3231 | A4 (SDA), A5 (SCL) | 5V | GND | | SG90 舵机 | D9 (PWM) | 外部 5V | GND | | 无源蜂鸣器 | D8 | - | GND | | 微动开关 | D3 | - | GND | | 绿色/红色 LED | D6 / D7 | 经 220Ω | GND |接着写依赖库清单。不要只写“需要安装 RTClib”,要给出完整库名和在 Library Manager 里能搜到的维护者名称,这能省去别人大量找库的时间。如果用了 Servo 库,注明这是 Arduino 内置库,不需要额外安装。最后写烧录参数:板型选 Arduino NANO、处理器选 ATmega328P (Old Bootloader)、波特率 115200。老款对 Arduino Nano 如果板型选错,会一直提示avrdude: stk500_recv()错误。
4.3 硬件学习和仿真:怎么用 Wokwi 快速验证逻辑
Arduino IDE 是编辑器,不是仿真器。如果你想在没拿到硬件前先验证业务逻辑,Wokwi 是目前很成熟的仿真平台。它内置 Arduino Uno、Nano、ESP32,还能虚拟 DS3231、舵机、按键。把diagram.json和sketch.ino丢进去,就能在浏览器里跑起来。仿真药箱的玩法是:用虚拟按键模拟微动开关,用虚拟舵机看挡板动作,把实际硬件调试时间缩短一半。仿真平台里没有真实的 DS3231 电池,但模块行为是模拟的,状态机和提醒逻辑验证没问题。
要注意仿真验证不了电流和电磁干扰,所以舵机供电问题必须在真机上测。Wokwi 适合验证的是代码逻辑,不是电气可靠性。如果你用 VS Code 替代 Arduino 编辑器,配合 PlatformIO 和 Wokwi 扩展也能跑仿真,和 Arduino IDE 的差异在串口监视器的使用习惯上,不影响烧录。
4.4 版本管理和改动记录要怎么写
项目源码配套文档说明,不只是 README 一次写完。药箱项目的硬件和固件是同步迭代的:今天换了一个蜂鸣器型号,明天调整了提醒时间,这些改动不记录,三天后你自己都看不懂。常见做法是在docs/CHANGELOG.md里按日期记录每一次改动。不用长句描述,写成“v0.3:提醒 5 分钟后不再重复,改为 4 小时后再响一次”这种可回滚的说明。改动记录不是给人看的,是给未来要加功能的自己看的。
5. 智能药箱调试技巧:上传超时、舵机复位和长期运行验证
5.1 Arduino 上传超时的排查顺序
上传时报avrdude: stk500_recv(): programmer is not responding,多数不是代码问题,而是环境问题。按这个顺序查:第一,工具-端口是否选了正确的串口,Windows 下如果板子插了没反应,先装 CH340 驱动;第二,工具-处理器是否选对,Arduino Nano 的 Old Bootloader 和 New Bootloader 不匹配会稳定复现这个报错;第三,按住板子上的复位键,点击上传,等 IDE 提示Uploading...时松手,手动绕过 bootloader 超时窗口。
5.2 舵机复位和卡死处理
舵机在药箱里最典型的故障是:第一次开锁正常,第二次开锁挡板只动一半,然后就嗡嗡响。这不是代码 bug,是物理结构和供电的问题。一是舵机臂和挡板之间的连接太紧,舵机因为堵转触发过热保护。二是两个舵机动作间隔太短,电源来不及恢复。代码层面要做的,是每次舵机动作之间至少留 500ms 间隔,并在开锁之前先写入一次servo.write(20)让舵机“醒”一下,防止上次停在中途。另一个技巧是让挡板的初始角度和复位角度留出 5 度空隙,避免舵机每次复位时都撞物理限位。
5.3 连续运行验证:把 24 小时压缩到 5 分钟
智能药箱最难验证的是长期可靠性,你不能真的等三天看它有没有漏。常见做法是加一个测试模式:把config.h里的REMIND_INTERVAL_MINUTES从 12 小时改成 5 分钟,然后让药箱连续跑 200 次提醒循环,同时把每次的todayTaken写进 Serial 输出。我一般会写一个脚本看串口日志,统计TAKEN和MISSED的数量,如果在压缩时间下连续跑 200 次没有一次状态错乱,再把参数改回真实值烧录。这个测试模式要留在config.h里不要删,因为它不仅验证代码,还验证了舵机寿命和滑轨结构在反复动作下不会卡住。
最后再检查一遍:提醒时间是 RTC 的,间隔计数是millis()的,状态切换是状态机的,三者不要混用。很多人最后出 bug,就是把todayTaken数组用 reset 函数在每天零点清空,却又依赖millis()判断零点,结果板子跑了 49 天millis()溢出后,清空逻辑就失灵了。统一用rtc.now()获取日期做每日重置,才是药箱可靠运行的前提。
本文还有配套的精品资源,点击获取