1. 项目背景与需求分析
1.1 为什么做这个WiFi无线温湿度监控项目
前阵子帮朋友搞了一个小型仓库的温湿度监测需求,仓库面积不大,但有几个死角平时根本没人去,等到发现潮湿发霉的时候已经晚了。传统做法是放个温湿度计,每天人工去抄一次数据,费时费力不说,数据还不连续,半夜的变化根本记录不到。
后来我给他做了一个基于STM32+ESP8266+DHT11+OLED的WiFi无线温湿度监控设备,成本低、功耗可控、代码量也不大,最关键的是把传感数据接到了WiFi网络上,手机随时能看,历史数据也能存。做完之后朋友又让我帮他朋友做了一台,我觉得这套组合确实值得写一篇完整的教程分享出来。
这套方案的核心架构很简单:DHT11负责采集温湿度,STM32做主控读取传感器数据并驱动OLED显示,ESP8266通过串口与STM32通信,把数据通过WiFi上传到云端或本地服务器。整个链路清晰,非常适合单片机入门到进阶这个阶段的开发者练手,也能直接改造成实际可用的产品原型。
1.2 这套组合的适用人群与场景
先说说这套方案适合谁来搞。如果你已经学过STM32的基础外设(GPIO、定时器、串口、I2C),但对WiFi通信、传感器时序、嵌入式联网这套还比较懵,那这个项目就是很好的跳板。它的难度不在某个单点,而在于把几个模块串起来,这恰恰是实际工程中最常遇到的情况。
适用场景我认为有三类:
- 环境监测类的原型验证,比如机房、仓储、种植大棚的温湿度监控
- 智能家居的小型节点,作为温湿度采集终端接入更大的物联网系统
- 教学实验和毕业设计,这个组合在课程设计和毕设里出现频率非常高
需要实话实说,DHT11本身精度一般,温度±2°C、湿度±5%RH,做精度要求高的科研项目它不够格。但做环境趋势监测、阈值告警、物联网原型验证,它的性价比是碾压级的。想要更高精度可以换DHT22或SHT30,代码改动成本也不大,后面我会提到怎么改。
2. 硬件选型与模块拆解
2.1 STM32主控选型建议
主控我选的是STM32F103C8T6,也就是大家常说的“蓝丸”核心板。说实话这个芯片已经被各路教程写烂了,但正是因为它资料多、例程全、踩坑记录丰富,才适合作为这个项目的起点。
选型逻辑很简单:
- 价格便宜,几块钱到十几块钱一片,炸了不心疼
- 外设资源够用:USART、I2C、GPIO、定时器全都有
- 3.3V供电,和ESP8266、OLED的电压域匹配,不需要额外电平转换
- CubeMX+HAL库的开发方式已经非常成熟,新手照着配置就能跑
如果你手上是其他型号的STM32也没关系,代码的移植成本主要集中在引脚定义和时钟配置上,核心逻辑完全通用。后面我会把和硬件强相关的部分单独标注出来。
2.2 DHT11传感器的通信原理详解
DHT11这块传感器很有意思,它用的是单总线协议,数据线和时钟线复用一根线。这也就意味着时序要求比较严格,尤其是起始信号和应答信号的拉低时间,搞不好就读出全0或者全1的数据。
通信时序大致是这样的:
- 主机拉低数据线至少18ms,然后释放,这是起始信号
- DHT11响应,拉低80us,再拉高80us,表示准备发送数据
- 之后连续发送40bit数据:8bit湿度整数、8bit湿度小数、8bit温度整数、8bit温度小数、8bit校验和
每bit的表示方式是用高电平持续时间来区分的。高电平持续26~28us表示0,持续70us左右表示1。这部分的读取代码用HAL库的延时函数做不太靠谱,因为HAL_Delay最小精度是1ms,根本卡不了微秒级的时间。我试过用空循环做微秒延时,但这依赖主频和编译优化级别,换一个优化选项时序就崩了,后面会给出我认为最稳的做法。
2.3 ESP8266、OLED的选型与分工
ESP8266我用的ESP-01S,这个模块体积小、价格低,GPIO只有两个但够用,反正它在项目里只负责串口透传数据到WiFi,不需要额外的引脚控制。如果你想要后续扩展更多功能,可以换ESP-12F或NodeMCU开发板,引脚多很多,烧录也方便。
需要注意ESP-01S的工作电流峰值能到300mA左右,配合WiFi发射时的瞬态电流更高。如果直接用STM32核心板上的3.3V稳压芯片给ESP8266供电,很容易出现电压跌落导致模块反复重启。我一开始就吃过这个亏,后来老老实实给ESP8266单独供电才稳定下来。
OLED屏用的是0.96寸I2C接口的SSD1306驱动方案,4个引脚:VCC、GND、SCL、SDA。I2C的方式只用两根线就能驱动,焊接和接线都省事。这个屏功耗低、刷新也够用,显示温湿度数据绰绰有余。
2.4 整体系统框图与数据流向
整个系统的工作流程可以这样理解:DHT11采集温湿度 → STM32读取并解析数据 → 数据同时送入两个方向——OLED本地显示、ESP8266串口发送。ESP8266通过WiFi连接路由器,再把数据以HTTP请求或TCP方式上报到云端服务器或局域网内的接收端。
这里面有一个关键设计:传感数据采样的频率不需要太高。DHT11本身的采样周期推荐在1秒以上,太快反而读不到有效数据。WiFi上报也可以设置一个合理间隔,比如5秒或10秒上报一次,既满足监控需求,又不会因为上报过于频繁导致ESP8266数据拥塞。
3. 开发环境搭建与工具准备
3.1 STM32开发环境配置注意点
STM32的开发环境现在主流的搭配是STM32CubeMX生成初始化代码,再用Keil MDK或者其他IDE编译下载。我推荐用CubeMX的原因很简单:这个项目涉及GPIO、USART、I2C、定时器多个外设的初始化,手写寄存器级别的配置代码对新手不友好,CubeMX图形化点选生成,省时省力。
具体配置项后面实操部分会详细展开。Keil这边记得选对Device型号——STM32F103C8,然后在Target标签页里把晶振频率改成8MHz,这个不设置后面串口波特率容易算错。
还有一个很常见的坑:Keil5兼容C51和STM32的安装问题。如果你之前装过C51版的Keil,再装MDK版可能会遇到编译器路径冲突,最典型的症状是编译时找不到ARMCC编译器。解决办法是把两个版本安装到不同目录,并且先装MDK再装C51,或者直接用纯净的MDK环境来开发STM32。
3.2 ESP8266固件初始化与验证
ESP-01S出厂默认固件通常是支持AT指令的,但不同批次可能版本不一样,稳妥起见建议先擦除再烧录一个统一版本的固件。烧录固件需要USB转TTL模块,接线是ESP8266的RX接TTL的TX、TX接RX,VCC接3.3V,GND共地,EN引脚要拉高。
烧录时有个坑:ESP-01S必须让GPIO0接地才能进入UART下载模式。如果GPIO0悬空或拉高,模块会直接进入运行模式,烧录工具会报连接失败。
固件烧好后用串口助手发送“AT”,返回“OK”基本就说明模块没问题了。再依次测试AT+CWMODE设置模式、AT+CWJAP连接WiFi,确认联网正常,这一步跑通了再往下进行。
3.3 手头常用的调试工具清单
嵌入式开发调试工具非常关键,我推荐以下几个:
- USB转TTL模块,我用的是CH340芯片方案的,驱动稳定
- 逻辑分析仪,对于调试DHT11这种时序敏感型传感器非常有用,能直接看波形,几十块钱的24MHz采样率的就够用
- 串口助手,SSCOM或XCOM都行,用于ESP8266调试和查看STM32的调试输出
- 万用表,检查供电电压和接线导通,排查硬件问题必备
逻辑分析仪在这个项目里我建议有条件就备一个。DHT11读不到数据的时候,靠猜是猜不出问题在哪里的,用逻辑分析仪抓一下波形,马上就能判断是传感器没响应还是时序不对,排查效率提升好几个量级。
4. 电路连接与硬件组装实操
4.1 接线表与引脚定义
下面是我在实际焊接中最终确定的接线方案:
| 模块 | 引脚 | 接STM32引脚 | 备注 |
|---|---|---|---|
| DHT11 | VCC | 3.3V | DHT11供电范围3.3V~5V |
| DHT11 | GND | GND | 共地 |
| DHT11 | DATA | PA6 | 需接4.7kΩ上拉电阻 |
| OLED | VCC | 3.3V | OLED供电 |
| OLED | GND | GND | 共地 |
| OLED | SCL | PB6 | I2C1时钟线 |
| OLED | SDA | PB7 | I2C1数据线 |
| ESP8266 | VCC | 外部3.3V电源 | 不能直接接核心板3.3V |
| ESP8266 | GND | GND | 与STM32共地 |
| ESP8266 | RX | PA2 | USART2_TX |
| ESP8266 | TX | PA3 | USART2_RX |
| ESP8266 | EN | 3.3V | 使能引脚拉高 |
DHT11的数据线接PA6,这个引脚是STM32的普通GPIO,配置为开漏输出并外接上拉电阻。开漏输出的好处是引脚既能拉低也能释放由外部上拉电阻拉高,刚好符合单总线“线与”的逻辑。
关于DHT11的上拉电阻,我直接用的4.7kΩ,这是常见的I2C和单总线标准值。实测稳定,如果你用10kΩ也可以,响应时间会稍慢但在这个低速场景没有问题。
4.2 供电方案的关键设计
供电是这套系统最容易被忽略的坑。ESP8266在WiFi发射瞬间的电流尖峰可能超过300mA,如果这时供电电压被拉低到3.3V以下,模块就重启了,表现就是你看到串口里不断输出乱码或者AT指令没响应。
我实际用的供电方案是:整体用USB 5V输入,通过一个AMS1117-3.3稳压模块给ESP8266单独供电,STM32核心板上的3.3V只给STM32和OLED、DHT11供电。两个3.3V电源在GND端共地,这样通信引脚的电平参考是一致的。
如果你手头有5V转3.3V的DC-DC模块,那就更稳了,效率比LDO高。但AMS1117对这个场景够用,成本也低。
4.3 面包板原型搭建到焊接固化的过程
我建议先面包板搭原型验证逻辑,再焊接到洞洞板或做PCB。面包板的好处是改线方便,出了问题可以快速排查,缺点是接触不良是常态,尤其是DHT11这种单总线,一旦接触不良读出来的数据就很诡异。
原型跑通之后,我把模块转移到了洞洞板上。这里有几个经验:
- 排针和排母结合使用,模块可以插拔,方便拆下来单独调试
- 电源线和地线用粗一点的线,至少0.5mm²,降低线阻
- DHT11和ESP8266的天线区域不要靠太近,以免WiFi射频信号干扰传感器数据
PCB定制是更进一步的做法,嘉立创免费打样就够用了。画原理图和PCB时注意DHT11旁边的退耦电容要靠近传感器放置,ESP8266下方的地平面要完整,天线区域下方净空。网上搜“DHT11原理图嘉立创画图”能出来一堆参考,照着画就行。
4.4 上电前必须做的检查清单
焊接完通电之前,按照下面清单逐项检查一遍:
- 电源正负极有没有接反(最简单也最容易犯的错误)
- 各模块的VCC电压是否在规格范围内
- 共地是否接好,3.3V和GND环路是否完整
- GPIO引脚有没有互相短接的可能
- DHT11的DATA线上有没有上拉电阻
- ESP8266的EN是否有上拉电平
我上电前都会用万用表的二极管档测一下电源和地之间的阻值,如果不正常先别通电。曾经有人直接把ESP8266和STM32核心板的3.3V接在一起导致整片发烫,排查半天才发现是核心板上的稳压芯片过载。
5. STM32端核心代码实现
5.1 CubeMX工程配置逐项说明
CubeMX配置这个项目,有几个重点区域需要仔细设置:
RCC设置:HSE选择Crystal/Ceramic Resonator,这是外部8MHz晶振。
SYS设置:Debug选择Serial Wire,否则下载一次程序后第二次就无法连接了,这是新手的常见问题。
GPIO配置:PA6配置为Output Push Pull或Open Drain都行。我习惯配置为开漏输出模式,初始电平High,配合外部上拉电阻。
I2C1配置:PB6和PB7作为I2C1的SCL和SDA,速率标准模式100KHz或者快速模式400KHz都可以,OLED屏驱动SSD1306在400KHz下工作正常。
USART2配置:异步模式,波特率115200,8位数据位,1停止位,无校验。这个串口用于与ESP8266通信。
USART1配置:也开一个异步模式,波特率115200,用于打印调试信息到PC。
时钟树:HCLK配置到72MHz,这个项目CPU频率足够用。I2C的时钟源选择系统时钟72MHz分频。
5.2 DHT11读取驱动代码解析——从时序到实现
DHT11驱动是整个代码里最讲究时序的部分。用HAL库的延时函数HAL_Delay做不了微秒级延时,我的方案是用SysTick或者DWT计数器来做。这里给出的是我在STM32F103上稳定运行的DHT11读取代码。
/* dht11.h */ #ifndef __DHT11_H #define __DHT11_H #include "main.h" #define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_6 #define DHT11_OUT_L() HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET) #define DHT11_OUT_H() HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET) #define DHT11_IN_READ() HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) #define DHT11_DATA_PORT_ENABLE_IN() do{ \ GPIO_InitTypeDef GPIO_InitStruct = {0}; \ GPIO_InitStruct.Pin = DHT11_PIN; \ GPIO_InitStruct.Mode = GPIO_MODE_INPUT; \ GPIO_InitStruct.Pull = GPIO_NOPULL; \ HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); \ }while(0) #define DHT11_DATA_PORT_ENABLE_OUT() do{ \ GPIO_InitTypeDef GPIO_InitStruct = {0}; \ GPIO_InitStruct.Pin = DHT11_PIN; \ GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; \ GPIO_InitStruct.Pull = GPIO_NOPULL; \ GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; \ HAL_GPIO_Init(DHT11_PORT, &GPIO_InitStruct); \ }while(0) uint8_t DHT11_ReadData(float *temperature, float *humidity); #endif/* dht11.c */ #include "dht11.h" static void DHT11_DelayUs(uint32_t us) { uint32_t count = us * 72; // 72MHz时钟下,1us约72个周期 while(count--) { __NOP(); } } static uint8_t DHT11_ReadByte(void) { uint8_t value = 0; for(int i = 0; i < 8; i++) { while(DHT11_IN_READ() == GPIO_PIN_RESET); // 等待低电平结束 DHT11_DelayUs(40); // 高电平持续40us后判断 if(DHT11_IN_READ() == GPIO_PIN_SET) { value |= (0x80 >> i); } while(DHT11_IN_READ() == GPIO_PIN_SET); // 等待高电平结束 } return value; } uint8_t DHT11_ReadData(float *temperature, float *humidity) { uint8_t data[5] = {0}; // 主机起始信号 DHT11_DATA_PORT_ENABLE_OUT(); DHT11_OUT_L(); HAL_Delay(20); // 拉低至少18ms DHT11_OUT_H(); DHT11_DelayUs(30); // 释放并延时30us DHT11_DATA_PORT_ENABLE_IN(); // 切换到输入模式 // 等待应答信号 if(DHT11_IN_READ() == GPIO_PIN_RESET) { while(DHT11_IN_READ() == GPIO_PIN_RESET); // 等待80us低电平结束 while(DHT11_IN_READ() == GPIO_PIN_SET); // 等待80us高电平结束 } else { return 1; // 无应答 } // 读取40bit数据 for(int i = 0; i < 5; i++) { data[i] = DHT11_ReadByte(); } // 校验 if((data[0] + data[1] + data[2] + data[3]) != data[4]) { return 2; // 校验错误 } *humidity = (float)(data[0] + data[1] * 0.1); *temperature = (float)(data[2] + data[3] * 0.1); return 0; }关于延时函数,我上面用的方法是基于CPU主频做空循环,这种方法有一个前提:编译器不能把空循环优化掉。STM32F103在72MHz下的空循环延时,实测在这个项目里是够用的。更稳的做法是用DWT计数器,系统滴答的精度更高,但代码复杂度上去了,新手容易搞懵。在DHT11这个场景下,40us级别的判断误差容忍度还算可以,所以空循环方案完全够用。
这里有一个很重要的设计:DHT11的数据引脚需要能够在输入和输出两种模式下切换。主机发完起始信号后必须切换到输入模式,否则总线一直被主机拉高,传感器的应答信号就发不出来。
5.3 OLED显示驱动——HAL库I2C读写SSD1306
OLED这块我用的是SSD1306驱动芯片的0.96寸屏。驱动OLED的核心是把显存(RAM)准备好,然后通过I2C发送到SSD1306。SSD1306内部有128×64位的显存,每个 bit 对应一个像素点。所以实际操作是:维护一个128×8字节的buffer,修改buffer内容,然后把整个buffer发给屏幕。
关于热词里有人搜“OLED屏的像素点由几层组成”,这里补充一下,这不是指物理结构,而是指显示驱动方式。SSD1306的显存按页(Page)组织,共8页,每页8行像素,写数据时先设置页地址和列地址,然后连续写数据。理解这个组织结构对驱动代码很关键。
汉字显示这里我多说一句,OLED显示汉字需要自己取模生成字库。取模软件可以选PCtoLCD2002,取模方式选择逐列式、阴码、逆向。我一开始取模方向设错了,显示的汉字镜像了,后来调了“逆向”选项才好。中文显示本质就是把16x16的点阵数据填到显存对应位置,理解了就很顺。
一个通用I2C函数就搞定OLED初始化,配置完整I2C起始地址后写入控制字节和命令。OLED驱动代码我用的标准的SSD1306 HAL库驱动,网上资源很多,核心是把初始化序列发送给SSD1306,配置显示时钟分频、复用率、偏移量、起始行、电荷泵等寄存器。这里我只列我的初始化序列,供参考:
static void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2]; buf[0] = 0x00; // 控制字节:Co=0, D/C#=0,表示后面跟的是命令 buf[1] = cmd; HAL_I2C_Master_Transmit(&hi2c1, 0x78, buf, 2, 100); } static void OLED_WriteData(uint8_t data) { uint8_t buf[2]; buf[0] = 0x40; // 控制字节:Co=0, D/C#=1,表示后面跟的是数据 buf[1] = data; HAL_I2C_Master_Transmit(&hi2c1, 0x78, buf, 2, 100); } void OLED_Init(void) { HAL_Delay(100); OLED_WriteCmd(0xAE); // 关闭显示 OLED_WriteCmd(0x20); // 设置内存寻址模式 OLED_WriteCmd(0x02); // 页寻址模式 OLED_WriteCmd(0xB0); // 设置页起始地址 OLED_WriteCmd(0xC8); // 设置COM扫描方向 OLED_WriteCmd(0x00); // 设置列地址低四位 OLED_WriteCmd(0x10); // 设置列地址高四位 OLED_WriteCmd(0x40); // 设置显示起始行 OLED_WriteCmd(0x81); // 设置对比度 OLED_WriteCmd(0x7F); OLED_WriteCmd(0xA1); // 设置段重映射 OLED_WriteCmd(0xA6); // 正常显示(非反显) OLED_WriteCmd(0xA8); // 设置多路复用比率 OLED_WriteCmd(0x3F); OLED_WriteCmd(0xA4); // 显示使用RAM内容 OLED_WriteCmd(0xD3); // 设置显示偏移 OLED_WriteCmd(0x00); OLED_WriteCmd(0xD5); // 设置时钟分频因子 OLED_WriteCmd(0x80); OLED_WriteCmd(0xD9); // 设置预充电周期 OLED_WriteCmd(0xF1); OLED_WriteCmd(0xDA); // 设置COM引脚硬件配置 OLED_WriteCmd(0x12); OLED_WriteCmd(0xDB); // 设置VCOMH电压 OLED_WriteCmd(0x30); OLED_WriteCmd(0x8D); // 设置电荷泵 OLED_WriteCmd(0x14); // 使能电荷泵 OLED_WriteCmd(0xAF); // 开启显示 }5.4 主循环逻辑与数据调度
主循环的逻辑比较简单,但有一个点要特别注意:DHT11的读取周期不能太短。我实测连续读取间隔低于1秒时,DHT11偶尔会无应答,这是传感器本身的限制。所以我在主循环里做了一个简单的调度,1秒采集一次,5秒在OLED上刷新一次,10秒通过ESP8266上报一次。
uint64_t lastReadTime = 0; uint64_t lastSendTime = 0; while(1) { if(HAL_GetTick() - lastReadTime >= 1000) { lastReadTime = HAL_GetTick(); if(DHT11_ReadData(&temp, &humi) == 0) { // 显示到OLED OLED_ShowTemperature(temp, humi); } else { OLED_ShowError(); } } if(HAL_GetTick() - lastSendTime >= 10000) { lastSendTime = HAL_GetTick(); ESP8266_SendData(temp, humi); } }6. ESP8266联网与数据上报
6.1 AT指令还是MQTT固件?我的选择
ESP8266的使用方案主要有两条路线:用出厂AT固件,或者刷NodeMCU固件做二次开发。我做这个项目选择的是AT固件方案,原因很简单:STM32主控已经很成熟,ESP8266只需要充当一个串口转WiFi的透明传输通道,用AT指令就能完成任务,不需要在ESP8266端写任何额外代码。
AT指令方案的经典流程:
AT // 测试模块是否正常 AT+CWMODE=1 // 设为Station模式 AT+CWJAP="SSID","密码" // 连接WiFi AT+CIPSTART="TCP","xxx.xxx.xxx.xxx",8080 // 建立TCP连接 AT+CIPSEND=数据长度 // 发送数据热词里有人搜“esp8266连接onenet失败”,我估计大概率是上报格式和OneNET要求的报文不匹配,或者接入鉴权参数没配置对。调试思路很简单:先用串口助手手动发AT指令测试连接,再用ESP8266的透传模式发送报文,一步步排查,别一上来就全链路联调。
6.2 STM32与ESP8266的串口通信实现
STM32的USART2用于和ESP8266通信,PA2是TX,PA3是RX。我使用HAL库的串口中断接收,配合一个接收缓冲区,这样可以处理ESP8266返回的异步消息。
这里常遇到的问题是:很多人直接用阻塞式HAL_UART_Transmit发送AT指令,然后用HAL_UART_Receive阻塞等待响应。但因为AT指令响应时间不固定,有些指令需要几秒钟才能返回,阻塞等待会把主循环卡死,DHT11的采样周期就会乱掉。我的做法是定义一组状态机。
typedef enum { ESP_STATE_IDLE, ESP_STATE_WAIT_OK, ESP_STATE_WAIT_DATA, } ESP_State; static uint8_t esp_rx_buffer[256]; static uint16_t esp_rx_len = 0; static volatile uint8_t esp_rx_ready = 0; void ESP8266_Init(void) { ESP8266_SendCommand("AT\r\n", "OK"); ESP8266_SendCommand("AT+CWMODE=1\r\n", "OK"); ESP8266_SendCommand("AT+CWJAP=\"YourSSID\",\"YourPassword\"\r\n", "OK"); } uint8_t ESP8266_SendCommand(char *cmd, char *expected) { // 清空接收缓冲区 esp_rx_len = 0; esp_rx_ready = 0; // 发送命令 HAL_UART_Transmit(&huart2, (uint8_t *)cmd, strlen(cmd), 1000); // 等待期望的响应 uint32_t timeout = HAL_GetTick() + 3000; while(HAL_GetTick() < timeout) { if(esp_rx_ready) { if(strstr((char *)esp_rx_buffer, expected) != NULL) { return 1; // 收到期望响应 } // 重新等待,因为AT响应可能分多条消息到达 esp_rx_len = 0; esp_rx_ready = 0; } } return 0; }还有一个优化过的技巧:把“AT+CIPSTART”的连接建立和“AT+CIPSEND”的发送逻辑做成一个独立的函数,这个函数先检测ESP8266是否已经连接,没有连接先建立连接,连接存在就直接发送,避免每次上报都重复建连。TCP连接的建立是有开销的,频繁建立释放不仅慢,还可能导致服务端资源泄漏。
6.3 数据上报的封装格式设计
上报数据格式我设计的是简单的JSON,因为接收端解析方便,也方便对接后续的云平台:
void ESP8266_ReportData(float temp, float humi) { char payload[128]; snprintf(payload, sizeof(payload), "{\"device_id\":\"dev_001\",\"temp\":%.1f,\"humi\":%.1f}\r\n", temp, humi); char cmd[160]; snprintf(cmd, sizeof(cmd), "AT+CIPSEND=%d\r\n", strlen(payload)); ESP8266_SendCommand(cmd, "OK"); HAL_UART_Transmit(&huart2, (uint8_t *)payload, strlen(payload), 1000); ESP8266_SendCommand("AT+CIPCLOSE\r\n", "OK"); }接收端如果只是验证用,可以自己在电脑上开一个TCP Server,或者用Node-RED、MQTT Broker之类更完整的方案。如果不想自己搭服务器,也可以直接把数据推到巴法云、点灯科技这类的物联网平台,他们有现成的App和接口,改造成本极低。
7. 系统联调与问题排查实录
7.1 上电后OLED不亮的排查
这是我自己做的时候第一次上电遇到的问题。检查思路按顺序走一遍:
- 万用表量OLED的VCC和GND之间的电压是否为3.3V
- 检查SCL和SDA是否接反(经常把PB6/PB7接反,屏幕就完全不亮)
- 检查I2C地址是否正确,SSD1306的7位地址通常是0x3C,左移一位后就是0x78。我之前用过一块地址是0x3D的屏,刚开始一直没显示,后来才发现地址不对
如果这些都查过还是不行,用逻辑分析仪抓I2C波形,看有没有ACK信号。如果没有ACK,大概率是屏的I2C地址不对或者接线问题。
7.2 DHT11读取失败的几种典型情况
DHT11读取失败的表现通常是返回1(无应答)或者返回2(校验错误)。
无应答的排查重点:
- 上拉电阻是否漏焊,DHT11数据线是开漏的,没上拉肯定不行
- 起始信号拉低时间是否足够,我程序里是20ms,够用
- 切换到输入模式的时机是否对,释放总线后要等传感器应答
校验错误的排查重点:
- 时序延时是否准确。空循环延时和主频强相关,确认系统主频是72MHz而不是8MHz
- 传感器距离MCU的线过长,我试过超过20cm的杜邦线就容易读到错误数据,改用短线和双绞方式有改善
- DHT11本身可能损坏,换一块试一下
还有一个很多人忽略的点:CubeMX生成的GPIO初始化默认是推挽输出,但DHT11的时序要求总线能被外部拉低,如果保持推挽输出会导致电平冲突。所以CubeMX里要把PB6配成开漏输出,我上面代码里在切模式时也做了处理。
7.3 ESP8266连接不上WiFi的排查思路
如果你遇到ESP8266连不上WiFi,不要急着怀疑模块坏了,按顺序排查:
- 确认供电。用万用表测模块VCC引脚在连接WiFi瞬间的电压,如果低于3.3V,就是供电不足,换独立电源
- 确认SSID和密码没错。有人密码里带特殊字符,AT指令里需要转义处理
- 确认路由器是2.4G频段。ESP8266不支持5G WiFi,这个坑很常见
- 确认路由器DHCP正常工作,或者手动设置静态IP
连接成功后还需要确认一件事:路由器是否做了客户端隔离。有些路由器开了“AP隔离”,设备之间无法通信,这种情况下ESP8266能连接路由器但上不了网、也访问不了局域网服务器。
7.4 常见问题速查表
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| OLED不亮 | I2C地址错误或接错线 | 检查地址(0x3C/0x3D),对照接线表检查 |
| OLED花屏 | 电源纹波过大或I2C速率过高 | 加退耦电容,速率降到100KHz |
| DHT11无应答 | 上拉缺失或供电异常 | 加4.7k上拉,确认VCC电压 |
| DHT11校验错误 | 时序不准或线路过长 | 检查主频时钟配置,缩短传感器线 |
| ESP8266不断重启 | 供电不足 | 独立供电,确保峰值电流满足 |
| ESP8266连不上WiFi | 5G频段或AP隔离 | 切到2.4G频段,关闭AP隔离 |
| ESP8266发送乱码 | 波特率不匹配 | 确认STM32与ESP8266通信波特率一致 |
8. 从原型到可用产品的扩展方向
8.1 功能增强与传感器升级
这套系统跑通之后,可以往几个方向扩展。
传感器升级是最容易的一步。DHT11换成DHT22,精度从±2°C提升到±0.5°C,湿度的精度提升也明显。代码改动只需要在数值处理上稍微调整,时序部分和工作流程基本不用动。换成SHT30则走I2C接口,和OLED挂了同一个总线,连GPIO都不用多占用。
也可以增加光照传感器BH1750或者土壤湿度传感器,扩展成一个完整的环境监测节点。STM32的外设资源足够,加传感器只是增加I2C设备地址和读取逻辑的事。
8.2 数据可视化与云端接入
当前的方案只是把数据上报到TCP Server,数据展示还需要自己处理。如果想让手机随时看数据,有几个成熟的方案:
- 接巴法云或点灯科技,直接用他们提供的App,做一个可视化界面,操作简便,适合快速验证
- 自建EMQX等MQTT Broker,用ESP8266的MQTT库直接发布消息,Node-RED订阅数据并展示到Dashboard
- 使用OneNET或阿里云物联网平台,功能强大,支持数据存储和告警规则,适合做正式产品
做可视化的时候有几个经验:数据存储要保留时间和设备ID,这样方便后续查询和分析。告警规则不要只设置阈值,建议增加“持续时间”判断,比如连续5分钟超过温度上限才告警,避免瞬时波动引起误报。
8.3 低功耗优化方向
如果你想做电池供电版本,这套系统有几个明显的功耗大户,可以针对性优化:
- DHT11可以做到1次/分钟采样,中间间隔完全不需要供电,可以用MOS管直接切断电源
- OLED在不需要刷新时进入睡眠模式,功耗能降很多
- ESP8266用Modem-sleep或者DTIM模式,或者改成定时唤醒上报,功耗可以再降一到两个数量级
实测过,如果不做任何功耗优化,这个系统的工作电流大约在80mA左右,做电池供电续航很差。做了定时唤醒和深度睡眠之后,平均电流可以降到几十mA甚至更低,看具体的上报频率。
8.4 我在这类项目里反复踩过的坑
做这个项目前后其实花了不少时间,回头总结几个比较典型的坑。
第一个坑是供电问题。一开始直接用STM32核心板的3.3V给ESP8266供电,结果模块反复重启,调试信息乱飞。后来测量才发现WiFi发射的瞬间电压跌到了2.8V。从那以后我凡是带ESP8266的项目,都单独给它设计供电,这已经成了我的习惯。
第二个坑是DHT11的时序问题。我用过多种延时方案,包括DWT精确延时、定时器延时、空循环延时。空循环虽然简单,但一旦开了编译器的优化选项,延时时间就可能不对,导致校验错误。后来我干脆在DHT11读取的代码文件里对延时函数做了常规优化限制,再没出过时序问题。
第三个坑是调试手段。刚开始我疯狂加串口打印,每个函数都打日志,结果日志太乱根本找不到重点。后来我养成了一个习惯:先确认硬件供电和波形,再用逻辑分析仪看时序,最后才看软件逻辑。硬件不过关的时候,软件怎么调都没用。