ARTICLE DETAIL

资讯详情

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

基于STM32与ESP8266的仓库环境监测控制系统设计与实现

基于STM32与ESP8266的仓库环境监测控制系统设计与实现 仓库环境管理这件事说大不大说小也不小。我最早接触这类需求是帮一个做食品原料存储的朋友看现场——几百平米的库房靠人工拿温湿度计巡检粉尘浓度全凭感觉通风除湿要么忘了开要么开了没人关。结果就是角落里的原料受潮结块损失一批货才想起来要上系统。这类场景其实非常典型监测靠人、控制靠手、数据靠纸三个环节全是断点。后来我用STM32做了一套仓库环境控制系统把温湿度采集、粉尘监测、自动通风除湿、ESP8266上云这几块串起来跑了大半年稳定性还不错。这篇就把整个设计和实现过程拆开讲包括选型逻辑、电路细节、代码框架、上云踩过的坑以及那些文档里不会写的经验。适合正在做STM32物联网项目、毕业设计或者想给自家仓库/车间/大棚做环境监控的朋友参考。1. 先想清楚这套系统到底要解决什么问题1.1 仓库环境的三个核心痛点很多人一上来就选型、画板子结果做到一半发现需求没理清。我建议先把痛点列出来。仓库环境管理本质上就三件事知道现在什么情况、判断要不要干预、干预之后有没有效果。第一是感知盲区。温湿度传感器只装在门口库房深处和货架顶层的实际环境完全不知道。我见过一个库房门口湿度60%最里面角落湿度85%因为通风口位置不对湿气全往死角堆。所以传感器布点比传感器精度更重要。第二是响应滞后。人工巡检一天两次温湿度超标可能已经持续了几个小时。粉尘浓度更是看不见摸不着等发现货物表面有积尘说明已经超标很久了。自动控制的价值就在于把响应时间从小时级压到秒级。第三是数据缺失。没有历史数据就没法分析环境变化规律。比如你知道下午三点湿度最高就可以提前开启除湿而不是等超标了再动作。数据上云之后这些规律才能被挖掘出来。1.2 为什么选STM32而不是Arduino或ESP32这个问题我被问过很多次。先说结论如果只是做个玩具ESP32一块板子全搞定但要做稳定运行的仓库系统STM32ESP8266的组合更合适。原因有几个。一是实时性。STM32的中断响应和定时器精度是硬件级的跑FreeRTOS做多任务调度很稳。仓库系统要同时处理传感器采集、继电器控制、串口通信、按键交互用Arduino的单循环架构容易卡顿。二是外设丰富。STM32的ADC多通道、多个UART、定时器捕获接粉尘传感器、温湿度传感器、ESP8266、显示屏都不冲突。三是工业环境适应性。STM32的宽温型号和抗干扰能力在仓库这种有电机、有变频器的环境里更可靠。当然STM32的门槛确实比Arduino高。标准库和HAL库的选择、时钟树配置、中断优先级这些对新手不友好。但一旦跑通后续扩展会轻松很多。我个人的做法是用HAL库CubeMX生成底层业务逻辑自己写兼顾开发效率和可控性。1.3 系统整体架构拆解整套系统我分成四层感知层DHT22或SHT30测温湿度GP2Y1010AU0F测粉尘可选加一个MQ-2测可燃气体。控制层STM32F103C8T6做主控负责采集、判断、驱动继电器。执行层继电器控制排风扇和除湿机LED蜂鸣器做本地报警。通信层ESP8266通过UART和STM32通信走MQTT协议上云。这里有个关键设计决策ESP8266只做透传不做业务判断。所有阈值判断、控制逻辑都在STM32里完成。为什么因为网络会断云平台会挂但仓库的通风除湿不能停。本地控制必须独立于网络存在。这个原则在后面代码里会体现得很明显。2. 硬件选型与电路设计里的那些细节2.1 主控与传感器的搭配逻辑主控选STM32F103C8T6也就是常说的蓝板子。72MHz主频、64KB Flash、20KB RAM跑这套系统绰绰有余。关键是便宜、资料多、社区活跃出问题好查。温湿度传感器我用过三种DHT11、DHT22、SHT30。DHT11精度太差湿度±5%仓库场景不够用。DHT22精度±2%单总线协议接线简单但响应速度慢两次读取要间隔2秒以上。SHT30是I2C接口精度±1.5%响应快但价格贵一些。如果预算允许我推荐SHT30预算紧张就用DHT22但要注意读取间隔。粉尘传感器选GP2Y1010AU0F夏普的经典款。它是模拟输出需要接STM32的ADC。这里有个坑它的LED驱动引脚需要PWM控制而且采样时机要和LED脉冲同步。很多人直接读ADC数据跳得厉害就是因为没做同步。具体做法是拉低LED引脚等280微秒读ADC再拉高等40微秒这样读出来的值才稳定。继电器模块选光耦隔离的别用那种直接驱动的。仓库里的排风扇和除湿机都是感性负载断电瞬间会有反向电动势不隔离容易烧STM32的IO口。我吃过这个亏一个继电器模块没隔离用了两周STM32的PA5就挂了。2.2 电源设计与抗干扰处理电源这块容易被忽视但恰恰是稳定性的关键。整套系统需要5V和3.3V两路5V给继电器、粉尘传感器、ESP82663.3V给STM32和温湿度传感器。我的做法是12V输入 → LM2596降压到5V → AMS1117-3.3降到3.3V。为什么用两级因为LM2596是开关电源效率高但纹波大直接给STM32供电会影响ADC精度。加一级LDOAMS1117做线性稳压纹波能压到几毫伏。抗干扰方面几个实操经验继电器线圈两端并联续流二极管1N4007吸收反向电动势。粉尘传感器的模拟输出线尽量短远离继电器和电机线。STM32的VDDA和VSSA之间加0.1uF和10uF电容ADC参考电压才稳。ESP8266的供电要单独走线它发射瞬间电流能到300mA和STM32共用一条线会导致复位。提示如果你发现STM32偶尔复位或者ADC读数乱跳先查电源。八成是ESP8266发射时的电流波动把电压拉低了。2.3 传感器布点与采样周期传感器装哪里比传感器本身更重要。我的布点原则是进风口一个、出风口一个、库房最深处一个、货架中层一个。四个点覆盖典型区域能看出空气流动是否合理。采样周期也要分传感器区别对待传感器采样周期原因温湿度5秒DHT22响应慢太快读不到有效值粉尘1秒需要多次采样做滑动平均继电器状态实时中断方式检测上云上报30秒太频繁费流量太慢失去意义粉尘数据我用的是滑动平均滤波取最近10次采样的平均值。原始数据波动很大直接上报会看到锯齿波。滑动平均之后曲线平滑很多判断阈值也更准。3. 软件框架从裸机到FreeRTOS的取舍3.1 裸机轮询和RTOS到底怎么选这个问题我纠结过很久。裸机轮询while(1)大循环写起来简单但有个致命问题任何一个任务阻塞整个系统就卡住。比如DHT22读取要等2秒这2秒里按键没响应、串口没收数据、继电器状态没更新。FreeRTOS能解决这个问题把不同任务分到不同优先级传感器采集任务优先级中周期执行控制判断任务优先级高实时响应通信任务优先级低网络慢不影响控制按键/显示任务优先级低但FreeRTOS也有代价RAM占用增加每个任务要独立栈空间、调试复杂度上升、栈溢出问题隐蔽。STM32F103C8T6只有20KB RAM开太多任务会紧张。我的建议是如果系统功能超过3个独立模块就上FreeRTOS。这套系统有采集、控制、通信、显示四块用RTOS更清晰。如果只是简单的温湿度采集继电器裸机就够了。3.2 任务划分与优先级设计具体任务划分如下// 任务优先级定义 #define TASK_PRIORITY_CONTROL 4 // 控制任务最高 #define TASK_PRIORITY_SENSOR 3 // 传感器采集 #define TASK_PRIORITY_COMM 2 // 通信 #define TASK_PRIORITY_DISPLAY 1 // 显示和按键 // 任务栈大小单位字 #define STACK_CONTROL 128 #define STACK_SENSOR 256 // 传感器任务用到浮点运算栈要大 #define STACK_COMM 512 // ESP8266通信缓冲 #define STACK_DISPLAY 128这里有个经验通信任务的栈一定要给够。ESP8266的AT指令响应、MQTT报文拼接字符串操作很吃栈。我一开始给256字跑一段时间就HardFault加到512才稳。任务间通信用消息队列不用全局变量。传感器任务采集完把数据打包成结构体丢进队列控制任务从队列取数据做判断。这样解耦改一个任务不影响另一个。3.3 关键数据结构设计数据结构设计得好后面代码写起来顺很多。我定义了两个核心结构体typedef struct { float temperature; // 温度摄氏度 float humidity; // 湿度百分比 float dust_density; // 粉尘浓度mg/m³ uint32_t timestamp; // 采集时间戳 } SensorData_t; typedef struct { float temp_high; // 温度上限 float temp_low; // 温度下限 float humi_high; // 湿度上限 float dust_high; // 粉尘上限 uint8_t fan_state; // 风扇状态 uint8_t dehumid_state; // 除湿机状态 uint8_t alarm_state; // 报警状态 } ControlConfig_t;SensorData_t用队列传递ControlConfig_t用互斥锁保护因为按键任务可能修改阈值控制任务要读取。这里注意浮点数在任务间传递要注意字节对齐结构体最好用__attribute__((packed))或者手动对齐否则可能出现数据错位。4. 温湿度与粉尘采集的代码实现4.1 DHT22单总线时序的坑DHT22用单总线协议时序要求很严。STM32的主频72MHz一个for循环延时可能就差几微秒。我建议用定时器做微秒级延时别用空循环。// 微秒级延时基于SysTick或TIM void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }DHT22的读取流程主机拉低至少1ms → 释放 → 等待DHT响应 → 读40位数据。这里最容易出错的是等待响应的时间窗口。DHT22响应会先拉低80us再拉高80us如果在这个窗口内没检测到就读取失败。我的做法是加超时重试连续读3次有一次成功就用。实测下来DHT22在湿度高的环境里偶尔会读失败重试能解决90%的问题。还有个细节DHT22两次读取间隔至少2秒。我见过有人放在1秒周期的任务里结果数据一直是上一次的。手册上写的是采样周期不低于2秒不是读取间隔但实际用下来间隔太短数据会漂。4.2 粉尘传感器的ADC采样与滤波GP2Y1010AU0F的输出是模拟电压对应粉尘浓度。典型值是干净空气0.9V左右每100ug/m³增加约0.5V。但这个关系不是线性的而且受温度影响。采样代码的关键是和LED脉冲同步float read_dust(void) { float sum 0; for (int i 0; i 10; i) { HAL_GPIO_WritePin(DUST_LED_PORT, DUST_LED_PIN, GPIO_PIN_RESET); delay_us(280); uint16_t adc_val 0; HAL_ADC_Start(hadc1); HAL_ADC_PollForConversion(hadc1, 10); adc_val HAL_ADC_GetValue(hadc1); HAL_GPIO_WritePin(DUST_LED_PORT, DUST_LED_PIN, GPIO_PIN_SET); delay_us(40); sum adc_val; HAL_Delay(10); } float voltage (sum / 10.0f) * 3.3f / 4096.0f; // 电压转浓度经验公式 float density (voltage - 0.9f) * 200.0f; return (density 0) ? 0 : density; }这里(voltage - 0.9) * 200是经验公式不同传感器会有差异建议用标准粉尘仪标定一下。我标定的时候发现实际系数在180到220之间取200是折中。滤波用滑动平均维护一个10元素的环形缓冲区#define FILTER_SIZE 10 static float dust_buffer[FILTER_SIZE]; static uint8_t dust_index 0; float filter_dust(float new_val) { dust_buffer[dust_index] new_val; dust_index (dust_index 1) % FILTER_SIZE; float sum 0; for (int i 0; i FILTER_SIZE; i) { sum dust_buffer[i]; } return sum / FILTER_SIZE; }注意滑动平均会引入滞后。如果粉尘浓度突然升高滤波后的值要过几秒才能反映出来。所以报警判断要用原始值做快速响应显示和上报用滤波值。4.3 数据校准与异常值剔除传感器用久了会漂。DHT22的湿度读数在潮湿环境里容易偏高SHT30相对稳定。我的做法是定期校准每三个月用标准温湿度计对比一次偏差超过3%就手动修正。异常值剔除用3σ原则如果某个读数偏离最近10次平均值超过3倍标准差就判定为异常丢弃不用。这个逻辑在粉尘传感器上特别有用因为偶尔会有灰尘颗粒直接落在传感器上导致单次读数爆表。int is_outlier(float val, float *buf, int len) { float mean 0, std 0; for (int i 0; i len; i) mean buf[i]; mean / len; for (int i 0; i len; i) std (buf[i] - mean) * (buf[i] - mean); std sqrtf(std / len); return (fabsf(val - mean) 3 * std); }5. 自动通风除湿的控制策略5.1 阈值判断与迟滞设计最简单的控制逻辑是湿度超过70%开除湿机低于70%关。但这样有个问题在阈值附近会频繁开关继电器咔咔响寿命骤减。解决办法是迟滞控制也叫回差控制开阈值和关阈值分开。比如湿度超过70%开低于60%才关。中间10%的区间是保持当前状态避免抖动。#define HUMI_ON 70.0f #define HUMI_OFF 60.0f #define TEMP_ON 30.0f #define TEMP_OFF 26.0f #define DUST_ON 150.0f #define DUST_OFF 100.0f void control_logic(SensorData_t *data, ControlConfig_t *cfg) { // 除湿机控制 if (data-humidity HUMI_ON) { cfg-dehumid_state 1; } else if (data-humidity HUMI_OFF) { cfg-dehumid_state 0; } // 风扇控制温度或粉尘任一超标就开 if (data-temperature TEMP_ON ||>typedef struct { uint32_t last_on_time; uint32_t last_off_time; uint8_t state; } RelayCtrl_t; void relay_set(RelayCtrl_t *relay, uint8_t target) { uint32_t now HAL_GetTick(); if (target relay-state) return; if (target 1) { if (now - relay-last_off_time 30000) return; // 30秒保护 HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_SET); relay-last_on_time now; } else { if (now - relay-last_on_time 30000) return; HAL_GPIO_WritePin(RELAY_PORT, RELAY_PIN, GPIO_PIN_RESET); relay-last_off_time now; } relay-state target; }另外排风扇和除湿机不要同时启动因为启动电流叠加可能超过电源容量。我的做法是错开2秒启动先开风扇2秒后再开除湿机。6. ESP8266上云从AT指令到MQTT6.1 ESP8266的固件选择与AT指令ESP8266有几种用法AT固件、NodeMCU、Arduino开发。做透传最省事的是AT固件STM32发AT指令ESP8266执行。但AT固件版本很多指令集有差异建议用安信可的AT固件文档全。基础AT指令流程AT // 测试 ATCWMODE1 // Station模式 ATCWJAPssid,password // 连WiFi ATCIPSTARTTCP,broker.emqx.io,1883 // 连MQTT服务器 ATCIPSENDxx // 发送数据这里有个大坑AT指令的响应时间不确定。连WiFi可能2秒也可能10秒。如果用阻塞式等待STM32就卡住了。我的做法是状态机超时重试每个AT指令发出去后等响应最多5秒超时就重发重发3次还不行就复位ESP8266。typedef enum { WIFI_IDLE, WIFI_CONNECTING, WIFI_CONNECTED, MQTT_CONNECTING, MQTT_CONNECTED, ERROR_STATE } NetState_t; void net_state_machine(void) { static NetState_t state WIFI_IDLE; static uint32_t timeout 0; switch (state) { case WIFI_IDLE: esp_send_cmd(ATCWJAP\ssid\,\pass\\r\n); state WIFI_CONNECTING; timeout HAL_GetTick(); break; case WIFI_CONNECTING: if (esp_wait_response(OK, 5000)) { state MQTT_CONNECTING; } else if (HAL_GetTick() - timeout 15000) { state ERROR_STATE; } break; // ... 其他状态 } }6.2 MQTT报文拼接与主题设计MQTT协议本身不复杂CONNECT、PUBLISH、SUBSCRIBE几种报文。但手动拼报文容易出错尤其是剩余长度字段的变长编码。我建议用现成的MQTT库比如MQTTClient或者paho的嵌入式版本。如果非要手写注意这几点CONNECT报文的协议名是MQTT级别4。客户端ID要唯一否则会互相踢下线。PUBLISH报文的QoS等级0最多一次1至少一次2恰好一次。仓库数据用QoS 1就够了。主题设计要有层次warehouse/room1/temperature、warehouse/room1/humidity。上报数据的JSON格式{ device_id: WH001, temp: 25.6, humi: 65.2, dust: 45.3, fan: 1, dehumid: 0, ts: 1700000000 }这里注意JSON里的浮点数要控制小数位数太多位浪费带宽。温度湿度保留1位粉尘保留1位就够了。6.3 断网重连与数据缓存网络不稳定是常态。ESP8266断线后要能自动重连重连期间的数据不能丢。我的方案是本地环形缓冲区STM32开一块内存做数据缓存最多存100条记录。上报成功就删除上报失败就留着等网络恢复后补传。#define CACHE_SIZE 100 typedef struct { SensorData_t data[CACHE_SIZE]; uint8_t head; uint8_t tail; uint8_t count; } DataCache_t; void cache_push(DataCache_t *cache, SensorData_t *data) { if (cache-count CACHE_SIZE) { cache-tail (cache-tail 1) % CACHE_SIZE; // 覆盖最旧的 cache-count--; } cache-data[cache-head] *data; cache-head (cache-head 1) % CACHE_SIZE; cache-count; }重连逻辑用指数退避第一次断线等1秒重连失败等2秒再失败等4秒最多等30秒。这样既不会频繁重试耗电也不会等太久。提示ESP8266在信号弱的地方会频繁掉线。如果仓库面积大建议加一个WiFi中继或者用ESP8266的外置天线版本。7. 实测中遇到的典型问题与排查过程7.1 粉尘数据跳变从怀疑传感器到定位电源系统跑起来第一周粉尘数据一直在跳从20跳到200又跳回来。我一开始怀疑是传感器坏了换了一个还是这样。后来用示波器看电源纹波发现5V线上有200mV的纹波正好和ESP8266发射的周期吻合。定位过程先断开ESP8266粉尘数据稳定了再接上又开始跳。说明是ESP8266发射时的电流波动通过电源耦合到了粉尘传感器。解决办法是给ESP8266单独加一个1000uF的电解电容并在粉尘传感器的供电脚加LC滤波10uH电感100uF电容。改完之后数据稳定在±5以内。这个坑的教训是模拟传感器和通信模块的电源一定要隔离。共用一条5V线通信模块的电流波动会直接污染模拟信号。7.2 ESP8266连不上AT指令超时的排查链路有次ESP8266死活连不上WiFiAT指令返回ERROR。排查步骤先确认波特率。ESP8266默认115200但有些固件是9600。用串口助手试了几个波特率确认是115200。检查供电。用万用表量ESP8266的VCC发现只有3.0V偏低。换了一个LDO电压到3.3V还是不行。查AT固件版本。发ATGMR返回的版本是0.9.x太老了。刷了新版AT固件问题解决。后来总结ESP8266对供电电压很敏感低于3.2V就容易出问题。而且不同批次的模块固件版本可能不一样拿到手先发ATGMR确认版本。7.3 继电器误动作中断优先级引发的连锁反应有段时间继电器会莫名其妙地开关没有超标也动作。查了很久发现是中断优先级配置冲突。串口中断优先级设得比定时器中断高串口数据量大时定时器中断被延迟导致控制任务的判断周期不稳定偶尔读到旧数据就误判。解决办法是重新分配中断优先级HAL_NVIC_SetPriority(USART1_IRQn, 2, 0); // 串口 HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); // 定时器更高 HAL_NVIC_SetPriority(EXTI0_IRQn, 3, 0); // 按键最低原则是实时性要求高的中断优先级高。定时器用于任务调度优先级要高于串口。按键这种人工交互优先级最低。7.4 数据上云丢包MQTT QoS与心跳设置上云初期发现云端数据有断档有时候几分钟没有数据。查了MQTT日志发现是心跳间隔太长。默认keepalive是60秒但网络不稳定时60秒内没收到心跳服务器就断开连接了。调整方案keepalive设为30秒QoS用1至少一次。另外PUBLISH报文要带message IDQoS 1的重传机制依赖这个ID。我一开始没设导致重传时服务器认为是新消息数据重复。// MQTT心跳设置 #define MQTT_KEEPALIVE 30 #define MQTT_QOS 1还有个小细节上报周期和心跳周期要错开。如果都是30秒可能同时发送增加网络拥塞。我把上报设在心跳后15秒错开半个周期。8. 系统优化与扩展方向8.1 低功耗设计思路如果仓库没有常电需要电池供电低功耗就很重要。STM32F103的低功耗模式有Sleep、Stop、Standby三种。Stop模式电流约20uAStandby约2uA。我的做法是采集时唤醒采集完进Stop模式。用RTC定时唤醒每5分钟采集一次。ESP8266上报完就断电用MOS管控制电源。这样平均电流能压到1mA以下2000mAh电池能用几个月。但低功耗和实时控制是矛盾的。如果仓库需要实时响应就不能深度睡眠。所以低功耗方案适合监测场景不适合控制场景。8.2 本地显示与按键交互加一个OLED屏SSD1306I2C接口能大幅提升现场体验。显示内容分两页第一页显示实时温湿度和粉尘第二页显示阈值和继电器状态。按键用三个上、下、确认用于修改阈值。按键处理要注意消抖。硬件消抖加0.1uF电容软件消抖用20ms延时确认。我试过只做软件消抖偶尔还是会误触发加了硬件电容之后很稳。8.3 多仓库组网与云端规则引擎单个仓库跑通之后扩展成多仓库就是加设备ID和主题前缀的事。云端可以用规则引擎做联动比如A仓库湿度超标自动通知B仓库管理员或者根据历史数据预测湿度变化趋势提前开启除湿。数据存储用时序数据库如InfluxDB比关系型数据库更合适因为环境数据是典型的时间序列写入量大、查询按时间范围。我试过用MySQL存一个月就几百万条查询变慢。换InfluxDB之后同样的数据量查询秒回。8.4 从毕业设计到产品化的差距很多朋友拿这套系统做毕业设计跑通就行。但如果要产品化还有几个差距要补外壳防护仓库粉尘大PCB要涂三防漆外壳要IP54以上。EMC测试工业环境有电磁干扰要通过CE或FCC认证。看门狗STM32的独立看门狗IWDG必须开防止程序跑飞。OTA升级ESP8266支持OTA但STM32的固件升级要自己做Bootloader。数据安全MQTT用TLS加密密码不要硬编码在代码里。这些在毕业设计里可以省略但实际部署时一个都不能少。我见过一个项目实验室跑得好好的装到现场一周就挂了就是因为没做防护粉尘进了PCB导致短路。整套系统从选型到跑通我花了大概三周时间其中一半时间在调传感器和网络。硬件成本算下来不到200块但省下的人工巡检和货物损失一个月就回本了。如果你也在做类似的项目建议先把本地控制跑稳再搞上云。网络可以断但仓库的通风除湿不能停这个原则贯穿始终。
返回列表