☰
STM32+ESP8266+DHT11低成本物联网温湿度采集实战指南
2026/10/27 7:48:25 网站建设 项目流程

简介:一套面向嵌入式初学者与物联网应用场景的完整工程示例,以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 或 5V,GND 接 GND,DATA 和 VCC 之间接一个 4.7kΩ 上拉电阻。为什么一定要上拉?DHT11 的数据线是开漏输出,靠上拉电阻把总线拉到高电平,如果省掉,读取时序会变得很不可靠,温度偶尔跳变,甚至首次读取就失败。

ESP8266 一般用 USART1(PA9/PA10)和 STM32 通信。这里有个容易搞混的点:STM32 的 PA9(TX)要接 ESP8266 的 RX,PA10(RX)接 ESP8266 的 TX,交叉连接。很多人按照两个模块的同名管脚直连,结果数据完全不通。如果你板子上还有 USB 转串口芯片占用 USART1,最好把 ESP8266 放到 USART2(PA2/PA3),保留 USART1 打印日志,调试时两路互不干扰。

我在实际工程里通常这样分配:USART1 接调试串口,波特率 115200;USART2 接 ESP8266,波特率 115200。DHT11 接 PA0,用一个定时器做微秒延时,不用 SysTick,因为 SysTick 还要给主循环提供节拍。STM32F103C8T6 最小系统板上通常印着 USART1,PA9/PA10 可能已经连到板载 USB-TTL,如果你再用这两根线接 ESP8266,会出现两个设备抢串口的情况,收发互相干扰。所以把 ESP8266 放在 USART2 是更省事的选择。具体接线如下表:

STM32F103C8T6DHT11ESP8266
3.3V / 5VVCC-
GNDGNDGND
PA0DATA-
PA2 (USART2_TX)-RX
PA3 (USART2_RX)-TX
3.3V-VCC
3.3V-CH_PD/EN
GND-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 指令有响应,一旦 AT+CWJAP 连接路由器的瞬间,模块电流一冲,电压跌落,然后模块直接重启,返回一堆乱码或“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 降压模块。判断方法很简单:在模块执行 AT+CWJAP 时用万用表测 VCC,如果低于 3.0V,就是供电不足。电平匹配方面,STM32F103 的 TX 是 3.3V 逻辑高,ESP8266 的 RX 也是 3.3V,可以直接连接;不需要额外接分压电阻。

2.3 用逻辑分析仪确认 DHT11 时序前,先检查这些硬件细节

在写代码之前,我一般会先把硬件链路验证一遍,避免后面软件调半天发现是接触不良。用逻辑分析仪是最快的验证手段,但即使没有逻辑分析仪,也可以用万用表检查几个关键点:

  1. DHT11 的 DATA 引脚在空闲时应该被上拉到 3.3V 左右。如果测到 0V,说明上拉电阻没接或模块坏了。
  2. ESP8266 的 CH_PD 引脚电压应该接近 3.3V,如果测到 0V,模块不会启动,AT 指令自然无响应。
  3. 用 USB-TTL 单独连接 ESP8266,发一个“AT\r\n”,看是否返回“OK”。如果这一步就失败,后面 STM32 的代码写得再好也没用。
  4. 检查两个模块的 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μs,1 是 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 固件),然后用 AT+MQTTCONN 连接云平台。但刷固件会遇到一个典型报错: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 AT+CWMODE=1 AT+CWJAP="your_ssid","your_password" AT+CIPMUX=0 AT+CIPSTART="TCP","192.168.1.100",8080 AT+CIPMODE=1 AT+CIPSEND

说明:ATE0关闭回显,方便程序解析;CWMODE=1表示 Station 模式,连接路由器而不是创建热点;CIPMUX=0是单连接;CIPSTART建立 TCP 连接;CIPMODE=1进入透传模式;CIPSEND后进入数据发送状态,之后 STM32 发给 ESP8266 的所有数据都会直接推送到服务器,直到发送+++才退出透传。

注意CIPSEND在透传模式下只需要发送一次,之后不需要每次上报都重新发。这是最容易搞错的地方。很多人以为每次上报都要重新CIPSEND,结果在透传模式下收到 ERROR。还要注意,ESP8266 连路由器后可能返回WIFI CONNECTED、WIFI GOT IP等状态信息,程序解析时不要只找 OK,因为AT+CWJAP的 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要根据命令区别设置,比如AT+CWJAP给 10 秒,AT+CIPSTART给 5 秒。用strstr找关键词比等待固定延时可靠得多,因为模块响应时间受网络波动影响很大。注意每执行完一条指令后,最好清空缓冲区,避免上一次的残留影响下一次判断。

然后是连接 Wi-Fi 和建立连接的调用顺序:

ESP8266_SendCommand("AT", "OK", 2000); ESP8266_SendCommand("ATE0", "OK", 1000); ESP8266_SendCommand("AT+CWMODE=1", "OK", 1000); ESP8266_SendCommand("AT+CWJAP=\"SSID\",\"PASS\"", "WIFI GOT IP", 10000); ESP8266_SendCommand("AT+CIPMUX=0", "OK", 1000); ESP8266_SendCommand("AT+CIPSTART=\"TCP\",\"192.168.1.100\",8080", "CONNECT", 5000); ESP8266_SendCommand("AT+CIPMODE=1", "OK", 1000); ESP8266_SendCommand("AT+CIPSEND", "OK", 1000);

注意:AT+CIPSTART成功后会返回CONNECT,不是OK。很多示例里写等待 OK,会导致误判失败。AT+CIPSEND在透传模式下会返回 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 连到电脑,看数据是否到达。注意上电后不要立刻发 AT,ESP8266 启动需要几百毫秒,建议在初始化代码开头加一个 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;偶尔有返回但连接路由器时卡在AT+CWJAP超时。

原因之一:供电不足。这是最常见的情况,参考第 2.2 节的单独供电方案。判断方法:在AT+CWJAP时用万用表测模块 VCC,如果电压跌到 3.0V 以下,就是供电问题。我还遇到过用杜邦线供电时,线太长太细,导致电流不够,换成粗线就好了。

原因之二:串口接线交叉错误。STM32 的 TX 必须接 ESP8266 的 RX,RX 接 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或类似错误码。这时需要重新执行AT+CIPSTART。更稳妥的做法是建立一个连接状态机,专门处理重连。这个我放在第 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μs(0)或 68μs(1)。如果偏差超过 5μs,优先检查 GPIO 翻转速度和延时函数。我遇到过读到的数据校验一直通过但温度偏高 2℃,后来发现是 Delay_us 少了 3μs,传感器位判断部分翻转,所以校验居然也是错的。这种玄学问题只有靠波形能定位。

6.3 从串口日志到云端看板的验证路径

最简单的验证顺序:先用 USB-TTL 加网络调试助手在电脑上跑通 AT 指令,确认模块能连上路由器并到达 TCP 服务器;再让 STM32 通过串口打印每次 AT 发送和返回,确认程序解析正确;最后看云端看板数据是否按预期间隔更新。每次只改一层,不要同时调硬件和协议。例如数据到了服务器但显示乱码,那就是包格式问题;数据没到服务器,就要抓串口日志看 ESP8266 返回什么错误码。

我做这个方案时,吃过最大的亏就是忽略供电,导致模块疯狂重启,排查了两天才发现是 USB-TTL 供电不足。从那以后,任何 ESP8266 项目我都会单独供电,并且准备一个逻辑分析仪。时序、电平、供电这三样稳了,剩下的只是状态机工作。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询