1. 整体设计与思路拆解1.1 这套方案到底解决什么问题做温湿度监控这件事我前前后后折腾过好几套方案从最早的单片机加LCD1602本地显示到后来加蓝牙模块用手机短距查看再到现在这套WiFi无线方案。对比下来STM32ESP8266DHT11OLED这套组合是我在实际项目里用得最顺手、也是被问得最多的一套。先说最直接的痛点传统温湿度监测的麻烦在于“看得见够不着”。传感器装在生产车间、暖通管道、机房角落、粮仓人不可能24小时蹲在旁边看。就算用LCD本地显示你也得走过去才能知道数据。这套方案的价值在于把“本地显示”和“远程上报”打通——DHT11负责采集环境温湿度STM32作为主控负责读取传感器数据、处理逻辑、驱动屏幕显示同时通过ESP8266模块把数据通过WiFi发出去。这样你坐在办公室打开浏览器或者手机App就能看到远在仓库里的实时温湿度。这套系统能做什么一句话总结低成本、低门槛地实现一个带本地显示和WiFi远程上报的微型物联网节点。适合的人群也很明确正在学STM32的嵌入式初学者想找一个综合性的练手项目做毕业设计、课程设计需要一套能展示完整数据链路的选题做小型环境监测、农业大棚、机房温控这类场景的工程师想快速验证一个监控节点。1.2 为什么选这四件套而不是其他方案很多人一上来就问Esp8266本身就能跑程序也能接DHT11为什么还要加个STM32这不是多此一举吗这个问题的答案要分开看。如果你只是“临时测一下温湿度发到手机”那ESP8266用Arduino IDE写个程序直接接DHT11就能干完全不需要STM32。但如果你做的是“项目”或者“产品原型”情况就不同了。第一ESP8266的IO口数量有限大部分封装只有两三个可用GPIO同时还要兼顾WiFi协议栈的运行。当你的系统里不只有温湿度传感器还要接继电器控制风扇、接按键、接蜂鸣器、接多路传感器时ESP8266的IO资源很快就会见底。而STM32F103C8T6这颗芯片36个引脚封装的也有超过20个可用GPIO外设资源从SPI、I2C、UART到定时器、ADC、PWM一应俱全扩展性完全不是一个量级。第二从工程分工上看ESP8266更适合专注于它最擅长的事——无线通信。我把ESP8266配置成透传模式STM32直接把AT指令和数据扔给它它负责打包成TCP包发出去。两者各司其职代码逻辑清晰出了问题也好排查。如果全堆在ESP8266上一旦WiFi连接异常整个采集和显示逻辑都会被拖累排查起来很痛苦。第三从学习价值来说这个组合同时涵盖了传感器时序读取、屏幕驱动、串口通信、AT指令解析、TCP/IP网络通信这几个嵌入式开发的核心技能点。把这些打通了你以后再去做更复杂的IoT项目底子就有了。1.3 总体架构和模块分工整个系统的数据流向是这样的DHT11温湿度传感器 → STM32主控读取处理 ├── OLED显示屏本地实时显示 └── ESP8266WiFi透传→ 无线路由器 → 服务器/手机这里我的设计原则是“采集与通信分离”。STM32只关心三件事传感器数据读得对不对、屏幕上显示什么、要发给上位机的数据报文怎么拼。ESP8266只关心一件事把收到的字节流稳定地发出去。这样分层之后每一层的调试都可以独立进行不会出现“屏幕不亮是因为WiFi没连上”这种薛定谔式的bug。2. 硬件准备与接线细节2.1 器件清单和引脚分配表先列一份我实际用的器件清单都是市面上容易买到、价格便宜的型号器件型号/规格参考价格说明主控STM32F103C8T6 开发板10-15元有蓝色Pill板最常见WiFi模块ESP8266-01S 或 ESP-12F8-15元推荐ESP-12F引脚间距更友好温湿度传感器DHT11蓝色4针模块3-5元单总线协议显示屏0.96寸 OLEDSSD1306I2C接口10-15元4针GND/VCC/SCL/SDA供电手机充电器 MicroUSB线 或 5V电源模块-电流500mA以上就够其他面包板、杜邦线若干10K电阻1个几元DHT11数据线上拉用引脚分配表STM32F103C8T6STM32引脚外设连接配置说明PA0DHT11 DATA开漏输出/上拉输入读取温湿度PB6OLED SCLI2C1时钟线STM32的I2C1默认引脚PB7OLED SDAI2C1数据线PA2ESP8266 TXDUSART2 RX接收ESP8266数据PA3ESP8266 RXDUSART2 TX发送指令给ESP8266GND所有模块GND必须共地3.3VOLED VCC、ESP8266 VCCDHT11模块多数支持3.3V~5V比较关键的点是ESP8266的供电。很多初学者栽的第一个跟头就是ESP8266供电不足——它启动瞬间的电流可以达到300mA以上如果用开发板上的3.3V稳压芯片扛不住模块就会反复重启。我一开始就吃过这个亏后来直接用了外部稳压模块或者确保USB口供电质量过关才稳定。2.2 接线时的两个关键注意点第一个是共地。STMMMM32、ESP8266、DHT11、OLED这四个模块的GND必须连在一起否则串口信号和I2C信号都会出现电平漂移现象就是数据时好时坏、OLED偶尔花屏。这个问题在线比较乱的时候特别容易发生一定要在接线前就把GND统一规划好。第二个是DHT11的上拉电阻。DHT11是单总线协议数据线在空闲时需要保持高电平所以DATA引脚到VCC之间需要接一个4.7K到10K的上拉电阻。如果你买的是成品模块那种蓝色四针小板板上已经集成好了上拉电阻直接用就行。但如果你是买的裸DHT11传感器那一定要自己焊一个上拉电阻不然后果就是读出来的温湿度永远是0或者偶尔有数据偶尔没有。OLED模块用I2C接口只需要两根线接线已经很简单了。这里有个小建议如果屏幕不亮先检查VCC是不是接到了3.3V而不是5V。虽然很多OLED模块标称支持3.3V~5V但实际上一部分模块接5V时间长了会发烫甚至烧掉保险起见我还是习惯接3.3V。2.3 开发环境搭建STM32CubeMX Keil5STM32开发环境的选择我推荐新手直接走STM32CubeMX生成初始化代码 Keil5或VS Code 插件写逻辑这条路而不是自己去手写寄存器初始化。原因很简单STM32的外设时钟树比较复杂GPIO复用、AFIO映射、I2C时序配置如果全手写初学者光初始化代码就能写几百行而且很容易写错。用CubeMX勾选一下就能生成逻辑清晰出问题的概率大大降低。CubeMX配置要点芯片型号选择STM32F103C8Tx不用选Flash更大的“CB”C8够用RCC中HSE设为Crystal/Ceramic Resonator使用外部8M晶振SYS中Debug选择Serial Wire不然第一次烧录后Debug接口就被占了PA0设为GPIO_Output后续代码中切换为开漏输出/输入模式PB6、PB7设为I2C1I2C速度选100KHzStandard Mode就好OLED显示不需要多快添加USART2异步通信波特率115200用于连接ESP8266时钟树可以把系统时钟设为64MHzF103最高支持到72MHz但64MHz够用且更稳定。生成代码后用Keil5打开在main.c里加逻辑代码。如果你更喜欢VS Code也可以装好ARM工具链后直接用但Keil5的调试和烧录对新手更友好我建议先别折腾工具链把精力花在调试业务逻辑上。3. 核心驱动开发从DHT11和OLED开始3.1 DHT11通信协议拆解DHT11看起来简单但实际上手最容易出问题的地方就是它的单总线时序。它的通信协议大致是这样的主机STM32先把总线拉低至少18ms然后释放总线告诉DHT11“我要开始采集了”DHT11回应一个约80us的低电平再拉高80us表示“我准备好了”然后DHT11开始发送40bit的数据依次是湿度整数、湿度小数、温度整数、温度小数、校验和每个bit的表示方式不是0或1的电平而是高电平持续的时间长度——高电平持续约26~28us表示“0”持续约70us表示“1”校验和等于前四个字节之和的低8位加对则数据有效。用生活化类比来解释这就像两个人用敲桌子来传话敲一下表示0连敲三下表示1然后规定好每次停顿的时间窗口。如果收消息的人时间窗口判断错了整句话就听岔了。那STM32怎么精确判断“这个高电平持续了多久”这里必须要有一个微秒级延时函数。STM32的HAL库原生提供的HAL_Delay()只能精确到毫秒级对DHT11时序来说完全不够用。我的做法是使用TIM2定时器做微秒延时或者用SysTick自己实现一个delay_us()。如果在读时序的过程中程序被打断比如中断里也执行了耗时行为那么读取结果就是错误的甚至会让后续的数据位全错位。所以我在实际代码里会在读取DHT11期间关闭优先级较高的中断保证时序窗口的纯净。3.2 DHT11读取代码框架下面是一段精简的DHT11读取函数框架我用HAL库写// 宏定义切换PA0方向 #define DHT11_OUT_1() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET) #define DHT11_OUT_0() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET) #define DHT11_IN() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) uint8_t DHT11_Read_Bit(void) { // 等待低电平结束然后测量高电平持续时间 while(!DHT11_IN()); // 等待低电平结束 delay_us(40); // 延时40us后采样 if(DHT11_IN()) return 1; // 如果40us后仍然是高则说明是1 else return 0; } uint8_t DHT11_Read_Byte(void) { uint8_t val 0; for(uint8_t i 0; i 8; i) { val 1; val | DHT11_Read_Bit(); } return val; } uint8_t DHT11_Read_Data(uint8_t *hum, uint8_t *temp) { uint8_t buf[5] {0}; // 起始信号 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_0; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); DHT11_OUT_0(); HAL_Delay(20); // 至少18ms低电平 DHT11_OUT_1(); delay_us(30); // 切换为输入模式 GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); delay_us(40); if(DHT11_IN()) return 1; // 如果没收到低电平响应说明传感器没就绪 // 等待80us低电平响应结束 while(!DHT11_IN()); // 等待80us高电平准备信号结束 while(DHT11_IN()); for(uint8_t i 0; i 5; i) buf[i] DHT11_Read_Byte(); // 校验 if((uint8_t)(buf[0]buf[1]buf[2]buf[3]) buf[4]) { *hum buf[0]; *temp buf[2]; return 0; // 成功 } return 2; // 校验失败 }这里有个细节我把PA0配置成开漏输出是因为开漏输出可以直接拉低总线然后又可以切换回输入模式读取传感器的电频。如果使用推挽输出就需要额外注意切换方向时可能造成的电平冲突。这段代码看起来简单但踩过的坑不少——尤其是delay_us(40)在64MHz主频下能不能准确延时的验证我建议先用逻辑分析仪或者示波器看一遍时序。3.3 OLED显示SSD1306驱动与汉字显示0.96寸OLED模块内置的是SSD1306控制器通过I2C接口通信I2C地址通常是0x3C也有的模块是0x3D可以用I2C扫描程序确认。OLED的核心操作其实就两条命令——设置写地址范围和写GRAM数据。SSD1306内部有一块128x64位的显示RAM你往哪个地址写像素屏幕对应位置就会亮。驱动OLED有两种路线用现成的开源库比如著名的U8g2、Adafruit SSD1306支持各种字体和绘图接口自己写精简驱动只实现画点、清屏、显示字符串、显示汉字代码更少也更容易理解原理。我的建议是学习阶段一定要自己写一遍精简驱动哪怕只是画点、清屏、显示ASCII字符。因为只有自己写过一遍才知道OLED的显示原理遇到问题时不至于两眼一抹黑。如果项目赶进度再切换到U8g2这种成熟库。自己写的时候核心函数就这几个void OLED_WriteCmd(uint8_t cmd); // 写命令例如0xAF开显示、0x00设置列地址低地址 void OLED_SetPos(uint8_t x, uint8_t y); // 设置显示光标到(x,y) void OLED_ShowChar(uint8_t row, uint8_t col, char ch); // 按行/列显示ASCII字符 void OLED_ShowCN(uint8_t x, uint8_t y, uint8_t n); // 在指定位置显示16x16汉字关于OLED显示汉字网上说的最多的就是“取模软件”。其实原理很简单一个16x16的汉字就是256个像素点的组合每个像素按“亮/灭”编码成bit16行每行16个bit也就是2字节16行一共32字节。用取模软件比如“PCtoLCD2002”或者“字模提取”工具选择“逐行式”或“列行式”生成对应的字模数组然后把这些字节按顺序写到屏幕的GRAM里就行。很多新手纠结取模方式是“阴码”还是“阳码”选错了显示出来就是反色。我的经验是先取模显示一个“温”字如果显示成长方形或者空白就把取模的反色选项勾选一下重新生成最快的方式就是对比测试别在理论上耗太久。3.4 把数据打通主循环逻辑主程序的核心逻辑其实很简单int main(void) { // 初始化... OLED_Init(); OLED_Clear(); OLED_ShowStr(2, 0, Temp: --.-C); OLED_ShowStr(4, 0, Hum: --.-%); uint8_t hum, temp; while(1) { if(DHT11_Read_Data(hum, temp) 0) { OLED_ShowNum(2, 6, temp / 10, 2); // 整数部分 OLED_ShowChar(2, 8, .); OLED_ShowNum(2, 9, temp % 10, 1); // 小数部分 // 类似的更新湿度 } HAL_Delay(1500); // DHT11建议两次读取间隔大于1s } }DHT11的数据格式是整数部分小数部分比如读回来的温度字节是25就表示25°CDHT11的分辨率默认是1°C所以一般小数部分读出来都是0但代码里还是要处理小数位。湿度同理比如读回来是68代表湿度68%RH。主循环里加一个HAL_Delay(1500)很有必要。DHT11的采样周期官方标称是1秒也就是说你至少间隔1秒再执行下一次读取才是合规的否则读出来的数据可能不稳定。这也决定了整个系统的监控刷新率1.5秒刷新一次完全够用不会给WiFi模块造成太大压力。4. WiFi通信与数据上报4.1 ESP8266和STM32怎么通信ESP8266模块通过串口和STM32通信默认波特率一般是115200。你需要用USB-TTL模块先把ESP8266接到电脑上用串口助手发AT指令测试一下确认模块正常后再接到STM32上。常用的AT指令就这几个功能指令返回测试通信ATOK设置WiFi模式ATCWMODE1OK1为STA模式连接路由器ATCWJAPSSID,密码OK或WIFI CONNECTED查询IP地址ATCIFSR返回IP地址建立TCP连接ATCIPSTARTTCP,192.168.1.100,8080CONNECT OK进入透传模式ATCIPMODE1OK开始发送数据ATCIPSEND返回 提示符退出透传发送OK我实际用的流程是开机后STM32先通过串口给ESP8266发ATCWMODE1设置成STA模式然后发ATCWJAP路由器名,密码连接家里的WiFi连接成功后用ATCIPSTARTTCP,服务器IP,8080建立TCP连接最后用ATCIPMODE1进入透传模式之后STM32往串口发送的所有数据ESP8266都会自动打包成TCP包发出去。这里要注意AT指令的结尾必须带\r\n。用C字符串表示就是\r\n实际在代码里发送ATCWMODE1\r\n。很多新手漏了回车换行导致ESP8266完全没有响应这是最常见的低级错误之一。4.2 服务器端最简单的接收方案当ESP8266建立TCP连接后你需要在网络另一端有一个TCP服务端来接收数据。最简单的方式就是在电脑上用Python起一个TCP服务器import socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((0.0.0.0, 8080)) server.listen(1) print(等待ESP8266连接...) conn, addr server.accept() print(f连接来自: {addr}) while True: data conn.recv(1024) if not data: break print(f收到数据: {data.decode()})把这段代码跑在电脑上ESP8266连接你电脑的局域网IP的8080端口数据就能实时显示在电脑屏幕上。如果你不在局域网内想远程访问那一般需要做云服务器转发或者用现成的IoT平台。我的建议是先用局域网跑通整个链路确认采集和通信都正常再考虑上云。4.3 让数据格式规范一些一开始我只是简单地把温湿度拼接成一个字符串发出去比如T:25.0 H:68.0但数据到了服务器端要处理、存储、画曲线这种非结构化的文本格式后期会很麻烦。所以我后来设计了一种简单的JSON格式报文{dev:dht01,t:25.0,h:68.0}STM32端拼JSON其实也很简单用printf或者snprintf拼接字符串即可char buf[64]; snprintf(buf, sizeof(buf), {\dev\:\dht01\,\t\:%d.%d,\h\:%d.%d}\r\n, temp/10, temp%10, hum/10, hum%10); HAL_UART_Transmit(huart2, (uint8_t*)buf, strlen(buf), 1000);之所以加上\r\n是因为TCP接收端通常使用换行符作为一条消息的结束标志。Python端recv收到的数据就能按行解析了。4.4 WiFi连接失败和丢包的处理实测下来ESP8266在家庭路由器环境下稳定性其实还可以但也不可避免地会遇到WiFi断开、路由器重启、信号波动这些问题。我的处理思路是这样STM32每隔一段时间检查一次ESP8266的TCP连接状态如果发现断开就重新执行连接流程。简单来说就是定时发送ATPING192.168.1.1这个指令可以测试WiFi链路是否正常如果发送失败说明ESP8266可能掉线了先发AT测试通信如果AT也没有响应就对ESP8266做一次硬件复位用GPIO控制RST引脚拉低再拉高复位后重新执行连接WiFi和TCP的流程。这个“三级检测”思路解决了我遇到的绝大多数断线问题。还有就是要避免在透传模式下长时间不发数据——有些路由器会因空闲超时断开TCP连接所以我一般让设备每10秒钟发一条心跳包保持连接活跃。5. 完整工程实录与代码骨架5.1 各模块初始化顺序整个系统的初始化顺序是有讲究的我的推荐顺序是初始化时钟、GPIO、I2C、UART等基础外设初始化OLED显示屏先显示一个启动界面初始化DHT11先读一次数据显示在OLED上初始化ESP8266完成WiFi连接和TCP建链进入主循环定时采集并上报。为什么要先显示OLED再初始化ESP8266因为ESP8266连接WiFi的过程可能要花几秒钟甚至十几秒如果先把ESP8266初始化了这段时间屏幕是黑的看起来就像死机了一样。先把OLED点亮哪怕显示个“Connecting WiFi...”提示用户体验完全不同排查问题的时候也能直观知道代码跑到哪一步了。5.2 主循环调度思路把采集和上报放在同一个循环里最简单但要注意不要让WiFi通信阻塞了采集节奏。我的调度逻辑是这样的uint32_t last_read 0; // 上次采集时间 uint32_t last_send 0; // 上次发送时间 while(1) { // 每1.5s采集并显示一次 if(HAL_GetTick() - last_read 1500) { last_read HAL_GetTick(); DHT11_Read_Data(hum, temp); OLED_Update(); } // 每5s发送一次数据 if(HAL_GetTick() - last_send 5000) { last_send HAL_GetTick(); ESP8266_Send_Data(hum, temp); } // 每30s检查一次链路状态 if(HAL_GetTick() - last_check 30000) { last_check HAL_GetTick(); ESP8266_Check_Link(); } }用HAL_GetTick()做非阻塞调度而不是直接用HAL_Delay()好处是各个任务的执行节奏不会互相卡住。比如WiFi重连可能耗时好几秒如果用DelayDHT11采集和OLED显示就会被卡住而用时间片轮询的方式采集和显示始终能维持自己的节奏。5.3 一个精简版ESP8266驱动骨架void ESP8266_SendCmd(char *cmd, char *ack, uint16_t timeout) { // 发送AT指令并等待期望的回复 HAL_UART_Transmit(huart2, (uint8_t*)cmd, strlen(cmd), 100); uint32_t start HAL_GetTick(); while(HAL_GetTick() - start timeout) { if(ESP8266_CheckRecv(ack)) return; // 收到期望回复 } } void ESP8266_Init(void) { ESP8266_SendCmd(AT\r\n, OK, 1000); ESP8266_SendCmd(ATCWMODE1\r\n, OK, 1000); ESP8266_SendCmd(ATCWJAP\MyWiFi\,\12345678\\r\n, OK, 10000); ESP8266_SendCmd(ATCIPSTART\TCP\,\192.168.1.100\,8080\r\n, OK, 5000); ESP8266_SendCmd(ATCIPMODE1\r\n, OK, 1000); ESP8266_SendCmd(ATCIPSEND\r\n, , 1000); }代码里的ESP8266_CheckRecv函数负责在串口接收中断中把收到的数据存到环形缓冲区里然后检查是否包含期待的字符串。这是串口通信编程里的基本功——不能简单地用HAL_UART_Receive阻塞等待因为AT指令的回复长度不固定、到达时间也不同用中断缓冲区的方式最可靠。6. 常见问题与排查技巧实录6.1 DHT11读不到数据的6个典型原因在项目过程中DHT11读不到数据是最常见的问题我总结了几种情况现象原因解决办法一直返回“未就绪”DHT11上拉电阻缺失加一个10K到VCC的上拉电阻数据校验经常失败时序延时不准或读取期间被中断打断校准delay_us读取期间关中断只有第一次能读到DHT11两次读取间隔太短主循环中确保间隔大于1秒数值恒为0GPIO配置错误开漏/推挽模式不对检查CubeMX中PA0的初始化数据偶尔乱跳供电电压不稳或线太长缩短杜邦线加一个100nF去耦电容其他引脚读都正常传感器硬件损坏换一个DHT11试试关于delay_us的校准我给出一个最简单的方法在GPIO上输出一个方波用逻辑分析仪或示波器实测高低电平的时间然后调整延时函数的循环次数直到误差在几微秒以内。没有示波器的话可以用DHT11本身的读取成功率来间接判断——如果校验经常失败基本就是延时不准。6.2 OLED花屏、不亮、显示乱码OLED的问题相对好排查我按经验频率排个序地址不对只显示“雪花”或者完全不亮用I2C扫描程序比如HAL_I2C_IsDeviceReady确认地址到底是0x3C还是0x3D接线问题SCL、SDA接反了或者VCC/GND接触不良重新插拔杜邦线复位问题单片机上电瞬间如果OLED的RES引脚没有被正确拉高屏幕会一直黑屏软件初始化时必须先对SSD1306执行软复位指令汉字显示反色取模方式设置不对在取模软件里切换“阴码/阳码”花屏且持续变化I2C速率过高从100KHz降到50KHz试试。还有一个小技巧OLED模块的刷新率不需要很高显示温湿度这种变化慢的数据自己写驱动时可以只在数据变化时才调用更新没必要强制60帧刷新。这样能降低主控负担对功耗也有好处。6.3 ESP8266连接不上WiFi或频繁掉线ESP8266相关的坑比较多我按严重程度排下来供电不足这是万恶之源。模块启动瞬间电流大USB口供电不稳时表现为反复重启。解决方案用独立的3.3V LDO稳压模块供电或者在模块电源引脚附近加一个大电容100uF以上AT指令没响应接线时TX和RX是不是接反了记得STM32的TX接ESP8266的RXSTM32的RX接ESP8266的TX交叉连接。还有波特率设置ESP8266默认115200但也有刷过固件的模块可能是9600或74880用串口助手先确认连不上路由器先检查SSID和密码是否真确、有没有特殊字符有些ESP8266对某些特殊字符处理不正常、路由器是否开启了MAC地址过滤TCP连接被路由器断开长时间没数据收发NAT空闲超时。对策就是定时发心跳包10秒发一次丢包严重WiFi信号弱或者同一信道干扰大。把ESP8266和路由器之间的距离拉近或者换5GHz路由器上设置兼容2.4GHz因为ESP8266只支持2.4GHz频段。6.4 串口打印乱码或数据错乱这个问题也经常被问到。STM32的USART电平是3.3V TTL但大部分USB-TTL模块可以兼容3.3V和5V如果你用的是5V供电的USB-TTL模块去连接ESP8266可能因为电平过高导致通信异常。稳妥的做法是确认USB-TTL模块上有3.3V电平跳线或者单独给ESP8266供电、只让USB-TTL模块的TX/RX参与通信。另外STM32和ESP8266通信的波特率必须严格一致。STM32的HAL库配置的波特率有时因为系统时钟配置不对实际波特率和标称值有偏差这时串口数据会断断续续或者全是乱码。时钟树里外设时钟源的选择APB2、APB1会直接影响UART波特率CubeMX生成代码时一定要让时钟配置正确。6.5 我的调试顺序建议最后分享一个个人经验。这个项目涉及四个设备如果我是一次性把所有代码写完再上电测试出了问题根本不知道从哪查起。我的调试习惯是“由底向上逐层打通”先单独测DHT11不接OLED、不接ESP8266只实现一个DHT11读取函数用串口打印到PC上确认数据正确再点亮OLED手动在代码里写死几个测试字符串和汉字确认真能显示然后把两者结合显示真实的温湿度数据再用USB-TTL单独测ESP8266模块在电脑上把AT指令全部调试通过确认能连WiFi、能发TCP最后上单片机把ESP8266和STM32的串口打通实现透传上报。这个过程每一步的验证时间不会太长但能大幅减少联调时候的“脑壳疼”。很多初学者喜欢一口气写几百行代码烧录后各种怪问题一起冒出来结果完全无法定位——这是嵌入式开发里最忌讳的做法。整个项目做完你会发现这套系统的潜力远不止“显示温度”这么简单。我后来给这个系统加了一个继电器模块当温度超过阈值时自动控制风扇启动又加了一个开关量输入接了一个门窗状态传感器。因为当初选了STM32做主控这些扩展就是多写几行代码的事完全不用动通信方案。如果你也刚接触这块建议先照着上面的步骤把数据链路跑通再根据自己的需求一点一点往外扩展。