ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Arduino的智能药箱:从硬件搭建到状态机闭环的嵌入式实践

基于Arduino的智能药箱:从硬件搭建到状态机闭环的嵌入式实践 简介面向电子、计算机等相关专业学生与开发者的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 舵机D9PWM、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__)把字符串放到 flashArduino 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. 项目源码整理与文档说明从文件结构到可复现的 README4.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()获取日期做每日重置才是药箱可靠运行的前提。本文还有配套的精品资源点击获取
返回列表