构建一个多传感器房间监控系统:用于实时追踪温度、湿度、光照和运动,并具备报警和睡眠模式功能
本项目是使用STM32F103C8“Blue Pill”芯片构建的房间监测装置,并在Wokwi中进行仿真。它可测量温度、湿度、环境光强和运动状态,通过OLED显示屏逐次显示一次读数,当房间温度过高或过低时会发出蜂鸣声,当房间无人时则自动关闭显示屏。
该项目由MSU-IIT的BCA182嵌入式系统编程课程(实验活动第1号)开发,采用PlatformIO、STM32Cube HAL以及原生FreeRTOS API实现。
动机
房间监测器需要以不同速度完成多项任务:每隔几秒采样一次传感器,当有人移动或转动旋钮时立即响应,并在温度超出范围时发出警报。将所有这些操作写成一个大循环,会导致时间安排难以理清。本项目的目标是学习实时操作系统(RTOS)如何解决这一问题:将任务拆分为多个子任务,根据各任务的响应速度设定优先级,并确保各任务之间安全通信。
功能特性
- 每2秒从DHT22读取一次温湿度
- 通过ADC从光敏电阻读取相对环境光照强度(0-100%)
- 使用PIR传感器检测运动
- SSD1306 OLED显示选定的测量数据
- 旋转编码器用于切换页面:温度、湿度、光照、运动(双向,带循环显示)
- 温度低于18°C或高于30°C时触发蜂鸣报警
- 活动/非活动状态机:无运动15秒后OLED关闭,再次有运动时恢复唤醒
- 6个FreeRTOS任务、3个队列、1个事件组、1个互斥锁,以及用于周期性采样的vTaskDelayUntil()
- 13个单元测试及使用cppcheck进行静态分析
电路
引脚连接:
•光敏电阻 AO 连接到 PA0(ADC1)
•DHT22 数据连接到 PA1
•蜂鸣器连接到 PA2(TIM2 PWM,1 kHz)
•PIR 输出连接到 PA3
•编码器时钟/数据信号连接到 PA4(中断)/ PA5
•串口发送/接收连接到 PA9 / PA10(USART1,115200)
•OLED SCL/SDA 连接到 PB6 / PB7(I2C1)
系统架构
四个输入(DHT22、光敏电阻、PIR和编码器)接入STM32,该芯片运行FreeRTOS系统,包含六个任务,并驱动三个输出:OLED显示屏、蜂鸣器以及串口日志。
代码被划分为多个模块:
•驱动模块与硬件通信(dht22.c、ldr.c、ssd1306.c、buzzer.c、log.c);
•各任务文件负责独立的任务(sensors.c、display.c、input.c、alarm.c、motion.c、system_state.c);
•logic.c中包含决策逻辑(如报警、页面导航、激活/非激活状态),不涉及硬件代码,因此可在PC上进行单元测试;
•main.c仅初始化硬件并调用app_main()函数,该函数创建所有任务。
FreeRTOS架构
任务及优先级(数值越高表示越紧急):
•MotionTask(优先级3,每100毫秒一次):读取PIR信号并触发运动检测;
•InputTask(优先级3,每50毫秒一次):将编码器的步数转换为页面切换;
•StateTask(优先级3,当检测到运动或每250毫秒一次):运行激活/非激活状态机;
•SensorTask(优先级2,每2秒一次,使用vTaskDelayUntil):读取DHT22和光敏电阻数据;
•AlarmTask(优先级2,每次新读取时触发):检查温度并控制蜂鸣器;
•DisplayTask(优先级1,每当有新数据或页面变化时执行):唯一一个绘制OLED内容的任务。
为何这些任务具有最高优先级:对人有反应的任务(如运动检测、编码器、状态变化)被赋予最高优先级,因为延迟的响应会明显察觉。传感器和报警任务以2秒为周期运行,因此几毫秒的延迟并不重要。OLED绘图是速度最慢且最不紧急的任务,因此其优先级最低,从不延迟用户输入。
任务之间的通信方式:
队列机制:SensorTask 通过两个队列将 SensorData 结构体(温度、湿度、光照、运动)发送给 DisplayTask 和 AlarmTask。InputTask 通过第三个队列将选定页面发送给 DisplayTask。每个队列仅存储最新的数据值。
事件组:用于表示 ACTIVE(激活)、MOTION(运动)、ALARM(报警)和 PIR 级别(PIR 指纹)。StateTask 设置 ACTIVE 状态,而显示、输入和报警任务均读取该状态。
互斥锁:保护串行端口,所有六个任务都使用该端口发送日志信息,因此两个任务永远不会互相覆盖打印。
工作原理
每2秒,SensorTask读取DHT22和光敏电阻的数据,并通过队列发送结果。它使用vTaskDelayUntil()函数,确保读取数据间隔精确为2秒,避免漂移。
DisplayTask等待新数据,并在OLED上显示所选页面。
转动编码器会触发一个短暂中断,仅用于计数步数。InputTask读取该计数值并更新页面。
AlarmTask检查每次新的温度值。当温度低于18°C或高于30°C时,蜂鸣器响起,OLED显示"! TEMP ALARM"。
MotionTask监控PIR传感器。若15秒内无运动,StateTask将状态切换为INACTIVE:OLED关闭,编码器被忽略,蜂鸣器静音。下一次检测到运动后,状态将恢复为ACTIVE。
测试与验证
单元测试:在PC上运行了13个测试,使用“pio test -e native”执行,全部通过。测试内容包括报警阈值(含精确18°C和30°C)、双向循环的页面导航,以及四种ACTIVE/INACTIVE状态的验证。
静态分析:“pio check”(cppcheck)未发现高风险或中等风险问题。低风险发现已审查并解释说明。大多数是误报,因为该工具无法识别通过FreeRTOS或中断表调用的函数。
在Wokwi中的功能测试:全部10项测试均通过。这些测试包括改变温度、湿度和光照,双向旋转编码器,当温度超过30°C时触发警报并再次关闭,以及系统在15秒后进入非活动状态,并在检测到运动时唤醒。
故障实验:我故意破坏了系统以观察会发生什么:
移除MotionTask中的延迟导致其占用CPU,其他所有任务停止运行。
为DisplayTask设置最高优先级在此模拟器构建中未出现明显问题,但意味着屏幕重绘可能会延迟用户输入。
移除互斥锁(mutex)在当前模拟器版本中没有导致输出混乱,但在真实硬件上使用标准端口时打印操作将不安全,因此保留了互斥锁。
演示
运行系统:OLED 显示温度并配有报警横幅,串口日志则显示任务活动情况。
遇到的挑战
最大的挑战是让 FreeRTOS 能够在 Wokwi 上正常运行。固件已构建并启动,但没有任何任务运行。通过在串口上打印 FreeRTOS 的断言信息,我发现标准的 Cortex-M 端口依赖了一些 Wokwi STM32F103 模型不完全支持的处理器特性:中断优先级位,以及 FreeRTOS 用于切换任务的 SVC/PendSV 异常。
最终版本使用了由同学项目改编的、兼容 Wokwi 的 FreeRTOS 版本(下文注明)。我在其中修复了另外两个问题:现在它以模拟器中有效的方式阻断中断,并且单独保存每个任务的临界区计数,从而避免了系统冻结。
其他问题:DHT22 在每次读取时都会失败,直到将数据引脚设置为带上拉电阻的输入模式;此外,蜂鸣器需要移至另一个引脚,因为其原始引脚被 RTOS 定时器占用了。
本文编译自hackster.io





