ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32+ESP8266+DHT11+OLED物联网环境监测方案详解

STM32+ESP8266+DHT11+OLED物联网环境监测方案详解 做物联网环境监测入门阶段最值得复刻的一套方案就是STM32 ESP8266 DHT11 OLED这个组合。它能让你摆脱USB线把采集到的温湿度数据通过WiFi发出去OLED屏本地实时显示调试时不用一直连电脑手机和电脑端也能远程看到数据。这套方案技术栈很全涉及GPIO控制、单总线时序、I2C驱动、串口通信、AT指令解析还有最基础的网络协议对刚学完STM32基础、想往物联网方向走的同学来说是性价比极高的一块跳板。这篇文章不打算只贴一堆代码我想把选型逻辑、硬件接线、DHT11那种容易踩坑的高精度时序、ESP8266联网流程以及我实际调试过程中遇到的一系列问题全部拆开讲。内容尽量完整方便你直接照着搭也方便在校做课程设计、电子设计竞赛选题时作为参考。1. 这套方案的整体设计与模块选型思路1.1 为什么要用四个模块而不是买成品先说结论市面上几十块钱就有现成的WiFi温湿度计自己折腾这套东西核心目的从来不是省钱而是把传感器采集、数据处理、联网传输、本地显示这一整条链路跑通。以后无论换什么传感器、换什么执行器这套思路都能直接平移。具体到每个模块它们各自承担的角色非常清晰模块职责选型理由STM32主控负责采集、处理、指令控制资料多CubeMX生成工程开发效率高DHT11温湿度采集单总线协议经典数字信号直读适合入门ESP8266WiFi联网、数据传输价格低AT指令成熟串口就能控制OLED 0.96寸本地显示温湿度I2C接口只需两根线驱动简单显示直观这里有个容易纠结的问题ESP8266本身也是一颗功能不弱的处理器为什么还要加一个STM32我的理解是这样ESP8266虽然有SDK可以独立开发但需要处理WiFi协议栈、TCP/IP栈、内存管理这些事情对新手来说门槛偏高而且ESP8266的GPIO资源有限ADC精度也一般还不方便跑复杂的应用逻辑。STM32作为主控负责传感器采集和业务逻辑ESP8266老老实实当通信模块用AT指令收发数据这种分工让整个系统的耦合度降到最低调试时可以单独测WiFi也可以单独测传感器问题定位非常快。1.2 数据从采集到上云的完整链路整套系统的数据流是这样的DHT11采集温湿度 - STM32 GPIO读取原始数据 - 计算得到温度和湿度 - 本地OLED显示 串口发送数据帧 - ESP8266接入WiFi - TCP/MQTT发送到服务端如果你做的是课程设计建议先把链路简化成传感器 - STM32 - OLED本地显示跑通之后再叠加ESP8266联网。一次引入太多不确定因素出问题很难判断是传感器时序错了还是WiFi配置错了。我最初做这个项目时就是按照这个顺序推进的第一步让OLED亮起来显示固定字符第二步读取DHT11并显示真实温湿度第三步才接ESP8266。每一步都有独立的验证手段不会出现屏幕黑屏但程序跑了三天这种尴尬局面。1.3 这套方案适合什么场景除了学习这套方案在实际场景中也能干活。我在办公室做过一个小的环境监测点STM32挂在角落OLED显示实时温度数据通过ESP8266发到家里一台小服务器。夏天看空调效果、冬天看取暖设备运行情况都很有参考价值。稍加改动也能用在机房温度告警、实验室环境记录、小仓库湿度监控、蔬菜大棚的环境监控中。你只需要在服务端加一个判断逻辑温度超过阈值就推送告警通知。本文后面会给出完整的数据格式和发送方式扩展起来不用改底层代码。2. 硬件接线与DHT11、OLED协议要点2.1 完整接线表与供电注意事项我用的主控是经典的STM32F103C8T6 最小系统板也就是大家常说的“蓝板”。ESP8266我用的是ESP-01SOLED是0.96寸SSD1306 I2C版本。接线如下STM32引脚连接目标说明PA0DHT11 DATA数据线必须加上拉电阻PB6OLED SCLI2C时钟线PB7OLED SDAI2C数据线PA2ESP8266 RX经转接接ESP8266的URXDPA3ESP8266 TX经转接接ESP8266的UTXD3.3V / GND各模块电源注意ESP8266单独供电策略关于供电这里有一个非常关键的经验不要让ESP8266从STM32板子的3.3V引脚取电。ESP8266在发射WiFi信号时峰值电流可以到300mA以上而STM32最小系统板上的AMS1117稳压芯片持续输出200mA左右就会发热掉电压WiFi模块直接重启或者连不上网。正确的做法是用单独的AMS1117-3.3V模块或充电宝/电池给ESP8266供电把所有模块的GND连在一起即可。DHT11的数据线需要外接一个4.7kΩ到10kΩ的上拉电阻到3.3V。因为DHT11是开漏输出没有内部上拉的话总线高电平会不稳定读取数据大概率失败。如果你用的是DHT11模块四线带电阻那种就省去了外接电阻这一步。2.2 DHT11单总线通信时序深入讲解DHT11用的是单总线协议一根数据线上完成主机发指令、从机响应的全过程。它的核心时序分成两个阶段第一步主机发起采集信号。主控把数据线拉低至少18ms然后释放并拉高20~40us。这时DHT11感知到起始信号会先拉低80us表示自己准备好了再拉高80us之后开始发送40bit数据。从高电平结束开始每个数据位前都有一个50us左右的低电平作为同步信号。第二步一位数据的读取。DHT11发送每一位数据时会先拉低50us表示“我要开始发一个位了”然后拉高。这个高电平持续时间的长短决定了这一位是0还是1高电平持续26~28us表示逻辑0持续70us左右表示逻辑1。简单说就是低电平之后的第一个40us如果引脚还是高电平就说明这一位是1如果已经变低了这一位就是0。数据内容为40bit的数据帧从高位开始发送湿度整数部分8bit、湿度小数部分8bitDHT11固定输出0、温度整数部分8bit、温度小数部分8bit固定输出0、校验和8bit。校验方法是前四个字节相加取结果的低8位如果能和校验字节对上数据才是有效的。实际操作中最容易翻车的点是延时精度。HAL库自带的HAL_Delay最小精度是1ms而DHT11的时序单位是微秒级很多人的数据读不出来就是因为for循环软延时被编译器优化掉了时序完全不对。后面代码部分我会给出基于DWT内核时钟的精确微秒延时方案。2.3 OLED的I2C地址与初始化流程SSD1306驱动的OLED屏I2C地址通常是0x3C有些屏是0x3D可以通过背面电阻修改。如果你用STM32CubeMX生成工程I2C地址在代码里写0x3C或在地址左移一位后写0x788位模式地址两块都遇到过建议用HAL库提供的函数时直接写7位地址0x3C更不容易混淆。OLED的驱动核心是写命令和写数据两种操作。命令用来设置显示模式、对比度、坐标页地址数据则是写入显示缓存中的像素信息。完整的SSD1306初始化序列大概有二十多条命令包括关闭显示、设置时钟分频、设置多路复用比例、设置起始行、设置列地址、打开电荷泵、设置显示模式等。这些初始化命令在网上的例程里都能找到建议直接选择一个成熟的驱动模板不要从零去啃SSD1306的数据手册。OLED显示文字的最小单位是8x16像素的字符点阵。128x64的分辨率可以分成8行16列来显示16x8点阵的汉字或者16行32列来显示英文字符。代码里需要准备字模数据从网上下载字模提取工具比如PCtoLCD2002选“阴码、逐行式、顺向”格式就可以直接在代码里使用。3. STM32端代码编写CubeMX配置、DHT11驱动与OLED显示3.1 用STM32CubeMX生成工程骨架开发环境我建议用Keil MDK5配合STM32CubeMX。新建工程时的关键配置项如下时钟树部分在RCC中选择HSE外部晶振在Clock Configuration里把系统时钟配置到72MHz这直接关系到后面的微秒延时计算。GPIO配置DHT11数据脚PA0设置成Output模式初始电平为High开漏输出后面代码会动态切换输入输出方向CubeMX里先配置成输出即可。I2C1配置PB6接SCLPB7接SDA速度选100KHzI2C标准模式。SSD1306不是高速器件400KHz也有可能出问题100KHz最稳。USART2配置用于和ESP8266通信波特率1152008位数据无校验1位停止位。如果要接收ESP8266的返回信息需要开启中断。配置完成后生成工程记得检查一下PB6、PB7是否正确映射到了I2C1的SCL、SDA有时候CubeMX自动分配引脚会出偏差手动核对一遍比较稳妥。3.2 DHT11驱动代码逐段解读先说最重要的微秒延时函数。DWT是Cortex-M3内核自带的调试监视定时器它有一个32位的自由计数器每个时钟周期加一次不需要额外的定时器外设非常方便static void delay_us(uint32_t us) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; uint32_t ticks us * (SystemCoreClock / 1000000); while (DWT-CYCCNT ticks); }接下来是DHT11的启动时序。整个过程是先把GPIO设为开漏输出拉低20ms再拉高30us然后把引脚切换为输入模式void DHT11_Start(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_PULLUP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_PORT, GPIO_InitStruct); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); delay_us(20000); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(30); GPIO_InitStruct.Pin DHT11_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DHT11_PORT, GPIO_InitStruct); }读取一个字节的函数如下核心逻辑就是上面讲的“低电平之后的40us引脚状态决定这一位是0还是1”uint8_t DHT11_ReadByte(void) { uint8_t value 0; for (int i 0; i 8; i) { while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET); // 等待50us低电平结束 delay_us(40); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { value | (0x80 i); // 高电平超过40us判定为1 while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET); // 等待这一位结束 } } return value; }完整读取温湿度的函数里要注意一个陷阱第一次读取时DHT11上电后还没稳定返回值可能全是0xff或者校验不过。所以代码里一般是连续读两次取第二次的结果或者第一次失败后延时100ms重试。我在main函数里加了一个简单重试逻辑uint8_t DHT11_Read(float *temp, float *humi) { uint8_t data[5] {0}; DHT11_Start(); // 等待DHT11响应80us低电平 80us高电平 HAL_Delay(1); // 简单方式实际项目建议用超时等待 if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) return 1; // DHT11无响应 for (int i 0; i 5; i) data[i] DHT11_ReadByte(); if ((data[0] data[1] data[2] data[3]) ! data[4]) return 2; // 校验失败 *humi data[0] 0.1f * data[1]; *temp data[2] 0.1f * data[3]; return 0; }这段代码有个需要优化的小问题就是等待DHT11响应时用了HAL_Delay(1)。在实际项目中这里应该改成带超时判断的等待不然DHT11没接好时程序会卡死在while循环里。下面的代码演示了带超时的等待模式uint16_t timeout 10000; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) if (--timeout 0) return 1;3.3 OLED显示与主循环逻辑OLED驱动我直接移植了成熟的SSD1306 HAL库例程核心接口只需要几个函数OLED_Init()、OLED_Clear()、OLED_ShowString()、OLED_ShowNum()。在main函数里的主循环大概是这个样子float temperature 0.0f, humidity 0.0f; char buf[32]; while (1) { if (DHT11_Read(temperature, humidity) 0) { OLED_Clear(); sprintf(buf, Temp:%.1f C, temperature); OLED_ShowString(0, 0, buf); sprintf(buf, Humi:%.1f%%RH, humidity); OLED_ShowString(0, 2, buf); } HAL_Delay(2000); // DHT11采样间隔必须大于1秒 }这里的HAL_Delay(2000)非常重要。DHT11数据手册明确写着两次采样间隔至少1秒因为传感器内部的热敏电阻和湿敏电容需要时间恢复到稳定状态读得太频繁不仅数据不准还会让DHT11进入一种“假死”状态怎么拉低都不响应。再提一个实操技巧OLED屏掉电重新上电后如果不执行初始化序列屏上会出现花屏或者完全不亮。所以程序启动时一定要调用OLED_Init()而且初始化要在DHT11第一次读取之前完成这样屏幕上才能第一时间显示“初始化中”的状态信息。4. ESP8266联网AT指令连接WiFi并上传数据4.1 串口连接与固件自检STM32和ESP8266通过串口连接ESP8266出厂默认烧写AT固件直接可以通过串口命令进行控制。把ESP8266的URXD接到STM32的PA2UTXD接到STM32的PA3GND共地3.3V单独供电。接好线后先发一个AT指令看模块是否回应这是最基础的链路自检。发送AT比较快的验证方式是直接用USB转TTL连接ESP8266到电脑打开串口助手发送AT收到OK说明模块是好的。如果发送AT没有回应大概率是下面几个问题供电不足模块不断重启RST引脚被拉低进了复位状态固件烧坏需要用ESP8266 FLASHER工具重新烧写AT固件这里推荐用安信可官方提供的AT固件包用ESP8266 FLASHER工具烧写时波特率选择115200SPI MODE选择DIOFLASH SIZE选择32Mbit其他参数保持默认即可。刷完固件后重新上电串口会先收到一段乱码接着是ready这表示固件正常启动。4.2 连接WiFi与TCP服务器的AT指令序列ESP8266连接WiFi的核心AT指令如下我用的是经典AT固件不同版本指令差别不大ATCWMODE1 // 设置为Station模式 ATCWJAP你的WiFi名称,WiFi密码 // 连接WiFi ATCIPMUX0 // 单连接模式 ATCIPSTARTTCP,192.168.31.100,8080 // 建立TCP连接 ATCIPSEND100 // 告诉模块要发送100字节数据连接成功后模块会返回WIFI CONNECTED和WIFI GOT IP建立TCP连接后会返回CONNECT OK。这些返回信息在调试阶段非常重要建议在STM32端代码里做一个简单的AT返回信息解析把OK或ERROR提取出来方便在OLED上显示当前联网状态。有人可能会问为什么用TCP而不是HTTP因为HTTP协议是建立在TCP之上的对嵌入式来说HTTP头是冗余的传输效率低。本地测试先用TCP裸传服务端用一个Python脚本监听某个端口接收数据即可数据格式自己定义比如用JSON。生产环境或者做课程设计想上云可以换成MQTT协议ESP8266 AT固件也支持ATMQTTCONN指令核心思路是一样的。4.3 STM32端AT指令控制与数据帧格式在STM32端直接使用HAL库的HAL_UART_Transmit函数往串口发送AT指令字符串即可。下面这段代码演示了发送数据前先发送CIPSEND设置长度然后再发送实际数据的过程char send_buf[128]; char at_buf[64]; sprintf(at_buf, ATCIPSEND%d\r\n, strlen(json_data)); ESP8266_SendString(at_buf); HAL_Delay(50); ESP8266_SendString(json_data);为了避免AT指令一次发送太长导致接收出错建议把数据帧长度控制在200字节以内。我实际用的JSON数据格式如下{device:stm32-dht11,temp:25.3,humi:58.6}服务端收到后解析JSON中的temp和humi字段存入数据库或者直接显示。如果只想做最简单的展示可以不用JSON直接传25.3,58.6这样用逗号分隔的裸文本解析起来更快但我还是推荐JSON因为以后加字段不需要改协议。关于串口接收的粘包问题ESP8266的AT指令返回是连续多行的如果STM32端用简单的一字一句查询方式很容易读到半个响应。调试阶段最省事的做法是发完AT指令后HAL_Delay(200~500ms)然后一次性读串口缓存里的所有数据不用逐行解析。如果以后要做的功能复杂每一步都依赖上一步的状态确认那就得非常认真地把串口接收做成状态机了。5. 常见问题排查与稳定性优化实操5.1 开发与烧录阶段的坑开发环境配置方面以下几个问题出现频率非常高STM32烧录报错很多人第一次用ST-Link下载程序时会看到Error: No STM32 target found! If your product embeds debug authentication, please ...这类错误。这个报错的常见原因是下载器接线错误、目标板供电不足或者芯片被读保护。ST-Link的SWDIO、SWCLK、GND三根线要接对3.3V可以不接但GND必须共地。如果确认接线没问题用STM32 ST-LINK Utility工具里的Connect按钮先测试一下能否识别到芯片识别不到再检查驱动。虚拟串口设备管理器感叹号板载CH340的虚拟串口如果在设备管理器里显示黄色感叹号基本都是驱动版本太老或者被其他软件覆盖了驱动。重新安装CH340驱动注意选择对应64位系统的版本问题一般能解决。Keil编译报错很多同一套代码用Keil4打开的工程和用Keil5打开的工程经常会出现各种宏定义错误。建议统一用Keil5并且装好对应芯片的Device Pack。如果之前还装了C51版的Keil要注意两个版本不要安装在同一个目录否则互相干扰这也是热词里Keil5兼容C51和STM32安装的经典痛点。5.2 运行阶段典型问题速查表现象可能原因排查方法DHT11读不到数据上拉电阻缺失、时序不对、供电不稳检查数据线是否接上拉用逻辑分析仪看波形DHT11数据一直不变采样间隔小于1秒传感器进入假死拉高数据线等待2秒再重新读取OLED不亮I2C地址错误、SCL/SDA接反、供电不对扫描I2C地址检查接线顺序OLED显示乱码字模取模方式不对、缓存刷新不完全重新取模使用逐行式阴码ESP8266连接WiFi失败供电不足、WiFi密码错误、天线受干扰单独供电串口AT测试重新连接ESP8266连上WiFi但连不上TCP服务器IP地址错误、服务器防火墙电脑上开TCP监听用手机热点做交叉测试数据发送后服务器没收到CIPSEND长度不对、发送时WiFi断线发送TCP数据前先检查连接状态失败重连这里专门讲一个ESP8266容易忽略的点很多人在室内测试时手机热点能连上换到路由器就失败。主要原因不是代码问题而是路由器开启了AP隔离导致站内设备之间无法互访。调试时建议先用手机热点排除网络环境因素再把锅甩给代码。5.3 稳定性优化三板斧如果你打算让这个系统长时间跑不能只做一个跑半小时就行的demo。我实际使用中总结了三个必须做的优化第一打开看门狗。STM32自带IWDG独立看门狗配置一个4秒超时在主循环里喂狗。如果程序因为DHT11卡死或者串口异常陷入死循环看门狗能自动复位系统比人工跑过去断电重启强太多了。第二加入WiFi断线重连机制。ESP8266在使用过程中可能因为路由器重启、信号干扰等原因掉线。如果只是上电时连一次WiFi断线后系统就成了瞎子。建议在主循环里定时发送ATCIPSTATUS查询连接状态如果返回状态不是3已建立TCP连接就执行一次完整的重连流程。注意重连时要先断开旧连接不然会报错。第三主板电源做好去耦。在ESP8266模块的VCC和GND之间并一个100uF电解电容和一个0.1uF陶瓷电容用来吸收WiFi发射时的电流尖峰。STM32板子的3.3V和GND之间也并一个100uF电容整体抗干扰能力会明显提升。写在最后的几点体会做这类物联网小项目的关键不是把一个模块完全学透再开始下一个而是先把链路打通再逐个环节深入。我第一次调DHT11时序时一整个下午都在1和0之间打转后来发现是延时函数被编译器优化了改用DWT之后一次通过。踩过几次坑之后我现在做任何传感器驱动第一件事就是先写一个可靠的微秒延时函数再开始琢磨协议。这套STM32ESP8266DHT11OLED的方案虽然简单但每一个环节都能学到真东西等你把WiFi打通那一刻看到另一端服务器实时刷新温度数据前面所有的折腾都值了。
返回列表