☰
STM32驱动DHT11温湿度传感器并在OLED屏幕显示
2026/10/15 1:32:05 网站建设 项目流程

简介:面向STM32入门与嵌入式通信实践的一份完整工程资源,基于STM32F103C8T6最小系统板,通过IIC协议驱动DHT11温湿度传感器,并将解析结果实时显示在0.96寸OLED屏上。内容从STM32初始化、GPIO模式配置、软件IIC时序实现,到DHT11数据读取与40位二进制解析,再到OLED驱动与字符显示,形成了一个可完整运行的闭环,适合高职学生、电子爱好者及刚接触裸机开发的初学者对照学习。压缩包共227个文件,总大小8.33MB,以C/H源码、Keil编译中间产物(d/o/crf)、hex/axf可执行文件及bat脚本为主,内含用户代码、驱动库、编译输出和列表文件等目录,便于分别查看功能模块,同时可执行文件也可直接烧录到开发板验证效果。目前已有1525人学习下载。通过研读这套工程,可以重点掌握IIC通信协议时序、DHT11温湿度解码原理以及OLED显存写入机制,梳理整个嵌入式数据采集与显示流程,是提升底层驱动编写和调试能力的实用参考资料。

1. 先点亮屏幕容易,拿到能用的温湿度数据难

用STM32F103C8T6最小系统板驱动DHT11温湿度模块并在OLED屏幕上显示,是很多开发者入门裸机外设的第一个完整闭环。这个组合看起来简单:一个传感器、一块屏幕、一块板子,但真正动手时你会发现,问题往往不在OLED的I2C时序,也不在DHT11的15ms长拉低,而在于“数据读到一半卡死”和“屏幕显示的速度赶不上数据变化”这两个软硬结合的玄学点。这篇文章不讲理论课本,而是把供电、上拉、时序容差、显存刷新和调试手段串成一条可复现的路径,你跟着做就能在半小时内看到稳定的温度和湿度数字。

这个方案适合两类人:一类是已经点亮过LED、跑过按键中断的初学者,想跨出“操作外部芯片”这一步;另一类是手头有最小系统板和OLED,想把传感器数据从串口printf换成可视化的工程师。下面所有代码用标准外设库实现,不依赖IDE自动生成的复杂工程,方便你迁移到自己偏好的开发环境里。

2. 最小系统板选型与开发环境:先解决供电和调试口

2.1 板子上的LDO稳压和VCC引脚为什么不能直接接传感器

STM32F103C8T6最小系统板常见的供电输入是USB的5V,板载一颗低压差线性稳压器把电压降到3.3V给芯片用。DHT11的供电范围是3.3V到5.5V,有人图省事直接把传感器VCC接在板子的3.3V输出上,结果发现数据引脚电平一直偏低,读不了正确数据。原因在于DHT11的通讯电平是跟随供电电压的,如果传感器用5V供电,它的数据引脚高电平也是5V,而STM32的GPIO在浮空输入模式下承受5V电压一般没问题,但如果你把传感器也接在3.3V上,同时数据线上又没有加上拉电阻,就会导致高电平建立时间太慢,时序判读出错。

我一般这样接线:DHT11的VCC接板子5V引脚,GND共地,数据引脚直接连到STM32的一个空闲IO,例如PA1。注意,DHT11的数据引脚在空闲状态下需要外部上拉,有些模块上已经焊了4.7K或10K电阻,但裸片或面包板版本没有。如果你怀疑读数不稳,先查这个上拉,再查代码里的输入模式设置,顺序不能反。

2.2 下载器选择与常见接线错误

最小系统板上的调试接口一般是四根线:SWDIO、SWCLK、GND、3.3V。用ST-Link或者任何兼容CMSIS-DAP的调试器都可以。第一次连接时最常犯的错是只接了SWD两根线,没接GND,导致调试器无法同步。另一个坑是板子上的BOOT0跳线状态,如果BOOT0被拨到1,芯片会进入系统存储器引导,FLASH里的程序不会运行,你烧录时会提示校验失败。

进入程序烧录前,建议先用调试器读一下芯片ID,确认连接正常。在Keil的工程配置里选择“ST-Link”调试器,然后在“Flash Download”里勾选“Reset and Run”,这样烧录完成后板子自动复位执行新程序。很多开发者烧录后还要手动按复位键,就是因为漏了这一步。

3. DHT11单总线驱动:时序容差比代码逻辑更决定成败

3.1 单总线协议的最小完整流程

