ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32仓储环境监测系统实战:传感器选型、FreeRTOS与远程告警

STM32仓储环境监测系统实战:传感器选型、FreeRTOS与远程告警 简介本资源是一套基于STM32F103C8T6的智能仓储环境监测系统完整工程实现面向嵌入式初学者、课程设计学生及物联网实践开发者解决小型仓储场景下温湿度、光照、烟雾等多参数实时监测与联动调控问题。项目融合Proteus仿真与真实硬件逻辑支持本地按键交互、OLED可视化显示及ESP8266远程监控具备加热、散热、通风、声光报警等闭环控制能力。压缩包共335个文件含56个编译中间文件.d、55个源码备份.zbak、55个链接配置.crf、55个目标文件.o、38个头文件.h及36个C源文件.c另有Keil工程.uvprojx、Hex固件、启动汇编与外设驱动代码如stm32f10x_adc.c、i2c.c等结构完整便于逐模块学习调试。目前已有79人学习下载提供可直接编译运行的全栈代码、清晰阈值设定逻辑、无线通信接口封装及典型传感器驱动范例是掌握STM32多外设协同开发与嵌入式系统工程化落地的优质实践素材。 前阵子帮朋友改造一个社区便利店的小仓库进去第一感觉就是闷货架角落的纸箱已经有点受潮发软了。配电箱旁边扔着一个老式温湿度计读数早就没人看了。这其实是很多中小仓储的常态——不是不知道环境需要监控而是市面上的成套设备要么太贵要么协议封闭没法二次开发。聊了几次之后我决定直接用 STM32 手搓一套环境监测系统出来把温湿度、空气质量、烟雾、火焰这几个关键参数全接了进来数据本地显示、远程上报、异常告警一条龙。这个项目从硬件选型到软件调通前后花了两周多中间踩了不少坑今天把整套设计和实现思路完整拆出来给正在做类似嵌入式项目的朋友一个参考。1. 仓储环境监测到底在测什么需求拆解与传感器选型1.1 影响仓储安全的核心环境参数很多刚入行的朋友一上来就想着多接几个传感器显得专业结果硬件堆了一堆代码却处理不过来。做仓储环境监测第一步必须先想明白一个问题哪些参数真正会酿成损失根据我实际跑现场的经验仓储环境最致命的是这三类问题温湿度失控导致货物霉变、变质尤其是食品、药品、纸制品、电子元器件仓库。湿度超过 60%RH 持续三天纸箱强度就会明显下降温度过高对锂电池、化学品仓库更是直接威胁。可燃气体、有害气体泄漏比如仓库里存放的酒精、清洗剂、杀虫剂这类挥发物浓度一旦超标轻则影响人员健康重则遇明火爆燃。火灾初期隐患包括烟雾和火焰。很多仓库火灾发现时已经烧大了就是因为在萌芽阶段没有任何感知手段。所以这套系统我最终确定的监测对象是温度、湿度、空气质量TVOC/可燃气体、烟雾、火焰。这五个参数基本覆盖了中小仓储 80% 以上的环境风险场景。1.2 传感器选型思路与关键参数对比传感器选型是整个项目里最需要做取舍的环节。我对比过不少方案最终选型如下监测参数传感器型号接口类型测量范围关键优势温度湿度SHT30I2C-40~125°C0~100%RH精度高±0.3°C±2%RH数字输出不用校准空气质量/可燃气体MQ135ADC 模拟量10~300ppmNH3可燃气体敏感便宜灵敏度可调检测范围广烟雾浓度MQ-2ADC 模拟量300~10000ppm可燃气体/烟雾经典烟雾传感器响应速度快火焰火焰传感器模块GPIO 数字量探测距离约 0.5~1.5m响应快直接输出高低电平这里想多聊两句选型逻辑。SHT30 虽然比 DHT11、DHT22 贵几块钱但它是真正的 I2C 数字传感器出厂校准过不需要自己写时序驱动更不用对着数据手册数微秒级的高电平宽度省下来的调试时间完全值回差价。DHT11 我也用过温漂和湿漂都比较明显在仓储这种需要长期稳定运行的环境里不够放心。MQ135 和 MQ-2 这两颗传感器网上吐槽漂移大、精度差的声音很多。但说实话这个精度对于环境监测来说是够用的——我们不需要知道空气中甲醛精确到 0.01ppm只需要知道浓度是不是显著高于正常基线。这类传感器本质是气敏电阻输出的是模拟电压接到 STM32 ADC 做阈值判断完全没问题。火焰传感器模块我选的是模拟量数字量双输出的那款只用了数字量输出通过电位器调节灵敏度阈值。红外火焰传感器对环境光比较敏感安装时要避免被日光直射或者被仓库照明灯近距离照射否则误报率会高得让你怀疑人生。1.3 预留接口的规划意识选型的时候我还做了一件事在主控板上预留了 2 路 5V 电源接口、2 路 ADC 输入、1 路 UART、1 路 I2C 和 3 个 GPIO。这样后续如果想接入水浸传感器、门磁传感器、红外人体感应或者加一路 RS485 接电表都不需要重新设计主板直接插上就能用。很多项目做到一半发现接口不够只能飞线或者重新打板这个坑我建议大家提前避开。2. 整机硬件框架与核心电路设计2.1 主控芯片选择为什么是 STM32F103C8T6主控我选了 STM32F103C8T6也就是大家常说的蓝丸芯片。可能有人会问这芯片都出来十几年了为什么还要用它答案很简单生态成熟、资料多、便宜、够用。F103C8T6 主频 72MHz64KB Flash20KB RAM有 3 个 USART、2 个 I2C、2 个 SPI、10 通道 12 位 ADC做环境监测这种中低速数据采集任务是绰绰有余的。我评估过要不要上 ESP32它的优势是自带 Wi-Fi/BLE省一颗通信芯片。但考虑到项目里还需要同时跑 FreeRTOS、多路 ADC 采样、传感器驱动、GUI 显示ESP32 的 ADC 线性度确实不如 STM32 的 12 位 ADC 稳定而且模拟采集部分做好隔离会比较费劲。干脆让 STM32 专心做数据采集和控制Wi-Fi 通信交给独立的 ESP8266 模块各司其职。另外一个重要考量是成本。F103C8T6 在正规渠道批量采购价大概在 5~7 元左右打样的话用最小系统板也就十几块钱整个 BOM 成本控制在 80 元以内非常适合项目推广和复刻。2.2 电源树设计24V 工业输入到 3.3V 内核供电仓储环境里常见的供电方式是 24V 工业电源所以我也按这个标准来设计电源树。系统供电链路是24V 输入 → DC-DC 降压到 5V → LDO 降到 3.3V。DC-DC 我用的是 MP1584 或 LM2596 模块前者效率高、体积小适合集成到板上后者是经典型号输出纹波略大但皮实。5V 这一路主要供给传感器加热电路MQ 系列传感器内部有个加热丝电流不小3.3V 供给 MCU、OLED 屏、ESP8266注意 ESP8266 模块如果是大功率版本3.3V 电流需求峰值可能达到 300mA 以上LDO 要用 AMS1117-3.3 的话负载能力勉强最好换用 RT9013 这类输出能力更强、压差更低的 LDO。电源设计里有几个细节必须注意DC-DC 的输出纹波对 ADC 采样影响很大尤其是 MQ 传感器输出的是毫伏级模拟信号纹波会把有效信号淹没。一定要在 DC-DC 输出端加 LC 滤波或者 π 型滤波再进 LDO。传感器加热丝和 MCU 供电要分路走避免加热丝瞬态电流拉低 3.3V 电压造成 MCU 复位。电源以及地线走线加粗用覆铜填充减少回路阻抗。2.3 传感器接口电路设计细节模拟量采集这块MQ135 和 MQ-2 的输出引脚直接进 STM32 的 ADC 引脚即可中间不需要额外放大。传感器的模拟输出范围通常是 0~VCC如果 VCC 是 5V而 STM32 ADC 参考电压是 3.3V那必须用分压电阻把信号降到 3.3V 以内否则会烧坏 ADC 引脚。我实际使用的分压方案是在传感输出和 ADC 引脚之间串联一个 10kΩ 电阻同时对地接一个 20kΩ 电阻这样理论分压比是 20/(1020) 2/35V 满量程输出时 ADC 引脚电压是 3.33V刚好卡在安全边界内。再并联一个 100nF 电容做低通滤波滤掉高频噪声。这个阻容参数不是拍脑袋定的是根据传感器输出阻抗通常几十 kΩ和 ADC 采样保持时间综合选择的。数字接口这边SHT30 的 I2C 需要 4.7kΩ 左右的上拉电阻到 3.3V。火焰传感器模块的数字输出是集电极开路结构也要接上拉。I2C 总线上同时挂了 SHT30 和 OLED 屏SSD1306 驱动注意检查两者的 I2C 地址是否冲突——SHT30 默认地址是 0x44常见 OLED 是 0x3C不冲突。3. 核心软件实现ADC 多通道 DMA 采样与数据处理3.1 为什么坚持用 DMA 而不是轮询读 ADC这个功能点我认为是整个软件部分最有含金量的环节。很多 STM32 新手写 ADC 采集就是死循环里 ADC_SoftwareStartConvCmd然后对着一个变量轮询转换结束标志再读寄存器拿数据。这种做法在小项目里能跑但在仓储监测这种需要同时处理多路传感器、Wi-Fi 通信、显示刷新的场景下问题就出来了ADC 转换期间 CPU 被阻塞72MHz 主频下虽然单次转换只要 10μs 左右但累积起来对系统响应的影响不小。多路 ADC 如果一条一条轮询代码嵌套深、逻辑散乱。如果中途被中断打断采样时刻的准确性无法保证后续滤波算法反而会引入更多噪声。DMA 方案是完全不同的思路ADC 在扫描模式下按通道顺序转换转换结果由 DMA 控制器自动搬运到内存数组里全部转换完成后触发 DMA 传输完成中断。整个过程中 CPU 完全解放只需要在中断里取数据就行。实测下来用 DMA 方式采集 5 路 ADCCPU 占用率几乎可以忽略不计而这 5 路信号如果轮询转换CPU 占用率会吃掉 15%~20%。3.2 ADC 多通道扫描 DMA 的完整配置流程直接分享我调试通过的配置代码。这里用的是 STM32 标准外设库HAL 库的逻辑也类似只是 API 名字不一样。void ADC_DMA_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; // 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置 ADC 输入引脚为模拟输入 // 通道0: PA0 (MQ135) 通道1: PA1 (MQ-2) 通道4: PA4 (备用) // 通道6: PA6 (火焰模拟量备用) GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AIN; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. DMA 配置外设地址固定为 ADC1 数据寄存器内存地址指向数组 DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adc_values; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize ADC_CHANNEL_NUM; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式持续采集 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, DMA_InitStructure); // 4. ADC1 配置扫描模式 连续转换 DMA 请求 ADC_InitStructure.ADC_Mode ADC_Mode_Independent; ADC_InitStructure.ADC_ScanConvMode ENABLE; // 扫描模式 ADC_InitStructure.ADC_ContinuousConvMode ENABLE; // 连续转换 ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel ADC_CHANNEL_NUM; // 采几路 ADC_Init(ADC1, ADC_InitStructure); // 5. 配置采样顺序和采样时间 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 3, ADC_SampleTime_239Cycles5); // 6. 使能 DMA 请求和 ADC ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // 7. 校准 ADCST 官方推荐上电后执行 ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); // 8. 启动 DMA 传输 ADC_SoftwareStartConvCmd(ADC1, ENABLE); }有个配置细节容易踩坑ADC_CHANNEL_NUM 这个宏必须和 DMA_BufferSize 的值保持一致。如果你在 ADC 里配置了 3 个通道但 DMA_BufferSize 写成 5DMA 一次传输完成中断永远不会触发数据搬运也会乱套。我早期调试时就是这里没对齐折腾了好几个小时。采样时间选了 239.5 个周期这是 ADC 采样时间选项里最长的一档。MQ 系列气体传感器的输出阻抗较高信号源驱动能力弱较长的采样时间可以确保采样电容完全充满读数更稳。代价是转换速度降低但环境监测根本不需要高速采样每路采样时长 239.5/72MHz ≈ 3.3μs3 路全部采集完也就 10μs 左右完全够快。3.3 传感器数据的滤波与标定ADC 原始值是 0~4095 的整数和实际的温度、气体浓度之间还隔着两层转换一是线性映射到电压值二是从电压值映射到物理量。电压换算很简单float voltage (float)adc_values[0] * 3.3f / 4095.0f * 1.5f; // 补偿分压系数 2/3这里乘 1.5 是因为前置分压电路衰减了 1/3要还原原始电压。如果你用的是 3.3V 供电的传感器模块不用分压就直接 4095 换算即可。滤波方面我用了滑动中位值平均滤波法思路是把每次采样放进一个长度为 5 的环形队列取中间 3 个值做平均既能滤掉尖峰脉冲又不会像纯平均滤波那样把真实变化过度平滑。实现代码#define FILTER_LEN 5 uint16_t Filter_Process(uint16_t new_value) { static uint16_t buffer[FILTER_LEN]; static uint8_t index 0; uint16_t sorted[FILTER_LEN]; uint8_t i, j; uint16_t temp; uint32_t sum 0; buffer[index] new_value; index (index 1) % FILTER_LEN; // 拷贝并排序 for (i 0; i FILTER_LEN; i) { sorted[i] buffer[i]; } for (i 0; i FILTER_LEN - 1; i) { for (j 0; j FILTER_LEN - 1 - i; j) { if (sorted[j] sorted[j 1]) { temp sorted[j]; sorted[j] sorted[j 1]; sorted[j 1] temp; } } } // 去掉最大最小值后取平均 for (i 1; i FILTER_LEN - 1; i) { sum sorted[i]; } return (uint16_t)(sum / (FILTER_LEN - 2)); }气体浓度的标定比较复杂。MQ 系列的灵敏度特性曲线在 datasheet 里是双对数坐标下的直线这意味着电压值和浓度不是简单的线性关系。对于环境监测预警场景我的做法是不做绝对浓度标定而是做相对阈值判断。先让传感器在干净空气环境预热 24 小时记录稳定电压基线比如 0.9V然后设两级告警阈值电压超过基线 1.3 倍时预警超过 1.8 倍时报警。这种方式省去了复杂的对数拟合实际运行中效果很可靠。4. 实时性与多任务FreeRTOS 任务划分与数据流设计4.1 为什么引入 FreeRTOS 而不是裸机主循环系统功能一多裸机主循环就会变得很难维护。拿我这个项目来说需要做的事情包括周期采集 5 路传感器数据1s 周期处理 SHT30 温湿度读数2s 周期刷新 OLED 显示屏500ms 周期ESP8266 收到远程指令时响应异步本地蜂鸣器告警逻辑事件触发串口输出调试日志异步如果全塞进一个大 while 循环最难受的点在于任何一个模块的阻塞操作比如等待 I2C 传输完成、等待 ESP8266 响应 AT 指令都会拖累其他模块的实时性。OLED 显示刷新到一半气体告警来了等屏刷完再去处理告警在安全监测场景里这是不能接受的。FreeRTOS 的解决思路是把不同的功能交给不同的任务任务间用队列、信号量通信。我给这个项目划分了 4 个任务任务名优先级周期/触发方式核心职责Sensor_Task31s 定时采集 ADC 三路、读取 SHT30、滤波、发布到数据队列Display_Task2500ms 定时从队列获取最新数据刷新 OLEDComm_Task4事件触发队列有消息组包、通过串口/DMA 发送给 ESP8266Alarm_Task1事件触发信号量检测到阈值越限时驱动蜂鸣器/上报告警Comm_Task 的优先级设得最高是因为它在响应远程控制指令比如远程关闭排风扇时要求尽量低的延迟。Sensor_Task 其次保证采集周期精确。Display_Task 是锦上添花类的工作优先级低一点没关系即使被抢占也不会产生安全问题。Alarm_Task 优先级最低但注意告警触发是由信号量唤醒的一旦触发就会立即抢占传感器任务所以优先级设低并不影响响应速度——这就是事件驱动和轮询的本质区别。4.2 串口空闲中断接收不定长数据这个项目里 ESP8266 和 STM32 之间的通信是全双工的STM32 要发数据给 ESP8266 上报也要接收 ESP8266 转发的远程指令。AT 指令的响应和主动上报都是不定长数据用传统的固定长度接收或者结尾字符判断都不够可靠。我的方案是用 STM32 的串口空闲中断IDLE interrupt配合 DMA 接收。基本原理是DMA 开启循环接收模式每收到一个字节自动存入缓冲区当串口总线出现空闲即连续 1 个字节时间内没有新数据到来硬件自动触发 IDLE 中断。在中断里读取 DMA 当前计数寄存器就能知道本次收到多少个字节然后从缓冲区中解析。void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART2); // 读 DR 清 IDLE 标志 uint16_t received_len DMA_GetCurrDataCounter(DMA1_Channel5); uint16_t data_len RX_BUF_SIZE - received_len; // 把数据交给数据处理函数 Parse_Command(rx_buffer, data_len); // 重新配置 DMA 指向缓冲区起始地址 DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUF_SIZE); } }这一招的应用非常广泛串口接收不定长数据用空闲中断是最优雅的解法比逐字节中断接收省 CPU也比固定帧长协议灵活得多。最开始我是在裸机下直接用空闲中断接入 FreeRTOS 之后改成了IDLE 中断里直接 xQueueSendFromISR 把数据块指针发送给协议解析任务注意中断服务函数里只能用带 FromISR 后缀的 API否则会触发断言。4.3 任务间通信的队列设计数据流是典型的生产者-消费者模型。Sensor_Task 是唯一的生产者它采集数据、滤波完成、计算好阈值状态后把一组数据打包成结构体typedef struct { float temperature; float humidity; uint16_t air_quality_raw; uint16_t smoke_raw; uint8_t flame_detected; uint8_t alarm_level; } SensorData_t;然后通过xQueueSend(DataQueue, sensor_data, 0)发送到队列。Display_Task 和 Comm_Task 都从这个队列取数据。这里有个坑要注意多个消费者同时从同一个队列取数据每条数据只会被一个消费者取走另一个消费者就收不到了。解决方案有三个一个队列只服务一个消费者其它任务通过共享内存加互斥锁的方式访问。用广播的方式Sensor_Task 直接把数据写到全局变量各任务各自加锁访问。队列里放指针每个消费者取走的是同一份数据的指针。考虑到数据量不大、实时性要求高我最终采用方案 2定义全局的volatile SensorData_t g_sensor_dataSensor_Task 更新完数据后设置一个标志Display_Task 和 Comm_Task 在各自的周期里检查标志并读取。因为 FreeRTOS 是单核系统只要保证读写操作是原子的这种方案足够了关键是不要在一个任务里跨多行读-改-写同一个变量。5. 远程通信与数据显示ESP8266 上报链路和本地 OLED 界面5.1 ESP8266 通信方案选型透传还是 MQTTESP8266 作为 STM32 和互联网之间的桥梁工作模式有几种选择最省事的是 AT 指令透传模式STM32 通过串口发送ATCIPMODE1进入透传模式之后所有发给 ESP8266 的数据都直接转发到 TCP 服务器。这种方式实现最简单但只能连固定 TCP 端口没有加密和鉴权不适合公用网络。进阶方案是 ESP8266 刷 MQTT 固件直接用 AT 指令连接 MQTT broker支持 QoS、用户名密码认证。不过 MQTT AT 固件对 Flash 大小有要求部分 1MB 版本的 ESP-01 刷不了完整的 MQTT 固件。我实际采用的是在 ESP8266 上跑 NodeMCU 固件Lua 脚本由 ESP8266 主动连接 MQTT broker同时在自己的 Lua 脚本里做了数据上报逻辑。STM32 只需要把传感器数据通过 UART 发给 ESP8266ESP8266 收到后组 JSON 包发布到 MQTT 主题。反过来远程控制指令通过 MQTT 订阅主题下发ESP8266 解析后通过串口转发给 STM32。这样分层的设计好处很明显STM32 端的串口协议非常简单只有上报数据帧和接收控制帧两种复杂的网络协议栈全部由 ESP8266 承担不占用 STM32 的资源。如果后面想换 4G 模块或者 NB-IoT 模块STM32 端代码几乎不用改。5.2 数据上报帧格式设计STM32 与 ESP8266 之间的串口协议我用的是自定义的帧格式帧头(2字节: 0xAA 0x55) 长度(1字节) 数据域 校验(1字节)数据域内容偏移内容类型0温度值 ×10uint16_t2湿度值 ×10uint16_t4空气质量ADC值uint16_t6烟雾ADC值uint16_t8火焰状态uint8_t9告警等级uint8_t温度值乘 10 是为了用整数传输避免浮点数在串口上的格式化开销比如 25.4°C 就传 254接收端再除以 10 还原。校验用的是简单的累加和从长度字节开始到数据域最后一个字节全部累加取低 8 位。为什么不用 JSON 直接传因为 STM32 F103 的 RAM 只有 20KB跑最小 FreeRTOS 配置加上各种缓冲区剩余可用内存并不多在串口层构造 JSON 字符串会很吃力。ESP8266 端收到二进制帧后再组 JSON 上报这是更合理的分工。5.3 OLED 显示的本地人机交互本地显示用了一块 0.96 寸 OLEDSSD1306128x64四线 I2C 接口。显示界面我分了两个页面每 5 秒自动切换页面一显示温湿度和当前时间如果接了 RTC大字号温度值。 页面二显示空气质量 ADC、烟雾 ADC 和火焰状态数据异常时在底部闪烁显示告警图标。OLED 驱动的移植我直接用了开源的 u8g2 库它支持 SSD1306 且对底层接口要求的改动很小。不过 u8g2 在 framebuffer 模式下手动刷新时u8g2_SendBuffer通过 I2C 发送 1KB 的数据需要几百毫秒期间会阻塞任务。为了不拖累其他任务我把 Display_Task 的周期设成了 500ms并且只在数据变化时才刷新——数字没变就不重绘OLED 的 I2C 总线流量和任务阻塞时间能减少一半以上。// 示例u8g2 显示温湿度 void Display_Update(void) { char line[32]; u8g2_ClearBuffer(u8g2); u8g2_SetFont(u8g2, u8g2_font_ncenB08_tr); snprintf(line, sizeof(line), Temp: %.1f C, g_sensor_data.temperature); u8g2_DrawStr(u8g2, 0, 12, line); snprintf(line, sizeof(line), Humi: %.1f %%, g_sensor_data.humidity); u8g2_DrawStr(u8g2, 0, 28, line); u8g2_SendBuffer(u8g2); }6. 可靠性设计Flash 存储、异常告警与长线抗干扰6.1 数据存储的掉电保护策略仓储环境可能存在断电风险断电后重新上电系统应该恢复到断电前的配置状态比如告警阈值有没有被人改过、设备 ID 是什么。这些关键参数我存进了 STM32 内部的 Flash。F103C8T6 的 Flash 有 64KB我把最后 2 页每页 1KB作为参数存储区。Flash 写入有个硬性限制必须先擦除整页然后才能写入擦除操作是以页为单位的。而且 Flash 的擦写寿命约 1 万次如果系统频繁写入参数必须做磨损均衡。我的实现思路是双页交替写入page1 和 page2 轮流使用每次写入时先读两个页的写入计数选择计数较小的页写入。当计数超过 0xFFF0 时触发一次整体搬移把最新数据写到新页并擦除旧页。这样实际寿命可以延长接近一倍。关键代码逻辑typedef struct { uint32_t magic; // 0xA5A5A5A5标识写入合法性 uint32_t write_count; // 写入计数 int16_t temp_alarm_high; // 温度告警上限×10 int16_t humi_alarm_high; // 湿度告警上限 uint16_t air_alarm_val; // 空气质量告警阈值 uint16_t device_id; // 设备编号 } SysConfig_t;写入前关闭全局中断防止写入过程中被任务打断void Flash_WriteConfig(SysConfig_t *cfg) { __disable_irq(); // 选择写入页交替 uint32_t target_page GetTargetPage(); FLASH_Unlock(); FLASH_ErasePage(target_page); FLASH_ProgramWord(target_page, cfg-magic); // ... 依次写入所有字段 FLASH_Lock(); __enable_irq(); }6.2 看门狗与任务死循环监控在 FreeRTOS 环境下独立看门狗IWDG的使用有点门道。如果直接在任务里喂狗一旦某个任务死循环看门狗可能被其他正常运行的任务喂掉起不到监控作用。更可靠的办法是任务监控用软件看门狗定时器比如 FreeRTOS 的软件定时器监测每个任务的心跳主循环的任务如果 5 秒内没更新心跳计数器就主动调用 NVIC_SystemReset() 复位整个系统。同时硬件 IWDG 的喂狗操作只放在 idle 钩子函数里这样即使所有任务都挂起idle 钩子还能跑硬件看门狗反而不会触发——所以软件监控必须配合硬件 IWDG 的双保险。我实际配置的 IWDG 溢出时间是 3 秒预分频 64重装载值 187540kHz 时钟源下约 3 秒。喂狗放在最低优先级的 Display_Task 里目的是保证如果这个任务还能执行说明高优先级的任务至少没把系统卡死如果连它都跑不动那系统大概率已经出问题了让看门狗复位是合理的兜底。6.3 长线传感器的抗干扰处理仓储现场传感器到控制器之间的距离往往不是开发板上那种几厘米而是可能拉到 10 米甚至更远。我实际布过的最长一路线是 MQ-2 到主控距离约 8 米中间和 220V 电源线在同一线槽里走了 2 米干扰问题立刻暴露出来ADC 读数在凌晨设备空载时波动异常大告警误报频繁。排查后确认了三个干扰源一是传感器模拟信号线没有屏蔽感应到了工频干扰二是信号和电源线平行走线过长产生了共模干扰三是控制柜里没有可靠的接地。整改方案如下模拟信号线全部换成屏蔽双绞线屏蔽层只在主控端单点接地。信号线远离动力线交叉时 90 度交叉。ADC 输入端加 RC 滤波截止频率设到 10Hz 左右R15kΩC1μF把工频和高频干扰滤掉。如果信号线需要超过 15 米建议改用 RS485 接口的传感器或者把传感器模块做成远端采集端通过 RS485 与主控通信模拟信号只走设备内短线。7. 调试过程中的真实排坑记录7.1 串口打印浮点数把系统搞崩了调试初期我用 printf 通过串口输出日志结果系统在打印浮点数时偶尔会卡死。查了一圈发现是 printf 的浮点格式化需要启用微库MicroLib否则会引入较大的 float 支持代码在内存受限的情况下可能触发堆栈溢出。解决办法是在 Keil MDK 的 Options 里勾选 Use MicroLIB或者在代码里自己实现fputc重定向到串口同时避免在中断服务函数里调用 printf。如果对实时性有严格要求干脆用固态整数来输出温度值乘 10 存成 uint16_t 打印省掉浮点转换。7.2 SHT30 读到的温湿度固定是 0SHT30 初始化后读取数据一直返回 0排查思路如下用示波器看 I2C 时钟和数据线确认有波形排除接线问题。检查上拉电阻I2C 总线必须有上拉否则 SDA/SCL 高电平驱动不足通信失败。STM32 内部的 I2C 引脚是开漏结构不完全依赖内部上拉的话可以用 4.7kΩ 外部上拉解决。检查地址7 位地址 0x44 左移一位后是 0x88写地址是 0x88读地址是 0x89。很多移植代码里直接把地址写成了 0x44 没有左移导致通信不上。最终发现我的问题在第 3 步驱动库里设置的地址是 0x44但接口层又自己做了左移等于实际发送的地址变成了 0x88 再左移完全错位。7.3 MQ135 输出的电压越来越高甚至爆表新买的 MQ 系列传感器首次上电会有严重漂移电压值会持续爬升十几个小时。这是正常的老化过程——传感器内部的敏感材料在初次加热时会释放吸附的气体需要预热稳定。很多新手不知道这一点看到读数飙升就以为传感器坏了。正确做法是新传感器先持续通电老化 24~48 小时再进行基线校准。我在代码里做了一个启动预热状态机上电后前 30 分钟内告警逻辑自动禁用只做数据记录30 分钟后再启用阈值判断。顺带提一句MQ135 长时间不通电后再次上电也需要 5~10 分钟的预热才能恢复到稳定输出。如果系统是断电重启型建议在代码里加一个预热延时逻辑避免重启瞬间误报警。7.4 FreeRTOS 任务中调用 HAL 库延时导致系统崩溃这个坑比较隐蔽。在 FreeRTOS 任务里如果直接调用 HAL 库的 HAL_Delay()基于 SysTick 中断实现会和 FreeRTOS 的 SysTick 配置产生冲突。FreeRTOS 默认使用 SysTick 作为系统时钟节拍HAL_Delay() 也要用 SysTick两个模块抢同一个硬件定时器结果就是任务在延时期间系统卡死或者调度异常。解决方法是FreeRTOS 任务里一律使用 vTaskDelay() 或 vTaskDelayUntil()不要在任务里调用 HAL_Delay()。如果必须在非任务上下文比如中断处理里做短延时使用 DWT 或者 TIM 实现的精确延时函数替代。7.5 ESP8266 连接 WiFi 不稳定刚开始测试时 ESP8266 经常掉线重连间隔十几分钟就断一次。排查后发现是电源问题ESP8266 发射时的峰值电流可达 300mA用 AMS1117-3.3 供电时压降不够电压跌到 2.7V 左右就触发了模块的欠压保护。解决方法是给 ESP8266 单独用一颗 RT9013-33低压差、最大输出 500mA供电并且在模块电源引脚旁边并联一个 470μF 电解电容和一个 100nF 陶瓷电容分别吸收低频和高频电流波动。改完之后连续跑了 72 小时稳定在线。另外ESP8266 的天线布置也有讲究不能贴地、不能靠近金属柜体否则信号衰减严重。我最后把 ESP8266 用延长线引出到配电柜顶部信号强度从 -75dBm 提升到 -55dBm丢包率显著下降。8. 系统联调、测试与现场部署效果8.1 功能测试项清单系统软硬件都调通之后我做了一份测试清单重点覆盖以下几个场景测试项测试方法预期结果温湿度采集精度与标准温湿度计并排放置对比误差温度 ±0.5°C湿度 ±3%RH气体报警触发打火机放气靠近 MQ1355 秒内触发本地蜂鸣器并上报告警烟雾报警触发点燃烟饼靠近 MQ-23 秒内触发告警并联动点亮 LED火焰报警触发打火机火焰靠近传感器1 秒内触发告警断电恢复正常运行中切断电源重启系统自恢复参数保持断电前设置WiFi 断线恢复断开路由器电源重新开启60 秒内自动重连数据续传远程指令响应APP 下发关闭风扇指令3 秒内执行并返回状态8.2 长期可靠性测试中的发现连续运行两周后发现两个之前没暴露的问题。第一个是 OLED 屏长时间显示同一画面导致烧屏残影。虽然这个 OLED 是临时调试用的但如果长期部署还是要考虑这个问题。临时对策是开启屏保系统设置了两分钟无操作后自动关闭显示屏幕亮度调低一档。如果后续产品化应该换成 LCD 屏或者加屏保逻辑。第二个问题是 MQ135 的零点漂移。运行一周后它的基线电压从原始标定的 0.9V 漂到了 1.05V导致误报警。我后来加了一个自动零点漂移校正逻辑每天凌晨 3 点仓库无人无货时读取 10 分钟内 ADC 的最小值作为新的基线更新存储。做这个改动之前要确认现场这个时段确实没有污染源否则会引入新的偏差。8.3 告警联动风扇、排气扇和蜂鸣器除了被动监测我还加了一个简单的执行器联动两个继电器分别控制排风扇和声光报警器。告警阈值触发逻辑如下温度超过 35°C 或湿度超过 70%RH打开排风扇。空气质量或烟雾超过二级阈值打开排风扇、触发蜂鸣器并上报远程告警。火焰检测触发所有继电器断电关闭排风扇防止助燃、蜂鸣器持续鸣响、远程推送紧急告警。继电器驱动用的是 8 路光耦隔离继电器模块STM32 的 GPIO 输出经过光耦隔离后驱动继电器线圈有效隔离了强电干扰。GPIO 输出端要注意加上下拉电阻防止 MCU 复位瞬间继电器误动作。9. 从项目原型到产品化的一些真实心得9.1 关于 STM32 工程架构的经验这个项目开始时我用的是标准外设库做到后期发现维护起来稍微有点吃力。如果从零再来的话我会直接选 HAL 库因为它的抽象层次更高换芯片型号时迁移成本低。不过标准库也有优势——代码执行效率高调试时能看到的寄存器细节更清楚。工程目录划分建议把 BSP板级驱动、APP业务逻辑、RTOS内核配置、MiddlewaresESP8266 协议、u8g2 库分成四个独立目录编译选项里开启-Wall把编译警告当错误处理。我在开发过程中被那种只有 warning 不报错的代码坑了好几次比如变量类型隐式转换、函数声明缺失在 Keil 里看不太出来但换到 GCC 工具链编译直接报错。9.2 功耗优化与低功耗模式的权衡仓储环境监测节点不一定都有市电可用部分场景需要电池供电。F103C8T6 支持 STOP 和 STANDBY 两种低功耗模式。在电池供电的升级方案里我的设计思路是主循环正常运行时电流约 30mA其中 MQ 系列传感器加热丝占大头约 20mA这个没法避免。进入低功耗前先关闭传感器加热通过 MOS 管切断加热丝电源再进入 STOP 模式整机电流可降到 5mA 以下。用 RTC 闹钟定时唤醒每 10 分钟唤醒一次采集数据后立即再睡。这样理论上两节 18650 电池可以撑一个多月。不过这个优化目前还在原型测试阶段如果后面有空再单独写一篇低功耗改造的文章。9.3 扩展方向从监测到联动再到预测性维护这个项目目前已经完成了监测 告警 远程查看的基本闭环。基于当前架构可以低成本扩展的方向有接入水浸传感器防漏水仓储场景。接入多路 RS485 传感器比如粉尘传感器、气压传感器实现更全面的环境感知。利用历史数据的趋势分析做预测性维护——比如连续记录三个月温湿度变化规律优化仓库的温控策略。联动物流系统货物入库时通过 RFID/NFC 读取货物信息自动匹配最佳存储环境参数。整个项目从需求梳理到初版调通核心逻辑并不复杂真正花时间的是反复调试传感器信号质量、处理各种边界条件、以及让系统在实际环境中稳定运行。如果你也在做类似的 STM32 环境监测项目希望这份记录能帮你少踩几个坑。最后再提一句硬件项目没有完美这回事系统稳定可复现、现场部署后不出幺蛾子就是最好的交付状态。本文还有配套的精品资源点击获取
返回列表