1. 项目缘起与整体设计思路
仓库环境管理这件事,没做过的人觉得不就是测个温湿度嘛,一个几十块钱的成品传感器就能搞定。但真在电子制造车间或者物料仓储现场待过就知道,事情远没有这么简单。我接手这个项目的起因很朴素:一个做SMT贴片加工的朋友找到我,说他们车间里有一批锡膏和MSD潮湿敏感器件,因为仓库湿度偶尔飙到70%RH以上,导致过炉后虚焊、立碑的比例明显上升,一批板子返修成本好几万。他们之前用的是一台壁挂式除湿机,靠人每天早晚各看一次墙上的温湿度计,手动开关。这种模式的问题很明显——人总有疏忽的时候,夜里湿度上来了没人管,第二天早上才发现,损失已经造成了。
所以这个项目的核心目标就一句话:让仓库自己感知环境、自己判断、自己动作,同时把数据传到远端,手机上随时能看。具体拆下来是四件事:第一,用STM32做主控,采集温湿度和粉尘浓度;第二,根据阈值自动控制通风风机和除湿设备;第三,通过ESP8266把数据上传到云平台;第四,本地要有屏幕显示和声光报警,方便现场人员第一时间知道异常。
为什么选STM32而不是直接用ESP32或者Arduino?这是很多人问我的第一个问题。ESP32确实自带WiFi,一颗芯片就能搞定采集和联网,看起来更省事。但在这个项目里我坚持用STM32+ESP8266的双芯片方案,理由有三点。一是工业现场的稳定性,STM32的定时器、ADC、中断响应这些底层资源非常成熟,跑裸机或者FreeRTOS都很稳,不会因为WiFi协议栈的调度影响到传感器采集的实时性。二是分工清晰,STM32专心做采集和控制逻辑,ESP8266专心做网络通信,两者通过串口AT指令交互,任何一方出问题都好排查。三是成本可控,STM32F103C8T6这颗芯片价格便宜、资料丰富,ESP8266-01S模块也就几块钱,整体BOM成本比用带WiFi的高端MCU要低。
整个系统的架构可以这样理解:STM32是大脑,负责思考和决策;传感器是眼睛和鼻子,负责感知;继电器和风机是手脚,负责执行;ESP8266是嘴巴,负责把情况汇报出去;OLED屏幕和蜂鸣器是表情,负责现场表达。这个比喻虽然糙,但能帮你快速建立整体认知。
在方案选型上,我做了几个关键决策,这里展开说一下背后的逻辑。
主控芯片选STM32F103C8T6。这颗芯片是Cortex-M3内核,72MHz主频,64KB Flash,20KB RAM,对于这个项目来说绰绰有余。它有3个USART,一个接ESP8266,一个接调试串口,还有一个可以留给以后扩展。有2个I2C,可以接OLED和温湿度传感器。有多个定时器,可以用来做软件延时和PWM输出。最关键的是,这颗芯片的生态太成熟了,网上随便一搜就有大量的例程和踩坑记录,遇到问题基本都能找到答案。
温湿度传感器选DHT22(AM2302)。市面上常见的还有SHT30、SHT31、AHT10等。DHT22的精度是±0.5℃和±2%RH,对于仓库环境监测来说够用了。它的优点是单总线协议、接线简单、价格便宜。缺点是响应速度慢,两次读取之间至少要间隔2秒,而且时序要求比较严格,需要微秒级延时。如果你对精度要求更高,可以换SHT30,I2C接口,精度±0.3℃和±2%RH,读取速度也快很多,但价格会贵一些。
粉尘传感器选GP2Y1010AU0F。这是夏普的一款光学粉尘传感器,输出模拟电压,通过ADC采集。它的原理是红外LED发光,粉尘颗粒反射光线到光电二极管上,输出电压与粉尘浓度成正比。这个传感器能测PM2.5和PM10的混合浓度,对于仓库环境来说足够。需要注意的是,它需要脉冲驱动LED,典型驱动周期是10ms,其中LED点亮时间0.32ms,要在点亮后0.28ms采样输出电压。这个时序如果用软件延时来做,会占用CPU资源,所以我用定时器中断来驱动。
ESP8266选01S模块。这个模块体积小、价格低、AT指令成熟。虽然它只有2个GPIO可用,但对于纯联网应用来说够了。我用的是安信可的AT固件,版本是1.7.x,支持MQTT指令。这里要提醒一句,ESP8266的AT固件版本很多,不同版本指令集有差异,建议固定用一个版本,不要随便刷。
云平台选OneNet或者阿里云IoT。这两个平台我都用过,OneNet的MQTT接入比较简单,适合快速验证;阿里云IoT功能更全,但接入流程稍微复杂一些。这个项目里我用的是OneNet的多协议接入,通过MQTT协议上传数据。ESP8266用AT指令连MQTT,需要先连WiFi,再连TCP,再发MQTT连接报文,流程比较长,后面会详细讲。
显示选0.96寸OLED。I2C接口,128x64分辨率,显示温湿度、粉尘浓度、设备状态足够了。相比LCD1602,OLED不需要背光、对比度高、视角广,而且I2C只需要两根线。
执行机构选继电器模块。用两个继电器分别控制通风风机和除湿机。继电器选5V驱动的,STM32的GPIO输出3.3V,需要加三极管驱动或者用光耦隔离模块。我直接用了带光耦的继电器模块,省事。
整体设计思路就这些,下面进入具体的细节解析。
2. 核心细节解析与实操要点
2.1 传感器采集的时序陷阱与解决方案
DHT22的时序是这个项目里第一个坑。它的单总线协议是这样的:主机拉低总线至少1ms作为起始信号,然后释放总线,DHT22响应后会拉低80us,再拉高80us,然后开始传输40位数据。每一位数据以50us低电平开始,高电平持续时间决定是0还是1——26-28us表示0,70us表示1。
问题在于,STM32的HAL库延时函数HAL_Delay()是毫秒级的,不够用。你需要微秒级延时。我试过用__NOP()循环来做,但不同优化等级下循环次数不一样,容易出问题。后来我改用定时器做微秒延时,用TIM4,预分频到1MHz,也就是每计数一次1us。这样延时函数就准了。
void delay_us(uint32_t us) { __HAL_TIM_SET_COUNTER(&htim4, 0); while(__HAL_TIM_GET_COUNTER(&htim4) < us); }但这里还有个坑:DHT22读取过程中不能被打断,否则时序会乱。所以读之前要关中断,读完再开。关中断的时间大概在5ms左右,对于这个项目来说可以接受,因为没有什么特别紧急的中断需要响应。
另一个坑是DHT22的读取间隔。手册上写的是不小于2秒,我实测下来,如果间隔小于1.5秒,读出来的数据会偶尔出错。所以我在代码里强制加了2秒的间隔,用定时器来调度,不依赖延时函数。
GP2Y1010AU0F的时序也不简单。它需要你给LED驱动脚一个脉冲:先拉低0.32ms(点亮LED),然后在0.28ms的时候采集模拟输出,最后拉高0.32ms(熄灭LED),整个周期是10ms。这个时序如果用软件延时来做,CPU基本被占满了。我的做法是用TIM3产生PWM,频率100Hz,占空比3.2%,然后用这个PWM去驱动LED。同时在PWM的上升沿触发ADC采样,用定时器触发ADC的方式,让硬件自动完成采样,CPU只需要在ADC中断里读数据就行。
这里有个细节:GP2Y1010AU0F的输出电压在无尘环境下大概是0.9V左右,随着粉尘浓度升高,电压会下降。所以计算浓度的时候要注意方向。我用的是这个公式:
粉尘浓度(ug/m3) = (V0 - V) / 0.005其中V0是干净空气下的输出电压,我实测大概是0.9V。但这个公式是经验公式,不同批次的传感器会有差异,建议用标准仪器校准一下。
2.2 ESP8266的AT指令联网流程与避坑
ESP8266的AT指令联网,说起来简单,做起来坑不少。整个流程是这样的:
- 发送
AT,确认模块响应 - 发送
AT+CWMODE=1,设置为Station模式 - 发送
AT+CWJAP="SSID","password",连接WiFi - 发送
AT+CIPMUX=0,设置为单连接模式 - 发送
AT+CIPSTART="TCP","183.230.40.39",6002,连接OneNet的MQTT服务器 - 发送
AT+CIPSEND=<length>,然后发送MQTT连接报文 - 等待服务器返回CONNACK
- 之后就是定期发送MQTT PUBLISH报文上传数据
这里第一个坑是AT指令的响应处理。ESP8266返回的字符串里包含\r\n,而且有时候会夹杂一些模块自己打印的调试信息。如果你用简单的字符串比较,很容易误判。我的做法是写一个状态机,逐字符接收,遇到\r\n就认为一行结束,然后判断这一行是否包含OK、ERROR、FAIL等关键字。
第二个坑是超时处理。ESP8266连WiFi的时候,如果密码错了,它会一直重试,不会返回FAIL。所以你必须自己加超时,比如10秒没连上就重新配置。我见过有人在这里卡了一整天,就是因为没加超时,程序一直死在等待里。
第三个坑是AT+CIPSEND的长度计算。MQTT报文是二进制的,里面可能有0x00,如果你用strlen算长度,遇到0x00就截断了。必须用实际的字节数。而且AT+CIPSEND之后,模块会返回>,然后你才能发数据。发完数据后,模块会返回SEND OK。这个流程必须严格按顺序来。
第四个坑是MQTT报文的构造。MQTT 3.1.1的CONNECT报文格式是固定的,但里面的Client ID、Username、Password长度是可变的。你需要按照MQTT协议规范来拼报文。我建议直接找一个现成的MQTT报文构造函数,不要自己从头写,容易出错。OneNet的MQTT接入参数是:服务器地址183.230.40.39,端口6002,Client ID用设备ID,Username用产品ID,Password用鉴权信息。
这里给一个CONNECT报文的构造示例:
// MQTT CONNECT报文 uint8_t mqtt_connect[] = { 0x10, 0x2A, // 固定头,剩余长度42 0x00, 0x04, 'M', 'Q', 'T', 'T', // 协议名 0x04, // 协议级别 0x02, // 连接标志,Clean Session 0x00, 0x3C, // 保持连接时间60秒 0x00, 0x0A, // Client ID长度10 // ... Client ID内容 // ... Username和Password };实际使用的时候,Client ID、Username、Password的长度要根据你的实际值来调整,剩余长度也要重新计算。
2.3 自动控制逻辑的设计与阈值设定
自动控制逻辑看起来简单,就是超过阈值开风机、开除湿机,但实际设计的时候要考虑几个问题。
第一,阈值怎么定?对于电子制造仓库,行业标准一般是温度18-28℃,湿度40%-60%RH。但不同物料要求不一样,锡膏一般要求湿度不超过60%,MSD器件要求更严,有的要求不超过50%。我建议把阈值做成可配置的,存在STM32的Flash里,通过串口或者按键可以修改。这样不用重新烧程序就能调整。
第二,回差怎么设?如果阈值是60%,湿度到了60.1%就开除湿机,降到59.9%就关,那除湿机会频繁启停,既费电又伤设备。所以要设回差,比如湿度超过60%开,降到55%才关。这个回差一般设5%RH左右。温度的回差可以设2℃。
第三,延时怎么加?传感器偶尔会有波动,如果单次读数超阈值就动作,可能会误触发。我的做法是连续3次读数都超阈值才动作,每次间隔2秒。这样能过滤掉大部分干扰。
第四,优先级怎么排?如果温度和湿度都超了,先开哪个?我的逻辑是:除湿机优先,因为除湿机工作时会发热,可能会让温度进一步升高,所以如果温度也超了,在开除湿机的同时也要开通风。如果只是温度超了,只开通风就行。
第五,手动模式怎么处理?现场有时候需要手动控制,比如维修的时候。所以要有手动/自动切换。我用一个按键来切换,按下后进入手动模式,再按一下切回自动。手动模式下,另一个按键用来手动开关风机和除湿机。
代码逻辑大概是这样:
if (auto_mode) { if (humidity > hum_high_threshold) { hum_alarm_count++; if (hum_alarm_count >= 3) { relay_dehumidifier = ON; relay_fan = ON; // 除湿时同时通风 } } else if (humidity < hum_low_threshold) { hum_alarm_count = 0; relay_dehumidifier = OFF; if (temp < temp_high_threshold) { relay_fan = OFF; } } if (temperature > temp_high_threshold) { temp_alarm_count++; if (temp_alarm_count >= 3) { relay_fan = ON; } } else if (temperature < temp_low_threshold) { temp_alarm_count = 0; if (humidity < hum_high_threshold) { relay_fan = OFF; } } }这里要注意,除湿机和风机的关闭逻辑要互相考虑,不能简单地把对方关掉。上面的代码里,关除湿机的时候要检查温度是否还高,关风机的时候要检查湿度是否还高。
2.4 电源设计与抗干扰措施
这个项目里,电源设计是容易被忽视但非常关键的一环。系统里有STM32(3.3V)、ESP8266(3.3V,但峰值电流能到300mA)、继电器(5V)、传感器(5V)、OLED(3.3V)。如果电源设计不好,会出现ESP8266联网时复位、ADC采集值跳动、OLED花屏等问题。
我的方案是:输入用12V/2A的开关电源,然后分三路。第一路用LM2596降到5V,给继电器、传感器、ESP8266供电。第二路用AMS1117-3.3降到3.3V,给STM32和OLED供电。第三路也是5V,但单独给ESP8266,中间加一个1000uF的电解电容和0.1uF的陶瓷电容,用来吸收ESP8266联网时的电流尖峰。
这里有个细节:ESP8266的峰值电流很大,如果和STM32共用一路3.3V,联网的时候可能会把STM32的电压拉低导致复位。所以一定要分开供电,或者在ESP8266的电源脚旁边加一个大电容。
抗干扰方面,ADC采集粉尘传感器的时候,读数会有跳动。我在软件里加了滑动平均滤波,每次采集10个值,去掉最大和最小,剩下的取平均。这样读数就稳多了。另外,继电器的线圈在断电时会产生反向电动势,可能干扰MCU。我在继电器线圈两端加了续流二极管,并且在继电器和MCU之间用了光耦隔离。
3. 实操过程与核心环节实现
3.1 硬件搭建与接线
先列一下完整的物料清单:
| 物料 | 型号 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1 | 核心板 |
| WiFi模块 | ESP8266-01S | 1 | 需配3.3V供电 |
| 温湿度传感器 | DHT22 | 1 | 单总线 |
| 粉尘传感器 | GP2Y1010AU0F | 1 | 模拟输出 |
| 显示屏 | 0.96寸OLED | 1 | I2C接口 |
| 继电器模块 | 5V光耦隔离 | 2 | 控制风机和除湿机 |
| 蜂鸣器 | 有源蜂鸣器 | 1 | 报警 |
| 按键 | 轻触按键 | 3 | 设置/手动/复位 |
| 电源 | 12V/2A开关电源 | 1 | 总输入 |
| 降压模块 | LM2596 | 1 | 12V转5V |
| 降压模块 | AMS1117-3.3 | 1 | 5V转3.3V |
接线方面,STM32的PA0接DHT22的数据脚,PA1接粉尘传感器的模拟输出,PA2和PA3做I2C接OLED,PA9和PA10做串口1接ESP8266,PA11和PA12接继电器,PA13接蜂鸣器,PB0、PB1、PB2接按键。
这里要注意,ESP8266的供电是3.3V,但它的TX和RX是3.3V电平,可以直接接STM32的串口。不过ESP8266的RX对电压比较敏感,如果STM32的TX输出3.3V,一般没问题。但如果你用的是5V的STM32板子,就要加电平转换。
DHT22的数据脚需要接一个4.7K到10K的上拉电阻到3.3V。我用的4.7K,实测稳定。粉尘传感器的LED驱动脚接STM32的PA8,用定时器PWM驱动。模拟输出接PA1,用ADC1的通道1采集。
继电器模块的输入接STM32的GPIO,输出接风机和除湿机的电源线。注意继电器的触点容量要够,风机和除湿机的启动电流可能比较大,建议选10A以上的继电器。
3.2 STM32工程搭建与关键代码
我用的是Keil MDK 5,配合STM32CubeMX生成初始化代码。这里说一下CubeMX的配置要点。
时钟配置:外部晶振8MHz,PLL倍频到72MHz。APB1分频到36MHz,APB2分频到72MHz。
串口配置:USART1,波特率115200,8位数据,1位停止,无校验。开启接收中断和空闲中断。空闲中断用来判断一帧数据接收完成。
定时器配置:TIM4用于微秒延时,预分频72-1,计数周期65535。TIM3用于粉尘传感器LED驱动,PWM模式,频率100Hz,占空比3.2%。TIM2用于系统滴答,1ms中断。
ADC配置:ADC1,通道1,连续转换模式,由TIM3的TRGO触发。DMA传输,缓冲区大小10。
I2C配置:I2C1,标准模式100kHz,用于OLED。
GPIO配置:继电器输出推挽,按键输入上拉,蜂鸣器输出推挽。
生成代码后,需要自己添加的部分主要是:DHT22驱动、OLED驱动、ESP8266驱动、MQTT报文构造、控制逻辑。
DHT22的驱动代码大概是这样:
#define DHT22_PIN GPIO_PIN_0 #define DHT22_PORT GPIOA void DHT22_Start(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT22_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT22_PORT, &GPIO_InitStruct); HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_RESET); delay_us(1200); HAL_GPIO_WritePin(DHT22_PORT, DHT22_PIN, GPIO_PIN_SET); delay_us(30); GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_PULLUP; HAL_GPIO_Init(DHT22_PORT, &GPIO_InitStruct); } uint8_t DHT22_ReadByte(void) { uint8_t byte = 0; for (int i = 0; i < 8; i++) { while (!HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN)); // 等待50us低电平结束 delay_us(40); byte <<= 1; if (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN)) { byte |= 1; } while (HAL_GPIO_ReadPin(DHT22_PORT, DHT22_PIN)); // 等待高电平结束 } return byte; }读取的时候要关中断,读完再开。读出来的40位数据是:8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。校验和等于前四个字节相加的低8位。
ESP8266的驱动我用的是状态机方式,不阻塞主循环。定义一个结构体:
typedef struct { uint8_t rx_buffer[256]; uint16_t rx_index; uint8_t rx_complete; uint8_t state; uint32_t timeout; } ESP8266_HandleTypeDef;串口空闲中断里把接收到的数据存入rx_buffer,置位rx_complete。主循环里检查rx_complete,然后根据state做相应的处理。
MQTT报文构造,我写了一个函数:
uint16_t MQTT_BuildConnect(uint8_t *buffer, const char *client_id, const char *username, const char *password) { uint16_t index = 0; uint16_t remaining_length = 10 + strlen(client_id) + 2 + strlen(username) + 2 + strlen(password); buffer[index++] = 0x10; // 剩余长度编码,这里简化处理,假设小于128 buffer[index++] = remaining_length; // 协议名 buffer[index++] = 0x00; buffer[index++] = 0x04; buffer[index++] = 'M'; buffer[index++] = 'Q'; buffer[index++] = 'T'; buffer[index++] = 'T'; // 协议级别 buffer[index++] = 0x04; // 连接标志 buffer[index++] = 0xC2; // Clean Session + Username + Password // 保持连接时间 buffer[index++] = 0x00; buffer[index++] = 0x3C; // Client ID buffer[index++] = 0x00; buffer[index++] = strlen(client_id); memcpy(&buffer[index], client_id, strlen(client_id)); index += strlen(client_id); // Username buffer[index++] = 0x00; buffer[index++] = strlen(username); memcpy(&buffer[index], username, strlen(username)); index += strlen(username); // Password buffer[index++] = 0x00; buffer[index++] = strlen(password); memcpy(&buffer[index], password, strlen(password)); index += strlen(password); return index; }上传数据的PUBLISH报文也类似,需要构造主题名和负载。OneNet的MQTT上传主题一般是$dp,负载是JSON格式。
3.3 云平台配置与数据上传
OneNet的配置流程是这样的:注册账号,创建产品,选择MQTT协议,然后添加设备。设备创建后会得到设备ID、产品ID和鉴权信息。这三个值就是MQTT连接时的Client ID、Username和Password。
数据上传的格式,OneNet支持JSON。我用的格式是:
{ "temperature": 25.6, "humidity": 55.2, "dust": 35 }上传的主题是$dp,QoS用0。ESP8266发送的时候,先发AT+CIPSEND=<length>,等模块返回>,然后把MQTT PUBLISH报文发过去。
这里有个细节:OneNet的MQTT服务器对报文长度有限制,单次上传不要超过1KB。我们的数据很小,没问题。
数据上传的频率,我设的是每30秒一次。太频繁了费流量,太慢了实时性不好。30秒对于仓库环境监测来说足够了。
3.4 本地显示与报警实现
OLED显示我用的是u8g2库,这个库支持很多种OLED,移植也方便。显示内容分四行:第一行温度,第二行湿度,第三行粉尘浓度,第四行设备状态。
u8g2_ClearBuffer(&u8g2); u8g2_SetFont(&u8g2, u8g2_font_6x10_tf); u8g2_DrawStr(&u8g2, 0, 12, "Temp:"); u8g2_DrawStr(&u8g2, 40, 12, temp_str); u8g2_DrawStr(&u8g2, 0, 26, "Humi:"); u8g2_DrawStr(&u8g2, 40, 26, humi_str); u8g2_DrawStr(&u8g2, 0, 40, "Dust:"); u8g2_DrawStr(&u8g2, 40, 40, dust_str); u8g2_DrawStr(&u8g2, 0, 54, "Fan:"); u8g2_DrawStr(&u8g2, 40, 54, fan_state ? "ON" : "OFF"); u8g2_SendBuffer(&u8g2);报警逻辑:当温度、湿度或粉尘任一超过阈值时,蜂鸣器以1Hz的频率鸣叫,同时OLED上对应的数值闪烁显示。如果超过阈值持续5分钟没有恢复,就认为是大问题,蜂鸣器长鸣。
按键功能:三个按键分别是设置键、加键、减键。设置键用来进入设置模式,加键和减键用来调整阈值。设置模式下,OLED显示当前阈值,按设置键切换要调整的参数。设置完成后,长按设置键保存到Flash。
4. 常见问题与排查技巧实录
4.1 调试过程中遇到的典型问题
问题一:DHT22读出来全是0或者全是1。
这个问题的原因通常是时序不对。我排查的时候用示波器看了波形,发现起始信号的低电平时间不够。手册上要求至少1ms,我一开始用了800us,就不行。改成1200us后正常了。另外,上拉电阻也很关键,我试过10K的,波形上升沿太慢,读出来偶尔出错。换成4.7K后稳定了。
问题二:ESP8266连不上WiFi。
这个问题的排查步骤是这样的:先用串口助手手动发AT指令,确认模块能正常响应。如果手动都连不上,那就是模块或者WiFi的问题。如果手动能连上,但程序里连不上,那就是程序的问题。我遇到过一次,是因为程序里发AT指令后等待时间太短,模块还没响应就发下一条了。后来把等待时间从1秒改成3秒,就好了。
问题三:MQTT连接被服务器拒绝。
OneNet的MQTT连接被拒绝,最常见的原因是Client ID、Username、Password填错了。注意,OneNet的Username是产品ID,不是产品名称。Password是鉴权信息,不是设备名称。这三个值一定要从OneNet的控制台里复制,不要手打。
问题四:ADC采集的粉尘值跳动很大。
这个问题的原因可能是电源纹波或者传感器本身噪声。我在电源脚加了100uF和0.1uF的电容,软件里加了滑动平均滤波,跳动从±20降到了±5以内。另外,GP2Y1010AU0F的输出阻抗比较高,STM32的ADC输入阻抗有限,建议加一个电压跟随器。我偷懒没加,但通过延长采样时间解决了。
问题五:继电器动作时STM32复位。
这是典型的电源干扰问题。继电器线圈断电时会产生反向电动势,通过电源线传导到MCU。我在继电器线圈两端加了续流二极管,并且在继电器和MCU之间用了光耦隔离,问题就解决了。另外,继电器的电源和MCU的电源要分开走线,不要共用一根线。
问题六:OLED显示花屏。
I2C的时序问题。STM32的硬件I2C有时候会卡死,我后来改用软件模拟I2C,就稳定了。软件I2C的延时用delay_us(2),速度虽然慢一点,但可靠性高很多。
4.2 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| DHT22读数全0 | 时序不对 | 示波器看波形 | 调整延时,换4.7K上拉 |
| ESP8266连不上WiFi | 等待时间不够 | 手动发AT测试 | 增加等待时间到3秒 |
| MQTT连接被拒 | 参数填错 | 核对OneNet控制台 | 复制粘贴,不要手打 |
| 粉尘值跳动大 | 电源噪声 | 示波器看电源纹波 | 加滤波电容,软件滤波 |
| 继电器动作复位 | 电源干扰 | 分开供电 | 加续流二极管,光耦隔离 |
| OLED花屏 | I2C时序 | 换软件I2C | 软件模拟I2C |
| 程序跑飞 | 看门狗未开 | 检查看门狗配置 | 开启独立看门狗 |
| 数据上传失败 | 报文格式错 | 串口打印报文 | 核对MQTT协议格式 |
4.3 实操心得与避坑建议
第一条心得:先分模块调试,再联调。我一开始想一口气把整个系统写完再调试,结果出了问题不知道是哪里的。后来改成先调DHT22,再调OLED,再调ESP8266,最后联调,效率高很多。每个模块单独调通后,联调的时候问题就少很多。
第二条心得:串口打印是最好的调试工具。在关键位置加串口打印,把传感器的值、ESP8266的响应、MQTT的报文都打印出来,一眼就能看出问题在哪。我习惯用printf重定向到串口,方便得很。
第三条心得:电源一定要留余量。我一开始用的是一个1A的电源,结果ESP8266联网的时候电流不够,模块反复复位。换成2A的电源后就好了。电源的余量至少要是实际电流的1.5倍。
第四条心得:AT指令的响应要逐字符解析。不要用strstr去匹配,因为ESP8266返回的数据里可能包含OK这样的字符串,但不是你想要的那个。我写了一个逐行解析的函数,遇到\r\n就认为一行结束,然后判断这一行的内容。
第五条心得:MQTT的Keep Alive要设合理。我一开始设的60秒,但OneNet的服务器有时候会主动断开。后来改成120秒,就稳定了。Keep Alive的意思是,如果在这个时间内没有收到任何报文,客户端就要发一个PINGREQ。如果服务器在这个时间的1.5倍内没收到任何报文,就会断开连接。
第六条心得:Flash存储要加磨损均衡。如果频繁写Flash存阈值,Flash会坏。我的做法是只在设置模式下才写Flash,而且写之前先读出来比较,如果值没变就不写。另外,STM32F103的Flash擦写次数大概是1万次,正常使用几年没问题。
第七条心得:看门狗一定要开。工业现场电磁干扰大,程序可能会跑飞。开启独立看门狗,超时时间设2秒,在主循环里喂狗。这样即使程序跑飞,也能自动复位。
第八条心得:外壳和接线要规范。我见过有人把板子直接放在铁皮柜里,结果短路烧了。建议用塑料外壳,接线用端子,不要直接焊。传感器和继电器的线要分开走,避免干扰。
这个项目从开始到稳定运行,大概花了三周时间。其中调试ESP8266和MQTT花的时间最多,大概占了一半。硬件搭建和STM32代码其实很快,两天就搞定了。如果你也要做类似的项目,我的建议是先把ESP8266的AT指令和MQTT报文调通,这部分是最容易卡住的。另外,OneNet的文档虽然全,但有些细节写得不清楚,遇到问题多搜搜社区,基本都能找到答案。
最后再分享一个小技巧:如果你觉得OneNet的MQTT接入太麻烦,可以先用HTTP协议上传数据,ESP8266发HTTP POST请求比MQTT简单很多。等HTTP调通了,再换成MQTT。这样能快速看到数据上传的效果,增加信心。