
自己动手做一套无线温湿度监控听起来是个不少教程都讲过的东西但真正把 STM32、DHT11、OLED 和 ESP8266 这四样凑到一起稳定跑起来中间坑其实不少。这篇文章我想把整套方案的来龙去脉、硬件接线、代码实现和排查思路重新捋一遍重点是把我自己做项目时踩过的坑和验证过的做法写出来给正在做课程设计、毕业设计或者单纯想用 STM32 入门物联网的朋友一个可以直接参考的完整版本。这套组合能干什么简单说就是STM32 通过单总线协议读取 DHT11 温湿度传感器数据一屏显示在 OLED 上另一路通过 ESP8266 模块连上家里路由器把数据定时推到服务器或者手机端。硬件成本不高核心芯片加起来几十块钱但覆盖的知识点非常典型单总线时序、I2C 驱动 OLED、串口 AT 指令控制 WiFi 模块再加上 STM32 的 GPIO 模拟、定时器、中断和低功耗思路几乎把嵌入式开发最常用的技能都串起来了。如果你刚刚学完 STM32 的基础外设想知道怎么把几个模块组合成一个完整系统这篇文章会帮你把从硬件选型到软件架构的每一环都拆开讲清楚。已经做过类似项目的朋友也可以重点看第五章的故障排查记录很多问题网上翻半天都找不到明确答案我在这里给的是实测结论。1. 项目拆解四个器件各司其职先搞清楚系统怎么搭1.1 系统整体架构与数据流向先别急着写代码第一步是把系统分成几个清晰的层次。这套温湿度监控系统本质上是一个“采集—处理—显示—上传”的四级结构。DHT11 负责采集环境温湿度它把物理量转换成数字信号通过单总线协议传给 STM32。STM32 是大脑负责按照 DHT11 的时序要求发起读取、解析 40 位数据、做校验然后把这些数据整理成适合展示和传输的格式。OLED 是本地显示终端STM32 通过 I2C 总线把温湿度数值画到屏幕上。ESP8266 则是无线通道STM32 通过串口给 ESP8266 发 AT 指令让它连接 WiFi 热点、建立 TCP 连接、进入透传模式然后把数据发送出去。整个过程的数据流非常清晰DHT11 - STM32 - OLED/ESP8266。这样的分工方式最大的好处是模块之间耦合度低每一部分都能单独调试。在系统设计时我建议刻意保持这种“每层职责单一”的思路。很多新手容易犯的错是让 STM32 一边读传感器一边等 WiFi 响应结果传感器读取时序被串口阻塞打断DHT11 数据经常读失败。正确做法是传感器读取放在定时中断或者主循环的固定时隙里串口发送只负责把已经组好的数据帧丢出去不关心 WiFi 模块那边什么时候回复。1.2 为什么选这四颗芯片而不是 Arduino 或者树莓派这套方案里最核心的选型决策是“为什么用 STM32 做主控”。如果你只是想快速看到结果Arduino 加 DHT11 库加 ESP8266 库半小时就能跑通。但用 STM32 的价值在于所有协议都得自己写DHT11 的时序得自己抠OLED 的驱动得自己调ESP8266 的 AT 指令状态机得自己理。这个“从零写一遍”的过程才是把单片机、通信协议和嵌入式系统设计真正学明白的路径。尤其是对于课程设计和毕业设计答辩时老师问到底层原理用 Arduino 封装库的人往往答不上来但自己写过 STM32 驱动的人每一个时序参数都能讲得清清楚楚。再说无线方案的选择。能实现无线数据传输的方式很多蓝牙、LoRa、Zigbee、WiFi 各有优劣。蓝牙适合短距离、低功耗但没法直接接入互联网LoRa 传得远但需要网关成本高Zigbee 组网能力强但开发门槛高。ESP8266 之所以成为首选是因为它自带完整的 WiFi 协议栈价格便宜而且模块里已经烧好了 AT 固件STM32 只需要通过串口发送文本指令就能让它连网。这意味着你不需要在 STM32 上移植任何 TCP/IP 协议栈硬件资源和开发难度都大幅降低。传感器选 DHT11 而不是 DHT22、SHT30 或者 BME280主要原因是它在课程设计和入门项目中性价比最高。DHT11 的精度在正负 2 摄氏度以内湿度精度正负 5% RH对于环境监控、机房告警、粮仓监测这类场景完全够用。DHT22 精度更高但价格翻倍SHT30 走 I2C 接口更规范但需要焊接和更复杂的驱动。DHT11 用单总线协议一条线既供电又传数据虽然时序要求苛刻但反而能让你把 GPIO 模拟时序练扎实。OLED 选 0.96 寸 I2C 接口的 SSD1306 方案也是出于同样的考虑I2C 只需要两根线比 SPI 版本少两根控制线驱动代码也更简洁。2. 硬件接线材料清单、引脚连接和三个容易翻车的细节2.1 材料清单与选型建议在开始接线之前先盘点一下需要准备的材料。我这里列的是我自己测试过的配置每一件都有它的用途不建议随意替换。器件型号数量用途主控开发板STM32F103C8T6 最小系统板1系统主控处理数据和外设控制温湿度传感器DHT11 模块蓝色 PCB 款1采集环境温湿度无线模块ESP8266 ESP-01S 或 ESP-12F1连接 WiFi实现数据上传显示屏0.96 寸 OLEDSSD1306I2C 接口1本地显示温湿度USB 转 TTLCH340 或 CP2102 模块1烧录 STM32、调试 ESP8266下载器ST-Link V21STM32 程序下载与仿真如果用串口下载可以省掉但强烈不建议省杜邦线母对母、公对母若干若干模块间连接面包板830 孔面包板1快速搭电路免焊接电阻4.7k 或 10k 上拉电阻1DHT11 数据线上拉关于开发板的选择STM32F103C8T6 这颗芯片是目前学习资料最丰富、性价比最高的选择淘宝上十几块钱到二十块钱一片板载 LED、按钮、USB 接口和外置 8MHz 晶振对这个小项目来说资源绰绰有余。ESP8266 模块我推荐 ESP-12F 封装因为 ESP-01S 只有两个 GPIO 且天线设计一般ESP-12F 信号更强引脚也更好接线。你手里的模块如果是 ESP-01S 也没关系本项目的接线方式完全一样只用到电源、地、TX、RX 四个引脚。OLED 屏一定要确认接口是 I2C 还是 SPI。I2C 版本的 OLED 背面只有四个引脚VCC、GND、SCL、SDASPI 版本通常有七个引脚。如果买错了驱动代码要全部重写。买回来先看背面的丝印写着 SCL/SDA 的是 I2C 版本写着 D0/D1/DC/RES/CS 的是 SPI 版本。本教程默认 I2C 版本。2.2 完整接线对照表接线是整个项目里最容易出问题的环节接错一根线轻则显示不正常重则烧毁模块。我先把完整接线表列出来每一根线都说明原因。STM32 引脚连接目标说明3.3VOLED VCC、DHT11 VCCOLED 和 DHT11 均使用 3.3V 供电GNDOLED GND、DHT11 GND、ESP8266 GND所有模块共地这是必须的PA0DHT11 DATA单总线数据线需要外接 4.7k-10k 上拉电阻到 3.3VPB6OLED SCLI2C 时钟线PB7OLED SDAI2C 数据线PA9USART1_TXESP8266 RXSTM32 发送数据给 ESP8266PA10USART1_RXESP8266 TX接收 ESP8266 的响应数据5V 或 3.3VESP8266 VCC见下方供电注意事项不要直接接 STM32 板载 3.3V 引脚关于 DHT11 的上拉电阻这里要特别强调一下。DHT11 的通信协议是基于单总线的总线上所有设备默认都应该是高电平只有设备主动拉低才能驱动总线。所以 DHT11 的 DATA 引脚必须接一个上拉电阻到 VCC。很多成品 DHT11 模块上已经集成了这个电阻你拿到模块后如果发现背面有 103 或者 104 的贴片电阻就不用再外接。但如果你用的是传感器裸件而不是模块就必须自己接上拉电阻否则数据线浮空读取结果会完全不可靠。2.3 三个特别容易翻车的硬件细节第一个细节是 ESP8266 的供电。ESP8266 在 WiFi 发射瞬间电流会冲到 300 到 400 毫安STM32F103 最小系统板板载的 AMS1117-3.3 稳压芯片往往只有 300mA 左右的输出能力如果直接把 ESP8266 接到开发板的 3.3V 引脚上WiFi 连接瞬间电压会被拉低导致模块不停重启。我最早做这个项目时就被这个问题折磨了两天现象是 ESP8266 上电后串口打印一堆乱码AT 指令时好时坏。解决办法有三个一是用单独的 5V 电源给 ESP8266 模块供电绝大多数 ESP8266 模块上带板载稳压芯片可以直接输入 5V二是用一个输出电流在 1A 以上的 AMS1117 模块独立给 ESP8266 供电三是用充电宝取电充电宝的 USB 输出能力普遍在 1A 以上实测非常稳定。如果你用的是 ESP-01S 这种不带稳压芯片的模块切记只能输入 3.3V但也要保证电流够大。第二个细节是电平匹配。STM32F103 和 ESP8266 都是 3.3V 逻辑电平所以它们之间的串口连接不需要电平转换。但如果你手头用的是 5V 的 Arduino 或者 5V 的 STM32 开发板就必须加电平转换电路或者用电阻分压把 TX 降到 3.3V否则 ESP8266 的 RX 引脚会长期过压轻则通信不稳定重则烧毁模块。本项目用的 STM32F103C8T6 是 3.3V 供电天然匹配可以放心直连。第三个细节是 DHT11 供电的选择。DHT11 官方手册给出的工作电压是 3.3V 到 5.5V但实测在 3.3V 供电时传感器的精度和响应速度会受到轻微影响。如果你的系统里没有 5V 电源直接用 3.3V 供电也能正常读取数据这在绝大多数应用场景下是可以接受的。如果系统里有 5V给 DHT11 模块供 5V 会更好但这时 DHT11 的数据线输出高电平是 5V对于 3.3V 容限的 STM32 来说存在过压风险需要串联电阻或加电平转换。所以我对大多数人的建议是既然项目里 ESP8266 可能需要单独的 3.3V 电源DHT11 也用 3.3V 供电统一电压域简化设计。3. 软件环境开发工具链与 STM32CubeMX 配置3.1 开发环境搭建Keil、CubeMX、驱动一次配齐写代码之前先要把开发环境搭利索。STM32 开发的主流组合是 STM32CubeMX 加 Keil MDK 加 ST-Link。CubeMX 负责图形化配置引脚和时钟自动生成初始化代码Keil 负责编写业务逻辑和编译下载。这套流程早些年还是手动写寄存器现在用 HAL 库加 CubeMX开发效率高很多而且生成的代码结构规范非常符合课程设计和工程化习惯。安装环境时有几个细节注意一下。STM32CubeMX 安装时需要选择芯片型号课程设计常用的是 STM32F103C8 系列直接在 Part Number 搜索框里输入 STM32F103C8 就能找到。Keil MDK 安装时要注意 Keil 5 默认不包含 STM32 的器件支持包需要单独下载安装 STMicroelectronics 公司的 Device Family Pack。下载器方面ST-Link V2 需要安装 ST-Link 驱动装好后电脑上会出现一个虚拟串口。这里我想专门说一个很多人会遇到的怪现象设备管理器里 ST-Link 的 Virtual COM Port 显示黄色感叹号设备状态提示“设备无法启动代码 10”。这个问题的根源是 ST-Link V2 固件版本太老和电脑系统不兼容。解决方法是下载 ST-Link 固件升级工具 ST-LinkUpgrade用 USB 连接 ST-Link软件里会自动检测到设备点击 Upgrade 升级到最新固件然后拔插一下黄色感叹号就消失了。这个坑我自己踩过也帮别人排查过十有八九是固件问题不是驱动问题。如果升级固件还是不行再考虑换一根数据线试试有些劣质 USB 线只能充电不能传数据也会导致设备识别异常。3.2 CubeMX 工程配置时钟、GPIO、I2C、串口一次配清楚打开 STM32CubeMX 新建工程选择 STM32F103C8T6 后第一件事是配置时钟。项目里 STM32 需要跑 72MHz 主频才能保证微秒级延时准确这也是 F103 的最高主频。在 Clock Configuration 页面里把 HSE外部高速时钟设置为 Crystal/Ceramic Resonator外部晶振频率填 8MHz然后在 HCLK 输入框里填入 72按回车软件会自动算出合适的 PLL 配置。F103 的 APB1 总线最高只有 36MHz这个限制对 I2C 外设很关键OLED 的 I2C 时钟挂在 APB1 上如果 APB1 超过 36MHz外设可能工作异常。然后是引脚配置。在芯片图上点击需要使用的引脚依次配置。引脚配置类型参数设置用途PA0GPIO_OutputOutput Open Drain, 初始为 HighDHT11 数据引脚PB6I2C1_SCLI2C 标准模式或快速模式OLED 时钟PB7I2C1_SDAI2C 标准模式或快速模式OLED 数据PA9USART1_TX异步模式115200-8-N-1ESP8266 通信PA10USART1_RX异步模式115200-8-N-1ESP8266 通信PA0 作为 DHT11 数据引脚的配置要特别说明。我在项目里用的是开漏输出模式这个模式的特点是当写 0 时引脚拉低写 1 时引脚呈高阻态相当于释放总线。这个特性非常适合单总线协议因为 DHT11 也要拉低总线来表示响应信号如果 STM32 用推挽输出当 DHT11 也在驱动总线时可能发生两个输出互相短路的隐患。开漏输出再配合外部上拉电阻一来实现了电平的线与逻辑二来在切换读取方向时只需要改 GPIOMODER 寄存器为输入模式不用改变输出数据寄存器代码更简洁。I2C 的速率建议选标准模式100kHz因为 OLED 的 SSD1306 控制器虽然支持 400kHz 快速模式但 STM32 的硬件 I2C 和 OLED 屏之间存在兼容性问题尤其是线比较长或者用了面包板的时候快速模式容易随机出错。100kHz 下传输一帧温度数据也就几毫秒完全感觉不到延迟。USART1 配置成 115200 波特率8 位数据无校验1 位停止位这是 ESP8266 AT 固件的默认参数。注意在 CubeMX 的 USART1 配置里把 NVIC 中断打开因为后续我们要用中断方式接收 ESP8266 的响应数据如果不开中断主循环轮询 RXNE 标志位会严重拖慢系统响应。3.3 HAL 库工程的几个常用技巧配置完成后点击 Project Manager 设置工程名和路径工具链选 MDK-ARM V5Code Generator 里勾选“Generate peripheral initialization as a pair of .c/.h files”这样每个外设都会生成独立的源文件和头文件代码结构清晰查问题的时候方便定位。生成代码后打开 Keil 工程有几个地方建议在写业务代码之前先做调整。一是把 HAL_Delay 函数验证一下点击魔术棒按钮在 Target 标签页里可以看到 Xtal 填的是 8MHz如果你在 CubeMX 里配置的时钟是 8MHz 外部晶振这个值不用动。二是全局宏定义里确认 USE_HAL_DRIVER 和 STM32F103xB 已经被定义如果没有编译会报一堆未定义错误。三是把编译器的优化等级改为 Level 0因为 DHT11 时序对延时函数精度要求高编译器优化可能导致延时循环的时间发生变化。这个问题在新手阶段特别隐蔽代码逻辑完全一样打开优化后传感器就读不出数据其实是优化把某些变量直接清掉了循环次数和寄存器操作都变了。4. 代码实现DHT11 时序、OLED 驱动、ESP8266 联网上传4.1 DHT11 单总线通信协议与底层驱动DHT11 的通信协议是这套系统里最考验基本功的部分。它使用单总线主机和传感器之间的每次完整通信分为两个阶段主机发起起始信号传感器响应并返回 40 位数据。这 40 位数据里高 16 位是湿度接下来 16 位是温度最后 8 位是校验和。每个参数的高 8 位是整数部分低 8 位是小数部分DHT11 的小数位恒为 0所以只用整数部分也完全够用。具体的时序是这样总线空闲时保持高电平。主机想要读取数据先把总线拉低至少 18 毫秒再释放总线并拉高 20 到 40 微秒。DHT11 检测到这个起始信号后会先把总线拉低 80 微秒表示响应然后把总线拉高 80 微秒紧接着开始发送数据位。每一位的传输都以 50 微秒的低电平开始然后是高电平高电平持续 26 到 28 微秒表示二进制 0持续 70 微秒表示二进制 1。所以读取每一位的关键就是测量高电平的持续时间超过 40 微秒判为 1否则判为 0。这个协议对时序的要求非常严格尤其是 18 毫秒的低电平时间必须够长很多新手读不出数据就是因为起始信号只有几毫秒传感器根本没有识别到。反过来如果低电平时间太长超过 30 毫秒也可能导致通信失败。官方手册建议的是 18 到 30 毫秒我在驱动里固定用 18 毫秒实测非常稳定。微秒级延时的精度是整个驱动能不能跑起来的关键。HAL 库自带的 HAL_Delay 只有毫秒级精度没法用于微秒延时。我的做法是用内核调试单元 DWT 实现微秒延时。DWT 有一个 32 位周期计数器频率和 CPU 主频一致72MHz 主频下每个计数代表 1/72 微秒。实现方法如下#include core_cm3.h static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void DWT_Delay_us(uint32_t us) { uint32_t startTick DWT-CYCCNT; uint32_t delayTicks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - startTick) delayTicks); }这个延时函数的原理很简单记录当前计数计算需要等待的周期数然后循环等待。这里有一个细节要注意DWT 计数器是 32 位的72MHz 下大约 59 秒才回绕一次我们的延时最多几十毫秒根本不会遇到回绕问题所以可以放心用。DHT11 的完整读取驱动我放在下面代码里的每一步都和协议时序对应建议对着协议看代码理解每一行为什么这么写。为了更好的稳定性我把 PA0 配置成开漏输出配合外部上拉电阻所以代码里输出高电平时总线是被上拉拉高的输出低电平时总线被拉低切换输入模式时总线仍被上拉逻辑非常自然。#include dht11.h #include main.h #define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_0 static void DHT11_Pin_Output(void) { 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_HIGH; HAL_GPIO_Init(DHT11_PORT, GPIO_InitStruct); } static void DHT11_Pin_Input(void) { 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); } uint8_t DHT11_Read_Data(uint8_t *humi, uint8_t *temp) { uint8_t data[5] {0}; uint8_t i, j; uint32_t timeout; DHT11_Pin_Output(); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); DWT_Delay_us(18000); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); DWT_Delay_us(30); DHT11_Pin_Input(); timeout 100; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (--timeout 0) return 1; } timeout 100; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 2; } timeout 100; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (--timeout 0) return 2; } for (i 0; i 5; i) { for (j 0; j 8; j) { timeout 100; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (--timeout 0) return 3; } DWT_Delay_us(40); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { data[i] | (0x80 j); } timeout 100; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (--timeout 0) return 3; } } } if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) { return 4; } *humi data[0]; *temp data[2]; return 0; }代码里读取数据位的逻辑是在每一位开始的 50 微秒低电平后延时 40 微秒再读引脚电平。如果此时引脚仍然为高说明这是一个高电平持续了至少 40 微秒的位也就是二进制 1。如果已经变成低电平说明高电平只持续了 26 到 28 微秒是二进制 0。这个判断方法的精度依赖 DWT_Delay_us 的准确性所以前面强调系统时钟必须正确配置为 72MHz。我在调试时发现如果有人不小心把 HSE 外部晶振配置成了 HSIRD 内部振荡器主频只有 8MHzDWT_Delay_us(40) 实际延时变成大约 360 微秒那结果是位数错位读出来全是 255根本没法用。4.2 OLED SSD1306 驱动与中文字库显示OLED 屏的驱动相比 DHT11 要友好得多SSD1306 控制器内置了 128x64 的显存MCU 只要通过 I2C 把显存数据传给它屏幕就会自动显示。整个过程分为三步初始化控制器、写命令、写显存数据。I2C 地址一般是 0x3C如果屏幕上电后无反应可以检查地址是不是 0x3D方法是在初始化代码里换一个地址试试。写命令和写数据的 I2C 数据包格式不同。SSD1306 规定I2C 通信的第一个字节是控制字节0x00 表示后面的字节都是命令0x40 表示后面的字节都是显存数据。对应的 HAL 库函数如下static void OLED_Write_Cmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, 0x3C 1, buf, 2, 100); } static void OLED_Write_Data(uint8_t data) { uint8_t buf[2] {0x40, data}; HAL_I2C_Master_Transmit(hi2c1, 0x3C 1, buf, 2, 100); }初始化序列是整个 OLED 驱动里最容易被忽略的部分。SSD1306 出厂后内部寄存器状态不固定必须发送一串初始化命令才能正常显示。下面是实测可用的初始化序列注释里标了每条命令的作用void OLED_Init(void) { HAL_Delay(100); OLED_Write_Cmd(0xAE); // 关闭显示 OLED_Write_Cmd(0xD5); // 设置时钟分频因子 OLED_Write_Cmd(0x80); OLED_Write_Cmd(0xA8); // 设置 multiplex 比率 OLED_Write_Cmd(0x3F); OLED_Write_Cmd(0xD3); // 设置显示偏移 OLED_Write_Cmd(0x00); OLED_Write_Cmd(0x40); // 设置显示起始行 OLED_Write_Cmd(0x8D); // 电荷泵开关 OLED_Write_Cmd(0x14); OLED_Write_Cmd(0x20); // 设置内存寻址模式 OLED_Write_Cmd(0x00); // 水平寻址模式 OLED_Write_Cmd(0xA1); // 段重映射 OLED_Write_Cmd(0xC8); // 扫描方向 OLED_Write_Cmd(0xDA); // 设置 COM 引脚硬件配置 OLED_Write_Cmd(0x12); OLED_Write_Cmd(0x81); // 设置对比度 OLED_Write_Cmd(0xCF); OLED_Write_Cmd(0xD9); // 设置预充电周期 OLED_Write_Cmd(0xF1); OLED_Write_Cmd(0xDB); // 设置 VCOMH 电压倍率 OLED_Write_Cmd(0x40); OLED_Write_Cmd(0xA4); // 全局显示开启 OLED_Write_Cmd(0xA6); // 正常显示非反显 OLED_Write_Cmd(0xAF); // 打开显示 OLED_Clear(); }有不少人反映 OLED 上电后白屏其实不是硬件坏了而是初始化命令没有发全或者漏了 0x8D 0x14 这条电荷泵开启命令。SSD1306 内部的 DC-DC 电荷泵默认是关闭的如果不打开它屏幕完全没有驱动电压即使后面所有命令都正确也只会白屏。这个点特别容易踩我帮人排查时十次里有八次都是这个原因。OLED 显存是按页组织的。128x64 的屏幕分为 8 页每页 8 个像素高、128 个像素宽。写入显存时先通过命令设置当前列地址和页地址然后连续写数据每写一个字节就填充纵向 8 个像素。所以如果你要显示一个汉字实际上是把汉字拆成 16x16 的点阵按页分成上下两半每半 2 行字节分别写入显存。字模可以用 PCtoLCD2002 软件生成设置字宽 16、字高 16、取模方式选择阴码、逐行式还是逐列式要和你代码里的写入方式对应。默认的显示函数一般按逐列式取模这样写完上半部分再写下半部分正好符合 SSD1306 的页写入方式。4.3 ESP8266 AT 指令联网与数据透传ESP8266 模块出场自带 AT 固件这意味着它已经是一个完整的 WiFi 网卡STM32 只需要通过串口给它发送文本指令它就会帮你完成扫描热点、连接路由器、建立 TCP 连接等工作。这个方案的优点是 STM32 完全不涉及 TCP/IP 协议栈内存占用小逻辑清晰。完整的联网流程分为四步。第一步确认模块响应发送AT模块回复OK。第二步设置工作模式为 STAStation发送ATCWMODE1。第三步连接 WiFi 热点发送ATCWJAP你的SSID,你的密码模块会依次返回WIFI CONNECTED和OK。第四步建立 TCP 连接发送ATCIPSTARTTCP,服务器IP,端口号返回CONNECT和OK。这四步如果用一个串口助手手动验证非常直观。但要在 STM32 程序里自动执行就需要写一个简单的状态机。我的做法是定义一个结构体保存当前 AT 步骤每发送一条指令就等待串口收到预期的关键字再进入下一步。这里有两个关键点一是每条 AT 指令之间要留至少 500 毫秒的延时尤其连接 WiFi 的指令模块扫描和连接可能需要 3 到 5 秒不能急着发下一条二是接收串口数据时要使用中断把收到的字节存进环形缓冲区主循环再在缓冲区里寻找关键字如果不用中断等待模块响应的过程中 DHT11 定时采集就可能被阻塞。连接 TCP 服务器后如果每次发送数据都要先发ATCIPSEND再等提示符会非常繁琐且容易超时。ESP8266 提供了透传模式开启后模块会把串口收到的所有数据原样发送到 TCP 连接。开启透传的指令是ATCIPMODE1然后发送ATCIPSEND模块回复后进入透传状态。之后 STM32 只要正常往串口写数据模块就会自动转发。退出透传模式需要发送注意这三个加号前后不能有其他数据否则会被当作普通数据发出去。ESP8266 的数据发送频率也需要控制。传感器数据每小时才变化几度没必要每秒上传。我的策略是每 5 秒读一次 DHT11每 2 秒刷新一次 OLED每 10 秒通过 ESP8266 发送一次数据。发送内容包括设备标识和温湿度数值格式用最简单的temp24.5humi60.2服务器端解析起来毫无压力。如果上传间隔过短不仅浪费流量还可能导致 ESP8266 发热严重长时间运行后掉线。发送函数的核心代码很简单主要逻辑是组一个字符串用 sprintf 格式化然后通过串口发送。这里要注意 sprintf 需要引入 stdio.h 头文件并且在 Keil 里勾选 Use MicroLIB否则浮点数格式化输出的代码体积会非常大在 STM32F103C8T6 上可能编译都过不去。void ESP8266_Send_Data(float temp, float humi) { char buf[64]; sprintf(buf, temp%.1fhumi%.1f\r\n, temp, humi); HAL_UART_Transmit(huart1, (uint8_t *)buf, strlen(buf), 1000); }4.4 主循环逻辑与整体代码组织整个系统的主循环不需要太复杂但要保证每个任务都有合理的执行频率。推荐的做法是定义一个tick计数器在 SysTick 中断里自增主循环根据计数器的差值判断各个任务的执行时间是否到达。这样避免了 HAL_Delay 阻塞式的延时方式让系统在数据上传期间仍然能及时响应 OLED 刷新和 DHT11 读取。主循环的大致逻辑是这样的上电后先初始化外设包括 I2C、USART、DWT然后 OLED 显示欢迎界面接着进行 ESP8266 的 AT 步骤初始化直到模块成功连接到服务器。之后进入主循环检查 5 秒定时标志到了就读取 DHT11如果读取成功就更新温湿度变量同时刷新 OLED 显示检查 10 秒定时标志到了就调用 ESP8266 发送函数上传数据。如果 DHT11 读取失败不阻塞程序直接跳过本次刷新等下一个周期再试OLED 上可以显示上次成功的数据。OLED 的显示内容可以分成三行第一行显示Temp:和温度值第二行显示Humi:和湿度值第三行显示 WiFi 连接状态。这样一眼就能看出系统运行是否正常。第三行状态可以定期查询 ESP8266 的ATCIPSTATUS指令返回值为STATUS:3表示 TCP 连接正常如果变成STATUS:0表示已断开程序就需要重新执行连接流程。这个自动重连机制在实际应用中非常必要因为路由器重启、ESP8266 掉电再恢复等情况都会导致连接断开如果没有重连逻辑系统就只能重启后才恢复上报。5. 联调实录常见故障排查与三个调试心得5.1 STM32 下载失败与 ST-Link 连接问题项目做到一半最影响心态的就是程序死活下载不进去Keil 报error: no stm32 target found! if your product embeds debug authentication, please。这个报错有几种常见原因。最常见的是 ST-Link 的三根线没接对SWDIO 接 PA13SWCLK 接 PA14GND 要共地。注意不是随便接两个 GPIO 就行STM32 的 SWD 调试接口默认就绑定在这两个引脚上除非你在 CubeMX 里手动重映射了否则不能更改。第二个常见原因是目标板供电不足。ST-Link V2 有些版本可以通过 SWD 座的第 3 脚给目标板提供 3.3V 电源但这个引脚的输出电流只有几十毫安。如果你的 STM32 板子同时连了 ESP8266 和 OLED整体功耗可能超过 100 毫安ST-Link 根本带不动导致芯片上电后电压被拉低调试器无法建立连接。正确做法是给开发板单独供电ST-Link 只连接 SWDIO、SWCLK 和 GND 三根线。第三个原因比较隐蔽之前下载过程序但程序里把 PA13 和 PA14 复用成了普通 GPIO或者开启了低功耗模式导致 SWD 引脚失效。解决办法是把 BOOT0 引脚拉高让芯片从系统存储器启动此时用户程序不会运行SWD 功能恢复再重新烧录即可。BOOT0 默认通过下拉电阻接地你用杜邦线把它接 3.3V烧录完再恢复就行。5.2 DHT11 读取失败的四个因素DHT11 读不出数据是这套系统里出现频率最高的问题我从现象到原因整理成了一张速查表。故障现象可能原因排查与解决返回值固定为 255数据线上拉电阻缺失或接线错误检查 DATA 引脚是否接 4.7k-10k 上拉到 VCC返回 1 或 2等待超时起始信号低电平时间不够确认低电平延时在 18ms 以上数据随机跳变微秒延时函数不准确确认系统主频为 72MHz编译器优化为 Level 0偶尔成功偶尔失败读取间隔小于 1 秒两次读取之间至少间隔 1 秒建议 2 秒我遇到过一个比较特殊的情况用杜邦线延长 DHT11 的接线超过 30 厘米后传感器工作时好时坏。原因是杜邦线产生寄生电容影响了高低电平的上升沿和下降沿导致 DHT11 无法准确识别总线状态。解决办法很简单把线剪短或者改用双绞线连接。这提醒我们DHT11 的单总线协议虽然简单但对物理层的质量还是有要求的不要在长线上抱侥幸心理。另外DHT11 的采样周期官方标注是 1 秒意思是两次读取之间至少间隔 1 秒以上。如果你在 while 循环里紧挨着连续读取后一次读取大概率会失败。很多人的排查方向一直在时序延时上打转其实只要在每次读取后加一个 1 秒延时问题就解决了。5.3 ESP8266 连不上 WiFi、连接 OneNET 失败和丢包ESP8266 的问题一般集中在三个阶段AT 指令无响应、连不上路由器、TCP 连接失败。AT 指令无响应的第一个排查项是波特率。AT 固件默认波特率是 115200但有些卖家发的模块可能改过。用串口调试助手分别尝试 9600、57600、74880、115200 这几个波特率如果某个波特率下发送AT能回OK就把程序里的串口配置改成对应值。注意 74880 这个波特率是 ESP8266 上电时打印启动信息的默认波特率有些人看到启动信息就以为 AT 通信波特率是 74880实际上 AT 通信默认还是 115200不要混淆。模块供电不稳也会导致 AT 指令时好时坏上电瞬间如果电压被拉低模块会不断重启。判断方法是看串口调试助手里是否频繁出现 boot 模式的打印信息。出现 boot 信息说明模块在反复上电复位就是供电问题不是软件问题。连不上 WiFi 时先用手机开一个热点把 ATCWJAP 指令里的 SSID 和密码改成热点信息排除路由器信道、加密方式等兼容性问题。如果连手机热点能成功再排查路由器设置比如关闭 MAC 地址过滤、确认密码类型是 WPA2-PSK 等。连接 OneNET 失败的本质其实是 TCP 建连失败。OneNET 平台要先用产品 ID 创建设备然后通过ATCIPSTART连接平台分配的服务器 IP 和端口有些 MQTT 协议还会要求设置设备 ID 和 APIKey任何一个不匹配都会导致无法正常通信。排查方法是在 STM32 里先不连 OneNET而是用电脑开一个 TCP Server 工具把 ESP8266 连接到自己电脑的局域网 IP 和端口如果局域网内能正常建连和数据收发说明硬件链路没问题问题出在平台侧的配置参数上。丢包问题也和平台侧有很大关系。公共云平台的免费实例在资源紧张时偶尔会断开空闲连接。ESP8266 在 TCP 连接空闲超过一定时间后路由器或服务器可能会把连接回收。解决办法是增加心跳包机制定期发送一个简短的、占用极小带宽的数据包比如每 30 秒发送一次ping服务器收到后回一个pong这样能保持 TCP 连接活跃避免被回收。我实测过不加心跳包时设备经常几小时掉线一次加了心跳包后连续运行一周都没有断过。5.4 串口打印的重要性三板斧调通法这个项目里串口打印调试信息是最有效的排错手段。我建议所有初学者都养成一个习惯在关键流程节点上打日志用串口助手实时观察程序执行到了哪一步。ESP8266 连接 WiFi 的每个 AT 步骤都打一次日志DHT11 每次读取返回什么值都打印出来OLED 初始化是否完成的标志也要打印。这样程序跑起来后你一眼就能看出是卡在传感器读取还是卡在网络连接。我自己有个“三板斧”调试顺序第一先保证 STM32 和传感器能本地工作OLED 能正常显示温湿度这时候不接 ESP8266第二用 USB 转 TTL 模块单独测试 ESP8266把每个 AT 指令在串口助手里跑一遍确认模块和固件没问题第三再把两块合到一起联调。这个顺序看起来慢实际上是最快的路径。很多同学把系统全部组装完再调出问题时不知道问题在 STM32 还是在 WiFi 模块一次要查一堆变量效率极低。6. 扩展方向这套系统还能往哪些地方长这套系统调通之后如果你还想继续扩展我有几个实际验证过的方向可以推荐。第一是数据上云。目前 ESP8266 只是把数据传到了局域网内的 TCP Server 或者简单的平台如果想手机随时随地查看需要接入完整的物联网云平台。可以把数据通过 HTTP POST 请求上报到早期一些支持 HTTP API 的云平台或者改用 MQTT 协议接入更主流的物联网平台。ESP8266 的 AT 固件本身就支持 MQTT 指令实际上很多云平台提供了针对 ESP8266 AT 指令的接入示例找到对应文档把 CIPSEND 的内容从temp24.5替换成 MQTT Subscribe 报文就可以。这样做之后手机 App 端只要订阅设备的主题就能实时收到温湿度数据。第二是低功耗改造。现在这个系统接的是一根 USB 线供电不适合电池供电的场景。如果要改用电池需要引入 STM32 的 STOP 模式和 ESP8266 的 Deep-sleep 模式。STM32 和 ESP8266 之间需要加一个 MOSFET 开关平时 ESP8266 完全断电STM32 进入 STOP 模式每 10 分钟通过 RTC 闹钟唤醒一次打开 ESP8266 的电源等它连上 WiFi 发完数据再重新关掉。这样改造后一节 18650 电池理论上能用几个月。这是从“能跑”到“能落地”最核心的一步也是很多商业智能传感器的设计思路。第三是多节点组网。这套系统只有一台设备如果要在几个房间部署每个房间放一个 STM32DHT11ESP8266然后共用一个服务器接收数据就能形成一个最小规模的物联网感知网络。服务器方便可以用电脑跑一个简单的 Python UDP Server每个节点用设备号区分这样不同房间的温湿度就能统一记录和展示。第四是增加控制功能。在 STM32 上接一个继电器模块程序里判断湿度超过某个阈值就自动打开加湿器温度超过阈值就开启风扇。这就是一个简单的智能环境控制系统加个阈值判断逻辑几分钟就能完成但可以让这个项目的应用价值提升一大截。毕业设计想做的“智能温室监控系统”“机房环境告警系统”本质就是在这个方案上加控制逻辑和告警机制。我个人在实际操作中的体会是这套系统最值得投入精力的地方不在于把代码跑起来而在于把通信过程理解透。DHT11 的时序、SSD1306 的显存映射、ESP8266 的 AT 状态机这三个点每一个都代表了一类嵌入式通信的经典模型。把这三个点吃透了后面再做蓝牙模块、LoRa 模块、4G 模块甚至各种协议转换都会觉得是相通的。最后再分享一个小技巧调试 OLED 显示汉字时电脑上取好模之后先别急着烧到单片机里用串口工具把字模数组打印出来对比一下软件预览很多显示错位的问题都是取模方向选错了软件上改一个选项重取一遍就解决不用反复烧录调试。