拓冰建站拓冰建站
首页 / 资讯中心 / 正文

基于STM32与FreeRTOS的多传感器房间监测系统设计

1. 项目缘起与整体设计思路1.1 为什么我要做这个多传感器房间监测系统去年年底搬进新租的房子朝北的小卧室一到冬天就阴冷潮湿。有段时间早上起来总觉得嗓子干、头昏后来买了个温湿度计才发现晚上关窗睡觉后湿度能飙到75%以上CO₂浓度更是没法看。市售的成品监测仪要么功能太单一要么数据只能本地看想做个联动控制还得买它全家桶。作为一个常年跟STM32打交道的人我决定自己撸一个。这个项目的目标很明确用一块STM32做主控挂载温湿度、空气质量、光照等多路传感器跑FreeRTOS做多任务调度OLED做本地实时显示同时预留串口和无线模块接口方便后续扩展。整套东西要能7×24小时稳定运行成本控制在百元以内代码结构要清晰到以后想加传感器直接插上去改几行就行。说白了这就是一个典型的嵌入式多传感器采集与实时显示系统。它解决的核心问题是把分散的、不同接口协议的传感器数据统一采集、处理、显示并且保证实时性和稳定性。适合谁看正在学STM32和FreeRTOS的嵌入式新手、需要做课程设计或毕业设计的同学、以及想自己动手做智能家居监测的DIY爱好者。只要你懂基本的C语言、会用Keil或VS Code跟着走就能复现。1.2 技术选型背后的取舍逻辑选型这件事我踩过坑也纠结过最后定下来的方案是STM32F103C8T6 FreeRTOS PlatformIO 0.96寸OLED。下面说说为什么这么选。主控选F103C8T6理由很实在。它是Cortex-M3内核72MHz主频64KB Flash、20KB RAM对于跑FreeRTOS加几个传感器任务来说绰绰有余。关键是便宜一片十几块钱资料铺天盖地出了问题搜一下基本都能找到答案。有人会问为什么不选F4或者G系列性能更强。我的看法是这个项目不需要浮点运算密集型处理F1完全够用多花的钱不如省下来买个好点的传感器。而且F103的HAL库和标准库都极其成熟FreeRTOS移植资料也最多对新手最友好。RTOS选FreeRTOS这是嵌入式领域用得最广的实时操作系统没有之一。它的内核源码精简任务调度、队列、信号量、事件组这些机制都有而且移植到Cortex-M3的代码是现成的。用RTOS的好处是传感器采集、数据处理、OLED刷新、串口输出可以拆成独立任务各自按自己的周期运行互不阻塞。如果不用RTOS你就得在一个大循环里手动管理时序加一个传感器就要改一遍逻辑后期维护很痛苦。开发环境选PlatformIO而不是Keil这个选择可能有人不认同。Keil确实是STM32开发的经典工具但它的编辑器体验、代码补全、版本管理都太落后了。PlatformIO基于VS Code代码补全、Git集成、库管理都是一流的而且跨平台。唯一的问题是PlatformIO创建工程时下载依赖包比较慢这个后面我会讲怎么解决。至于网上有人问“stm32 编程 platformio vs code”好不好用我的回答是一旦用顺了回不去Keil。显示选0.96寸OLEDSSD1306驱动IIC接口。为什么不用LCDOLED自发光、对比度高、功耗低、体积小而且IIC只需要两根线接线简单。0.96寸128×64的分辨率显示几行传感器数据完全够用。价格也便宜十几块钱一片。唯一要注意的是市面上有些OLED模块质量参差不齐会出现“oled 0.96批量点不亮”的情况这个后面排查章节会详细说。2. 核心细节解析与实操要点2.1 传感器选型与接口协议解析这个项目我用了三类传感器覆盖环境监测的核心指标。温湿度传感器选DHT22AM2302。它用单总线协议一根数据线就能读温湿度精度是±0.5℃和±2%RH对于室内监测足够。有人会推荐SHT30IIC接口精度更高但价格贵一倍多。DHT22的缺点是读取周期不能太快两次读取之间至少要间隔2秒否则数据会出错。这个特性直接影响了任务周期的设计。空气质量传感器选MQ-135。它输出模拟电压需要接STM32的ADC引脚。MQ-135对NH3、NOx、CO2等都有响应虽然不能精确测量某种气体的浓度但用来判断空气质量趋势完全够用。它需要预热刚上电时读数不准一般要预热24小时才能稳定。这个坑我踩过刚上电看到数值乱跳以为坏了其实是正常现象。光照传感器选BH1750。IIC接口直接输出lux值精度高、响应快。它和OLED可以挂在同一条IIC总线上地址不冲突BH1750默认0x23OLED默认0x3C。这里要注意IIC总线的上拉电阻很多模块自带上拉如果多个模块都带上拉并联后阻值会变小可能导致通信失败。我一般只保留一处的上拉其他模块的上拉电阻拆掉。关于“嵌入式 5种通信协议”这个项目实际用到了三种单总线DHT22、IICOLED和BH1750、ADCMQ-135。如果后续加无线模块还会用到UART或SPI。理解这些协议的时序和电气特性是嵌入式开发的基本功。2.2 FreeRTOS任务划分与优先级设计任务划分是这个项目的核心设计点。我的原则是按实时性要求分优先级按数据流向分任务。系统一共创建了5个任务任务名称优先级周期职责SensorTask32秒读取DHT22、BH1750、MQ-135DisplayTask2500ms刷新OLED显示CommTask21秒串口输出传感器数据MonitorTask15秒检查系统状态、堆栈使用LedTask11秒心跳灯闪烁SensorTask优先级最高因为传感器读取有时序要求尤其是DHT22的单总线协议读取过程中不能被长时间打断。DisplayTask和CommTask优先级相同它们只消费数据不产生数据晚一点执行没关系。MonitorTask和LedTask优先级最低属于辅助功能。任务之间通过队列传递数据。SensorTask读完后把数据打包成一个结构体发送到数据队列。DisplayTask和CommTask都从这个队列接收数据。这里有个细节如果两个任务都从同一个队列接收一条消息只会被其中一个任务收到。所以我的做法是创建两个队列SensorTask往两个队列各发一份或者用事件组通知让消费任务自己去读全局变量。我选的是后者用事件组更省内存。关于“freertos 队列”和“freertos二值信号量”的使用场景我的经验是队列用于传递数据信号量用于同步状态。比如DHT22读取完成后可以用二值信号量通知DisplayTask数据已更新。但队列本身就有同步功能所以这个项目里信号量用得不多。2.3 OLED驱动与显示布局设计OLED驱动我用的HAL库IIC方式没有用硬件IIC而是用软件模拟IIC。为什么因为STM32F103的硬件IIC有已知的bug在某些情况下会死锁。软件模拟IIC虽然速度慢一点但稳定可靠而且引脚可以任意指定。网上搜“hal库驱动oled代码”能找到很多现成的但质量参差不齐建议自己照着SSD1306数据手册写一遍理解命令和数据的区别。显示布局我设计成四行Temp: 24.5C Humi: 58% AQI: 120 Lux: 350 Status: OK Time: 00:12:34第一行显示温湿度第二行显示空气质量和光照第三行显示系统状态第四行显示运行时间。字体用的是6×8和8×16两种标题用大字体数值用小字体。这里要注意OLED的显存布局SSD1306的GDDRAM是分页的128×64分成8页每页8行。写数据时要先设置页地址和列地址再写数据。关于“oled屏幕动画展示”如果想做开机动画或者动态效果可以用取模软件生成多帧图片然后快速切换。但要注意刷新率IIC速率有限全屏刷新一次大概需要20-30ms动画帧率不要超过30fps否则会卡顿。3. 实操过程与核心环节实现3.1 PlatformIO工程创建与依赖配置先说PlatformIO创建工程慢的问题。这是因为PlatformIO默认从国外服务器下载平台包和工具链网络不好的时候确实很慢。我的解决办法是第一次创建工程时耐心等它下载完之后所有工程都复用这些包就不会再慢了。另外可以在PlatformIO的设置里把“Check for updates”关掉减少不必要的网络请求。创建工程的步骤在VS Code中打开PlatformIO Home点击“New Project”Name填“RoomMonitor”Board选“STM32F103C8 (20k RAM. 64k Flash)”Framework选“STM32Cube”点击Finish等待工程创建完成工程创建好后打开platformio.ini添加依赖库[env:bluepill_f103c8] platform ststm32 board bluepill_f103c8 framework stm32cube upload_protocol stlink monitor_speed 115200 lib_deps adafruit/Adafruit SSD1306^2.5.7 adafruit/Adafruit GFX Library^1.11.5 claws/BH1750^1.3.0 marcoschwartz/DHT22^1.0.0这里我用了Adafruit的SSD1306库和GFX图形库BH1750和DHT22也有现成的库。用库的好处是开发快坏处是代码体积大、不够灵活。如果你Flash紧张可以自己写驱动。3.2 FreeRTOS移植与任务创建在PlatformIO中使用FreeRTOS需要在platformio.ini中添加build_flags -D USE_HAL_DRIVER -D STM32F103xB -D USE_FREERTOS然后在代码中包含FreeRTOS头文件#include FreeRTOS.h #include task.h #include queue.h #include event_groups.h创建任务的代码xTaskCreate(SensorTask, Sensor, 256, NULL, 3, NULL); xTaskCreate(DisplayTask, Display, 512, NULL, 2, NULL); xTaskCreate(CommTask, Comm, 256, NULL, 2, NULL); xTaskCreate(MonitorTask, Monitor, 128, NULL, 1, NULL); xTaskCreate(LedTask, Led, 128, NULL, 1, NULL); vTaskStartScheduler();堆栈大小单位是word256word就是1KB。DisplayTask堆栈给512word因为OLED库函数调用层次深堆栈小了会溢出。关于“freertos堆栈溢出检测”可以在FreeRTOSConfig.h中开启#define configCHECK_FOR_STACK_OVERFLOW 2然后实现vApplicationStackOverflowHook函数在堆栈溢出时打印任务名并复位。3.3 传感器数据采集实现DHT22的读取代码void DHT22_Read(float *temp, float *humi) { uint8_t data[5] {0}; // 主机拉低至少1ms然后释放 HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_RESET); vTaskDelay(pdMS_TO_TICKS(2)); HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_SET); // 等待DHT22响应 // ... 时序代码省略 *humi (data[0] 8 | data[1]) / 10.0f; *temp ((data[2] 0x7F) 8 | data[3]) / 10.0f; if (data[2] 0x80) *temp -*temp; }这里的关键是时序DHT22对时间很敏感拉低和释放的时间要精确。用vTaskDelay只能精确到tick如果tick是1ms那拉低2ms没问题。但等待响应的时候需要用微秒级延时可以用DWT_Delay_us或者空循环。BH1750读取就简单多了IIC读两个字节void BH1750_Read(float *lux) { uint8_t data[2]; BH1750_Start(); BH1750_SendByte(BH1750_ADDR 1); BH1750_WaitAck(); BH1750_SendByte(0x10); // 连续高分辨率模式 BH1750_WaitAck(); BH1750_Stop(); vTaskDelay(pdMS_TO_TICKS(180)); // 读取数据 // ... *lux (data[0] 8 | data[1]) / 1.2f; }MQ-135用ADC读取void MQ135_Read(uint16_t *adc) { HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 100); *adc HAL_ADC_GetValue(hadc1); HAL_ADC_Stop(hadc1); }ADC值需要转换成电压再根据传感器特性曲线估算空气质量。我简化处理直接用ADC值分档0-1000优1000-2000良2000-3000中3000以上差。3.4 OLED显示刷新实现OLED初始化void OLED_Init(void) { OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0xD5); // 设置时钟分频 OLED_WriteCmd(0x80); OLED_WriteCmd(0xA8); // 设置多路复用率 OLED_WriteCmd(0x3F); OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0x40); // 设置起始行 OLED_WriteCmd(0x8D); // 电荷泵设置 OLED_WriteCmd(0x14); OLED_WriteCmd(0x20); // 内存寻址模式 OLED_WriteCmd(0x00); OLED_WriteCmd(0xA1); // 段重映射 OLED_WriteCmd(0xC8); // 扫描方向 OLED_WriteCmd(0xDA); // COM引脚配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0x81); // 对比度 OLED_WriteCmd(0xCF); OLED_WriteCmd(0xD9); // 预充电周期 OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDB); // VCOMH OLED_WriteCmd(0x40); OLED_WriteCmd(0xA4); // 全局显示开启 OLED_WriteCmd(0xA6); // 正常显示 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); }显示刷新用OLED_ShowString函数每500ms更新一次。这里要注意IIC写入OLED时如果任务被高优先级任务打断可能导致IIC时序错乱。所以我在IIC操作前后加了临界区保护taskENTER_CRITICAL(); OLED_ShowString(0, 0, buffer, 16); taskEXIT_CRITICAL();但临界区不能太长否则影响其他任务响应。更好的做法是用互斥量保护IIC总线。4. 常见问题与排查技巧实录4.1 OLED点不亮问题排查“oled 0.96批量点不亮”是很多人遇到的问题我总结了几种情况现象可能原因解决方法完全无显示供电不足检查VCC是否3.3V电流是否够有背光无内容IIC地址错误用扫描程序确认地址通常是0x3C或0x3D显示乱码初始化序列不对对照SSD1306手册检查命令部分像素不亮显存写入错误检查页地址和列地址设置间歇性闪烁IIC速率过高降低IIC时钟频率到100kHz我遇到过一次批量点不亮最后发现是模块上的上拉电阻没焊。有些便宜的OLED模块为了省成本上拉电阻是空焊盘需要自己补4.7kΩ电阻。这个坑很隐蔽因为万用表测通断是通的但IIC就是通信不上。4.2 FreeRTOS任务卡死与堆栈溢出任务卡死最常见的原因是堆栈溢出。开启堆栈溢出检测后如果某个任务堆栈不够会触发钩子函数。我遇到过DisplayTask堆栈溢出原因是Adafruit库里的drawPixel函数递归调用太深。解决办法是把DisplayTask堆栈从256word增加到512word。另一个常见问题是优先级反转。如果低优先级任务持有互斥量高优先级任务等待而中优先级任务在运行就会导致高优先级任务被阻塞。FreeRTOS的互斥量支持优先级继承可以缓解这个问题。但最好的办法是设计时避免长时间持有互斥量。还有“freertos下载部署”的问题有些人用ST-Link下载后程序不运行可能是下载地址不对。STM32F103的Flash起始地址是0x08000000如果链接脚本设置错了程序就跑不起来。在PlatformIO中这些是自动配置的一般不会出错。4.3 传感器数据异常处理DHT22读出来全是0或者全是1通常是时序问题。我的排查步骤是用逻辑分析仪抓单总线波形看拉低和释放时间对不对检查上拉电阻DHT22数据线需要4.7kΩ到10kΩ上拉确认供电电压DHT22是3.3V-5V但3.3V时稳定性稍差读取间隔是否小于2秒太频繁会导致传感器不响应MQ-135读数一直很高可能是预热不够。MQ-135需要预热24小时以上刚上电时读数会从高慢慢降下来。如果急着用可以先用软件做零点校准把当前读数当作基准。BH1750读数一直是0检查IIC地址是否正确。BH1750的地址由ADDR引脚决定接GND是0x23接VCC是0x5C。有些模块默认ADDR悬空地址不确定需要手动接。4.4 系统稳定性优化经验这个项目我连续跑了三个月总结了几条稳定性经验第一看门狗必须开。我用的是独立看门狗IWDG超时时间设4秒。MonitorTask每5秒喂一次狗如果系统卡死看门狗会复位。注意喂狗间隔要小于超时时间我设的5秒喂狗、4秒超时是矛盾的实际应该反过来。正确做法是超时时间设10秒每5秒喂一次。第二电源要干净。OLED和传感器同时工作时电流波动大如果电源纹波大会导致ADC读数跳动。我在电源输入端加了一个100μF电解电容和0.1μF陶瓷电容。第三IIC总线要加保护。如果IIC通信过程中从机拉低SDA不放总线会死锁。可以在IIC初始化时发送9个时钟脉冲强制从机释放总线。第四日志要留。我在CommTask里通过串口输出系统状态包括各任务堆栈使用、队列剩余、传感器读数。出问题时看日志就能定位。关于“stm32 ota”升级这个项目预留了串口接口后续可以通过串口接收固件包写入Flash的备份区然后跳转Bootloader实现OTA。但OTA涉及分区管理和固件校验复杂度较高建议先把基础功能跑稳再考虑。4.5 常见问题速查表问题排查方向快速解决程序下载后不运行启动模式、复位电路检查BOOT0是否接地按复位键OLED不亮供电、地址、初始化换模块测试扫描IIC地址传感器读数异常时序、上拉、预热逻辑分析仪抓波形加延时任务卡死堆栈、优先级、互斥量开堆栈检测看串口日志系统频繁复位看门狗、电源、硬件故障关看门狗测试测电源纹波IIC通信失败上拉、速率、总线死锁降低速率加总线恢复代码ADC读数跳动参考电压、滤波、电源加电容软件做滑动平均串口无输出波特率、引脚、时钟检查时钟配置换USB线这个项目从画原理图到代码调试完成前后花了大概两周的业余时间。中间踩了不少坑但也正是这些坑让我对STM32和FreeRTOS的理解深了一层。如果你也在做类似的东西我的建议是先把最小系统跑通再一个一个加传感器每加一个就测试稳定性不要一次性全接上再调那样出了问题很难定位。代码结构上驱动层和应用层要分开传感器驱动写成独立的.c和.h文件换传感器时只改驱动层应用层不动。这样以后想加个PM2.5传感器或者甲醛传感器直接照着现有驱动的接口写一个就行扩展性很好。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门