
简介一套面向嵌入式初学者与物联网应用场景的完整工程示例以STM32F103微控制器为主控外接DHT11温湿度传感器与ESP8266 WiFi模块实现环境温湿度数据的采集、处理和远程网络上传。资源包共收录224个文件压缩后约4.05MB不仅包含C源码、头文件、启动文件、Keil工程配置文件、可烧录hex及中间编译产物还附有一键清理脚本文件类型覆盖从源码到烧录的完整工具链目录组织明确便于在MDK环境中直接编译和调试。当前已有1904人浏览学习。借助工程源码可深入研究DHT11单总线时序读取、ESP8266的AT指令交互、串口收发与TCP透传等内容工程内还封装了STM32标准外设库的定时器、ADC、I2C、CAN、USART等驱动有助于理解底层寄存器配置与应用逻辑掌握从GPIO模拟时序到WiFi网络通信的完整流程。无论是单片机课程设计、毕业设计还是智能家居环境监测项目这套代码都能提供可直接参考的软硬件联调思路。1. 为什么说 STM32F103 ESP8266 DHT11 是低成本物联网温湿度采集的黄金组合实际做物联网项目时温湿度监测的需求出现频率非常高。最常见也最顺手的组合就是 STM32F103 做主控DHT11 做温湿度采集ESP8266 负责联网上传。一个 DHT11 几块钱一个 ESP8266 模块十几块加上 STM32F103C8T6 最小系统板整套硬件不到三十块钱却能覆盖大棚、鱼缸、机房、宿舍等大多数场景。这个方案难的不是读 DHT11而是 ESP8266 的 AT 指令时序和掉线重连——很多人以为串口发几个 AT 就能跑通结果卡在连接超时和回显解析上。这篇文章把从接线、读时序、AT 指令上传到掉线恢复的完整路径写一遍照着做能省掉不少血泪排查时间。适合做毕设、课设或者想快速搭一个能用的物联网数据链路的同学。2. 搭建最小硬件链路STM32F103 与 DHT11、ESP8266 的接线和供电方案2.1 引脚分配DHT11 接哪个 GPIO、ESP8266 占用哪组串口常见做法是STM32F103 的 PA0 接 DHT11 的 DATA 引脚DHT11 VCC 接 3.3V 或 5VGND 接 GNDDATA 和 VCC 之间接一个 4.7kΩ 上拉电阻。为什么一定要上拉DHT11 的数据线是开漏输出靠上拉电阻把总线拉到高电平如果省掉读取时序会变得很不可靠温度偶尔跳变甚至首次读取就失败。ESP8266 一般用 USART1PA9/PA10和 STM32 通信。这里有个容易搞混的点STM32 的 PA9TX要接 ESP8266 的 RXPA10RX接 ESP8266 的 TX交叉连接。很多人按照两个模块的同名管脚直连结果数据完全不通。如果你板子上还有 USB 转串口芯片占用 USART1最好把 ESP8266 放到 USART2PA2/PA3保留 USART1 打印日志调试时两路互不干扰。我在实际工程里通常这样分配USART1 接调试串口波特率 115200USART2 接 ESP8266波特率 115200。DHT11 接 PA0用一个定时器做微秒延时不用 SysTick因为 SysTick 还要给主循环提供节拍。STM32F103C8T6 最小系统板上通常印着 USART1PA9/PA10 可能已经连到板载 USB-TTL如果你再用这两根线接 ESP8266会出现两个设备抢串口的情况收发互相干扰。所以把 ESP8266 放在 USART2 是更省事的选择。具体接线如下表STM32F103C8T6DHT11ESP82663.3V / 5VVCC-GNDGNDGNDPA0DATA-PA2 (USART2_TX)-RXPA3 (USART2_RX)-TX3.3V-VCC3.3V-CH_PD/ENGND-GPIO0正常悬空烧录时拉低注意DHT11 的 VCC 在 3.3V 下也能工作但传感器响应速度会略慢如果数据线超过 5 米建议接 5V模块内部有稳压。ESP8266 的 VCC 必须接 3.3V不能接 5V否则大概率烧模块。CH_PD 要拉高到 3.3V否则模块不工作。GPIO0 平时悬空或接高只有烧录固件时才拉低。2.2 供电和电平匹配3.3V 还是 5V为什么 ESP8266 必须单独供电ESP8266 在 Wi-Fi 发射瞬间电流能达到 300mA 以上STM32F103 的 3.3V LDO 根本带不动。如果直接从一个最小系统板的 3.3V 引脚取电表现就是平时 AT 指令有响应一旦 ATCWJAP 连接路由器的瞬间模块电流一冲电压跌落然后模块直接重启返回一堆乱码或“WIFI DISCONNECT”。这就是很多人说 ESP8266 连不上 Wi-Fi 的真正原因之一。解决办法是单独供电用一个 AMS1117-3.3 或类似低压差稳压芯片给 ESP8266 供电并在模块 VCC 和 GND 之间并联一个 470μF 电解电容和一个 0.1μF 陶瓷电容。如果手头没有稳压芯片也可以用 5V 电源经过降压模块给 ESP8266 供电但要注意共地——电源的 GND 必须和 STM32 的 GND 连在一起否则串口电平无参考点通信会失败。还要注意 AMS1117-3.3 的输入输出压差最小 1V如果你用 3.7V 锂电池直接供电AMS1117 输出只有 2.7V 左右ESP8266 会处于“能上电但不稳定”的状态。这种情况下建议用 ME6211 这类低压差 LDO或者用 DCDC 降压模块。判断方法很简单在模块执行 ATCWJAP 时用万用表测 VCC如果低于 3.0V就是供电不足。电平匹配方面STM32F103 的 TX 是 3.3V 逻辑高ESP8266 的 RX 也是 3.3V可以直接连接不需要额外接分压电阻。2.3 用逻辑分析仪确认 DHT11 时序前先检查这些硬件细节在写代码之前我一般会先把硬件链路验证一遍避免后面软件调半天发现是接触不良。用逻辑分析仪是最快的验证手段但即使没有逻辑分析仪也可以用万用表检查几个关键点DHT11 的 DATA 引脚在空闲时应该被上拉到 3.3V 左右。如果测到 0V说明上拉电阻没接或模块坏了。ESP8266 的 CH_PD 引脚电压应该接近 3.3V如果测到 0V模块不会启动AT 指令自然无响应。用 USB-TTL 单独连接 ESP8266发一个“AT\r\n”看是否返回“OK”。如果这一步就失败后面 STM32 的代码写得再好也没用。检查两个模块的 GND 是否真正连通很多杜邦线看起来插上了实际接触不良会导致串口数据乱飞。如果你有逻辑分析仪可以把通道接到 DHT11 DATA采样率至少设为 1MHz触发方式设为下降沿。跑一次读取程序正常波形应该是主机拉低约 20ms然后释放DHT11 拉低约 80μs 再拉高约 80μs随后连续 40 个 bit每个 bit 先低电平约 54μs再高电平 23μs逻辑 0或 68μs逻辑 1。如果某个 bit 高电平宽度只有 10μs 或超过 100μs说明上拉电阻选得不对或者 GPIO 翻转速度太慢延时函数有偏差。这些检查做完再进入软件部分。否则等到代码跑起来DHT11 读出来永远是一个值或者 ESP8266 返回一堆乱码排查成本会高很多。3. 在 STM32F103 上读 DHT11标准库下的单总线时序与校验代码DHT11 的单总线协议说简单也简单说坑也坑。整个过程分三步主机拉低总线至少 18ms 发送起始信号然后释放总线DHT11 应答拉低约 80μs 再拉高约 80μs随后连续输出 40 位数据每一位用不同的高电平持续时间表示 0 或 1。单总线是半双工主机必须严格切换 GPIO 方向。好多初学者会在这翻车使用 GPIO 输出模式发送完起始信号后忘记把引脚切换成输入模式就去读数据结果读到的全是高电平。STM32 标准库里切换方向并不复杂但要注意执行时间。我的做法是写一个 Delay_us 函数用定时器计数器实现微秒延时而不是用 for 循环空转。因为编译器的优化等级不同for 循环的延时误差会很大。如果你用的是 CubeMX 生成的 HAL 库工程读取函数里尽量不要用 HAL_GPIO_ReadPin 和 HAL_GPIO_WritePin 去翻转 GPIO因为它们每次调用都要做参数校验和锁保护时延差异很大。更好的做法是直接操作寄存器GPIOA-ODR ~GPIO_Pin_0;和GPIOA-IDR GPIO_Pin_0。HAL 库的初始化可以继续用但核心读取逻辑用寄存器这样时序才稳定。3.1 DHT11 单总线协议起始信号、40 bit 数据格式和校验原理先看数据格式。DHT11 一次完整返回 40 bit排列顺序是湿度整数8bit、湿度小数8bit、温度整数8bit、温度小数8bit、校验和8bit。注意湿度小数和温度小数在 DHT11 上通常显示为 0但数据帧里仍然存在不能直接忽略。校验和的算法是前四个字节相加取低 8 位等于第五个字节。例如读到的数据是 0x32 0x00 0x1C 0x00 0x4E那么 0x32 0x00 0x1C 0x00 0x4E校验通过。如果校验失败说明时序不对或干扰严重最稳妥的做法是丢弃这一帧等下一次读取。DHT11 的采样间隔建议不小于 1s传感器内部采样周期就是 1s 左右。如果你在 for 循环里连续读第二次读到的数据其实是第一次的缓存值看起来“固定不变”别怀疑代码有问题这是器件特性。每一位的时间特征0 的电平序列是 54μs 低电平 23μs 高电平1 是 54μs 低电平 68μs 高电平。主机在读位时先等待引脚变低再等待引脚变高然后测量高电平的持续时间。持续超过 50μs 记为 1否则为 0。很多代码里用“延时 40μs 后再读电平”来判断这是可行的但前提是延时函数要准。3.2 标准库读取代码GPIO 方向切换与微秒延时实现下面给出我在标准库工程里使用的 DHT11 驱动代码可以整体复制到 dht11.c 中。这里省略了 GPIO 时钟使能的初始化只保留核心读取部分。代码假设 DHT11 接在 PA0。// dht11.h 中的宏定义 #define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_Pin_0 #define DHT11_RCC RCC_APB2Periph_GPIOA // 切换为输出模式推挽速度 2MHz void DHT11_Pin_Output(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_2MHz; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); } // 切换为输入模式浮空输入 void DHT11_Pin_Input(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin DHT11_GPIO_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_GPIO_PORT, GPIO_InitStructure); } // 用 TIM3 做微秒延时TIM3 时钟 72MHz预分频 71得到 1MHz 计数频率 void Delay_us(uint32_t us) { TIM_SetCounter(TIM3, 0); while (TIM_GetCounter(TIM3) us); }初始化 TIM3 时设置预分频为 71自动重载为 0xFFFF这样计数器频率就是 72MHz / 72 1MHz每计数一次恰好 1μs。Delay_us的最大延时是 65535μs足够 DHT11 使用。注意TIM_SetCounter清零到while判断之间有几个时钟周期但因为只影响起始时刻不影响单总线位的相对宽度。接下来是从 DHT11 读取一个字节的函数uint8_t DHT11_ReadByte(void) { uint8_t i, byte 0; for (i 0; i 8; i) { // 等待低电平开始 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET); // 低电平约 54us等待它结束 Delay_us(40); // 此时如果是高电平并持续 68us记为 1否则为 0 if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET) { byte | (uint8_t)(0x80 i); // 等待剩余的高电平结束 while (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET); } // 如果是 0高电平只有 23us上面 Delay_us(40) 已经过了高电平不需要额外等待 } return byte; }这里有个细节读每一位时先while等低电平然后Delay_us(40)再读电平。0 的高电平是 23μs1 是 68μs延时 40μs 后再读0 已经结束变成了低电平1 仍然是高电平。这个判据在很多代码里都能用但延时函数必须准。如果你发现读到的字节校验总是失败优先用逻辑分析仪看波形而不是来回调Delay_us的参数。完整读取函数如下uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; uint8_t i; // 起始信号拉低至少 18ms然后释放 DHT11_Pin_Output(); GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); Delay_ms(20); GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN); Delay_us(30); // 释放后延时 20-40us // 切换输入检测应答 DHT11_Pin_Input(); // 等待 DHT11 拉低应答信号 if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_SET) return 1; // 总线未被拉低传感器未响应 Delay_us(80); // 应答低电平约 80us然后拉高约 80us if (GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN) Bit_RESET) return 2; // 未收到高电平应答 Delay_us(80); // 跳过应答后的高电平进入 40 位数据 for (i 0; i 5; i) { data[i] DHT11_ReadByte(); } // 校验 if ((data[0] data[1] data[2] data[3]) ! data[4]) return 3; *humidity data[0]; *temperature data[2]; // DHT11 的温度整数部分最高位为符号位1 表示负温度 return 0; }返回码的含义1 是传感器没应答2 是应答时序不对3 是校验失败0 是成功。建议把返回码通过串口打印出来这样能直接知道卡在哪一步。3.3 把原始数据换算成温湿度并做合理性判断DHT11 返回的是整数和小数两部分。对于 DHT11湿度小数位通常为 0但程序里仍然要保留避免以后换 DHT22 时又要改结构。换算代码如下float humidity data[0] data[1] * 0.1; float temperature data[2] data[3] * 0.1;如果是负温度温度整数部分最高位为 1需要先处理if (data[2] 0x80) { temperature -((data[2] 0x7F) data[3] * 0.1); } else { temperature data[2] data[3] * 0.1; }数据合理性判断DHT11 的标称湿度范围是 20%-90%温度范围是 0-50℃。如果读出的湿度超过 100% 或温度超过 50℃大概率是时序不对或引脚接触不良。我会在读取函数里加入范围检查不满足就返回错误码 4。注意 DHT11 的精度只有 ±5% 湿度和 ±2℃ 温度不要指望它像 SHT30 一样精准。如果项目要求高精度选型时就该换传感器而不是在这里调代码。读到的数据跳变 1℃ 是正常的不要当 bug 修。4. 通过 ESP8266 把数据送上网AT 指令、透传模式和串口对接4.1 ESP8266 固件与工作模式选择AT 指令还是 MQTT 固件常见做法是使用出厂自带的 AT 固件。AT 固件的优点是简单稳定只要通过串口发指令就能用缺点是协议解析要自己写掉线重连也不算优雅。也可以刷 MQTT 固件比如安信可的 MQTT AT 固件然后用 ATMQTTCONN 连接云平台。但刷固件会遇到一个典型报错A fatal esptool.py error occurred: failed to connect to ESP8266: timed out waiting for packet header。这个坑我在避坑章节单独说。对于新手我建议先用 AT 固件把 TCP 透传跑通。很多云平台都支持 TCP 端口接入比如巴法云、OneNET 等同时也有 MQTT 端口。用 TCP 方式发送简单的数据报文一样可以完成上报。我个人的习惯是前期调试用 AT TCP产品原型需要稳定订阅/推送时再切 MQTT。原因很简单TCP 透传模式下的 AT 指令少出错点少更容易定位问题。4.2 连接 Wi-Fi 与建立 TCP 透传的 AT 指令序列这里给出一套我验证过的指令序列适用于 ESP8266-01S / ESP8266-12F 等模块。默认波特率 115200每行以\r\n结尾。AT ATE0 ATCWMODE1 ATCWJAPyour_ssid,your_password ATCIPMUX0 ATCIPSTARTTCP,192.168.1.100,8080 ATCIPMODE1 ATCIPSEND说明ATE0关闭回显方便程序解析CWMODE1表示 Station 模式连接路由器而不是创建热点CIPMUX0是单连接CIPSTART建立 TCP 连接CIPMODE1进入透传模式CIPSEND后进入数据发送状态之后 STM32 发给 ESP8266 的所有数据都会直接推送到服务器直到发送才退出透传。注意CIPSEND在透传模式下只需要发送一次之后不需要每次上报都重新发。这是最容易搞错的地方。很多人以为每次上报都要重新CIPSEND结果在透传模式下收到 ERROR。还要注意ESP8266 连路由器后可能返回WIFI CONNECTED、WIFI GOT IP等状态信息程序解析时不要只找 OK因为ATCWJAP的 OK 在连接成功后才返回延迟可能长达几秒。另外一个非常常见的现象ESP8266 只支持 2.4GHz Wi-Fi如果你的路由器是 5GHz 优先模块可能搜不到信号。把路由器设置成双频共存或者先用手机热点测试。手机热点也要注意关掉“5GHz 优先”否则仍然连不上。4.3 在 STM32 端发送 AT 指令并解析 ESP8266 返回的代码框架STM32 需要同时维护两个串口一个调试一个对接 ESP8266。发送指令的代码可以封装一个发送函数但更好的做法是用环形缓冲区接收避免丢数据。下面给出一个最小但可用的框架使用阻塞方式发送指令串口中断接收返回用strstr查找关键词。uint8_t esp_buffer[256]; uint16_t esp_len 0; void USART2_IRQHandler(void) { if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { uint8_t ch USART_ReceiveData(USART2); if (esp_len sizeof(esp_buffer)) esp_buffer[esp_len] ch; } } void USART_SendString(USART_TypeDef *USARTx, const char *str) { while (*str) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); USART_SendData(USARTx, (uint16_t)(*str)); } } uint8_t ESP8266_SendCommand(const char *cmd, const char *expect, uint32_t timeout_ms) { esp_len 0; USART_SendString(USART2, cmd); USART_SendString(USART2, \r\n); uint32_t start GetTick(); while (GetTick() - start timeout_ms) { if (esp_len 0) { esp_buffer[esp_len] \0; // 添加字符串结束符 if (strstr((char *)esp_buffer, expect) ! NULL) return 1; } } return 0; }参数说明expect可以是OK、WIFI GOT IP、CONNECT等关键词timeout_ms要根据命令区别设置比如ATCWJAP给 10 秒ATCIPSTART给 5 秒。用strstr找关键词比等待固定延时可靠得多因为模块响应时间受网络波动影响很大。注意每执行完一条指令后最好清空缓冲区避免上一次的残留影响下一次判断。然后是连接 Wi-Fi 和建立连接的调用顺序ESP8266_SendCommand(AT, OK, 2000); ESP8266_SendCommand(ATE0, OK, 1000); ESP8266_SendCommand(ATCWMODE1, OK, 1000); ESP8266_SendCommand(ATCWJAP\SSID\,\PASS\, WIFI GOT IP, 10000); ESP8266_SendCommand(ATCIPMUX0, OK, 1000); ESP8266_SendCommand(ATCIPSTART\TCP\,\192.168.1.100\,8080, CONNECT, 5000); ESP8266_SendCommand(ATCIPMODE1, OK, 1000); ESP8266_SendCommand(ATCIPSEND, OK, 1000);注意ATCIPSTART成功后会返回CONNECT不是OK。很多示例里写等待 OK会导致误判失败。ATCIPSEND在透传模式下会返回 OK然后进入透传状态。发送数据时直接用USART_SendString(USART2, payload)payload 里包含温湿度数据格式可以自己定义比如 JSON 或者简单的字符串。char payload[64]; sprintf(payload, {\temp\:%.1f,\hum\:%.1f}\r\n, temperature, humidity); USART_SendString(USART2, payload);协议格式如果对接 TCP 服务器可以设计成简单的 JSON 行协议。如果对接巴法云这类平台它们的 TCP 接入有各自的数据格式要求需要按平台文档来。最省事的验证方式是先在 PC 上开一个 TCP Server 或者使用网络调试助手把 ESP8266 连到电脑看数据是否到达。注意上电后不要立刻发 ATESP8266 启动需要几百毫秒建议在初始化代码开头加一个 1 秒延时。5. 避坑清单DHT11 读不到、ESP8266 连接超时、串口乱码的排查路径5.1 DHT11 数据脚一直为高或读出的温度是 0℃现象DHT11_ReadData总是返回 1或者读回来的温度湿度始终是 0。原因之一引脚切换不彻底。主机发送起始信号后GPIO 还停留在推挽输出模式没有切到输入导致数据线一直被强拉高DHT11 无法拉低应答。解决方法是确保DHT11_Pin_Input()在释放总线之后马上调用而且调用前不能有任何GPIO_SetBits操作把输出寄存器置高。原因之二上拉电阻缺失。如果你买的是 DHT11 模块板上通常已经带了 4.7kΩ 上拉电阻但如果你直接买裸探头必须外接 4.7kΩ 到 VCC否则空闲时数据线是浮空的测到的电平随机。用万用表量一下空闲电压就知道。原因之三延时函数偏差太大。用 for 循环空转做的Delay_us在 Keil 默认优化等级下可能有几十微秒的偏差读位判断会错。解决方法是改用定时器延时或者用 DWT 的 CYCCNT 寄存器。我推荐 DWT它不占用定时器而且精度很高。初始化 DWT 后直接用DWT-CYCCNT计数实现微秒延时注意 Cortex-M3 内核上需要先使能 TRCENA 位。5.2 ESP8266 AT 指令超时无响应或返回 ERROR现象发AT后没有任何返回或者直接返回 ERROR偶尔有返回但连接路由器时卡在ATCWJAP超时。原因之一供电不足。这是最常见的情况参考第 2.2 节的单独供电方案。判断方法在ATCWJAP时用万用表测模块 VCC如果电压跌到 3.0V 以下就是供电问题。我还遇到过用杜邦线供电时线太长太细导致电流不够换成粗线就好了。原因之二串口接线交叉错误。STM32 的 TX 必须接 ESP8266 的 RXRX 接 TX。有些模块丝印的 TX/RX 是 Arduino 视角接反了也会不通。这时用一个 USB-TTL 单独验证模块是最好的办法模块好或坏一眼就能看出来。原因之三GPIO0 被拉低。如果 GPIO0 接地模块进入烧录模式不会响应普通 AT。正常情况下 GPIO0 要悬空或接上拉。很多新手把 GPIO0 和 CH_PD 搞混或者把 GPIO0 当作使能脚接了低电平导致模块一直不工作。原因之四ESP8266 的波特率被改过。比如固件里默认波特率被改成 9600你按 115200 发 AT 自然没反应。解决方法是先用 USB-TTL 以 74880、9600、115200 等多个波特率分别发AT总有一个能对上。如果都不行可能需要重新刷固件。5.3 串口打印中文乱码与数据错位现象STM32 通过 USART1 打印调试日志能收到数据但全是乱码或者 ESP8266 返回的数据断成两截。原因之一波特率不匹配。检查三处STM32 串口初始化、外部晶振频率、ESP8266 固件默认波特率。STM32F103 如果外部晶振是 8MHz但库文件里的HSE_VALUE写成了 12MHz会导致实际波特率偏移打印出来乱码。解决方法是核对system_stm32f10x.c里的时钟配置用串口发送一个已知字符串比如“Hello”在逻辑分析仪上测量波特率。原因之二串口接收溢出没有处理。STM32 的 USART 接收寄存器如果没及时读走下一个字节到来时会置位 ORE溢出错误如果不读 SR 再读 DR后续数据全部错位。解决方法是中断处理函数开头先读一下USARTx-SR再读USARTx-DR。这个坑在连续接收 ESP8266 的长数据时特别明显表现为数据拼接出现坏字节。原因之三Keil 编辑器的中文编码和串口工具不一致。STM32 源码文件里用 UTF-8 编码写的中文字符串在 Windows 的调试助手里可能显示乱码。解决方法是统一编码。我把串口日志里的固定中文改成拼音或英文从根上省掉这个问题。如果你想用中文把 Keil 的 Encoding 设为 UTF-8同时串口工具也设为 UTF-8 解码。5.4 烧录 ESP8266 固件时遇到 esptool.py failed to connect: timed out现象你用 esptool.py 刷固件提示A fatal esptool.py error occurred: failed to connect to ESP8266: timed out waiting for packet header。原因没有让模块进入烧录模式。正确步骤是先将 GPIO0 拉低然后接通 VCC 和 CH_PD上电瞬间模块进入 UART Download 模式如果先上电再拉低 GPIO0模块已经在正常运行模式自然无法握手。解决方案把 GPIO0 接 GND按一下复位或重新上电然后马上运行烧录命令。另外检查 USB-TTL 的 TX 是否接了 ESP8266 的 RX以及是否共地。还有一个容易忽略的点有些 USB-TTL 的 3.3V 输出电流不足也会导致握手超时。手动给 ESP8266 外加一个 3.3V 供电但 GND 要接在一起。如果用的是自动下载电路还需要确认 DTR/RTS 的时序不然模块没有正确进入下载模式。最简单的做法是手动接一根跳线到 GPIO0。5.5 数据上传后云端不更新或者上报间隔不稳定现象本地串口打印数据正常但云平台看板不刷新或者数据时有时无。原因TCP 连接已经断开但 STM32 不知道还在往透传管道里发数据。解决方法是定期发一个自定义心跳包服务器断开时会收到 TCP 复位ESP8266 会返回CLOSED或类似错误码。这时需要重新执行ATCIPSTART。更稳妥的做法是建立一个连接状态机专门处理重连。这个我放在第 6 章展开。还有一个常见情况ESP8266 透传模式下如果长时间不发数据部分路由器会把 TCP 连接空闲回收。这时模块不会主动通知你但下次发数据时服务器已经收不到。解决方法是把上报间隔控制在 30 秒以内或者在应用层做应答确认。如果云平台支持尽量用 MQTT 的PUBACK来判断上报是否成功。6. 让方案更稳自动重连、掉线恢复和用逻辑分析仪校准时序6.1 连接状态机与掉线恢复不要在主循环里顺序执行一长串 AT 指令否则一旦失败就卡住。常见做法是维护一个枚举状态STATE_INIT、STATE_WIFI_CONNECT、STATE_TCP_CONNECT、STATE_TRANSPARENT、STATE_RECONNECT。每次只执行当前状态对应的步骤收到预期返回值才跳转到下一个状态。例如在STATE_TRANSPARENT状态下如果 ESP8266 返回CLOSED就跳回STATE_TCP_CONNECT先重连 TCP。这个状态机代码不多但比把 AT 指令全堆在 while 里可靠得多。6.2 用逻辑分析仪校准 DHT11 时序如果你发现读取不稳定别急着换传感器。用逻辑分析仪抓取 DATA 脚波形对比数据手册里的时序。重点看起始信号的低电平是否超过 18ms读到每一 bit 时的高电平宽度是否符合 23μs0或 68μs1。如果偏差超过 5μs优先检查 GPIO 翻转速度和延时函数。我遇到过读到的数据校验一直通过但温度偏高 2℃后来发现是 Delay_us 少了 3μs传感器位判断部分翻转所以校验居然也是错的。这种玄学问题只有靠波形能定位。6.3 从串口日志到云端看板的验证路径最简单的验证顺序先用 USB-TTL 加网络调试助手在电脑上跑通 AT 指令确认模块能连上路由器并到达 TCP 服务器再让 STM32 通过串口打印每次 AT 发送和返回确认程序解析正确最后看云端看板数据是否按预期间隔更新。每次只改一层不要同时调硬件和协议。例如数据到了服务器但显示乱码那就是包格式问题数据没到服务器就要抓串口日志看 ESP8266 返回什么错误码。我做这个方案时吃过最大的亏就是忽略供电导致模块疯狂重启排查了两天才发现是 USB-TTL 供电不足。从那以后任何 ESP8266 项目我都会单独供电并且准备一个逻辑分析仪。时序、电平、供电这三样稳了剩下的只是状态机工作。希望帮到你。本文还有配套的精品资源点击获取