DHT11使用单总线通信,协议里最核心的是起始信号和位时序判定。主机先把数据线拉低,保持18ms以上,然后释放并延时20到40μs,此时传感器会响应,把总线拉低80μs表示应答,再拉高80μs准备发送数据。之后传感器连续输出40位数据,分为湿度整数部分、湿度小数部分、温度整数部分、温度小数部分和校验和,每部分都是8位,校验和等于前四个字节之和的低8位。

很多人从网上抄代码时,只关注“读取40位”,却忽略了时序里的容差范围。DHT11规格书给的数据是:表示“0”时高电平持续20至26μs,表示“1”时高电平持续50至70μs。关键在于,你不能用一个固定阈值去判断,而要在高电平开始后循环检测电平变化,同时计数时间。如果采样间隔太粗糙,比如纯粹用空循环延时,就容易把所有位都判成0或1。

3.2 可复现的驱动实现与参数说明

下面这段代码是基于标准外设库的读取函数,用SystemCoreClock校准过的延时来保证微秒级精度。由于DHT11对时序要求不算极端严格,我们只要保证主机长拉低大于18ms、释放后20到40μs内开始检测响应、位采样时能区分20μs和60μs的差别即可。

#include "stm32f10x.h" #include "dht11.h" static void DHT11_Delay_us(uint32_t us) { uint32_t ticks = us * (SystemCoreClock / 1000000); while (ticks--) { __NOP(); } } uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] = {0, 0, 0, 0, 0}; uint8_t bit_value = 0; GPIO_InitTypeDef gpio; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Pin = DHT11_DATA_PIN; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DHT11_DATA_PORT, &gpio); // 主机拉低至少18ms,设置为低电平后保持 GPIO_ResetBits(DHT11_DATA_PORT, DHT11_DATA_PIN); DHT11_Delay_us(19000); // 释放总线,拉高,等待传感器响应 GPIO_SetBits(DHT11_DATA_PORT, DHT11_DATA_PIN); DHT11_Delay_us(30); // 切换为输入模式,浮空输入 gpio.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(DHT11_DATA_PORT, &gpio); // 检测响应:传感器拉低80us后拉高80us uint32_t timeout = 0; while (GPIO_ReadInputDataBit(DHT11_DATA_PORT, DHT11_DATA_PIN) == Bit_SET && timeout < 100) { timeout++; DHT11_Delay_us(1); } while (GPIO_ReadInputDataBit(DHT11_DATA_PORT, DHT11_DATA_PIN) == Bit_RESET && timeout < 100) { timeout++; DHT11_Delay_us(1); } for (int i = 0; i < 40; i++) { // 等待数据位开始,低电平约50us while (GPIO_ReadInputDataBit(DHT11_DATA_PORT, DHT11_DATA_PIN) == Bit_RESET); // 高电平宽度决定0或1 uint32_t high_width = 0; while (GPIO_ReadInputDataBit(DHT11_DATA_PORT, DHT11_DATA_PIN) == Bit_SET) { high_width++; DHT11_Delay_us(1); } bit_value = (high_width > 35) ? 1 : 0; buf[i / 8] <<= 1; buf[i / 8] |= bit_value; } // 校验:前四个字节和低8位等于第五个字节 if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { *humidity = buf[0]; *temperature = buf[2]; return 0; } return 1; }

这段代码的关键逻辑是先用推挽输出模式拉低总线,再切回浮空输入模式读取信号,避免输出模式下电平冲突。高电平计数变量high_width的单位是微秒左右,35μs作为0和1的分界点,留出了足够的容差空间。我在实际调试中用逻辑分析仪量过,DHT11发0时高电平约23μs,发1时约58μs,这个阈值非常安全。

注意,在读取40位数据的过程中,如果其中某一位的等待时间过长,说明总线被传感器拉死或者线路接触不良,代码会卡在while循环里。为了避免整个系统假死,我建议在外层加一个超时退出,例如在每条while条件里增加timeout计数,超过500μs就放弃本次读取,并返回1表示失败。上面代码里为了可读性省略了部分超时,实际项目中必须补上。

3.3 为什么读取间隔至少要1秒

DHT11的规格书上写着采样周期不能小于1秒,实际用下来1.5秒到2秒最稳妥。如果你用定时器触发读取,每隔500ms读一次,你会发现多次读取后数据全部变成零或者校验失败。原因是传感器内部的湿敏电阻还没完成充放电平衡,上电初期尤其明显。我在自己做的气象站项目里,DHT11读取周期设置在2秒,并在第3次连续校验失败时才显示警告,这样既不会刷屏也不会误报。

4. OLED显示:SSD1306初始化序列与显存刷新策略

4.1 I2C地址和硬件连接的几个关键点

