仓库这地方,看着不用费心,真出问题全是大事。潮了,纸箱发软、面粉结块;粉尘大了,电气设备打火;温度高了,有些化学品直接就不安全了。前阵子给一家做粮油仓储的朋友做了一套环境自动控制系统,主控用STM32F103C8T6,传感器选了DHT22温湿度模块和GP2Y1010AU粉尘传感器,执行端是两个继电器带排风扇和除湿机,联网用ESP8266把数据推到巴法云,手机小程序里随时能看温度、湿度、粉尘浓度,还能远程切换控制模式。这篇文章把完整设计思路、硬件选型、程序逻辑和调试中踩过的坑全部记录下来,给准备做仓库环境控制或类似物联网监测项目的朋友,提供一份能直接抄作业的参考。
1. 系统整体设计与方案选型
1.1 需求拆解:仓库环境到底要控什么
先别急着选芯片,把需求列清楚。
仓库不同于农业大棚,也不同于机房,它的环境痛点集中在三块:温度、湿度、粉尘。温度过高会导致纸张脆化、化学品分解、部分农产品加速变质;湿度过大是仓库最常见的杀手,墙面凝露、货物受潮发霉、金属件氧化;粉尘则是电子仓库和粮仓的大问题,浓度高了既影响设备可靠性,也有粉尘爆炸风险。
除了这三个物理量,还有一个被很多人忽略的维度——通风换气。通风不是简单开个风扇,它是把温度、湿度、粉尘三个指标联动起来的手段:湿度大了要通风排湿,粉尘超了要通风除尘,温度高了也要通风散热。所以我最终把系统功能定义为“实时监测+阈值控制+远程接管”:传感器持续采集数据,控制器根据预设阈值自动启停通风和除湿设备,同时把状态和数据发送到云平台,给管理者一个远程干预的入口。这个功能定位决定了后面的所有选型和工作量。
1.2 硬件选型:为什么是这些芯片和传感器
主控选STM32F103C8T6,理由很实在:这是市面上资料最全、上手成本最低的32位MCU,Cortex-M3内核,72MHz主频,板载64KB Flash和20KB RAM。这套项目里同时跑DHT22驱动、粉尘ADC采样、继电器逻辑和ESP8266串口通信,资源完全够用,编译下载用ST-Link加Keil MDK,二十块钱的板子跑得稳稳的。如果预算更低或者需要更多串口,可以考虑STM32F103RCT6或者GD32替代,但F103C8T6对初学者是最友好的入门选择。
温湿度传感器选DHT22。之前也用DHT11做过原型,精度真心不够,DHT11温度±2℃、湿度±5%RH,仓库这种需要长时间跟踪变化趋势的场景,测出来的数据波动会让人误判。DHT22把精度做到温度±0.5℃、湿度±2%RH,分辨率0.1,最关键的是单总线协议,一根线就能读数据,价格也就五六块钱。如果你想直接上I2C,选SHT30也行,但驱动代码要重写,本文还是以DHT22为主线。
粉尘传感器选夏普GP2Y1010AU,这是一颗光学灰尘传感器,原理是红外LED照射空气中颗粒物,光电二极管接收散射光强度,输出模拟电压。它测的是PM10级别的粉尘浓度,不是细颗粒物PM2.5,但对仓库环境来说,主要监控对象是扬尘和大颗粒悬浮物,这个量程和响应速度够了。它的典型输出在无尘环境约0.6V,灵敏度0.5V对应0.1mg/m³,算浓度非常方便。另一个方案是攀藤PMS5003激光传感器,能测PM2.5和PM10,UART直接输出数字量,精度高但价格是GP2Y1010AU的五六倍,功耗也大,看需求选。
执行端用两个5V继电器,一个控制排风扇,一个控制除湿机,继电器模块带光耦隔离最好。ESP8266选ESP-01S或者ESP-12F都行,我手上ESP-01S多,就用它做AT指令透传,板载一颗完整的ESP8266EX芯片,跑原厂AT固件,串口和STM32交互,稳定够用。
1.3 系统架构:从传感器到云端的完整链路
整条链路分四层:感知层、控制层、执行层、云平台层。感知层就是DHT22和GP2Y1010AU,一个用单总线读温湿度,一个输出模拟电压给ADC;控制层是STM32,负责定时采集、数据滤波、阈值判断,并通过两个GPIO控制继电器;执行层是排风扇和除湿机;云平台层由ESP8266承担,它通过串口接收STM32发来的数据帧,解析后再走TCP连接推送巴法云。
这里要说一下为什么把ESP8266独立出来而不是直接用带WiFi的MCU。F103C8T6本身不带WiFi,加一颗ESP8266是最短路径;另一个考虑是稳定性,ESP8266的AT固件在网络异常时偶尔会卡死,独立模块出问题只影响上云功能,不影响本地自动控制。仓库环境控制系统最核心的安全底线绝对是本地自动控制,不管网络好不好,传感器检测到湿度超限,继电器就必须动作。把这个设计原则想清楚,后面写程序的时候模块划分就自然了。
2. 硬件接线与核心模块实现
2.1 温湿度采集:DHT22的单总线时序
DHT22的单总线协议其实不难,难的是时序处理。整个读流程分三步:主机发送起始信号、等待传感器响应、连续读取40位数据。起始信号是主机把数据线拉低至少18ms再释放,释放后传感器会把总线拉低80us再拉高80us表示响应,之后每1位数据都是先拉低50us,然后用高电平持续时间区分逻辑0还是逻辑1,逻辑0高电平维持26-28us,逻辑1高电平维持70us左右,最后读出40位数据。
40位数据按湿度高8位、湿度低8位、温度高8位、温度低8位、校验和排列。校验和是前四个字节相加取低8位,如果对不上,这一帧数据直接丢弃。接线很简单,DHT22的DATA引脚接STM32一个普通GPIO,比如PA0,模块上自带上拉电阻。程序里切换GPIO模式是关键,发起始信号时把PA0设为推挽输出,发送完毕立刻切回上拉输入,DHT22才能正常拉低总线响应。切换模式用HAL库要写GPIO_InitTypeDef然后重新初始化,这一步如果做得不及时,总线电平不对,传感器是不理你的。我踩过这个坑,最开始在切换模式后没加小延时,导致起始信号后传感器响应超时,后面在切模式后加一个2us左右的延时,问题就解决了。
读位数据的延时控制建议使用DWT计数器或者定时器做微秒级延时,尽量不要用循环翻NOP的方式,因为编译器优化级别不同,同样是100个周期的循环,在-O0和-O2下实际延时差出一两倍,时序一乱读出来的全是校验错误。
2.2 粉尘监测:GP2Y1010AU的PWM驱动与ADC采样
GP2Y1010AU有六个引脚,我们只关心四个:VCC接5V、GND接地、V-LED需要接频率100Hz、脉宽0.32ms的脉冲信号,Vout输出模拟电压。这里提醒一下,V-LED一定不能直接接5V常高电平,它是靠脉冲电流点亮LED,并用脉冲间隔让光电二极管恢复,常亮会导致输出饱和,粉尘浓度读数严重偏大。我见过有人直接接5V后读到的值一直0.4mg/m³以上,就是这个原因。
脉冲怎么产生?STM32定时器PWM派上用场。用TIM3_CH1映射到PB6,配置PWM频率100Hz,占空比3.2%,加上一个S8050三极管做电平转换,把3.3V的PWM提升到5V驱动V-LED。如果你用的是集成式模块,模块上通常已经把驱动电路做好了,只需要给模块一个PWM引脚,而AOUT直接接STM32的ADC引脚,比如PA1,配置为ADC1的通道1,采样周期选短一些的比如1.5周期,配合DMA连续采样更省心。
浓度换算公式是:粉尘浓度(mg/m³) = (Vout - V0) × 0.2,V0是无尘环境下的零点电压,典型值0.6V。举个例子,ADC读到电压1.6V,对应浓度(1.6-0.6)×0.2=0.2mg/m³。随着传感器老化零点电压会漂移,建议系统上电后在干净环境里自动标定一次零点,把实测无尘电压作为V0代入,精度会好很多。
采样时机也要讲究。GP2Y1010AU的输出在LED脉冲点亮后约0.28ms时达到峰值,这时候采到的电压最接近真实浓度。STM32做同步采样有几种办法:PWM中断里延时0.28ms再启动ADC,或者用定时器输出比较通道触发ADC。实际工程里我用了更土但可靠的办法——在主循环里固定每100ms读一次ADC,连续读10次取平均,再用一阶低通滤波平滑,实测精度足够仓库监控使用。毕竟这不是科研仪器,不需要每次都在峰值点精确采样,稳定趋势才是关键。
2.3 自动通风除湿:继电器控制与滞回逻辑
执行端的关键不是硬件,是控制逻辑怎么写。仓库环境的特点是变化慢、惯性大,如果来个简单的阈值比较,比如湿度大于75%就开、小于75%就关,会有一个恶心的问题——传感器在阈值附近抖动时,继电器吸合和释放会频繁弹跳,继电器触点寿命快速消耗,排风扇电机频繁启停,冲击电流也让人心疼。解决办法是加入滞回控制,专业叫法就是施密特触发器原理。
我给除湿机设了两对阈值:湿度上升到75%时开启,降到65%时关闭;排风扇的粉尘阈值为0.15mg/m³开启、0.10mg/m³关闭;温度联动则设为40℃开启排风扇、35℃关闭。这么设计,湿度在65%~75%之间时设备状态不改变,系统有了一个“静默区”,启停次数大幅降低。这个逻辑看起来简单,但实际运行效果和直接阈值比较差别巨大,继电器寿命能差出一个数量级。
继电器硬件上有个细节很多人忽略:继电器线圈是感性负载,断电瞬间会产生反向电动势,如果不做续流,电压尖峰可能把STM32的GPIO口或三极管击穿。市面上很多继电器模块在线圈两端已经并了续流二极管,选模块时注意看电路,建议选带光耦隔离的,这样MCU和继电器之间没有电气连接,抗干扰能力强很多。我在面包板原型阶段用过一个没光耦的模块,继电器一动作,模块上指示灯闪一下,STM32就复位一次,后来排查出来是继电器吸合瞬间电源被拉掉,主控供电不稳。解决方法是继电器单独供5V电源,和STM32共地但不在同一路稳压输出,同时给STM32的VDD和VDDA引脚加上100uF电解电容。
2.4 ESP8266上云:AT指令与透传模式
ESP8266的角色是STM32和云平台之间的桥梁。我用的方案是AT指令透传,流程很清晰:先用AT+CWMODE=1把ESP8266设为Station模式,然后用AT+CWJAP="WiFi名","密码"连接无线路由,接着AT+CIPSTART="TCP","bemfa.com",9501建立到巴法云服务器的TCP连接,最后AT+CIPMODE=1开启透传模式并发送AT+CIPSEND。之后,STM32只需要通过串口往ESP8266发送的数据,就会原样送到云平台。透传模式里想退出,发不带回车的+++即可。
连接巴法云的协议非常轻量。设备连上bemfa.com的9501端口后,发送类似下面的文本帧即可完成主题订阅:
cmd=1&uid=你的私钥&topic=仓库环境&msg=温度:25.6,湿度:62.3,粉尘:0.12云平台会把这个msg推送到小程序或App对应的Topic下。这个方案的优点是不用理解MQTT的完整报文结构,AT指令透传直接发字符串就行,对单片机程序来说非常友好。
AT指令通信的一个坑是回显和OK的判定。ESP8266的AT固件默认开启回显,串口调试助手里看着没毛病,但在STM32代码里解析回复就很麻烦,因为收到的是“AT+CWMODE=1\r\nOK\r\n”这样交织在一起的字符串。建议上电后先发AT+E0关闭回显,然后每条AT指令发送后再等OK超时,超时就重发。超时时间设长一点,实际环境里WiFi连接指令AT+CWJAP返回OK可能要等5到10秒,不要用统一的三秒超时。
3. 软件控制逻辑与云平台对接
3.1 主程序状态机:轮流采集、集中判断、统一控制
系统的程序不要写成一锅粥,我习惯用一个大状态机:初始化、空闲等待、传感器采集、数据处理、控制输出、上云上报六个状态。初始化里做时钟配置、GPIO初始化、ADC校准、ESP8266连线;空闲等待是防止主循环跑飞,用SysTick做2秒钟时基;传感器采集状态里依次操作DHT22和粉尘ADC;数据处理状态做均值滤波和阈值判断;控制输出状态更新继电器;上云上报状态把数据拼成巴法云协议帧发出去。
typedef enum { SYS_INIT, SYS_IDLE, SYS_SENSOR, SYS_PROCESS, SYS_CONTROL, SYS_REPORT } sys_state_t; void main_loop(void) { while (1) { switch (state) { case SYS_INIT: init_all(); state = SYS_IDLE; break; case SYS_IDLE: if (time_tick >= 2000) state = SYS_SENSOR; break; case SYS_SENSOR: read_dht22(); read_dust(); state = SYS_PROCESS; break; case SYS_PROCESS: filter_data(); calc_threshold(); state = SYS_CONTROL; break; case SYS_CONTROL: update_relay(); state = SYS_REPORT; break; case SYS_REPORT: send_to_cloud(); state = SYS_IDLE; break; } } }这里要说明一点,不要每个传感器各写各的中断回调然后直接操作全局变量,数据竞争问题在单片机上很隐蔽。我特意把采集和控制拆成两个时间维度:采集是2秒一次,控制判断是5秒一次,上报是10秒一次。这样各模块之间时间错开,代码可读性和稳定性都高很多。而且即使某一时刻ESP8266卡在发送上,控制逻辑也不会被拖住,因为在状态机里上云上报是一个独立分支,发送卡住可以通过超时跳出。
3.2 数据滤波与浓度换算:ADC抖动怎么办
GP2Y1010AU的输出电压本身有噪声,再加上LED脉冲和市电工频干扰,ADC读到的数据会上下跳。直接拿这个跳动的数据做阈值判断,即使有滞回控制,临界点附近也有可能造成误动作。我在工程里用了两层滤波:第一层是滑动平均,把10次采样存入数组,每次取平均替代当前值;第二层是一阶惯性滤波,当前输出=上次输出×α+本次采样×(1-α),α取0.8左右。这两层的组合效果是既有较快的响应速度,又能把50mV级别的高频抖动压下去。
#define FILTER_BUF 10 uint16_t adc_buf[FILTER_BUF]; uint8_t adc_idx = 0; /* 一阶惯性滤波 */ float dust_filtered; float alpha = 0.8f; float get_dust_mg(float adc_voltage) { dust_filtered = alpha * dust_filtered + (1.0f - alpha) * adc_voltage; return (dust_filtered - v0_dust) * 0.2f; }换算浓度时按前面给的公式进行。DHT22读出来的温湿度是数字量不用处理,粉尘是ADC原始值,先除以4096乘以3.3得到电压,再减去零点电压乘以0.2得到mg/m³。如果你用的是3.3V供电的集成模块,ADC参考电压和模块参考电压要保持一致,否则算出来的电压值系统性偏差。我在调试时发现过一个有意思的现象:板载3.3V稳压芯片输出实际是3.35V,而HAL库默认把参考电压当成3.3V,导致粉尘浓度整体偏大约1.5%。严谨的做法是用STM32内部参考电压引脚VREFINT做比例换算,或者用万用表实测后把参考电压常量改掉。
3.3 云平台配置:以巴法云为例的Topic方案
巴法云是我试过的最适合嵌入式玩法的云平台之一,因为它的接入成本极低。先去巴法云官网注册账号,控制台里创建一个主题,比如“仓库环境”,系统会给每个主题分配一个秘钥,这一串字母数字组合本质上就是设备身份。STM32代码里把秘钥和主题名写死即可,云平台的App端或小程序端也用同一主题订阅,这样设备上报的数据就会实时显示在手机屏幕上。
Topic方案的好处是解耦:设备端只负责往Topic里推消息,App端只负责从Topic里收消息,两边不需要互相知道IP。这个模型非常适合仓库这种单点监控场景。如果以后要做多仓库,每个仓库建一个Topic,比如“仓库A环境”“仓库B环境”,App端按Topic维度展示就行,代码改动量很小。
在巴法云后台还可以配置报警规则,比如湿度连续5分钟高于75%就推送微信通知。这个功能对仓库管理特别有用,因为管理者不可能一直盯着App,异常报警反而是刚需。我建议在做完基本采集上报后,第一时间把报警规则加上,整个系统才算真正可用。
3.4 数据上报协议:让App和Web端读懂你的数据
上报的字符串格式要和云端、App端约定好。我建议用固定分隔符的纯文本协议,比如:温度:25.6,湿度:62.3,粉尘:0.12,状态:自动。这样小程序端拿到字符串后按冒号和逗号分割即可,不需要引入JSON库。如果未来要扩展成批量数据,再考虑上JSON格式。
上报频率也要控制。仓库环境变化是以分钟记的,10秒上报一次足够。太频繁一方面增加WiFi模块的负担,另一方面巴法云这类免费云平台有流量配额,数据量太大容易触发限制。我实测下来每10秒一帧,每帧不到50字节,一个月流量才几百KB,完全在免费额度内。
这里额外提一个建议:状态字段可以设计得细一点,除了“自动”“手动”,最好带上具体设备的开关状态,比如“除湿:开,风扇:关”。这样远程查看时,不用去猜设备现在到底在干什么。我的上报帧最终格式是:温度:25.6,湿度:62.3,粉尘:0.12,除湿:开,风扇:关,模式:自动。
4. 调试验收与问题排查实录
4.1 DHT22读不到数据、校验错误的排查
DHT22不响应、读出来全0、校验错误是最常见的三个坑。先检查接线和上拉,DHT22数据线必须上拉到3.3V或5V,模块自带的话可以省略;其次检查GPIO切换时序,起始信号后要切回输入模式等待响应,很多驱动代码是忙等状态,如果延时函数用的是不精确的软件延时,响应窗口会错过;最后检查采样间隔,DHT22手册要求两次读取间隔至少1秒,读太频繁传感器会不响应。我之前在一台长电线的板子上调试,线材过长导致信号反射,时序波形畸变严重,后来把数据线缩短到20cm以内,问题消失。
还有一个隐蔽问题:有些DHT22模块用的是3.3V供电,数据线却上拉到5V,这时GPIO输入高电平超过VDD会让传感器内部状态混乱。解决方法是把数据线的上拉电阻改到3.3V,或者换用5V供电的模块。这类问题不会每次都出现,但会周期性随机报错,排查起来很磨人。
4.2 粉尘传感器电压漂移与零点校正
GP2Y1010AU的零点电压随着温度和器件老化会漂。零漂的表现是密闭洁净环境里读数不是0而是0.05甚至0.08mg/m³,阈值判断容易被误导。解决方法是软件自动零点校准:系统上电等待10秒,如果此时判断环境中粉尘应该比较低,连读50次取最小值作为零点电压存入Flash。注意这个方法只适用于风机还没启动、环境稳定的时刻,如果你一上电风扇就开始转,粉尘被吹起来,零点校准会失败。我最后加了一个标志位,只有系统上电后第一次采集且粉尘读数明显低于0.05mg/m³才更新零点。
如果传感器附近有振动或气流扰动,零点还会短时间内跳变。这种情况下可以适当提高滑动平均的采样次数,让滤波效果更强。但也不要过度,否则浓度突变时系统反应太慢,排风扇迟迟不动作,就失去意义了。我调试下来的经验是10次滑动加0.8的惯性滤波比较均衡。
4.3 ESP8266连接超时与AT指令卡死恢复
ESP8266的问题多到可以单独写一篇。我记三个高频问题。第一,模块上电后串口发AT没反应,大概率是波特率不对,原厂固件默认115200,但也有改过9600的模块,先发AT试手,不行就轮流试4800、9600、19200、38400、57600、74880、115200、230400。第二,AT+CWJAP连接WiFi超时,注意模块只支持2.4G频段,路由器如果是双频合一模式,ESP8266可能连不上5G信号,把2.4G单独开一个SSID。第三,透传模式下卡在发送状态,模块不回复也不接收新数据,此时发送+++退出透传,再发AT+CIPCLOSE关闭连接重新建立即可。
频繁出现卡死就要考虑是不是串口缓冲溢出。ESP8266的AT固件串口FIFO有限,如果你一次性发送超过256字节的数据帧,模块会丢数据,所以控制每帧长度在100字节内。我在实际中还遇到一个问题:STM32串口发送数据太快,ESP8266来不及处理,导致部分指令被丢弃。解决办法是每发一条AT指令后必须等待OK或者错误码再发下一条,不要无脑连续发送。
4.4 继电器动作导致MCU复位的处理
这个问题在处理电磁干扰时特别容易遇到。现象非常典型:继电器吸合瞬间,系统重启,程序跑完初始化后又正常,看起来偶尔抽风。原因大概率是供电回路被拉低或者继电器线圈反向电动势干扰复位引脚。我最终的解决方案有三点:第一,整个系统的电源分配改为STM32的LDO输入和继电器驱动电源分别占两路,中间用电感隔离;第二,给MCU复位引脚加一个0.1uF电容,避免尖峰误触发复位;第三,继电器线圈两端确认有续流二极管,驱动三极管的基极串1k电阻限制驱动电流。做完这三步,继电器随便开关,系统纹丝不动。
如果继电器模块带光耦,注意光耦的输入侧和输出侧电压要独立,光耦两侧的GND最好分开,只在共地处汇合。如果光耦两侧共用3.3V的GND,隔离效果会大打折扣。但也要注意,光耦输出侧如果直接由MCU的3.3V供电,那光耦实际上只是电平转换而不是真正的隔离,真正隔离需要两侧独立电源。对于仓库环境控制系统,用到光耦模块已经能解决大部分干扰问题,彻底隔离方案在工业场景才需要。
这套系统我从原型搭到正式上线,前后改了三版。最深的一个体会是:环境控制类项目,硬件和软件从来不是最难的部分,最难的是把需求翻译成可靠的逻辑——比如滞回控制怎么防止继电器弹跳,零点校准怎么避免使用场景的干扰,ESP8266卡住的时候本地控制逻辑必须不受影响。如果你也想做类似的系统,建议先跑通本地闭环,再去折腾上云。云端随时可以替换,本地控制的可靠性才是仓库环境安全的命根子。后续想扩展的话,可以加一块OLED显示本地数据,或者换成PMS5003做PM2.5精确监测,再或者用两套ESP8266做多仓库分组上报,玩法很多,底层这套框架完全够用。