构建一个小型房间监控器:监测温度、湿度、光照和运动状态
这是一个小型房间监控器。它可实时监测温度、湿度和光照,并检测房间内是否有人。每次只显示一个读数在 OLED 上,通过旋转编码器切换不同页面。当温度超出 18–30°C 范围时,蜂鸣器会发出提示音。若连续 15 秒无人移动,屏幕将自动关闭,感应功能暂停,直到有人进入房间后才会重新启动。
该系统运行在 STM32F103C8 “Blue Pill” 微控制器上,基于 FreeRTOS 操作系统,使用 STM32Cube HAL 驱动,不包含 Arduino 代码。整个项目完全在 Wokwi 模拟器中完成开发与测试,未使用任何实体电路板。
四个显示页面:温度、湿度、光照和运动。角落的数字表示当前页面位置。
我为何制作这个项目
本次设计要求从零开始构建一个真正支持并发的 FreeRTOS 系统,而非单一的大循环结构,并辅以单元测试、静态分析和文档记录。
正是这一限制,使该项目变得尤为有趣。单个循环对于少量传感器来说运行良好,但它掩盖了真实产品需要回答的问题:
•当显示屏变慢时会发生什么?
•哪些任务可以被允许等待?
•屏幕属于谁?
•报警系统如何得知房间为空?
将工作拆分为多个任务,迫使每个问题都必须得到明确的回答。
同样的模式也出现在实际产品中:周期性感知、显示、报警以及由占用状态驱动的休眠模式。例如温控器面板、会议室显示屏,或仓库和温室的气候监测设备。
电路连接
各部分如何与蓝色芯片(Blue Pill)连接:
•DHT22(温度和湿度)→ PB12,单线协议
•光敏电阻模块 → PA0,ADC
•PIR运动传感器 → PA3
•KY-040旋转编码器 → PA1(中断)和PA2
•SSD1306 OLED → PB6/PB7,I2C 地址 0x3C
•被动蜂鸣器 → PA6,由定时器 TIM3 的 2 kHz PWM 信号驱动
•串行日志 → PA9/PA10,波特率 115200 baud
它是如何工作的?
第六个任务 ProcessingTask 是来自课程练习中避免忙循环的遗留部分。我根据任务必须响应的速度来设置其优先级,而不是根据它看起来“重要”的程度:
MotionTask 和 InputTask 会响应人类活动,因此它们排在前面。每个任务每次事件仅执行微秒级别的操作,因此先运行它们几乎不会消耗其他资源。
DisplayTask 最后运行。人眼无法察觉屏幕延迟数毫秒的情况。此外,每次屏幕更新都会让 CPU 占用一段时间(硬件上约 25 毫秒,Wokwi 上约 160 毫秒),而以最低优先级运行时,它永远无法延迟其他任何任务。
任务之间的通信方式
队列用于传递数据。SensorTask 将每次读取的数据同时发送给 AlarmTask 和 DisplayTask。FreeRTOS 队列将每个项目只交给一个接收者,因此每个消费者都拥有自己的队列。
编码器中断保持极小。它只需将 +1 或 −1 入队并返回即可。InputTask 然后计算页面的切换。
使用队列可避免轮询。DisplayTask 同时等待读取、页面变化和状态变化;AlarmTask 则等待读取和状态变化。每当任一队列有数据时,它们就会立即被唤醒。
一个事件组广播多个任务读取的状态:系统是否处于活动状态、是否存在运动,以及警报是否被触发。
一个互斥锁(mutex)保护着所有任务写入的串行端口,防止一个任务的日志行被另一个任务截断。由于是互斥锁而非普通的信号量(semaphore),因此FreeRTOS的优先级继承机制得以发挥作用:高优先级任务在等待低优先级任务时,不会被中间的任务阻塞。
当房间为空时进入睡眠状态:
- OLED屏幕关闭;
- SensorTask停止读取;
- 铃声静音;
- 编码器被忽略。
MotionTask持续采样,下一次运动将重新唤醒并恢复全部状态。
几个值得仔细注意的细节:
- 无漂移现象。SensorTask使用vTaskDelayUntil()函数,该函数会安排每次从上一次唤醒后的时间间隔。如果使用普通延迟,读取时间会不断累积到每个周期中,导致采样值逐渐漂移。
- DHT22传感器不依赖定时器。传感器通过信号保持高电平的时间来编码每一位数据。驱动程序无需硬件定时器进行计时,而是统计每次高电平期间的循环次数,并将其与之前固定长度的低电平进行比较。这种方式适用于任意时钟频率。调度器在响应所需5毫秒期间被暂停,因此没有任何任务可以中断它。
真实光照读数。光照强度以传感器量程的0–100%表示,并未转换为勒克斯(lux),因为这需要进行校准,而我目前没有该功能。
决策与硬件分离。报警阈值、蜂鸣器规则、页面导航和睡眠定时器均为纯函数,不包含任何硬件或FreeRTOS代码,这使得它们易于单元测试。
测试与验证
单元测试:共23个测试在一台装有pio测试的PC上运行。这些测试覆盖了所有报警阈值(17.9、18.0、25.4、30.0和30.1°C)、蜂鸣器规则、包含循环的页面导航以及睡眠定时器。为了证明测试能够发现实际错误,我在代码副本中植入了八个真实漏洞;每个漏洞至少导致一个测试失败。
静态分析:pio检查(Cppcheck)最初报告了67项低严重性问题。其中大多数为误报,原因是Cppcheck一次只检查一个文件,未能识别文件之间的调用。我修复了真正的问题,并记录了其余部分。目前该工具未再报告任何缺陷。
在Wokwi中:我运行了功能测试,涵盖温度、湿度、光照、编码器方向、报警、15秒超时以及唤醒功能。串行日志显示了每项结果。
挑战
Wokwi 的 CPU 并不是 Cortex-M3。它的型号跳过了 svc 指令,忽略中断屏蔽,并以一种方式无限循环地触发上下文切换。这三种情况都破坏了 FreeRTOS 标准的 Cortex-M3 驱动。我改用了 Cortex-M0 驱动,该驱动在 M3 上运行正常,同时添加了一个小的绕过方案来解决上下文切换触发问题。
OLED 从未启动。后来发现时钟设置失败且悄无声息:Wokwi 始终未报告内部振荡器为“就绪”。HAL 在中途放弃,还把外设总线速率设置为 1/16,对显示屏的 I2C 来说太慢。我通过串口打印实际时钟值发现了这个问题,并通过仅设置总线分频器来修复。
在空房间中静音报警。一旦检测到暂停,就不再有读数到达,因此正在响铃的警报将永远不会收到停止指令。给 AlarmTask 设置一个接收状态变化的队列后,问题得以解决。
本文编译自hackster.io