0.96寸OLED屏绝大多数使用SSD1306驱动芯片,I2C接口的模块默认地址是0x78(7位地址0x3C左移一位),但也有些厂家把地址设置为0x7A。如果你的屏幕一直亮不起来,先用I2C总线扫描一下设备地址。常见做法是用一个简单的I2C读操作,枚举0x30到0x3D范围内所有地址,看哪些有ACK响应。显示模块的SCL和SDA接STM32硬件I2C对应的引脚,也就是PB6和PB7,这两个脚内部是开源模式,需要外部上拉电阻。多数OLED模块上已经带好,但如果是自己焊的转接板,必须补两个4.7K上拉到3.3V。

供电电压方面,OLED模块的VCC可以接3.3V,不要接5V,因为模块上的稳压芯片通常只支持低压差输入,接5V可能导致发热或烧坏。接线顺序也很重要:先接GND和VCC,再接SCL和SDA,避免带电插拔I2C线导致芯片闩扣效应。

4.2 关键初始化序列和显示代码

SSD1306上电后默认处于关屏状态,要发送一系列命令才能点亮并显示内容。下面是常用的初始化流程,包含电荷泵开启、段重映射和扫描方向设置。

void OLED_Init(void) { // 延时等待屏幕电源稳定 delay_ms(100); OLED_WriteCmd(0xAE); // 关闭显示,初始化期间保持关屏 OLED_WriteCmd(0xD5); OLED_WriteCmd(0x80); // 设置显示时钟分频因子 OLED_WriteCmd(0xA8); OLED_WriteCmd(0x3F); // 设置多路复用率,64行 OLED_WriteCmd(0xD3); OLED_WriteCmd(0x00); // 显示偏移为0 OLED_WriteCmd(0x40); // 起始行指向0 OLED_WriteCmd(0x8D); OLED_WriteCmd(0x14); // 开启电荷泵,内部升压 OLED_WriteCmd(0x20); OLED_WriteCmd(0x02); // 设置内存寻址模式为页寻址 OLED_WriteCmd(0xA1); // 段重映射,左右反转 OLED_WriteCmd(0xC8); // 扫描方向,上下反转 OLED_WriteCmd(0xDA); OLED_WriteCmd(0x12); // 设置COM引脚硬件配置 OLED_WriteCmd(0x81); OLED_WriteCmd(0xCF); // 设置对比度,0xCF是常用亮度 OLED_WriteCmd(0xD9); OLED_WriteCmd(0xF1); // 设置预充电周期 OLED_WriteCmd(0xDB); OLED_WriteCmd(0x40); // 设置VCOMH电平 OLED_WriteCmd(0xA4); // 关闭全局显示,内容来自RAM OLED_WriteCmd(0xA6); // 正常显示模式,非反显 OLED_WriteCmd(0xAF); // 开启显示 OLED_Clear(); }

这段序列里的0x8D、0x14必须配对使用,漏掉电荷泵开启会让屏幕永远保持黑屏。0xA1和0xC8组合在一起,能让显示方向变成我们平时习惯的从左到右、从上到下,如果不设置这两条,字体会镜像显示。0x81后面跟的对比度值需要根据实际环境光调整,室内一般用0xCF,阳光下适当调大,但要留意亮度过高时会有轻微残影。

显示字符串时,推荐先构造一个显存数组,把每个像素的亮灭状态写入数组,再通过一次性写RAM命令刷新整屏。这样避免了逐字刷屏产生的闪烁。下面是一个简单的显示配套函数:

void OLED_ShowTemperature(uint8_t temp_int, uint8_t temp_dec) { char str[16]; sprintf(str, "T:%d.%dC", temp_int, temp_dec); OLED_ShowString(0, 0, str); } void OLED_ShowHumidity(uint8_t hum_int, uint8_t hum_dec) { char str[16]; sprintf(str, "H:%d.%d%%", hum_int, hum_dec); OLED_ShowString(0, 16, str); }

注意,sprintf在资源紧张的单片机上可能会占用较多栈空间,我在工程里把栈大小调整到0x800以上。如果你用的是标准外设库固件包下默认的启动文件,栈设置通常足够,但如果你新建工程时裁剪了启动文件,就要留意硬件错误中断。另一个细节是百分号的显示,OLED字库一般是ASCII字符,写入“%”不会引起问题,但如果直接把湿度整数和小数分开显示,就要在中间手动加上一个点号字符。

4.3 刷新策略:整屏刷新还是局部刷新

OLED的显存是128×64位,SSD1306内部有1KB的显示RAM。每次写入RAM都走I2C,整屏刷新大约需要传输1KB数据,在400KHz的I2C频率下耗时约20ms。如果每秒只刷新一次温湿度,完全够用;但如果你在界面上加了动态数值不断跳动的效果,整屏刷新会产生明显的拖影。我一般会把固定的单位字符串和动态数值分开,只更新数字部分所在的页。SSD1306的页寻址模式下,一页是8像素高,你可以单独写某一列的RAM区间,这样既省时间又避免闪烁。

5. 避坑指南:DHT11和OLED组合最常见的5个翻车现场

5.1 现象:屏幕显示“T:0C”而湿度正常

原因:DHT11的温度传感器部分有时序敏感问题,如果主机读数的延时不够精准,温度高位字节容易读成0x00。我在调试时用示波器抓过温度数据位,发现发送“1”时高电平宽度只有约40μs,比规格书的50至70μs偏窄,这是传感器个体差异导致。解决方法是把位判决阈值从35μs降到30μs,并在校验失败时增加一次重读,不要直接把0x00作为真实温度。

5.2 现象:I2C设置后OLED全屏雪花

原因:OLED模块供电不足,或者SCL/SDA的线序接反。经常有人把SCL和SDA交叉连接,屏幕会乱码但不完全黑屏。解决方法是先确认模块型号和引脚定义,再看是否只有一根数据线有波形,最后检查电源纹波。避免使用面包板长跳线跑400KHz的I2C,超过10cm的杜邦线会造成信号振铃和误码。

5.3 现象:主程序偶尔卡死在DHT11读取里

原因:DHT11在响应阶段如果数据线被传感器内部拉低后没有正常释放,你的while等待低电平结束时会一直死循环。解决方法是给每个等待循环加超时计数,一旦超过500μs就退出并返回错误。不要相信DHT11永远会正常应答,电磁干扰、供电抖动和接线松动都可能让它“哑巴”。

5.4 现象:读取频率很高但数据长时间不变

原因:DHT11的数据更新周期本身就很长,湿度在环境稳定时可能几十秒都不变,而且它的分辨率只有1%和1°C。把显示精度写成“T:25.3C”会看到小数部分始终是0,这不是代码问题。解决方法是接受它的物理分辨率,也可以直接显示整数部分,避免自己被“小数位恒为0”误导。

5.5 现象:烧录后OLED正常但DHT11读数始终校验失败

原因:DHT11数据引脚和OLED的SDA共用了一个GPIO端口或同一条I2C总线。DHT11的单总线时序要求GPIO在输入输出间快速切换,如果和I2C设备共享引脚,切换会影响另一个设备的通信。解决方法是把DHT11放在独立的GPIO上,例如PA0或PA1,并且确保初始化时先初始化I2C,再初始化单总线GPIO。

6. 进阶验证技巧:用逻辑分析仪校准时序,向真实产品靠拢

当你把DHT11和OLED都跑通后,不要急着庆祝,先用逻辑分析仪抓一段DHT11的完整时序波形,验证你的代码是否符合数据手册。具体做法是把PA1引脚接到逻辑分析仪的通道上,在读取函数周围加一个翻转测试IO,然后观察触发波形:起始信号的低电平时间是否在18ms以上、传感器应答后的80μs低电平和80μs高电平是否清晰、40位数据的每个“1”是否都在50μs以上。这一步能帮你排除“恰好能跑但边界不稳定”的隐患。

如果是用模拟I2C驱动OLED的工程,也建议把SCL和SDA同时接到分析仪上,确认起始条件和停止条件完整,地址字节的最后一位是0表示写模式。大多数“屏幕偶尔花屏”的问题,在这个环节都能发现是SCL高电平毛刺或多字节写过快导致。

我个人的习惯是,在上电初始化时把DHT11读取失败次数记录到一个静态变量里,连续失败5次后OLED显示“Sensor Error”,而不是继续显示上次的缓存值。这避免用户看到过期的温湿度数据做出错误判断。代码上只需要把读取函数的返回值作为判断条件,并增加一个计数器重置逻辑即可。

另一个值得投入的小改造是给DHT11供电增加一个由GPIO控制的MOS管开关,在不需要采样时切断传感器电源,把整机功耗从十几毫安降到微安级别。用STM32F103C8T6做电池供电设备时,这一步必不可少。测试方法是用万用表串联在电源回路里,对比传感器常供电和分时供电两种模式下的平均电流。

希望这个温湿度显示小项目能成为你深入STM32外设开发的跳板。我当初在这个方案上卡了一整天,最后发现只是DHT11的上拉电阻焊盘虚焊,从那以后我拿到任何新模块都会先确认硬件原理图再写代码,这个习惯帮我少走了很多弯路,希望也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询