有段时间没发这种偏底层的折腾记录了。前阵子朋友问我能不能搞一套能远程看温湿度的东西,我第一反应就是STM32+ESP8266+DHT11+OLED这套经典组合——东西便宜、资料多、改起来自由。整个项目从下单买零件到调通大概花了两个晚上,中间踩了几个不算深但很磨人的坑,顺手把过程整理成一篇完整教程。不管你是刚学STM32的学生,还是想给家里做个环境监测小系统的业余玩家,这篇文章应该能让你少走不少弯路。
1. 整体方案设计与器件选型
1.1 为什么是这个组合
先说结论:这套组合最大的优势是便宜、成熟、每颗芯片都有海量现成资料。
- 主控选STM32F103C8T6,也就是大家常说的“蓝色药丸”,Cortex-M3内核,72MHz主频,接口丰富,关键是代码资源全网最多。你遇到的问题,几乎都能在论坛上搜到同类讨论。
- ESP8266负责联网。这颗芯片虽然出了很多年,但至今仍是物联网入门绕不开的模块,支持TCP/IP协议栈,可以通过串口AT指令直接操作,不需要额外写复杂的网络协议栈。
- DHT11是温湿度传感器,三根线(VCC、GND、DATA),单总线协议,读取温湿度就够用了。精度虽然不高,但胜在便宜、稳定、适合监控场景。
- OLED选的是0.96寸I2C接口版本(SSD1306),四根线,刷个温湿度显示非常方便。
这套方案解决的核心问题就是:传感器采集现场数据 → 单片机处理后本地显示 → 同时通过ESP8266把数据传上WiFi,让手机或电脑在浏览器里远程查看。适合机房、花房、储物间、宠物窝等场合的温度/湿度监测。
1.2 系统架构与数据链路
整个系统的数据流向大概是这样的:
DHT11(采集温湿度) ↓ GPIO单总线 STM32F103C8T6(数据处理 + OLED显示 + 串口控制ESP8266) ↓ I2C ↓ USART OLED显示屏幕 ESP8266(WiFi透传) ↓ TCP/HTTP 路由器/局域网/云平台STM32是大脑中枢,DHT11每次读到的温湿度数据,一方面送给OLED刷新显示,另一方面通过串口丢给ESP8266。ESP8266负责把数据打包成HTTP请求发到服务端。
这里有个设计上的小细节值得注意:为什么不直接用ESP8266读DHT11然后省掉STM32?技术上完全可行,但代价是你得用NodeMCU固件+Lua脚本或者Arduino环境去调ESP8266的GPIO时序,而DHT11的时序要求又比较严格,用非实时系统去折腾单总线时序容易出幺蛾子。让STM32这种实时性强的MCU来做数据采集和本地逻辑,让ESP8266专注干连网这件事,各司其职,稳定得多。
1.3 方案选型的两个分支
我这次做的是“局域网内远程查看”,也就是ESP8266作为TCP客户端,连上路由器后往局域网内一台服务器(或电脑上跑的Python服务)发数据。这样做的好处是不依赖外网,数据走内网,速度也快。
如果你想把数据放到公网随时查看,完全可以沿用同样的硬件,把ESP8266的连接目标换成OneNET、阿里云IoT、巴法云这类物联网平台。ESP8266通过AT指令建立TCP连接,发送MQTT或HTTP协议的数据包即可。这一步基本上只改代码不改硬件。这也是我把联网部分做成单独章节的原因——硬件链路是一样的,变的只是协议内容。
2. 硬件清单、电路连接与工程初始化
2.1 器件清单
| 器件 | 型号/规格 | 数量 | 参考价格 |
|---|---|---|---|
| 主控板 | STM32F103C8T6最小系统板 | 1 | 10~15元 |
| WiFi模块 | ESP8266-01S(或NodeMCU) | 1 | 6~15元 |
| 温湿度传感器 | DHT11(蓝壳) | 1 | 3~6元 |
| 显示屏 | 0.96寸OLED SSD1306 I2C | 1 | 8~15元 |
| 面包板/洞洞板 | 面包板亦可 | 1 | 几块钱 |
| 杜邦线 | 母对母、公对母若干 | 若干 | 几块钱 |
我自己用的是ESP8266-01S(8个引脚的黑色小模块),因为体积小,适合做进盒子。NodeMCU板子对新手更友好,带USB转串口,不用额外买下载器。如果你手头是NodeMCU,引脚定义和AT指令逻辑是一样的,但要注意ESP8266的TX/RX对应关系:NodeMCU的板载串口和GPIO引出的串口有区别,建议直接用板载USB串口,省事。
2.2 接线表(ESP8266-01S版本)
STM32F103C8T6和ESP8266-01S之间按下面的接法走:
| STM32引脚 | ESP8266-01S引脚 |
|---|---|
| PA9 (USART1_TX) | RXD |
| PA10 (USART1_RX) | TXD |
| 3.3V | VCC(接3.3V,注意电流) |
| GND | GND |
| 不接 | EN/CH_PD(必须接3.3V,否则模块不工作) |
| 不接 | GPIO0(正常运行时悬空或接3.3V,下载固件时接GND) |
| 不接 | GPIO2(悬空即可) |
| 不接 | RST(悬空即可) |
DHT11接STM32的PA1引脚(也可以用其他GPIO),OLED接I2C1:SCL→PB6,SDA→PB7。
这里有个新手容易翻车的点:ESP8266-01S的工作电流波动很大,WiFi发射瞬间电流能达到200~300mA以上。如果你直接把它接到STM32板子上的3.3V引脚,很可能导致电压被拉低,表现就是ESP8266连不上网、反复重启、或者STM32也跟着复位。稳妥的做法是用一个AMS1117-3.3稳压模块,从5V输入降压出3.3V,并在ESP8266的VCC和GND之间并一个100uF的电解电容(或者10uF+0.1uF组合),这能吸收瞬时电流尖峰。这个电容真的很重要,我一开始跳过它,ESP8266怎么都连不上AP。
2.3 STM32 CubeMX工程初始化
用STM32CubeMX生成基础工程,版本建议1.9.0以上。配置项如下:
- SYS:Debug Serial Wire;Timebase Source选TIM1(不要让SysTick被HAL_Delay占用后冲突)
- RCC:HSE Crystal/Ceramic Resonator
- 时钟树:HSE 8MHz → PLL → SYSCLK 72MHz,APB1 36MHz,APB2 72MHz
- PA9/PA10:USART1,异步模式,115200-8-N-1
- PB6/PB7:I2C1,Fast Mode 400kHz
- PA1:GPIO_Output,初始电平High(DHT11数据线默认高)
- 工程设置里选生成静态库或独立.c/.h都行,我用独立方式方便后期拆分
时钟树这里多说一句。STM32F103的USART1挂在APB2总线上(72MHz),USART2/3挂在APB1上(36MHz),如果你要跑更高波特率或者精确波特率,务必确认外设时钟源正确。我刚开始图省事,时钟树默认没改,结果串口输出全是乱码,后来才想起来是时钟配置的问题。
3. DHT11 数据读取:时序、代码与避坑
3.1 DHT11 单总线协议
DHT11用的是单总线协议,数据线只有一根,双向通信。读一次完整数据的流程是:
- 主机拉低数据线,持续至少18ms(我习惯拉低20ms),作为起始信号。
- 主机释放数据线(拉高),延时20~40us,然后切换为输入模式等待从机应答。
- DHT11会把数据线拉低约80us,表示应答,再拉高80us,然后开始发送40位数据。
- 每一位数据以50us低电平开头,后面跟着高电平,高电平持续26~28us代表“0”,持续70us左右代表“1”。
- 40位数据 = 湿度整数 + 湿度小数 + 温度整数 + 温度小数 + 校验和。校验和等于前四个字节之和的低8位。
判别“0”还是“1”的关键就是那个高电平持续时间。所以延时要准,尤其是读位中间的延时。用循环延时或DWT(数据观察点与跟踪单元)延时都可以。我最终用的是DWT,比HAL_Delay精确很多。
3.2 HAL库方式读取DHT11的完整代码
先贴核心的数据读取函数。注意HAL_Delay只能做到毫秒级,读位的地方不能用。我这里在工程里移植了一个DWT微秒延时函数,精度很好。
/* dht11.c */ #include "dht11.h" #include "main.h" extern TIM_HandleTypeDef htim1; // 仅用于延时参考的话可以忽略 #define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_1 static void DHT11_Delay_us(uint32_t us) { // 用DWT实现微秒延时,比软件循环更稳定 CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while (DWT->CYCCNT - start < ticks); } static void DHT11_Start(void) { // 主机起始信号:至少拉低18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); DHT11_Delay_us(20000); // 释放总线,上拉电阻会把电平拉高 HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); DHT11_Delay_us(30); } static uint8_t DHT11_ReadBit(void) { // 等待50us低电平结束 while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); DHT11_Delay_us(30); // 此时读到的电平如果在40us前还是高,说明是1;否则是0 if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); // 等剩余高电平结束 return 1; } else { return 0; } } static uint8_t DHT11_ReadByte(void) { uint8_t val = 0; for (int i = 0; i < 8; i++) { val <<= 1; val |= DHT11_ReadBit(); } return val; } uint8_t DHT11_Read(float *humidity, float *temperature) { uint8_t data[5] = {0}; DHT11_Start(); // 等待应答 uint32_t timeout = 100000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET) { if (--timeout == 0) return 1; // 超时,DHT11未响应 } while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET); // 低电平约80us while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET); // 高电平约80us for (int i = 0; i < 5; i++) { data[i] = DHT11_ReadByte(); } // 校验:前四个字节相加低8位等于第五个字节 if ((uint8_t)(data[0] + data[1] + data[2] + data[3]) != data[4]) { return 2; // 校验失败 } *humidity = data[0] + data[1] / 10.0f; *temperature = data[2] + data[3] / 10.0f; return 0; }这段代码有几个关键点:
- 起始信号后的释放要精确。拉低18ms以上是为了让DHT11识别到读取请求,但释放后到读取应答前的这30us不能太长,否则DHT11可能已经错过窗口,导致之后所有位都读不到。
- 读位函数里为什么延时30us再判断?因为每一个bit都是从50us低电平开始,然后才是高电平部分。高电平持续26~28us是0,持续70us是1。等低电平结束后延时30us,此时若是1,引脚仍为高电平;若是0,引脚已经回到低电平。这个30us是经验值,实测在高速MCU上很稳定。
- 循环读GPIO是有超时保护的。如果DHT11坏了或者线没接对,就会卡死在while里。所以一定要加超时变量,否则主程序直接卡死。
3.3 DHT11 实测坑
第一,DHT11数据读取间隔至少要1秒以上。DHT11本身采样周期是1~2秒,你读太频繁,它还没更新数据就会返回旧值,甚至可能因为总线操作太密集而返回错误。我在主循环里用一个500ms的定时器做节流,实际2秒读一次,显示和上报都够用。
第二,不要在读到数据后立刻再去读。DHT11的电源对纹波敏感,如果你的电源同时在给ESP8266供电,WiFi发包瞬间电压跌落可能导致DHT11读取值跳变。如果只做监控,这种精度无所谓;但如果做控制(比如湿度低了自动加湿),建议对采集值做滑动平均或去抖处理,强烈建议。
4. OLED 显示:驱动移植与动态刷新
4.1 SSD1306 驱动基础
0.96寸OLED分辨率是128x64,驱动芯片SSD1306,走I2C接口。I2C地址默认是0x78(7位地址0x3C),有些模块是0x7A(0x3D),买的时候看下商家标注。驱动要做的核心事情就是往SSD1306的GRAM里写数据,然后由驱动芯片自动刷新到屏幕上。
用HAL库驱动OLED其实并不难,核心就是发送一帧完整数据,或者设置页地址后只更新局部区域。但有一个新手容易踩的坑:I2C传输速率不能一味追求高。SSD1306虽然支持400kHz快速模式,但有些国产OLED屏用400kHz会出现花屏或时序错误。我在STM32上直接把I2C1调到100kHz标准模式,照样很流畅,稳得一批。
4.2 HAL库驱动OLED显示温湿度
以4针I2C接口为例,接线:VCC→3.3V、GND→GND、SCL→PB6、SDA→PB7。
驱动部分,我用的布局是:第1行显示温度,第2行显示湿度,第3行显示IP或者连接状态,第4行留着显示时间或者滚动提示。代码里通过取模软件生成字体点阵数组,HAL库的I2C transmit函数逐页发送。
/* oled.c 核心写屏逻辑 */ #define OLED_ADDR 0x3C // 7位地址,HAL库左移一位后使用 static void OLED_WR_Byte(uint8_t dat, uint8_t cmd) { uint8_t buf[2]; buf[0] = cmd ? 0x40 : 0x00; // 0x00为命令,0x40为数据 buf[1] = dat; HAL_I2C_Master_Transmit(&hi2c1, (OLED_ADDR << 1), buf, 2, 100); } void OLED_ShowCHinese(uint8_t x, uint8_t y, uint8_t no) { uint8_t t, adder = 0; OLED_Set_Pos(x, y); for (t = 0; t < 16; t++) { OLED_WR_Byte(Hzk[2 * no][t], 1); OLED_WR_Byte(Hzk[2 * no + 1][t], 1); } } void OLED_ShowString(uint8_t x, uint8_t y, char *str) { while (*str) { if (*str >= 0x80) { // 如果是中文字符编码 OLED_ShowCHinese(x, y, GetChineseIndex(str)); str += 2; x += 16; } else { OLED_ShowChar(x, y, *str++); x += 8; } } } void Display_EnvInfo(float temp, float humi, uint8_t *ipStr) { char strBuf[32]; OLED_Clear(); OLED_ShowString(0, 0, "环境监控系统"); sprintf(strBuf, "Temp:%.1fC", temp); OLED_ShowString(0, 2, strBuf); sprintf(strBuf, "Humi:%.1f%%", humi); OLED_ShowString(0, 4, strBuf); OLED_ShowString(0, 6, (char *)ipStr); }注意OLED_ShowString里对中文的处理,我用的字库是16x16汉字库,取了模放在数组里,索引函数GetChineseIndex根据字符串内容返回对应的字模下标。这个方法比较土,但简单直接。如果你觉得字符/汉字混排比较麻烦,还有个偷懒的办法——直接用取模工具生成图片,把整屏显示内容当图片刷上去,不过那样动态数据刷新会非常麻烦,实际项目不推荐。
4.3 OLED刷新闪烁问题的处理
新手玩OLED最容易遇到的现象:数据更新时屏幕整个闪一下或者局部闪烁。原因是把OLED_Clear()和所有文字一次性重绘,耗时较长,每次重绘都会短暂全屏灭掉。
解决办法有几种:
- 局部刷新:只更新变化的数值区域,不整体清除。比如温度只在第4行的13列位置显示,那就单独Set_Pos到那一块,只写那几个字符。
- 分段清屏:OLED_Clear()变成了OLED_ClearArea(x, y, w, h),只清指定区域。
- 避免每帧全清:先清除旧值区域,再显示新值。
我用的是局部刷新方案,代码类似这样:
void OLED_UpdateValue(uint8_t x, uint8_t y, float newVal, const char *suffix) { OLED_ClearArea(x, y, 48, 16); // 只清一个区域 OLED_Set_Pos(x, y); OLED_ShowNum(x, y, (int)newVal); OLED_ShowChar(x + 8 * 2, y, '.'); OLED_ShowNum(x + 8 * 3, y, (int)(newVal * 10) % 10); OLED_ShowString(x + 8 * 4, y, (char *)suffix); }如果不做局部刷新,OLED每次更新会占据大约15~20ms,这个时间对显示来说不算长,但你在主循环里如果还干别的活,整个系统节奏就会被打乱。DHT11读取又不能被打断,所以我的建议是:把OLED刷新放到主循环的尾端,而不是紧跟在传感器读取之后,防止传感器时序被I2C操作拖垮。
5. ESP8266 联网:AT指令、TCP连接与数据上报
5.1 ESP8266 的三种玩法
ESP8266有三种常见玩法:
- AT指令固件模式:STM32通过串口发AT指令控制ESP8266联网,ESP8266内部完成WiFi和TCP/IP协议栈。这是我最推荐的方式,因为主控侧代码简单,只需要处理串口收发和状态解析。
- NodeMCU固件+Lua脚本:直接在ESP8266上跑Lua脚本,不依赖外部MCU,但开发效率低,调试麻烦。
- Arduino环境:用Arduino IDE给ESP8266写代码,适合快速原型,但整套环境要重新配置,跟STM32风格的C代码有差异。
这里要特别说一句,有热词提到“arduino ide开发esp8266的nodemcu的管脚有咽些”——其实就是管脚映射关系。不同开发板的管脚定义差异很大,AT固件模式里尤其要注意。ESP8266-01S上引出的GPIO0/GPIO2/GPIO15/RX/TX各有用处,GPIO15在大部分模块上要接GND(否则启动失败),GPIO0在运行时要保持高电平,GPIO2不能接低电平。这些启动条件决定了模块能不能正常工作。
5.2 ESP8266 AT指令初始化流程
用USB转TTL先单独把ESP8266调通,确认固件版本和AT指令能执行,再接到STM32上联调。ESP8266上电后的初始化流程是:
AT # 测试指令,返回 OK AT+CWMODE=1 # 设置Station模式,可以连接路由器 AT+CWJAP="SSID","密码" # 连接WiFi,注意引号是英文的 AT+CIPSTART="TCP","192.168.1.100",8080 # 建立TCP连接到服务器 AT+CIPSEND=43 # 发送数据长度(字节数) > GET /upload?t=25.3&h=60.2 HTTP/1.1\r\nHost:192.168.1.100\r\n\r\n用串口助手发的时候,注意每发一条AT指令要以回车换行(\r\n)结尾。很多人测试AT时卡住,就是因为只发了“AT”没发回车。
连接路由器这一步要耐心等,返回WIFI CONNECTED后再等它获取到IP,返回WIFI GOT IP。如果返回FAIL或超时,检查密码、SSID附近是否有同名热点干扰,以及你的路由器是否开启了AP隔离——AP隔离开启后,即使连上WiFi,也访问不了局域网内其他设备。
5.3 STM32作为主控:AT指令发送与状态解析
主循环里发一个AT指令,不能一股脑全丢给串口,而是要走“发送指令 → 等待响应 → 解析结果 → 切换到下一步”的状态机。这样可以避免串口缓冲溢出,也能在超时后做重试。
关键代码片段:
typedef enum { WIFI_AT_TEST, WIFI_SET_MODE, WIFI_CONNECT_AP, WIFI_GET_IP, WIFI_TCP_LINK, WIFI_SEND_DATA, WIFI_IDLE } WifiState; void Wifi_Task(void) { switch (wifiState) { case WIFI_AT_TEST: UART_SendString("AT\r\n"); wifiState = WIFI_SET_MODE; break; case WIFI_SET_MODE: UART_SendString("AT+CWMODE=1\r\n"); wifiState = WIFI_CONNECT_AP; break; case WIFI_CONNECT_AP: UART_SendString("AT+CWJAP=\"yourSSID\",\"yourPass\"\r\n"); wifiState = WIFI_GET_IP; break; case WIFI_GET_IP: UART_SendString("AT+CIFSR\r\n"); wifiState = WIFI_TCP_LINK; break; case WIFI_TCP_LINK: UART_SendString("AT+CIPSTART=\"TCP\",\"192.168.1.100\",8080\r\n"); wifiState = WIFI_SEND_DATA; break; case WIFI_SEND_DATA: // 先发AT+CIPSEND=length,然后等'>'符号再发数据 if (waitGtSign) { UART_SendString(httpPacket); wifiState = WIFI_IDLE; } break; case WIFI_IDLE: default: break; } }串口接收中断里维护一个环形缓冲区或者简单的状态解析器,识别OK、FAIL、WIFI GOT IP、CONNECT等关键词。这里有个经验:不要用strstr在接收中断里做解析,里面太慢。正确做法是接收中断只把数据塞进缓冲,解析逻辑放到主循环里跑,或者用一个简单的状态机逐字符扫描关键词。
5.4 实测数据上报
我用的是TCP直连,在自己电脑上开了一个Python套接字服务,监听8080端口,收到数据直接打印。STM32发的是HTTP格式请求:
GET /upload?temp=25.3&humi=60.2 HTTP/1.1 Host:192.168.1.100:8080用HTTP的好处是后期如果想接网页端或者各类云平台,协议格式不用大改。如果是接OneNET这类云平台,需要按它的数据流格式上传,一般会要求HTTPS或MQTT协议,ESP8266的AT固件原生不支持HTTPS,需要装带TLS的固件或者走MQTT。这也是很多人查“esp8266连接OneNET失败”的原因——固件不对、或者端口用的是加密端口。如果你一定要上云平台,建议给ESP8266刷带MQTT的固件,或者用NodeMCU直接跑MQTT库,ST的部分只负责采集数据,把联网任务完全交给ESP8266处理,两边通过串口协议通信。这种架构更符合实际产品形态。
5.5 供电与稳定性问题
前面提过供电问题,这里再单独强调一遍。ESP8266在TCP发送瞬间,电流会有一个脉冲式的跳变,如果电源压不住,模块会直接重启,现象表现为串口打印出一堆乱码(重启时的Boot信息)。解决办法是:
- 在模块VCC与GND之间并一个470uF电解电容。
- 如果还不行,给ESP8266独立供电,用一颗AMS1117-3.3从5V取电,STM32的3.3V只供自己用。
- 串口电平要匹配。STM32的USART是3.3V电平,ESP8266的UART也是3.3V电平,可以直接相连。但如果你用的是5V单片机或者某宝上一些电平不标准的ESP8266模块,建议中间加一颗电平转换芯片或者分压电阻,避免烧模块。
6. STM32 与 ESP8266 串口联调:数据格式与稳定机制
6.1 自定义通信协议
当STM32不只是把数据丢给ESP8266,而是需要双向交互(比如从云端收到控制指令控制继电器),就必须设计一套清晰的通信协议。我的做法很简单:使用帧头、类型、长度、数据、校验的结构。
帧格式:0xAA 0x55 TYPE LEN DATA... CHECKSUM- 0xAA 0x55:帧头,用于同步
- TYPE:1字节,表示数据帧类型(0x01温湿度上报、0x02控制指令、0x03心跳)
- LEN:1字节,DATA区长度
- DATA:变长数据
- CHECKSUM:从TYPE到DATA所有字节累加取反
STM32发送数据前填好帧结构,校验好,才把整个buffer通过串口发给ESP8266。ESP8266侧用状态机逐字节解帧,解析出温湿度后再封装成网络上报包。这样做的好处是后期扩展控制指令时,不需要改动底层串口收发逻辑。
6.2 断线重连与看门狗
WiFi这东西,谁用谁知道,不可能永远稳定。路由器重启、信号干扰、TCP连接被服务端关闭,这些都是家常便饭。所以ESP8266必须要有自动重连机制:
- 每次上报数据前检查TCP连接是否还活着。怎么检查?发送
AT+PING=<IP>指令或者在TCP连接建立后通过AT+CIPSTATUS查询连接状态。 - 如果查询发现连接状态不是
CONNECT OK,先AT+CIPCLOSE关闭残留连接,再重新执行AT+CIPSTART。 - 给STM32开独立看门狗(IWDG),主循环里正常跑完一遍就喂狗。如果因为某个函数卡死(比如DHT11读超时没处理好),看门狗能自动复位整个系统。
实测下来,加了重连和看门狗之后,系统可以连续运行一两周不重启。当然这不算什么了不起的成绩,但作为自己搭的监控小工具,已经够用了。如果你做的是工业级产品,还要做更多容错,比如错误计数、告警输出、日志上报。
6.3 串口数据丢失与解析
有热词提到“esp8266丢包最简单三个步骤”,反映的是串口收发数据丢失的痛点。我的解法是三个字:降速率、加超时、加重试。
- 降速率:串口波特率不用追求高,9600或115200都行。115200虽然快,但如果你用USB转TTL的时候线材质量差、干扰大,反而容易出错。我最终定在115200,但接收方做了超时判断。
- 加超时:串口接收中断收到一个字节后,如果后续500ms内没有新字节到来,就认为这一帧数据接收完毕,可以开始解析。
- 加重试:如果校验失败或者解析超时,直接丢弃该帧,等下一帧。不要试图去修复一个错乱的帧。
7. 常见问题排查与实战调试记录
7.1 问题速查表
| 现象 | 可能原因 | 排查/解决办法 |
|---|---|---|
| OLED 白屏/黑屏 | I2C地址不对、供电不足、接线错 | 检查ADDR地址;用I2C扫描程序打印出实际地址;单独给OLED供电 |
| OLED 花屏 | I2C速率过高或线过长 | 降低I2C速率到100k;缩短杜邦线长度(<20cm) |
| DHT11 一直读0 | 接线错、数据引脚没有上拉 | DHT11数据线接3.3K~10K上拉到3.3V;用万用表确认引脚电压 |
| DHT11 偶尔超时 | 读取间隔太短、供电波动 | 增加读取间隔到1s以上;在DHT11 VCC/GND之间并10uF电容 |
| EPS8266 无AT响应 | 模块没进入AT模式、接线错、供电不足 | 确认EN接3.3V、GPIO0悬空;检查TX/RX是否交叉连接;加电容 |
| ESP8266 连接路由失败 | SSID或密码错、路由器隔离 | 核对SSID大小写;确认加密方式;临时开放路由器2.4G |
| STM32 串口乱码 | 波特率不对、时钟树配置错 | 核对CubeMX时钟树,确保USART挂载总线频率正确 |
| ESP8266 发数据后重启 | 供电不足、电流尖峰 | VCC并大电容;独立供电;检查串口电平是否匹配 |
| 连不上OneNET等平台 | 固件不支持TLS/MQTT、端口错误 | 刷带MQTT/TLS的固件;确认平台接入协议 |
7.2 排查顺序的经验
很多新手遇到问题就把代码翻来覆去改,其实大部分问题都出在硬件或者电源上。我的排查顺序是:先看电源电压 → 再看接线 → 然后用示波器/逻辑分析仪看时序 → 最后才怀疑代码。
举个例子,如果你发现DHT11读到的湿度稳定在99.9%不动,绝大多数情况是传感器引脚悬空或者上拉电阻没接好,而不是代码问题。DHT11的DATA引脚在空闲时是高电平,如果有数据,说明总线状态有问题。用示波器看单总线波形是最稳妥的,没有示波器就先用万用表量电压——PIN脚空闲时应该是3.3V,如果量出来是0V,查上拉。
7.3 调试工具建议
逻辑分析仪是真香。便宜的8通道24MHz的就够用了,用来抓DHT11时序、I2C波形、UART数据都非常方便。
串口调试助手方面,建议找一个支持HEX显示和定时发送的工具(比如SSCOM或者XCOM)。有些事情只有在HEX模式下才能看清——比如字符串是不是带了不该带的字符,AT指令是不是被拆包了。
8. 从局域网到云平台:扩展方向与后续改造
系统跑通局域网内监控后,你可能会想把它接到互联网,实现随时随地查看数据。这个方向我列几条路线,方便你按需选型:
- 内网穿透:在局域网内跑一个服务接收数据,然后用frp之类的内网穿透工具暴露到公网。优点是协议简单,缺点是你得有台常开的电脑或小主机。
- 云平台:OneNET、阿里云物联网、巴法云各有各的接入方式。基本都是创建产品、定义数据流、用MQTT协议上报。MQTT比HTTP更适合物联网场景,因为长连接省电、实时性强,服务器也能主动下发指令控制设备。
- 本地自建服务:树莓派或者软路由上部署Node-RED或者Home Assistant,通过MQTT接收数据,再做报警规则或联动智能家居设备。这是最“极客”的玩法。
如果是后面要往产品方向做,我建议直接切到MQTT+JSON结构化数据,不要再用自定义的HTTP GET参数。因为MQTT是物联网设备接入的主流标准,生态成熟、调试工具多(比如MQTT X),后续加设备、加数据可视化都方便。
我自己后续打算把STM32换成ESP32-S3,一步到位支持WiFi+BLE,省掉ESP8266,板上资源也更多,可以顺带接个小风扇控制继电器,做成一个能根据湿度自动控制的“恒湿箱”。这套老方案就当做一个练手的好项目,每一步都踩得很扎实,再往深了做也不慌。