最近在做一个小型环境监测项目,打算把办公室角落里的温湿度数据实时传到手机上看。选型时几乎没犹豫就定了STM32+ESP8266+DHT11+OLED这套组合:主控用STM32F103C8T6,通信交给ESP8266,传感器用DHT11,显示用0.96寸OLED。这套方案在嵌入式圈子里算是经典得不能再经典的入门搭配,网上资料多、踩坑记录全、成本压到五十块以内,关键是做完之后你能真正理解一颗MCU是怎么把传感器数据变成网络数据的。
这篇文章就围绕这套系统,从硬件连接、时序驱动、数据上云到问题排查,把我实际做过的完整流程和踩过的坑都写出来。适合刚学完STM32基础、想做一个完整物联网小项目的朋友参考,也适合想快速验证ESP8266模块用法的同学直接抄作业。
1. 这套方案到底在做什么
在做任何一个项目之前,先把需求想清楚比什么都重要。这个项目的本质需求其实只有一句话:把传感器采集到的温湿度数据,通过无线网络发送到远程端,同时在本地设备上显示出来。听起来简单,但拆开之后就涉及四个关键模块的协作,每个模块的职责都比较清晰。
1.1 四颗芯片的分工
先看看这套系统里的每个角色都在干嘛。
STM32F103C8T6是整个系统的大脑,负责所有逻辑控制。它要干的事情包括:定时读取DHT11的数据、解析温湿度值、把数据发给OLED显示、再通过串口把数据塞给ESP8266模块。选择STM32而不是直接用ESP8266的GPIO去读DHT11,原因很简单:ESP8266的AT固件模式下,用户程序跑在MCU里,ESP8266只做透传,这样逻辑更清晰,出错也好排查。
ESP8266在这套方案里是"通信兵"。它本身也是一颗可以独立编程的芯片,但在大多数入门项目里,它跑的是官方AT固件,STM32通过串口向它发送AT指令就能控制它连接网络、建立TCP连接、发送数据。这种做法的好处是开发门槛非常低,你不需要去折腾ESP8266的SDK和编译环境,只要会串口收发就能上手。
DHT11是温湿度采集前端。它内部有一个电阻式湿度传感器和一个NTC热敏电阻,通过单总线协议把数据传给MCU。精度虽然一般(湿度±5%RH,温度±2℃),但对于环境监控这种场景已经够用。关键是它便宜、电路简单、驱动代码也不复杂,特别适合用来理解单总线通信协议。
OLED屏是本地显示终端。0.96寸的I2C接口OLED,分辨率128x64,功耗低、对比度高、显示效果好。它能实时显示当前温湿度和WiFi连接状态,这样调试的时候不用开着串口助手也能直观看到系统在干什么。
1.2 为什么选这套组合而不是其他方案
有人可能会问,既然ESP8266本身就能跑代码,为什么还要加一颗STM32?确实,用ESP8266直接读DHT11、驱动OLED、上报数据,技术上完全可行,而且成本更低、体积更小。但作为学习项目,STM32+ESP8266的组合有它的独特价值。
STM32是绝大多数嵌入式学习者入门的第一颗芯片,GPIO操作、定时器、串口中断、I2C这些基本功都要在STM32上练。这个项目把STM32的串口、GPIO、I2C都用上了,相当于一次综合实训。而ESP8266则承担了"连接互联网"这个新技能点,让你知道MCU是怎么通过AT指令和外部通信模块协作的。
另外从工程角度看,主控和通信模块分离也更符合真实产品的设计思路。很多工业设备就是工业级MCU加通信模块的组合,主控负责逻辑和可靠运行,通信模块只做数据管道,这样后期升级通信方案(比如换成4G模块)时,主控代码几乎不用大改。
至于传感器为什么不选DHT22或者SHT30,理由也简单:DHT11零门槛。它的单总线协议虽然时序要求严格,但GitHub上大把现成驱动可以参考,拿来学习正合适。等你把DHT11搞明白了,后面换DHT22、SHT30也就是改改驱动的事。
2. 硬件连接与初始化:先把地基打牢
这套系统的硬件连接不算复杂,但有几个细节处理不好就会让后面调试很痛苦。我做的第一版就因为在ESP8266的供电上偷懒,导致模块频繁重启,折腾了一个晚上才找到原因。
2.1 元器件清单与选型细节
核心器件清单如下:
- STM32F103C8T6最小系统板一个(蓝色板,带USB转串口)
- ESP8266-01S或ESP8266-12F模块一个
- DHT11温湿度传感器模块一个(买集成模块,别买裸片)
- 0.96寸OLED屏幕一块(I2C接口,4针的那种)
- 若干杜邦线、面包板
- 5V/3.3V电源(USB供电就行)
选型的时候有几个坑要提醒一下。ESP8266模块尽量选ESP-01S,不要选老版的ESP-01,因为老版ESP-01下载IO0和IO2没引出,调试不方便,而且01S改了天线匹配电路,信号更好。DHT11一定要买模块版而不是裸片,模块上集成了上拉电阻和去耦电容,直接插面包板就能用,裸片还要自己搭外围电路,麻烦且容易出错。OLED屏注意买I2C接口的(四针:VCC、GND、SCL、SDA),别买成SPI接口(七针)的,虽然SPI刷新更快,但接线多、驱动复杂,对这个项目来说I2C完全够了。
2.2 引脚分配与接线表
我的接线方式比较常规,都是市面上教程最常见的接法,方便你对照参考:
| 模块 | 引脚 | STM32引脚 |
|---|---|---|
| DHT11 | DATA | PA0 |
| DHT11 | VCC | 3.3V |
| DHT11 | GND | GND |
| OLED | SCL | PB6(I2C1_SCL) |
| OLED | SDA | PB7(I2C1_SDA) |
| OLED | VCC | 3.3V |
| OLED | GND | GND |
| ESP8266 | TX | PA10(USART1_RX) |
| ESP8266 | RX | PA9(USART1_TX) |
| ESP8266 | VCC | 3.3V |
| ESP8266 | GND | GND |
| ESP8266 | CH_PD(EN) | 3.3V |
这里有个关键点:ESP8266的RX接的是STM32的TX(PA9),TX接的是STM32的RX(PA10),这是交叉接法,串口通信的基本规矩。很多人第一次接的时候会把TX对TX、RX对RX,结果数据完全不通,这一点一定要记牢。
2.3 关于供电和电平的注意事项
ESP8266的供电是整个项目里最容易出问题的地方。这个模块在发射WiFi信号的瞬间电流会冲到300mA甚至更高,如果你用STM32板上那个AMS1117稳压器供电,电压会被瞬间拉低,模块就会重启。我踩过的坑就是这个问题,现象是ESP8266每隔几秒就重启一次,串口输出乱码。
解决办法有两个。第一,尽量用外部5V稳压模块单独给ESP8266供电,比如用一个LM2596降压模块或者直接找USB转TTL的5V输出脚供电。第二,如果你一定要用3.3V供电,那就在ESP8266的VCC和GND之间并一个大电容,比如470uF电解电容,用来缓冲电流尖峰。实测加了电容之后,模块稳定性明显提升。
关于电平匹配也要说一句。STM32的串口是3.3V逻辑电平,ESP8266也是3.3V逻辑电平,两者直接相连没问题。但如果你的USB转TTL工具是5V的,千万别直接用它给ESP8266下载固件,需要加电平转换电路,否则大概率烧模块。我在调试初期就烧坏过一个ESP-01,血泪教训。
OLED和DHT11接3.3V就行,这两个器件功耗很低,不会对供电造成压力。整个系统用电脑USB口供电绰绰有余。
3. DHT11与OLED的驱动细节
硬件接好之后,第一件事不是急着写ESP8266的通信代码,而是先把DHT11和OLED调通。这两个模块调通之后,系统就有了本地显示和数据采集能力,再往后接网络才不至于两眼一抹黑。
3.1 DHT11的通信时序拆解
DHT11用的是单总线协议,也就是说数据线只有一根,既做传输又做接收,而且时序要求非常严格。第一次写DHT11驱动的朋友通常会在这个地方卡住,因为不理解它那个"起始信号+应答+40bit数据"的完整过程。
完整通信过程分成四步。第一步,主机(STM32)把数据线拉低至少18ms,然后再拉高20-40us,这是起始信号,告诉DHT11"我要开始读数据了"。第二步,DHT11收到起始信号后会拉低数据线约80us作为应答,然后再拉高约80us,表示"我准备好了"。第三步,DHT11开始连续发送40bit数据,每一位数据都是以50us的低电平开始,然后高电平持续时间长短来表示0或1——高电平约26-28us表示0,高电平约70us表示1。第四步,数据发送完毕后,数据线被拉低50us,然后释放总线,完成一次通信。
这40bit数据是有结构的:前16位是湿度数据(整数+小数),中间16位是温度数据(整数+小数),最后8位是校验和。校验规则很简单:前四个字节相加,如果低8位等于校验字节,说明数据有效。
驱动代码里最容易出问题的就是位时间判断。在实际写代码时,我建议用定时器捕获而不是纯延时轮询来判断高电平持续时间,精度更高。如果非要用延时的方案,那就要把延时函数校准得非常准,否则在时序边缘就会误判0和1。
3.2 初始化DHT11的常见坑
DHT11的上电启动时间比较长。很多人在上电后立刻就去读数据,结果读回来的全是0xFF或者校验失败。这是因为DHT11上电后需要一个稳定过程,文档上写的是上电后要等1秒才能发送指令。
另外一个常见的错误是:每次读取DHT11的间隔不能太短。DHT11官方手册说读取周期最小间隔是1秒,实际体验下来,如果你的主循环里每100ms就去读一次,DHT11会越来越"迟钝",表现为读到的数据不变或者通信异常。我在代码里加了一个软件定时器,保证每次读取间隔至少1.5秒,之后数据稳定多了。
还有一点值得注意:DHT11的DATA引脚在STM32端要配置成开漏输出并外接上拉电阻。如果你买的模块已经带了上拉电阻,那直接用推挽输出也行。但如果用的是裸片接法,一定要加一个4.7k-10k的上拉电阻,否则数据线浮空,时序根本没法看。
3.3 OLED显示驱动的两种方案
OLED的驱动代码在这个项目里相对简单,因为它是I2C接口,底层通信协议由STM32的硬件I2C外设处理,你只需要关心显示逻辑。
有两个方案可选。第一个方案是用ST官方的HAL库加STM32CubeMX初始化I2C,然后移植一个开源的SSD1306驱动库。这些库通常提供了DrawPixel、DrawLine、DrawString等函数,往里面传入坐标和字符串就能在OLED上显示内容。HAL库的好处是:CubeMX帮你把I2C的时序配置、时钟使能、引脚复用全处理好了,你不用面对寄存器的细节,对于初学者很友好。
第二个方案是老工程师喜欢的寄存器配置法,直接用标准外设库或者HAL库底层的I2C寄存器操作来初始化SSD1306。这个方案代码更精简,执行效率更高,但理解难度也更高。对新手来说,我不建议一开始就啃寄存器,先用现成库把功能跑通,然后回头再看库函数底层的实现,学习路径更顺。
不管用哪种方式,OLED上要显示中文和英文字符集。SSD1306默认是英文字库,一个字符占6x8像素。如果要显示中文,你需要自己准备字模数组,把汉字取模后写入Flash,然后按坐标绘制。这个属于显示进阶玩法,本项目先显示英文和数据就行,中文可以放到后面的增强阶段。
4. 数据上云的实现思路
DHT11和OLED能正常工作之后,就到了这个项目最有意思的部分:让数据通过ESP8266传到网络上。这里我用的是最基础的AT指令方案,逻辑清晰,也方便你一步步看数据是怎么从STM32的串口流向WiFi再到达服务器的。
4.1 配置ESP8266连接网络
先把ESP8266模块通过USB转TTL接到电脑上,用串口助手发AT指令做初始化测试。模块上电后,第一件事是发送"AT"确认应答,如果返回"OK",说明固件正常工作。接下来依次发送以下指令:
AT+CWMODE=1设置WiFi模式为Station模式,也就是让ESP8266作为终端去连接路由器。
AT+CWJAP="你的WiFi名称","你的WiFi密码"连接指定的无线路由器。返回"WIFI CONNECTED"然后是"WIFI GOT IP",说明已成功获得IP地址。
AT+CIPSTART="TCP","192.168.1.100",8080建立一个到目标服务器的TCP连接。这里的IP地址和端口要改成你自己服务器的,如果只是本地测试,可以先连本机或者另一台电脑。
AT+CIPMODE=1 AT+CIPSEND开启透传模式并开始发送数据。透传模式下,STM32发给串口的所有数据都会原封不动地通过TCP发向服务器,非常方便。要退出透传模式,需要发送"+++"再等一秒。
这一套指令组合是整个ESP8266通信的基础。可以把它写成一个初始化函数放在STM32代码里,上电后按顺序执行,然后再进入主循环发送数据。
4.2 数据发送与平台接收
数据上报的逻辑是:STM32每1.5秒读一次DHT11,然后把温湿度格式化成一个字符串,比如"TEMP:25.0,HUM:60.0",通过串口发给ESP8266。ESP8266在透传模式下会把这个字符串直接推送到TCP服务器的对应端口。
如果你想让数据在网页上实时展示,最简单的办法是用Node.js写一个TCP服务,把收到的数据用WebSocket广播到浏览器页面。我当时的做法是用Python写了一个Flask服务,后台开一个TCP线程接收ESP8266的数据,然后通过SocketIO推送到前端页面,手机和电脑浏览器都能实时查看。
这里有个重要的工程细节:ESP8266透传模式下,偶尔会出现数据发送失败的情况。原因是TCP连接可能因为网络波动断开,但STM32并不知道。解决办法是在STM32和ESP8266的通信中加一个简单的应用层心跳协议。比如STM32每隔10秒向ESP8266发送"AT+PING"或者在应用层数据里加一个序号,如果连续几次没有收到服务器确认,就重新执行"AT+CIPSTART"建立新连接。
4.3 断线重连与异常处理
数据显示乱码、丢包、断线,这些都是我在实际调试中遇到过的问题。有个问题特别典型:ESP8266在透传模式下如果服务器主动断开TCP连接,模块会退出透传并输出"CLOSED"提示。这时候如果你没有处理这个状态,STM32还在继续往串口写数据,这些数据就全部丢失了。
我后来在代码里增加了状态机机制。STM32的串口接收中断里会实时检查ESP8266返回的内容,如果发现"CLOSED"或者"ERROR"字符串,就置一个标志位,主循环检测到标志位后,会重新执行初始化流程,重新连接TCP。这个改动虽然不大,但对系统稳定性的提升非常明显,实测可以做到断线后5秒内自动恢复。
还有一种情况是WiFi信号本身不稳定,导致ESP8266连接路由器失败。在程序初始化阶段,我设置了最多重试10次连接路由器的逻辑,每次失败后重启ESP8266模块(通过GPIO控制CH_PD引脚拉低再拉高),实测对信号不佳的场景有奇效。
5. 我的调试过程与踩坑记录
调试这个项目的过程中,我积累了不少经验。下面挑几个比较典型的故障场景来复盘,这些问题在搜索引擎里被问得非常多,如果你的系统也出了类似现象,可以对照排查。
5.1 ESP8266 AT指令不响应
故障现象是STM32发送AT指令,ESP8266毫无反应。排查步骤我建议按顺序来。首先确认模块有没有正常上电,用万用表量一下VCC引脚电压是否稳定在3.3V,电压低于3.0V基本就是供电不足。其次检查TX和RX接线有没有交叉,这是新手最容易犯的错误。然后确认USB转TTL工具的TXD要接ESP8266的RXD,RXD接ESP8266的TXD,共地也是必须的。
如果这些都正常但模块还是没反应,可能是模块进入了刷机模式。ESP8266的IO0引脚如果接地,上电后模块会进入下载模式而不是运行模式,此时就算发AT指令也不会有任何返回。解决办法是让IO0悬空或者接高电平,重新上电。
还有一个细节容易被忽略:ESP8266上电后需要大约2秒的启动时间,如果你在代码里上电后立刻发AT指令,大概率收不到回复。正确做法是上电后延时3秒以上,再开始发送AT指令。
5.2 DHT11数据全零的问题
我遇到过一次DHT11读回来的数据全部为0,温度0.0,湿度0.0,非常诡异。检查接线、换传感器都没有效果。
最后定位到问题是代码初始化顺序有误。DHT11的驱动代码要求在初始化时把数据线拉高至少1秒,让传感器进入稳定状态。而我在初始化函数里,配置完GPIO后就立刻开始读取,没有给传感器留出稳定时间。解决办法是在DHT11初始化函数里加上1000ms的延时,然后再执行第一次读取,问题就消失了。
如果你读回来的数据一直在固定值附近跳动而且精确到0,还有一个可能是IO口没有正确配置成开漏输出加外部上拉。DHT11的数据线在没有外部上拉的情况下,读回来的电平会异常,导致解析出的数据永远是0。检查一下你买的是模块版还是裸片,模块上的上拉电阻焊没焊。
5.3 OLED屏幕显示乱码
OLED显示乱码的原因通常有两个:I2C速率不对和地址配置错误。SSD1306的I2C地址一般是0x78(8位地址)或者0x3C(7位地址),不同厂家的模块地址可能不一样,我拿到的一块屏是0x3C,另一块就是0x3D。
还有一个容易踩的坑是I2C时钟速率。SSD1306支持的最大I2C时钟是400kHz,但有些国产屏的时序余量不足,在400kHz下会乱码或者不亮。解决办法是手动把I2C时钟降到100kHz,刷新速度会慢一点,但稳定才是第一位的。
最后补充一个经验级的建议:OLED显示屏非常怕静电和带电插拔。在调试过程中,如果你频繁插拔OLED的杜邦线,很容易把屏幕驱动芯片击穿,导致屏幕永久性花屏或者完全不亮。我烧坏过一块屏之后,就再也不敢带电插拔了,实验时都是先断电再改线。
6. 进阶方向建议
整套系统跑通之后,你会对嵌入式物联网项目的完整链路有一个非常清晰的认识。如果觉得这个版本太基础、想继续深挖,这里有几个方向可以参考。
6.1 从查询模式改成中断或定时唤醒
当前版本的STM32主循环一直在轮询DHT11并上报数据,这在教学场景下没问题,但真实产品不会这么干。可以考虑用STM32的低功耗模式加RTC唤醒,每隔5分钟醒来一次,读取传感器并上报,然后又睡回去。这样电池供电的话,续航可以从几小时提升到几个月。
6.2 接入MQTT实现手机远程查看
把ESP8266从TCP透传模式切换成MQTT模式,对接上海量的免费MQTT服务器,然后直接用手机上的MQTT调试工具订阅对应主题,就能随时随地查看温湿度。MQTT协议相比裸TCP的好处是:有主题订阅机制、有QoS消息可靠等级、天然支持多端接收。实现起来就是在ESP8266上跑MQTT客户端,用AT指令或者直接刷入MQTT固件,代码量也不大。
6.3 传感器替换与多节点扩展
DHT11的精度和响应速度在工业场景下确实不够看。你可以把传感器替换成SHT30或者DHT22,驱动代码做一下适配就好。如果你有多个房间要监控,还可以用ESP8266做多节点采集,每个节点带一个传感器,通过路由器统一汇聚到服务器。节点之间做时间同步和数据格式统一,是很好的分布式系统练手项目。
我个人在实际调试这套系统时,最大的感受是:越简单的模块越容易在细节上翻车。DHT11的时序、ESP8266的供电、OLED的I2C地址,任何一个地方出问题,都会让你觉得代码写得明明没问题却跑不通。所以我的习惯是每接入一个模块就单独测试一个模块,每跑通一个功能就保存一版代码,不要一口气把所有代码写完再上电调试,那样出了问题就很难定位了。