简介:一个基于STM32F103、DHT11与ESP8266的物联网温湿度采集上传工程,面向嵌入式入门及中级开发者。项目从底层驱动到网络传输构成完整闭环:STM32负责读取DHT11数据,经ESP8266以AT指令方式接入无线网络,再通过HTTP上传至服务器。工程基于标准外设库编写,包含定时器、ADC、I2C、USART等驱动模块,可作为学习STM32外设编程、单总线时序解析和Wi-Fi通信的参考样本。压缩包共224个文件,大小约4.05MB,以C源文件、头文件为主,同时包含编译生成的o目标文件、d依赖文件、hex/axf可执行文件及uvprojx工程配置,便于直接打开或烧录验证。已有1904人学习下载。整套源码注释清晰、目录结构规整,适合对照学习STM32与ESP8266协同工作的完整流程,并可在此基础上扩展为远程监控、环境采集等应用。
1. STM32F103+ESP8266+DHT11:三十五分钟能跑通的温湿度上云最小系统
做单片机项目的人迟早会遇到一个需求:在办公室里或者车间角落,想知道环境温度和湿度,又不想拉着网线过去。最常见的低成本方案是 STM32F103 做主控,ESP8266 负责联网,DHT11 负责采集温湿度。这套组合之所以满大街都是,是因为它足够简单、资料足够密、成本足够低,一个温湿度采集节点做下来,物料成本二十块钱出头,而整个软件链路短到不需要上操作系统,用裸机轮询都能跑完。
这套方案适合谁?正在做课程设计、毕业设计,或者给公司做环境监控、仓库监测、机房告警的开发者都合适。你从网上拖下来的工程包里面通常就是三块:读写 DHT11 的驱动、ESP8266 的 AT 指令串口驱动、主循环里的业务逻辑。按标题《stm32_esp8266.rar_DHT11 STM32F103_STM32 8266_STM32+ESP8266+DHT11》这个名字来理解,它本质上是把这三个模块拼成一个「采集—连接—上报」的完整闭环。下面我们从 DHT11 的时序讲起,一路走到云平台收到数据,中间把联调时最容易翻车的几个点全部挑明。
2. 先把 DHT11 读利索:HAL 库单总线时序与必配的上拉电阻
DHT11 是一个典型的单总线传感器,只有一根 DATA 线,既要输出又要输入,所以它的驱动难点不在协议有多复杂,而是时序的严格性和微秒级延时的准确性。用标准库也好,用 HAL 库也好,卡住大多数人的其实不是库本身,而是两个基础问题:GPIO 模式切换没弄对,以及延时不是微秒级。
2.1 DHT11 的 40 bit 数据帧和时序门槛
主机发起一次读取的过程是这样:先把 DATA 线拉低至少 18ms,让传感器从空闲状态醒过来,然后拉高 20~40us,之后释放总线,切换到输入模式。此时 DHT11 会先回应一个 80us 的低电平,再拉高 80us,表示「我准备好了」,随后开始发送 40 bit 数据。这 40 bit 里,前 16 bit 是湿度整数部分和小数部分,中间 16 bit 是温度整数部分和小数部分,最后 8 bit 是校验和,要求前四个字节相加等于校验和。
难点在于每一位数据的读取。DHT11 在传输每一位时,都会先拉低 50us,然后拉高,拉高持续的时间长短是 0 和 1 的区别:大约 26~28us 算 0,70us 左右算 1。这个判断窗口非常窄,你必须在拉高之后等待合适的时间点去采样电平,采样太早,0 和 1 都是高电平,采样太晚,可能已经进入下一位的低电平区间了。所以一个靠谱的微秒级延时函数,是读取 DHT11 的第一块地基。
关于原理图,我用立创EDA(嘉立创)画过一个最小应用电路:STM32F103C8T6 的 PA0 接 DHT11 的 DATA,DHT11 的 VCC 接 3.3V,GND 接地,DATA 与 VCC 之间接一个 4.7kΩ 上拉电阻。很多人抄原理图时把这个电阻漏了,然后读出来的一直是 0 或者干脆无响应,原因就在这。上拉电阻是 DHT11 正常工作的前提,不要省略。
2.2 HAL 库读 DHT11:代码与参数说明
在 STM32F103 上用 HAL 库驱动 DHT11,核心就是用 DWT 或者定时器实现微秒延时。HAL_Delay 只能提供毫秒级延时,直接拿去读 DHT11 时序肯定会失败,这一点是很多人从标准库换到 HAL 库之后遇到的第一个大坑。我一般用 DWT 实现微秒延时,它不需要额外占用定时器,在 Cortex-M3 内核上开箱即用。
// 使用 DWT 实现微秒延时,替代 HAL_Delay 的毫秒限制 static void DWT_Delay_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; DWT->CYCCNT = 0; } static void DWT_Delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }DWT_Delay_Init 在 main 函数开头调用一次即可,延时精度依赖 SystemCoreClock 配置正确。如果你的系统时钟改过,比如从默认的 8MHz 倍频到了 72MHz,只要 SystemCoreClock 变量和实际一致,这个延时就是准的。下面这段是读取 DHT11 完整数据的部分:
// 读取 DHT11 的 40 bit 原始数据,成功返回 0 uint8_t DHT11_ReadRaw(uint8_t data[4]) { GPIO_InitTypeDef gpio = {0}; uint8_t bits[40] = {0}; uint32_t timeout = 0; // 配置 PA0 为推挽输出,用于发起起始信号 gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); DWT_Delay_us(20000); // 起始信号:拉低至少 18ms,留余量到 20ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); DWT_Delay_us(30); // 拉高 20~40us HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 释放总线 // 切换为输入模式,等待传感器响应 gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; // 输入模式下开启内部上拉,与外部 4.7k 配合 HAL_GPIO_Init(GPIOA, &gpio); // 等待 DHT11 拉低 80us 作为响应开始 timeout = 200; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) != GPIO_PIN_RESET) if (--timeout == 0) return 1; // 等待拉高 80us,表示响应结束 timeout = 200; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) != GPIO_PIN_SET) if (--timeout == 0) return 1; // 逐位读取 40 bit 数据 for (int i = 0; i < 40; i++) { timeout = 200; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) != GPIO_PIN_SET) if (--timeout == 0) return 1; DWT_Delay_us(40); // 关键采样点:40us 处读电平判 0/1 bits[i] = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0); timeout = 200; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) != GPIO_PIN_RESET) if (--timeout == 0) return 1; } // 组合 40 bit 为 4 字节 for (int i = 0; i < 4; i++) { data[i] = 0; for (int j = 0; j < 8; j++) data[i] = (data[i] << 1) | bits[i * 8 + j]; } // 校验:前三个字节相加等于校验字 uint8_t sum = data[0] + data[1] + data[2]; if (sum != data[3]) return 2; return 0; }这段代码里最值得注意的是采样点 40us 的选择。DHT11 在每一位传输时,低电平是 50us,高电平是 26~28us(代表 0)或 70us(代表 1)。从低电平结束、高电平开始算起,在 40us 时刻去读电平:如果是 1,此时仍处于高电平区间;如果是 0,此时已经落入下一位的低电平区间。为什么不用更早的时刻?因为如果高电平是 28us 的 0 位,早于 28us 读它依然是高电平,无法区分 0 和 1;而在 40us 这个点,0 和 1 刚好拉开差距。
2.3 读出来全 0 或湿度恒 0:先查这四个点
如果你把代码烧进去发现温度和湿度都是 0,不要急着怀疑芯片坏了。我第一次调的时候就因为接线问题折腾了一晚上,后来发现是杜邦线接触不良。最常见的故障点按概率排序是:第一,DATA 引脚和 VCC 之间漏了外部上拉电阻,内部上拉通常也能跑通但信号边沿变差,长线时尤其明显;第二,读温湿度的引脚因为 GPIO 配置顺序不对,先切了输入模式再去拉低起始信号,导致时序完全乱掉;第三,微秒延时用的是 HAL_Delay,延时不精确导致采样点整体偏移;第四,DHT11 的 VCC 用的是 STM32 板子的 3.3V 而不是独立的 5V 电源,长期运行电压跌落。
我一般调试时会先固定到某个 GPIO 上,用示波器看 DATA 线上的波形。正常波形里能清晰看到起始信号、响应信号和 40 个脉冲。如果波形只有一个起始信号然后什么都没有,说明传感器没响应,优先查上拉电阻和供电。如果脉冲个数对但读出数据校验错,优先怀疑采样点位置。没有示波器时,可以串口打印原始 bits 数组,观察 0 和 1 的分布是否均匀且接近 50% 对 50%,如果几乎全是 1,说明采样点太早,如果几乎全是 0,说明采样点太晚。
3. 让 ESP8266 干活之前:AT 固件入门与本机连接的三步验证
ESP8266 这块模组有两种玩法:一种是刷 NonOS SDK 自己写 Lua 或 C 逻辑,另一种是刷官方的 AT 固件,让 STM32 通过串口发 AT 指令去控制它。标题里明确提到了 8266,而且从工程结构看,用的是 ATM 指令控制的方式,这也是最省事、最好调试的方式。先别急着写 STM32 代码,把 ESP8266 单独拿到串口调试助手里验证通了再往下走。
3.1 确认固件版本和串口参数:9600 还是 115200
市面上 ESP8266 模块的串口波特率有两个常见值,老一批的 AT 固件默认 9600,新一批的默认 115200。接到串口上发 AT,如果没有回应,先别怀疑模块坏了,把波特率切换着试一下。我常用的顺序:先在 115200 下发 AT\r\n,若无响应,再在 9600 下发一次;若仍无响应,检查模块的 VCC 供电电流,ESP8266 在 Wi-Fi 连接瞬间电流会冲到 300mA 以上,普通 USB-TTL 的 3.3V 输出可能撑不住,导致模块反复重启,表现就是你发什么都不回。
另外注意串口调试助手的换行设置,AT 指令必须用 \r\n 结尾,而不是 \n。很多人在这个细节上翻车,明明波特率是对的,但指令迟迟得不到响应,最后发现是调试助手只在行尾加了 \n,ESP8266 固件不认。用串口助手时,把「发送新行」选项里改成 CR+LF。
3.2 AT 指令连 Wi-Fi 与 TCP:最小指令序列
确认模块能响应 AT 之后,下一步是让它连上路由器并创建 TCP 连接。这里我一般会先不接 STM32,用串口调试助手把下面的指令一条一条手动发一遍,确认家里或者办公室的 Wi-Fi 和目标服务器都通:
AT\r\n AT+CWMODE=1\r\n AT+CWJAP="你的WiFi名称","WiFi密码"\r\n AT+CIPSTART="TCP","bemfa.com",9501\r\n AT+CIPMODE=1\r\n AT+CIPSEND\r\n参数说明:CWMODE=1 是 Station 模式,让 ESP8266 作为客户端去连接无线路由器;CWJAP 填路由器的 SSID 和密码,注意别带空格;CIPSTART 里的 TCP 连接以到服务器地址和端口为例,如果你的目标是 OneNET,那就填它的接入 IP 和端口;CIPMODE=1 是开启透传模式,进入透传之后,所有从串口收到的数据都会原封不动转发到 TCP 服务端,服务器发来的数据也会直接出现在串口接收里。
需要提醒的是,Wi-Fi 密码和 SSID 里如果包含逗号或引号,AT 指令的解析会出错,常见的做法是把这些特殊字符去掉,或者配置 ESP8266 保存 SSID 和密码(AT+CWJAP 成功后发 AT+CWJAP_STORE=1)。但更稳妥的办法是不要用太复杂的 SSID 做开发验证,等逻辑跑通了再换正式网络。
3.3 STM32 侧串口收发框架
Esp8266 用串口和 STM32 通信,在 STM32 侧要做一个收发框架,本质很简单:一个串口发 AT 指令,一个串口收 ESP8266 的响应,然后解析响应里的关键字符串。HAL 库的做法是使用串口中断接收不定长数据,配合一个环形缓冲,主循环里再从缓冲中取数据做字符串匹配。
// 串口中断服务函数:把收到的字节放入环形缓冲 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { uint8_t ch; ch = (uint8_t)(huart->Instance->DR & 0xFF); if (ring_buffer_write(&esp_rx_buf, ch) == 0) { // 缓冲已满,丢弃最旧数据并记录溢出标志 esp_rx_overflow = 1; } HAL_UART_Receive_IT(&huart2, &rx_tmp, 1); } }环形缓冲的身位很关键,ESP8266 的 AT 响应有时会一次性来很长一串,比如 CWJAP 连接的 WIFI CONNECTED、WIFI GOT IP 分两行返回,CIPSTART 也会有 CONNECT OK 或 DNS FAIL 等多行返回。缓冲设计不够大,或者读走得不够快,就可能在主循环里只看到半截响应,匹配「OK」时永远失败。我一般把环形缓冲设为 512 字节,主循环每 10ms 检查一次。
匹配响应时,不要一上来就匹配完整字符串,先匹配关键词再判断状态:比如匹配到 "GOT IP" 才能发 CIPSTART,匹配到 "CONNECT OK" 才能发 CIPSEND。如果你不按状态去控制,极大概率出现上电后 ESP8266 还没就绪就发指令,然后在 timeout 上反复等待。这个状态机逻辑是整条链路上最容易写乱的地方,我后面章节会给出完整的状态机代码示例。
4. STM32+ESP8266+DHT11 数据上行:拼接 JSON 交给云平台
DHT11 读到了,ESP8266 也能连上网络了,剩下的事就是把温湿度数据拼成一段文本,通过 ESP8266 的透传通道发到云端。这个环节看起来简单,但实际上有好几个坑,尤其是编码问题和不同云平台协议的差异。
4.1 数据帧设计与 GBK 转 UTF-8 的坑
我见过不少人把温湿度数据直接拼成 "temp:25.5,humi:60.3" 就发到云平台上,结果云端解析出来是乱码或者压根解析不了。原因在于 STM32 的 Keil 工程默认编码是 GBK,中文字符串常量在源码里以 GBK 存储,而物联网平台普遍要求 UTF-8。如果你要发的数据里带中文,比如设备名「客厅传感器」,在 STM32 上写代码时就必须转成 UTF-8。
STM32 上做 GBK 转 UTF-8,常用做法是用表格法:把 GBK 编码和 UTF-8 编码的映射表做成数组,运行时查表转换。但更务实的方式是尽量避免在设备端发中文,让云平台那边把设备名和地点信息预设好,设备只发数字和英文键值。我第一次做的时候在设备端发了中文的「温度」字段,结果云端一直显示乱码,后来改成只发 temp 和 humi,问题立刻消失。这不是偷懒,而是把所有编码转换压力集中到服务端,设备端永远只发 ASCII。
// 拼接发送数据帧:只使用 ASCII 字符,规避 GBK/UTF-8 问题 char frame[96]; uint8_t dht_data[4]; float temperature = 0, humidity = 0; if (DHT11_ReadRaw(dht_data) == 0) { temperature = dht_data[2] + dht_data[3] * 0.1f; humidity = dht_data[0] + dht_data[1] * 0.1f; snprintf(frame, sizeof(frame), "{\"type\":\"1\",\"msg\":\"%.1f|%.1f\"}", temperature, humidity); }这段代码里用了 snprintf 而不是 sprintf,主要是防止缓冲区溢出。紧凑型 MCU 上栈空间本来就紧张,万一温度值的位数超过预期,sprintf 就可能把栈写穿。%.1f 会把小数点后多于 1 位截断,如果 DHT11 分辨率本身是 1 位小数,这个精度刚好够。另外注意 snprintf 的返回值要检查,如果返回值大于等于 frame 缓冲区长度,说明被截断了,此时不能发送,否则云端收到的 JSON 不完整。
4.2 巴法云与 OneNET 的接入差异
巴法云的接入方式是建立 TCP 长连接到服务器,发数据时使用它规定的主题格式。OneNET 则支持 HTTP 和 MQTT 两种方式,HTTP 方式需要在 URL 中带设备 ID 和 APIKey。如果你下载的工程包里同时有对这两者的支持,建议优先在巴法云上跑通,因为它的 TCP 接入方式最简单,没有加密握手,也方便用 ESP8266 的透传模式直接发。巴法云的 TCP 服务器是 bemfa.com 端口 9501,接入后发 "type:1|topic|msg" 格式的数据,其中 topic 是在巴法云平台创建的主题名称,msg 是你的数据内容。但我们在前面拼接 JSON 时用的就是巴法云的格式,直接发 frame 即可。
OneNET 走 HTTP 方式时,数据要放在 HTTP 请求体里,而且请求头里要带 api-key。ESP8266 的 AT 固件支持 AT+CIPSTART 建立 TCP 连接后直接发 HTTP 报文,所以也可以做,但要把 GET 请求的 URL 编码处理好。我的经验是:初学用巴法云,产品化再切 OneNET 或其他云平台,因为 TCP 长连接的模式更接近单片机工程师的直觉,调试时用一个网络调试助手就能在电脑上看数据。
4.3 完整读写状态机与预期效果
把 DHT11 读取、ESP8266 状态控制、数据发送整合到 main 循环里,最忌讳的就是一上电就在 main 里按顺序执行一遍然后进入死循环。正确做法是一个有限状态机,把整个流程拆成若干状态,每个状态设置了对应的超时时间。下面这段是核心结构:
typedef enum { ESP_STATE_RESET = 0, ESP_STATE_CWMODE, ESP_STATE_CWJAP, ESP_STATE_WAIT_GOTIP, ESP_STATE_CIPSTART, ESP_STATE_CIPMODE, ESP_STATE_CIPSEND, ESP_STATE_SEND_DATA, ESP_STATE_RECONNECT } esp_state_t; // 在 main 循环里轮询状态机,strstr 匹配串口缓冲中的关键词 while (1) { switch (esp_state) { case ESP_STATE_CWJAP: if (esp_rx_found("OK")) esp_state = ESP_STATE_WAIT_GOTIP; else if (esp_rx_found("FAIL")) { esp_state = ESP_STATE_CWJAP; // 重试连Wi-Fi,不做下一步 } break; case ESP_STATE_WAIT_GOTIP: if (esp_rx_found("WIFI GOT IP")) { esp_send_at("AT+CIPSTART=\"TCP\",\"bemfa.com\",9501\r\n"); esp_state = ESP_STATE_CIPSTART; } break; case ESP_STATE_CIPSTART: if (esp_rx_found("CONNECT OK")) { esp_send_at("AT+CIPMODE=1\r\n"); esp_state = ESP_STATE_CIPMODE; } break; case ESP_STATE_CIPMODE: if (esp_rx_found("OK")) { esp_send_at("AT+CIPSEND\r\n"); esp_state = ESP_STATE_CIPSEND; } break; case ESP_STATE_CIPSEND: { // 透传模式下,收到 CIPSEND 返回的 '>' 后开始发数据 if (esp_rx_found(">")) { esp_send_frame(); // 发送前面拼接好的 JSON 帧 esp_state = ESP_STATE_SEND_DATA; } break; } case ESP_STATE_SEND_DATA: // 进入此状态后等待下一次采集周期,再切换回 CIPSEND 状态 HAL_Delay(3000); // 3 秒上报一次 esp_state = ESP_STATE_CIPSEND; break; default: break; } esp_rx_clear(); }这里特别要说明几个参数:CWJAP 状态里如果收到 FAIL,不要立刻重连,要等一段时间。ESP8266 连续连接失败后进入保护机制,你越着急重连它越不响应,我一般会等 5 秒再重试。CIPSTART 状态里如果收到 DNS FAIL 而不是 CONNECT OK,说明域名解析失败,这时要去查 Wi-Fi 是否真的连上外网,而不是排查 STM32 代码。SEND_DATA 里的 3 秒间隔是人为设定的,发太快云平台容易限流,发太慢又体现不出数据的实时性,3 秒是一个比较合理的中间值。
把这段代码跑通后,你会看到串口打印里出现 "> {"type":"1","msg":"25.0|60.0"}",同时巴法云平台端的设备数据栏里出现了对应数值。到这一步,硬件链路就算完整了。
5. 联调避坑手册:ESP8266 连不上、DHT11 全 0、数据乱码的排查手册
做这套方案的过程中,我前前后后整理了五个高频踩坑点,按「现象—原因—解决」的格式写出来,你可以把它们当作排查手册来用,每一条都是我自己或者身边同事亲手翻过的车。
5.1 串口调试助手发 AT 超时无响应
现象:ESP8266 接 USB-TTL,串口调试助手波特率 115200,发 AT\r\n 后没有任何回显,模块的红色电源灯亮着。
原因:第一次遇到这种问题时我差点以为是模块烧了,后来发现是 USB-TTL 的 3.3V 供电能力不足。USB-TTL 上那个 3.3V 往往是 AMS1117 之类的 LDO 从 5V 转出来的,最大输出电流通常只有 500mA,而 ESP8266 连接 Wi-Fi 的瞬间电流会冲到 300mA 以上,电压被拉低后模块直接复位,表现就是按下复位键后能正常回一次 AT 或乱码,之后再发就毫无反应。另一个常见原因是 ESP8266 模组的 RST 引脚没有被拉高,悬空状态下模块处于复位状态,自然不工作。
解决:给 ESP8266 单独接一个 3.3V 稳压模块供电,不要从 USB-TTL 取电。RST 引脚通过 10kΩ 电阻上拉到 VCC。如果手头有外部电源,用示波器看模块上电瞬间的 VCC 波形,电压如果有明显掉坑,基本就是供电不足。
5.2 透传模式下数据发到一半卡住
现象:代码烧进去后,第一次发数据成功,云端能看到一条记录,之后再也不更新。重新上电又好了,过一会儿又断。
原因:你把 CIPSEND 之后的透传模式当成永久状态了。ESP8266 的透传模式在 TCP 连接断开时会自动退出,回到非透传状态。很多人在状态机里没有处理这个退出,主循环还继续往串口塞数据,数据全都丢在了串口缓冲里,同时由于状态机停留在 CIPSEND 等待「>」符号,没有任何机制去重新触发连接。这是不做状态机的必然结果。
解决:在状态机里增加一个「连接异常退出」的分支,每次发完数据后延时几秒,然后主动向串口发「+++」退出透传、再发 AT+CIPSTART 重新建连。用>进入发送时设置为超时 5 秒,如果 5 秒内没有收到>,就认为连接已经断开,主动走重连分支。注意,+++前面不需要加 AT,它本身就是退出透传的指令。
5.3 DHT11 读出的温度和湿度对不上仪表读数
现象:DHT11 读出来的温度和手边温湿度计差 2~3℃,湿度差 5%RH。不要以为这是软件问题,这是 DHT11 本身的精度上限。确切说,DHT11 的温度误差是 ±2℃,湿度误差是 ±5%RH,这在这个价位上已经算不错了。如果你想更高精度,方案是换 DHT22(也叫 AM2302)或者 SHT30,但那样的话代码就得重新写了。
解决:如果你就是不想换硬件,唯一能做的是校准。在稳定环境下记录 DHT11 读数和标准仪器读数的差值,然后在代码里做线性校正:校正温度 = 原读数 + 固定补偿值。注意这个补偿值在不同温湿度区间不一定相同,如果你要求严格,需要做多点校准。我一般只在固定环境下补偿一次,对于监控告警类应用绰绰有余。
5.4 STM32 复位后 ESP8266 连接状态丢失
现象:按下 STM32 的复位键重新运行程序,DHT11 的数据发送不出去,打印日志发现状态机卡在 CIPSTART 等待 CONNECT OK,但单独测试时 ESP8266 是通的。
原因:STM32 复位导致串口期间发出过乱码或者半截 AT 指令,ESP8266 收到了残缺指令,状态被搞乱了。这个问题的根源在于:ESP8266 不像 STM32 那样有严格的上电延时,STM32 上电后初始化串口,I/O 可能输出过高电平,让 ESP8266 误认为收到了数据。
解决:STM32 启动时先给 ESP8266 一个硬复位,把 ESP8266 的 RST 引脚接到 STM32 的一个普通 GPIO 上,main 函数初始化串口之前先拉低 RST 至少 100ms,然后拉高,给模块一个干净的上电时序。如果没有多余 GPIO,也可以在初始化完串口后,延时 2 秒再发第一条 AT 指令。为什么是 2 秒,因为是经验值,ESP8266 上电到固件稳定的时间大约在 1.5 到 2 秒之间。
5.5 云端收到的数据偶发乱码
现象:数据大部分正常,但偶尔会出现几个特殊字符或者方框,尤其是在设备刚上电的那一条数据。
原因:STM32 串口刚初始化时,发送缓冲是空的,但 DMA 传输或者串口外设可能残留了上一次的数据。更常见的原因是 ESP8266 在进入透传模式前,串口上残留了回车换行符等数据,这些残留数据在进入透传后被转发到了服务器。另外,如果你在程序里用了中文注释,并且 Keil 工程编码不是 UTF-8,字符串常量里含有中文字节的话也可能被算进这种乱码里。
解决:进入透传发送数据前,先清空 STM32 的串口接收缓冲,再向 ESP8266 发一条「AT+CIPMODE=1」并等待 OK,随后清空缓冲再发 CIPSEND。这个方法核心思路:每次数据传输前都让链路处于干净状态。如果发的是 JSON 数据,把所有键名改成纯英文,值是纯数字,这样即使混入几个控制字符,云端侧的解析也能以字段分隔符来容忍。
6. 进阶:掉线自动重连与看门狗复位,这套组合的最后一块短板
3 秒一次上报的数据链路跑通后,接下来要面对的是长期运行的可靠性问题。DHT11、ESP8266 这套组合最大的短板是:如果路由器重启、网络波动、云端断开,ESP8266 可能会陷入一个无法自恢复的状态,这时候只能断电重启。针对这个问题,有两个非常实在的优化手段。
第一个是硬件看门狗兜底。在 main 循环里,每完成一次完整的数据上传和状态机轮询之后,喂一次独立看门狗。如果把看门狗超时设为 10 秒,一旦状态机卡在某个状态超过 10 秒,STM32 自动复位,整个系统重新来一遍上电流程,DHT11 和 ESP8266 都会随之重新初始化。这招虽然粗暴,但绝对有效。我在实际项目中见过一个节点跑了 45 天才因为路由器升级掉了一次线,最终就是靠看门狗复位的。
第二个是主动心跳检测。透传模式下,每 30 秒主动发一条心跳报文到云端,如果连续 3 次发心跳后都没有收到云端的回执,就主动断开 TCP 连接重连。这里要提醒一下:巴法云这类平台的 TCP 长连接会在空闲一段时间后主动断开,如果只发数据不发心跳,你可能已经断线了却不知道。我的心跳报文就是一句{"type":"1","msg":"ping"},云端收到会回一个 pong 报文,用 AT+CIPMODE=1 透传模式下直接读串口就能拿到。
把看门狗和心跳这两个机制加进去之后,这套系统的可靠性才真正到了一个能放到现场长期运行的程度。我这个做法的血泪经验是:不要以为单片机上电就能跑一辈子,网络方案必须把复位和重连当成第一优先级来设计。最终希望这个方案能帮你在自己的项目里少踩几个坑,把精力放到更有意思的事情上去。
本文还有配套的精品资源,点击获取