STM32 HAL库实战:DHT11温湿度采集与OLED屏显完整工程解析

📅 发布时间:2026/10/20 1:52:58
STM32 HAL库实战:DHT11温湿度采集与OLED屏显完整工程解析
简介这是一份基于STM32 HAL库的DHT11温湿度传感器与OLED显示屏联调项目包面向物联网和嵌入式开发学习者解决传感器数据采集与图形显示的综合实践问题。资源共193个文件压缩包6.9MB核心包括C源文件、HAL库驱动头文件、编译中间文件.o/.crf及工程配置文件.ioc/.uvprojx等涵盖I2C、TIM、UART等驱动代码工程结构完整可直接在Keil/MDK环境中参考。已有772人浏览学习。通过此项目包读者可掌握DHT11单总线时序读取、温湿度数据解析温度0~50℃、湿度20%~90%RH、SSD1306 OLED驱动显示并理解HAL库对GPIO与I2C等外设的统一管理方式同时可根据源代码分析传感器初始化、数据校验与异常处理流程还可参考项目的MDK-ARM编译配置与调试信息快速迁移至同系列STM32开发板适合作为物联网课程设计或入门实践的参考工程。1. 物联网入门最该碰的一个项目HAL库DHT11温湿度传感器加OLED屏显如果你翻过不少物联网项目大概会有个感觉很多Demo不是跑在开发板上而是跑在作者的截图里。代码贴了一堆教程跳过接线最后你烧进去屏不亮或者数据全是0。而这套工程不太一样它把DHT11温湿度采集和0.96寸OLED显示串成了一条完整链路从STM32CubeMX生成初始化代码到单总线时序驱动再到SSD1306显存刷新每一步都是能直接落地的。换句话说你拿到的不只是一个传感器例程而是一个「传感器采集加显示交互」的最小物联网节点原型。适合刚学完HAL库基础、想正经做一个带输出设备小项目的开发者也适合需要快速出原型验证环境数据的场景。读完这篇你不仅能跑通还能知道改哪里、怎么加逻辑。2. 工程全貌从硬件选型到HAL库分层结构2.1 这套系统用到什么硬件为什么这么选先看物料清单。这套工程的核心是一块STM32F103C8T6最小系统板俗称蓝丸价格便宜、资料多HAL库支持也最成熟。传感器用的是DHT11三脚封装VCC、DATA、GND单总线协议一条线既做供电又做数据通信。显示部分是一块0.96寸I2C接口OLED控制器是SSD1306分辨率128x64蓝黄双色或纯蓝都有I2C只需要SDA和SCL两根线。为什么选这三样组合而不是用DHT11加1602液晶或者DS18B20加数码管关键在两点。第一DHT11单总线时序和OLED的I2C时序是完全不同的两种通信范式一个靠微秒级延时模拟时序一个靠硬件I2C外设自动传输。你在一套工程里同时接触这两种后续换任何传感器和屏幕套路都是一样的。第二OLED显示温湿度比数码管或串口打印直观得多这也让这个项目天然适合做成桌面小气象站或环境监测节点的原型。2.2 STM32CubeMX配置了什么引脚、时钟、I2C外设工程不是纯手写寄存器而是基于STM32CubeMX生成的HAL库工程。我一般会先新建一个CubeMX项目选芯片型号STM32F103C8Tx然后按下面这张表分配引脚。功能引脚配置说明DHT11数据线PA0GPIO_Output推挽默认高电平OLED SCLPB6I2C1_SCL开漏上拉开启OLED SDAPB7I2C1_SDA开漏上拉开启调试串口PA9/PA10USART1用于打印调试信息时钟树方面把HSE设为8MHz晶振PLL倍频到72MHz主频APB1总线时钟设为36MHz——这直接影响I2C1的时钟源如果这里配错I2C通信速率会不对。I2C1的参数我习惯配成Standard Mode100KHz因为OLED模块走杜邦线连接时400KHz快速模式容易受干扰导致花屏或偶发通信超时。CubeMX生成代码后PA0默认是推挽输出。注意这里有个重要细节DHT11的数据线在读取时需要切换方向从输出模式切到输入模式。HAL库不像标准库那样直接改CRL寄存器而是用GPIO_InitTypeDef重新初始化的方式切换模式或者直接操作GPIOA-CRL寄存器。这套工程里用的是后者因为切换速度快时序不被库函数拖累。2.3 工程目录结构哪些文件是核心哪些不用改CubeMX生成的目录结构大致长这样我拆给你看。Core/ Inc/ # 头文件包括main.h、gpio.h、i2c.h Src/ # main.c、gpio.c、i2c.cHAL初始化代码 Drivers/ CMSIS/ # ARM内核头文件 STM32F1xx_HAL_Driver/ # HAL库源码不用动 User/ dht11.c/.h # DHT11驱动 ssd1306.c/.h # OLED显示驱动 display.c/.h # 温湿度显示业务逻辑Core目录下的gpio.c和i2c.c是CubeMX生成的基本上不用碰。User目录是后加的存放你手写的驱动代码。这样分层的用意很直接硬件初始化和业务逻辑隔离将来换平台或者换传感器只改User层Core层保持稳定。3. 核心模块拆解DHT11单总线时序的完整实现3.1 单总线协议为什么时序比协议本身更重要DHT11用的是单总线协议一条数据线上完成主机发指令、从机发数据。没有时钟线没有片选线全靠高低电平的持续时间来区分0和1。这套工程里DHT11驱动最核心的部分就是主机的一次读时序。完整通信过程分四步。第一步主机把数据线拉低保持至少18毫秒这是启动信号告诉DHT11准备干活。第二步主机释放总线拉高20到40微秒然后切换到输入模式等待从机响应。第三步DHT11会主动拉低80微秒再拉高80微秒这是响应信号。第四步从机连续发送40位数据湿度整数、湿度小数、温度整数、温度小数、校验和每个位是50微秒低电平加26到28微秒高电平表示0或者70微秒高电平表示1。这40位数据怎么读关键在于读取每一位时先等数据线变低再从低电平开始计时大约40到50微秒后读取电平状态。如果读到高这一位是1如果读到低这一位是0。手写这个逻辑时微秒级延时函数是必须的HAL_Delay只能毫秒级精度完全不够用。3.2 DHT11驱动代码逐段拆解先把完整驱动代码贴出来再逐段说明。我用的是阻塞式读取一次完整采集耗时大概25毫秒对温湿度这种慢变信号来说完全够用。#include dht11.h #include main.h // 微秒延时基于DWT计数器精度比HAL_Delay高 static void DHT11_Delay_us(uint32_t us) { DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; while(DWT-CYCCNT us * (SystemCoreClock / 1000000)); } // 复位DHT11并等待响应 static uint8_t DHT11_Reset(void) { uint8_t timeout 0; // 主机拉低至少18ms GPIOA-BRR GPIO_PIN_0; HAL_Delay(20); // 释放总线拉高20-40us GPIOA-BSRR GPIO_PIN_0; DHT11_Delay_us(30); // 切换PA0为输入模式 GPIOA-CRL ~(0xF 0); // 清空PA0配置 GPIOA-CRL | (0x4 0); // 浮空输入 // 等待从机拉低80us timeout 0; while((GPIOA-IDR GPIO_PIN_0) ! 0) { if(timeout 100) return 1; // 超时返回错误 DHT11_Delay_us(1); } // 等待从机拉高80us timeout 0; while((GPIOA-IDR GPIO_PIN_0) 0) { if(timeout 100) return 1; DHT11_Delay_us(1); } return 0; }这段代码的关键点有三个。一是GPIOA-BRR和GPIOA-BSRR直接操作寄存器避免调用HAL_GPIO_WritePin的库函数开销。二是模式切换用CRL寄存器改写比重新调用HAL_GPIO_Init快得多时序上更稳。三是超时机制用计数变量加循环判断防止DHT11没接好时程序卡死在while里。参数说明超时上限100微秒这个值是根据DHT11数据手册里80微秒响应时间留的余量调太小容易误判调太大故障时反应慢。3.3 读取40位数据位判断的细节和校验逻辑复位完成之后接下来就是一位一位把40个bit读出来。每一位的读取过程都遵循同样的模式先等数据线变低再延时40微秒采样电平。static uint8_t DHT11_ReadBit(void) { uint8_t timeout 0; uint8_t bit 0; // 等待数据线变低 timeout 0; while((GPIOA-IDR GPIO_PIN_0) ! 0) { if(timeout 100) return 0xFF; // 超时 DHT11_Delay_us(1); } // 低电平持续50us后采样 DHT11_Delay_us(40); // 采样点高电平为1低电平为0 if((GPIOA-IDR GPIO_PIN_0) ! 0) { bit 1; } // 等待当前位结束 timeout 0; while((GPIOA-IDR GPIO_PIN_0) 0) { if(timeout 100) return 0xFF; DHT11_Delay_us(1); } return bit; } uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] {0, 0, 0, 0, 0}; if(DHT11_Reset() ! 0) return 1; // 读取40位数据 for(int i 0; i 40; i) { uint8_t bit DHT11_ReadBit(); if(bit 0xFF) return 1; buf[i / 8] 1; buf[i / 8] | bit; } // 校验和前四个字节相加低8位等于第五个字节 uint8_t checksum (buf[0] buf[1] buf[2] buf[3]) 0xFF; if(checksum ! buf[4]) return 1; *humidity buf[0]; *temperature buf[2]; return 0; }这个实现里有几个参数值得解释。延时40微秒采样是因为DHT11每个数据位都是50微秒低电平开头高电平持续26到28微秒表示070微秒表示1在低电平开始后40微秒采样刚好落在高电平区间能准确区分0和1。校验和的算法是前四字节相加取低8位和第五字节比对相等才认为数据有效。返回值1表示读取失败0表示成功这样调用方可以根据返回值决定显示什么占位符。4. OLED显示模块SSD1306驱动和温湿度界面绘制4.1 SSD1306的显存模型和I2C传输方式OLED屏能显示内容核心是SSD1306控制器内部有一块1KB的显存分辨率为128x64分成8页每页128字节每字节对应一列8个像素点。你要显示什么就往对应页的对应字节写位图数据。I2C传输时每次写入的格式是控制字节加数据字节。这套工程用I2C1外设通信地址是0x787位地址0x3C左移一位。我常用的判断方法是如果你的OLED模块背面没有焊接电阻改动那地址基本就是0x78偶尔有模块是0x7A对应7位地址0x3D。地址不对的表现是屏幕完全没反应或者初始化卡死在HAL_I2C_Init后的第一次通信。显存管理方面工程在内存里开了一个128x8字节的缓冲区所有绘制操作先改缓冲区最后一次性刷到屏幕。这种离屏渲染的好处是画面不会闪烁也避免频繁I2C写操作占用总线导致传感器读取超时。4.2 字体取模和温湿度绘制函数OLED显示文字需要先准备字库。这套工程里内置了16x8的ASCII字库也就是每个字符占2页、每页8个字节。实际显示温湿度时需要把数字转换成字符串再逐字符绘制。#include ssd1306.h #include display.h #include dht11.h extern I2C_HandleTypeDef hi2c1; // 显示缓冲区和屏幕初始化 static uint8_t display_buffer[128 * 8]; static SSD1306_t screen; void Display_Init(void) { screen.width 128; screen.height 64; screen.buffer display_buffer; SSD1306_Init(screen, hi2c1, 0x78); SSD1306_Clear(screen); SSD1306_Update(screen, hi2c1, 0x78); } void Display_ShowEnvironment(void) { uint8_t humidity 0; uint8_t temperature 0; char line1[16]; char line2[16]; if(DHT11_ReadData(humidity, temperature) 0) { snprintf(line1, sizeof(line1), Temp: %d.%d C, temperature / 10, temperature % 10); snprintf(line2, sizeof(line2), Humi: %d.%d %%, humidity / 10, humidity % 10); } else { snprintf(line1, sizeof(line1), Temp: --.- C); snprintf(line2, sizeof(line2), Humi: --.- %%); } SSD1306_Clear(screen); SSD1306_DrawString(screen, 0, 0, line1, Font_7x16); SSD1306_DrawString(screen, 0, 32, line2, Font_7x16); SSD1306_Update(screen, hi2c1, 0x78); }这段代码的逻辑很直接读传感器数据拼字符串清屏绘制两行文字刷新屏幕。注意snprintf里我把DHT11的整数和小数分开拼接因为DHT11返回的湿度值是整数部分和小数部分分开的字节比如buf[0]45表示湿度45%buf[1]0表示小数部分为0。温度同理buf[2]25表示25度。4.3 I2C写入函数和刷屏机制SSD1306_Update函数的本质是一次性把整个缓冲区通过I2C写入屏幕。常见做法是开启128x8字节的连续写模式先发控制字节0x40表示后续都是数据然后连续发送128字节。void SSD1306_Update(SSD1306_t *dev, I2C_HandleTypeDef *i2c, uint16_t addr) { for(uint8_t page 0; page 8; page) { uint8_t cmd 0xB0 page; // 设置页地址 SSD1306_WriteCmd(dev, i2c, addr, cmd); SSD1306_WriteCmd(dev, i2c, addr, 0x00); // 列地址低字节 SSD1306_WriteCmd(dev, i2c, addr, 0x10); // 列地址高字节 HAL_I2C_Mem_Write(i2c, addr, 0x40, I2C_MEMADD_SIZE_8BIT, dev-buffer[page * 128], 128, 100); } }这里每页写入前都要重新设置页地址和列地址因为SSD1306没有自动跨页功能。参数I2C_MEMADD_SIZE_8BIT表示控制字节是8位100是超时时间单位毫秒。如果I2C总线上有其他设备或者线路过长100毫秒不够用的可能性不大但若频繁超时优先检查地址和上拉电阻而不是调大超时值。5. 从CubeMX到跑通完整流程加五个高频踩坑现场5.1 十分钟跑起来完整操作流程假设你手上已经有一块STM32F103C8T6蓝丸、一个DHT11模块、一块0.96寸OLED。接线按这个顺序来DHT11的VCC接3.3V或5V都可以但数据线必须接PA0且数据线和VCC之间最好加一个4.7K到10K的上拉电阻——DHT11模块板上一般自带如果是裸传感器就必须外接。OLED的VCC接3.3VGND接GNDSCL接PB6SDA接PB7。打开ST-Link或串口下载工具把编译好的hex文件烧进板子。烧录成功后OLED应该先全屏点亮一下这是SSD1306初始化时的测试画面然后显示当前温湿度。用嘴对着DHT11哈一口气湿度数值应该在几秒内往上跳这是传感器正常工作的直观验证。如果数值纹丝不动先别改代码用万用表量一下DHT11的VCC是不是真的有电压——这个项目里电源接触不良导致的故障占比相当高被我列为排查第一顺位。调试阶段建议打开串口1波特率115200在main函数的while循环里每2秒打印一次温湿度数据和DHT11_ReadData的返回值。返回值0表示成功1表示超时或校验失败通过返回值能快速区分是时序问题还是传感器硬件问题这招血泪经验总结下来最省时间。5.2 避坑一OLED白屏地址0x78还是0x7A现象代码烧进去OLED一直白屏或者完全不亮I2C初始化不报错。原因I2C地址不对或者模块上电时序不对。解决先把地址改成0x7A试一次大概率屏幕就有反应。如果两个地址都试过还是白屏检查OLED模块的VCC是不是接到了3.3V——部分模块接5V会烧毁稳压芯片接3.3V才是安全的。还有一种情况是SDA和SCL接反了这种情况通常屏幕会有微弱亮光但显示乱码。按照白屏到地址到供电到接线的顺序排查基本五分钟内能定位。5.3 避坑二湿度一直是0%DHT11上电稳定时间不够现象温度显示正常湿度显示0%串口打印的返回值时好时坏。原因DHT11上电后需要至少1秒的稳定时间期间不能发任何指令。如果主循环里第一次读取就发生在系统上电后几百毫秒传感器还在内部初始化返回的数据就是无效的。解决在main函数的初始化阶段加一个HAL_Delay(2000)确保DHT11完全就绪后再开始采集。另外注意连续两次读取之间间隔要大于1秒DHT11的采样周期就是这么慢读太频繁反而拿不到新数据。5.4 避坑三校验和失败的玄学其实是GPIO模式切换慢了现象串口不停地报校验和错误偶发能读到正确数据但成功率很低。原因从输出模式切到输入模式时如果用HAL_GPIO_Init重新配置这个函数内部会做很多事切换耗时可能超过10微秒但DHT11的响应窗口就只有80微秒左右慢一步时序就全乱了。解决改成直接操作GPIOA-CRL寄存器切换模式这是这套工程里我最坚持的做法。寄存器切换是几个CPU周期的事不需要关中断也不需要额外的延时补偿。5.5 避坑四OLED重影和闪烁问题出在刷屏方式现象屏幕显示内容有残影或者刷新时能看到明显的闪烁。原因直接在屏幕上绘制没有显存缓冲区。每次写一个字节就通过I2C发一次屏幕的页地址频繁跳变画面就跟放PPT一样。解决所有绘制操作先写内存缓冲区全部画完再一次性Update到屏幕。如果在Update前后加了延时闪烁会加剧——正确做法是不加延时连续刷新人眼看起来反而稳定。5.6 避坑五温度显示负数格式混乱现象环境温度零下时显示变成类似-20.-5这种奇怪格式。原因DHT11温度字节的最高位是符号位0表示正温度1表示负温度。如果直接把buf[2]当成无符号数打印负温度会变成一个很大的正数或者小数部分和整数部分的拼接完全错乱。解决先判断温度字节的最高位负温度时需要对整个数值做补码转换再按带符号数格式化输出。字段宽度用%d而不是%u这样才能正确显示负号。6. 二次开发方向从固定显示到灵活采集系统这套工程跑通之后改造空间其实比想象中大。按投入产出比排序我列几个方向供你选。第一个方向是采集策略改造。现在主循环是固定延时然后读一次你可以改成定时器触发采集比如用TIM2产生1秒中断在中断回调里置一个标志位主循环检测到标志才执行读取加刷新。这样的好处是采集间隔精确可控也方便后续加按键切换显示模式。改造代码量不超过50行但整个程序结构就从轮询变成了事件驱动这是往正规嵌入式工程靠拢的第一步。第二个方向是数据可视化增强。SSD1306的1KB显存虽然不大但画折线图足够了。你可以维护一个48点的环形缓冲区每10分钟存一次温度值然后用SSD1306_DrawLine把相邻点连起来画出一根温度趋势曲线。这个改造会逼着你仔细读一遍显示驱动里画线和画矩形的实现对理解坐标系的映射关系很有帮助。第三个方向是接入物联网平台。DHT11加OLED只是感知层和展示层要真正上云可以加一个ESP8266模块串口透传把温湿度数据发到MQTT服务器。改造思路是保持DHT11采集代码不变把Display_ShowEnvironment里的snprintf内容同时发给ESP8266的串口云平台侧订阅Topic就能实时看到数据。这时你会发现这套工程的分层结构帮了大忙——采集和显示分离接入网络只是多一个输出通道。我这几个项目做下来最大的感受是单总线时序驱动这种东西光看文章觉得懂了一上手就是各种翻车而且翻车的点几乎都在时序边界上。所以拿到这套工程后你需要先原样跑通再用逻辑分析仪或者示波器看一下DHT11数据线上的波形和你自己写的代码对照一遍然后再开始改。从那以后我每次移植DHT11驱动到新平台都强制自己先画一遍时序图再写代码这个习惯帮我少踩了很多坑。希望帮到你。本文还有配套的精品资源点击获取