news 2026/9/9 4:15:41

STM32+ESP8266+DHT11+OLED温湿度监控系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32+ESP8266+DHT11+OLED温湿度监控系统设计与实现

1. 整体设计与思路拆解

1.1 这套方案到底解决什么问题

做温湿度监控这件事,我前前后后折腾过好几套方案,从最早的单片机加LCD1602本地显示,到后来加蓝牙模块用手机短距查看,再到现在这套WiFi无线方案。对比下来,STM32+ESP8266+DHT11+OLED这套组合,是我在实际项目里用得最顺手、也是被问得最多的一套。

先说最直接的痛点:传统温湿度监测的麻烦在于“看得见,够不着”。传感器装在生产车间、暖通管道、机房角落、粮仓,人不可能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显示屏(本地实时显示) └── ESP8266(WiFi透传)→ 无线路由器 → 服务器/手机

这里我的设计原则是“采集与通信分离”。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寸 OLED,SSD1306,I2C接口10-15元4针,GND/VCC/SCL/SDA
供电手机充电器 + MicroUSB线 或 5V电源模块-电流500mA以上就够
其他面包板、杜邦线若干,10K电阻1个几元DHT11数据线上拉用

引脚分配表(STM32F103C8T6):

STM32引脚外设连接配置说明
PA0DHT11 DATA开漏输出/上拉输入,读取温湿度
PB6OLED SCLI2C1时钟线,STM32的I2C1默认引脚
PB7OLED SDAI2C1数据线
PA2ESP8266 TXDUSART2 RX,接收ESP8266数据
PA3ESP8266 RXDUSART2 TX,发送指令给ESP8266
GND所有模块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 + Keil5

STM32开发环境的选择,我推荐新手直接走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设为I2C1,I2C速度选100KHz(Standard Mode)就好,OLED显示不需要多快;
  • 添加USART2异步通信,波特率115200,用于连接ESP8266;
  • 时钟树可以把系统时钟设为64MHz,F103最高支持到72MHz,但64MHz够用且更稳定。

生成代码后用Keil5打开,在main.c里加逻辑代码。如果你更喜欢VS Code,也可以装好ARM工具链后直接用,但Keil5的调试和烧录对新手更友好,我建议先别折腾工具链,把精力花在调试业务逻辑上。

3. 核心驱动开发:从DHT11和OLED开始

3.1 DHT11通信协议拆解

DHT11看起来简单,但实际上手最容易出问题的地方就是它的单总线时序。它的通信协议大致是这样的:

  1. 主机(STM32)先把总线拉低至少18ms,然后释放总线,告诉DHT11“我要开始采集了”;
  2. DHT11回应一个约80us的低电平,再拉高80us,表示“我准备好了”;
  3. 然后DHT11开始发送40bit的数据,依次是:湿度整数、湿度小数、温度整数、温度小数、校验和;
  4. 每个bit的表示方式不是0或1的电平,而是高电平持续的时间长度——高电平持续约26~28us表示“0”,持续约70us表示“1”;
  5. 校验和等于前四个字节之和的低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个像素点的组合,每个像素按“亮/灭”编码成bit,16行每行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°C;DHT11的分辨率默认是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模式AT+CWMODE=1OK(1为STA模式)
连接路由器AT+CWJAP="SSID","密码"OK或WIFI CONNECTED
查询IP地址AT+CIFSR返回IP地址
建立TCP连接AT+CIPSTART="TCP","192.168.1.100",8080CONNECT OK
进入透传模式AT+CIPMODE=1OK
开始发送数据AT+CIPSEND返回> 提示符
退出透传发送+++OK

我实际用的流程是:

  1. 开机后STM32先通过串口给ESP8266发AT+CWMODE=1,设置成STA模式;
  2. 然后发AT+CWJAP="路由器名","密码"连接家里的WiFi;
  3. 连接成功后用AT+CIPSTART="TCP","服务器IP",8080建立TCP连接;
  4. 最后用AT+CIPMODE=1进入透传模式,之后STM32往串口发送的所有数据,ESP8266都会自动打包成TCP包发出去。

这里要注意,AT指令的结尾必须带\r\n。用C字符串表示就是"\r\n",实际在代码里发送"AT+CWMODE=1\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连接状态,如果发现断开就重新执行连接流程。简单来说就是:

  1. 定时发送AT+PING="192.168.1.1",这个指令可以测试WiFi链路是否正常;
  2. 如果发送失败,说明ESP8266可能掉线了,先发AT测试通信;
  3. 如果AT也没有响应,就对ESP8266做一次硬件复位(用GPIO控制RST引脚拉低再拉高);
  4. 复位后重新执行连接WiFi和TCP的流程。

这个“三级检测”思路解决了我遇到的绝大多数断线问题。还有就是要避免在透传模式下长时间不发数据——有些路由器会因空闲超时断开TCP连接,所以我一般让设备每10秒钟发一条心跳包,保持连接活跃。

5. 完整工程实录与代码骨架

5.1 各模块初始化顺序

