基于Espressif ESP32微控制器构建实时房间监控系统
在服务器机房、药品存储柜和温室等日常设施中,管理人员必须密切监测环境条件。服务器机房若温度过高,可能导致设备突然停机;药品储藏柜若温度降至冰点以下,将导致胰岛素和疫苗变质。为避免此类问题,设施通常会使用监控箱来监测温度、湿度、光照和运动情况,一旦环境条件变得不安全,便会自动发出警报。
本项目在ESP32微控制器上实现了一个环境房间监控器,使用原生ESP-IDF C/C++语言和FreeRTOS系统。该系统可测量房间内的环境参数,通过旋转旋钮在OLED屏幕上显示读数;当温度超过设定阈值时,会触发2000赫兹的硬件蜂鸣报警;此外,在静止15秒后,显示屏将自动关闭以节省能源。
动机
大多数初学者教程都会在单个循环(通常称为超级循环或 while(1) 循环)中构建微控制器项目。在这种结构中,处理器按顺序依次执行任务:读取传感器、等待、更新屏幕、等待、检查报警。
然而,当连接使用低速通信协议的传感器时,这种简单的循环就会出现问题:
•读取数字 DHT22 传感器需要处理器对电脉冲进行定时,最长可达 25 毫秒。
•通过 I2C 总线刷新 SSD1306 OLED 屏幕又需要额外 10 到 15 毫秒。
•标准的 delay() 调用会导致时间漂移,因为延迟只有在传感器读取完成后才会开始。
如果用户在处理器忙于执行这些程序中的某项任务时,转动旋钮或经过运动传感器,处理器就会被卡住,完全错过电信号,导致设备反应迟缓、无响应。
为了解决这一问题,本项目将程序拆分为六个独立的 FreeRTOS 任务。实时调度器会为处理最紧急任务的任务分配 CPU 时间,并在用户输入或检测到运动时暂停较慢的后台任务。
功能
•通过基于紧急程度的抢占式调度,确保六个独立的FreeRTOS任务能够及时响应,从而避免用户输入和报警延迟。
•使用vTaskDelayUntil()实现无漂移的周期性采样,使传感器读数严格按2.0秒间隔定时更新。
•采用带类型约束的FreeRTOS队列进行安全的任务间通信,而非使用未受保护的全局变量。
•通过FreeRTOS互斥锁(serialMutex)保护串行诊断输出,防止打印信息相互混杂。
•利用事件组(systemEvents)进行状态信号传递,用于运动检测和屏幕超时标志。
•通过物理旋转编码器旋钮实现四页屏幕导航,并支持双向连续循环。
•当温度低于18.0°C或高于30.0°C时,ESP32硬件PWM定时器发出2000 Hz的音频温度警报。
•具备节能状态机:在无运动15.0秒后关闭OLED屏幕,一旦检测到运动即立即唤醒。
•在主机PC上使用Unity执行13个自动化单元测试,验证报警计算、导航逻辑及状态转换。
组件
该系统使用可安装在物理面包板上的组件,或可在Wokwi中进行仿真:
电路
所有传感器和模块共用一个地线(GND),并从ESP32的3.3V电源引脚供电:
图1:Wokwi电路原理图及硬件面包板接线。
系统架构
良好的嵌入式架构将物理硬件驱动程序、操作系统通信工具和计算逻辑分别划分为不同的层次。这种结构有助于代码的维护,并允许在没有实际硬件的情况下,于计算机上测试计算规则。
该项目分为三个层次:
•硬件驱动层(sensors.cpp、display.cpp、input.cpp、motion.cpp、alarm.cpp):直接与ESP-IDF硬件驱动程序交互,用于GPIO引脚、ADC1、I2C和PWM。
•FreeRTOS同步层(rtos_objects.cpp、main.cpp):创建并管理队列、互斥锁、事件组以及任务生命周期。
•纯决策逻辑层(alarm_logic.cpp、display_mode.cpp、system_state.cpp):包含纯粹的计算函数,不依赖硬件头文件,因此可在自动化单元测试套件中完全进行测试。
图2:三层系统架构,显示物理传感器、ESP-IDF驱动程序以及通过IPC队列连接的FreeRTOS任务。
FreeRTOS架构
系统将职责分配给六个任务:
为什么任务优先级重要
说明:在FreeRTOS中,数字越大表示优先级越高。处理器将始终运行优先级最高的就绪任务。优先级根据任务的紧急程度和执行时间来确定:
•优先级3(最高:输入任务和运动任务):用户旋钮点击和动作脉冲时间很短(仅持续几毫秒)。如果处理器无法立即处理这些信号,信号就会被传递,用户会感觉点击未响应。由于这些任务的处理时间不到0.1毫秒,将其设为优先级3可确保即时响应,同时不延迟其他任务。
•优先级2(中等:报警任务和状态任务):评估温度限制以及检查15秒无操作超时是系统安全相关的任务。它们的执行时间少于0.5毫秒,运行迅速,且能释放CPU时间。
•优先级1(最低:传感器任务和显示任务):通过I2C更新OLED显示屏需要10到15毫秒,读取DHT22传感器则需要25毫秒。如果将显示任务设置为高优先级,那么15毫秒的屏幕更新将导致编码器旋钮无法响应,从而错过用户的点击操作。因此,将显示任务设为优先级1,可让旋钮转动随时中断屏幕绘制。
图3
它是如何工作的
无漂移的周期性采样
SensorTask 不使用 vTaskDelay(),因为该函数会相对于调用时点产生延迟并导致时间漂移。而是使用 vTaskDelayUntil(),该函数根据上一次唤醒的时间戳计算绝对的时钟目标值。即使 DHT22 读取一次需要 10 毫秒,下一次需要 25 毫秒,SensorTask 仍会在精确的 2.0 秒间隔内唤醒。
队列安全通信
•sensorQueue(容量 5):将 SensorTask 的完整传感器读数发送到 DisplayTask。
•alarmQueue(容量 1):使用 xQueueOverwrite() 将最新的温度值发送给 AlarmTask。如果报警任务暂时繁忙,只需获取最新数值即可判断当前是否需要响铃。
•displayModeQueue(容量 1):将 InputTask 的当前页面更新信息发送到 DisplayTask。
•encoderQueue(容量 16):从中断例程接收转向方向,并传递给 InputTask。
事件组信号
systemEvents 包含两个位标志:
•BIT0(EVENT_MOTION):当 PIR 传感器检测到运动时设置为 1,运动停止时设置为 0。
•BIT1(EVENT_INACTIVE):StateTask 在连续 15 秒无运动后设置为 1,通知显示屏清空内容。
互斥锁保护
所有任务共享一个UART串行端口。SensorTask、AlarmTask和StateTask在打印前会先获取serialMutex,并在完成打印后释放,从而防止终端上的打印内容相互交错或混乱。
无活动状态机
系统运行于两种状态:
•ACTIVE:OLED屏幕亮起,每2秒更新一次遥测数据,旋转编码器在不同屏幕之间切换。
•INACTIVE:当连续15.0秒无运动时,系统进入INACTIVE状态并关闭OLED屏幕以节省功耗并保护硬件。一旦有人经过PIR传感器,系统立即切换回ACTIVE状态,屏幕随即亮起。
图4:能源管理状态机转换示意图。
测试与验证
在PlatformIO的开发计算机上使用Unity对纯计算逻辑进行了测试(13项通过):
图6:自动化Unity主机单元测试套件在原生环境中全部13个测试用例均通过。
结果:
•test_alarm(5项测试):确认17.9°C触发低报警,18.0°C正常,24.0°C正常,30.0°C正常,30.1°C触发高报警。
•test_display_mode(4项测试):验证顺时针导航(温度→湿度→光照→运动→温度)以及逆时针导航,双向循环显示。
•test_system_state(4项测试):验证系统在14.99秒后保持活动状态,在15.00秒后进入非活动状态,15.00秒后仍保持非活动状态,且运动可覆盖超时功能。
Wokwi功能验证(FT-01至FT-10)
在Wokwi模拟器中,所有十个要求均得到验证:
•温度和湿度滑块在OLED屏幕上2秒内更新(FT-01、FT-02)。
•光敏电阻滑块将原始ADC数值映射为0%到100%的光照等级(FT-03)。
•旋转旋钮可清晰地向前和向后翻页(FT-04、FT-05)。
当温度低于18°C或高于30°C时,蜂鸣器发出2000 Hz频率的提示音;在21.4°C时停止鸣响(FT-06、FT-07)。
系统在检测到运动时保持活跃,静止14.98秒后屏幕变黑,并在检测到新运动时立即唤醒(FT-08、FT-09、FT-10)。
故意故障实验
CPU饥饿:移除stateTask中的vTaskDelayUntil()后,导致其占用Core 0的100%,使空闲任务陷入饥饿状态,并在5.2秒时触发ESP-IDF任务看门狗定时器崩溃。这证明了在抢占式系统中,每个任务都必须阻塞或让出CPU。
优先级偏移:将stateTask的优先级提升至5级并未导致其他任务饥饿,因为它执行时间不足0.2毫秒,且仅休眠50毫秒,表明阻塞持续时间与优先级同等重要。
互斥锁移除:移除serialMutex后,编码器快速转动和温度警报发生冲突,导致串行线上出现乱码。
演示
系统在Wokwi中持续运行,向串行控制台输出清晰的遥测数据,并在OLED屏幕上逐次显示一个测量页面。
图5:实时操作演示,展示活跃的OLED显示页面、传感器读数以及FreeRTOS实时日志。
遇到的挑战
Flash大小配置:PlatformIO默认使用2MB Flash设置,而模拟的ESP32板拥有4MB Flash。此问题通过更新sdkconfig.esp32dev文件中的CONFIG_ESPTOOLPY_FLASHSIZE="4MB"以及flasher_args.json来解决。
已弃用的LEDC中断标志:在较新的ESP-IDF版本中,编译器将channelConfig.intr_type = LEDC_INTR_DISABLE标记为已弃用。移除该行后,警告问题得以解决,因为驱动程序默认会禁用中断。
本文编译 自hackster.io