整个系统的初始化顺序是有讲究的,我的推荐顺序是:

  1. 初始化时钟、GPIO、I2C、UART等基础外设;
  2. 初始化OLED显示屏,先显示一个启动界面;
  3. 初始化DHT11,先读一次数据,显示在OLED上;
  4. 初始化ESP8266,完成WiFi连接和TCP建链;
  5. 进入主循环,定时采集并上报。

为什么要先显示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重连可能耗时好几秒,如果用Delay,DHT11采集和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("AT+CWMODE=1\r\n", "OK", 1000); ESP8266_SendCmd("AT+CWJAP=\"MyWiFi\",\"12345678\"\r\n", "OK", 10000); ESP8266_SendCmd("AT+CIPSTART=\"TCP\",\"192.168.1.100\",8080\r\n", "OK", 5000); ESP8266_SendCmd("AT+CIPMODE=1\r\n", "OK", 1000); ESP8266_SendCmd("AT+CIPSEND\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的RX,STM32的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库配置的波特率有时因为系统时钟配置不对,实际波特率和标称值有偏差,这时串口数据会断断续续或者全是乱码。时钟树里外设时钟源的选择(APB2APB1)会直接影响UART波特率,CubeMX生成代码时一定要让时钟配置正确。

6.5 我的调试顺序建议

最后分享一个个人经验。这个项目涉及四个设备,如果我是一次性把所有代码写完再上电测试,出了问题根本不知道从哪查起。我的调试习惯是“由底向上,逐层打通”:

  1. 先单独测DHT11:不接OLED、不接ESP8266,只实现一个DHT11读取函数,用串口打印到PC上确认数据正确;
  2. 再点亮OLED:手动在代码里写死几个测试字符串和汉字,确认真能显示;
  3. 然后把两者结合:显示真实的温湿度数据;
  4. 再用USB-TTL单独测ESP8266模块:在电脑上把AT指令全部调试通过,确认能连WiFi、能发TCP;
  5. 最后上单片机,把ESP8266和STM32的串口打通,实现透传上报。

这个过程每一步的验证时间不会太长,但能大幅减少联调时候的“脑壳疼”。很多初学者喜欢一口气写几百行代码,烧录后各种怪问题一起冒出来,结果完全无法定位——这是嵌入式开发里最忌讳的做法。

整个项目做完,你会发现这套系统的潜力远不止“显示温度”这么简单。我后来给这个系统加了一个继电器模块,当温度超过阈值时自动控制风扇启动;又加了一个开关量输入,接了一个门窗状态传感器。因为当初选了STM32做主控,这些扩展就是多写几行代码的事,完全不用动通信方案。如果你也刚接触这块,建议先照着上面的步骤把数据链路跑通,再根据自己的需求一点一点往外扩展。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:15:17

AFE芯片:高精度模拟信号采集的核心引擎

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:14:42

ArcGIS二次开发实战:ArcPy构建评价单元综合限制级别判断矩阵工具

搞过国土空间规划、自然资源评价或者项目选址评估的人&#xff0c;大多数都被同一件事折磨过&#xff1a;手里攥着一份评价单元&#xff0c;面前摊着生态保护红线、永久基本农田、地质灾害易发区、洪水影响区、水源保护区、矿产压覆区……一长串限制性图层&#xff0c;要做的工…

作者头像 李华
网站建设 2026/9/9 4:13:47

ponytail:轻量级前端开发代理工具实战指南

1. “ponytail”不是发型&#xff0c;是前端工程里一个正在冒头的轻量级构建代理工具最近在几个前端技术群和 GitHub Trending 页面上反复刷到ponytail这个词——它既不是 TikTok 上的新编发教程&#xff0c;也不是某位设计师的个人品牌缩写&#xff0c;而是一个刚发布不到三个…

作者头像 李华
网站建设 2026/9/9 4:13:14

AI编程工具链实战指南:Codex与Claude Code本地化应用

我无法根据您提供的输入生成符合要求的博文内容。原因如下&#xff1a;输入中仅提供了项目标题"ruflo"&#xff0c;但未提供任何有效上下文&#xff1a;缺少【项目正文】&#xff08;即原始零散描述&#xff09;缺少【关键词】的具体明确列表&#xff08;当前仅罗列大…

作者头像 李华
网站建设 2026/9/9 4:12:46

opencode实战:从安装配置到Skills、LSP与Playwright的AI编码代理指南

一开始看见 opencode 这个词&#xff0c;很多人会以为它又是一个"套了终端壳的 AI 聊天窗口"。但真正在项目里跑起来之后&#xff0c;你会发现它完全是另外一类东西&#xff1a;它启动后直接附着在当前工程目录上&#xff0c;能自己读代码、执行命令、调用 LSP 拿到编…

作者头像 李华
网站建设 2026/9/9 4:12:43

从“无标题”到完整交付:一套高效内容创作五步法

我太熟悉“无标题”这三个字了。每次新建一个文档、打开一个空白画布&#xff0c;或者准备动手做一个新项目时&#xff0c;系统默认给我的就是这行字。很多人在这一步停下来&#xff0c;盯着光标发呆&#xff0c;然后陷入一种奇怪的焦虑&#xff1a;名字都还没有&#xff0c;怎…

作者头像 李华