STM32 HAL库驱动DHT11温湿度模块:单总线时序与稳定读取实践
2026/9/16 2:09:07 网站建设 项目流程

简介:面向嵌入式及物联网初学者的DHT11温湿度模块驱动代码,适用于Arduino、STM32等常见微控制器平台,解决环境温湿度采集与集成难题。压缩包共3个文件,包含C源文件与头文件以及一个内嵌压缩包,整体仅2KB,代码精简清晰,便于阅读和移植。已有311人学习下载。驱动代码覆盖传感器初始化、命令发送、40位数据读取、校验和验证及错误处理等完整流程,并附带典型时序说明与移植要点,可帮助开发者快速理解单总线通信机制,直接嵌入LCD显示、数据存储或远程上报等实际项目。这份资源对刚接触DHT11或需要轻量级驱动参考的开发者,是实用且高效的参考资料。

1. dht11温湿度模块的坑:时序比“温湿度”本身更难

第一次用STM32F1读dht11温湿度模块,你大概率会卡在一个诡异的地方:万用表量电压正常,串口也能打印“数据”,但温湿度总在0和65535之间跳来跳去。问题出在单总线时序上——dht11只用一根DATA线完成双向通信,0和1靠高电平持续时间来区分,HAL_Delay这种毫秒级延时根本没法满足微秒级的采样窗口。这篇文章会用HAL库写一个最小可用的dht11温湿度模块驱动,把单总线协议、上拉电阻、校验和重试一次讲清楚。适合正在调STM32F1裸机或HAL工程的嵌入式开发者,也适合刚学会看原理图、想搞懂时序的人。

2. dht11温湿度模块的单总线协议与40位数据格式

2.1 DHT11的bit怎么用电平宽度表示

DHT11的数据线在空闲时被上拉电阻保持为高电平。开始通信时,主机先把总线拉低至少18ms,然后释放,这个低电平脉冲让传感器从休眠状态醒来。接着DHT11会主动拉低总线约80us,再释放并拉高约80us,表示“我准备好了”。这就是响应信号。

响应之后,传感器连续输出40bit数据。每个bit都从一段约50us的低电平开始,然后总线被释放。关键在高电平持续多长时间:

  • 如果高电平持续26us到28us,代表逻辑0。
  • 如果高电平持续70us,代表逻辑1。

所以软件读取bit的标准做法是:先等低电平结束,然后延时约40us再采样总线电平。如果读到高,就是1;如果读到低,就是0。因为逻辑0的高电平已经结束,总线回到低;而逻辑1的高电平还在持续。这个采样点必须落在30us到50us之间,选40us左右最安全。

伪代码可以写成这样:

while (DATA_PIN == 0); // 等低电平结束 delay_us(40); // 延时到bit电平的中后段 bit = DATA_PIN; // 高=1,低=0

这段逻辑是DHT系列通用的,换到DHT21、DHT22时只需要调采样延时,协议骨架不变。

2.2 从40位数据里拆出温湿度与校验值

40位数据的排列顺序是固定的,高字节在前。

字节位置内容例子
第1字节湿度整数部分0x45,即69%RH
第2字节湿度小数部分0x00
第3字节温度整数部分0x01,即1℃
第4字节温度小数部分0x00
第5字节校验和0x46

实际温湿度为:湿度 = 第1字节 + 第2字节 * 0.1,温度 = 第3字节 + 第4字节 * 0.1。上面的例子就是湿度69.0%,温度1.0℃。校验和是前四个字节相加后取低8位,如果结果不等于第5字节,这次读取就应该扔掉。

有一个很多资料没提的细节:当温度低于0℃时,第3字节最高位会被置1。比如收到0x81 0x00,代表-1.0℃,不是129℃。所以在做温度判读时,要先检查第3字节的bit7,决定是否需要做有符号数转换。

3. dht11温湿度模块接线与STM32F1的GPIO设计

3.1 原理图与上拉电阻:从嘉立创画图到实物板

DHT11有两种常见形态:四脚直插封装和四线模块。直插封装引脚定义为VCC、DATA、GND、NC(空脚),模块板通常只要接VCC、GND、DATA三条线。

DATA线必须接上拉电阻,阻值范围4.7k到10k。上拉电阻把空闲总线拉到高电平,同时配合开漏输出实现双向通信。如果省掉上拉,你会发现读到的数据全是0,因为总线没法回到高。模块版本一般已经在板上焊好了上拉电阻和0.1uF去耦电容,但自己画原理图时这两样必须补上。

用嘉立创画图时,我一般会在DATA引脚旁边放一个4.7k电阻到VCC,再在电源引脚旁放0.1uF电容到GND。封装上注意四脚DHT11的NC脚不要悬空到其他网络,画PCB时让DATA走线尽量短,避免靠近电源或PWM信号线。

常见连接方式:

DHT11引脚STM32F1引脚外部电路
VCC3.3V或5V0.1uF电容到GND
DATA任意GPIO,如PB84.7k上拉到VCC
GNDGND
NC不接悬空

注意DHT11供电范围是3.3V到5.5V,但DATA脚电平会跟随VCC。如果STM32F1是3.3V供电,建议DHT11也接3.3V,避免5V电平直接灌进芯片引脚。

3.2 用HAL库把引脚配置成开漏输出

GPIO配置有个选择:用推挽输出加方向切换,还是用开漏输出加外部上拉。我更推荐后者。

开漏输出模式下,写1等于释放总线,由上拉电阻拉高;写0等于强拉低。读取时直接用HAL_GPIO_ReadPin读引脚电平即可,不需要在输入和输出模式之间来回切。STM32F1的GPIO输出寄存器不影响读取到的IDR寄存器,所以开漏模式天然适合单总线。

初始化代码:

__HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_8; // 数据线接PB8 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull = GPIO_NOPULL; // 外部已有上拉 GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速模式,减少边沿变形 HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8, GPIO_PIN_SET); // 初始置高

这段代码的关键是GPIO_MODE_OUTPUT_OD。如果误用推挽输出,又忘记切换方向,主机拉高时会和DHT11的低电平输出打架,轻则读错数据,重则损坏引脚。GPIO_NOPULL也不是随手写的,开漏模式下打开内部上拉会让外部上拉计算变得不稳定,最好只保留一个。

4. 用HAL库写dht11温湿度驱动:最小可用的读取实现

4.1 微秒延时与位读取函数

HAL_Delay只能做到毫秒级,而DHT11要求微秒级延时,所以要先准备一个delay_us。这里用Cortex-M3的DWT计数器,不需要额外占用定时器:

static void delay_us(uint32_t us) { DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; while (DWT->CYCCNT < us * 72); // 72MHz主频,1us约72周期 }

位读取函数跟随协议逻辑:先等低电平开始,再等低电平结束,延时40us采样总线。

static uint8_t DHT11_ReadBit(void) { uint16_t timeout = 500; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_SET && timeout--); timeout = 500; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_RESET && timeout--); delay_us(40); return (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_8) == GPIO_PIN_SET) ? 1 : 0; }

完整读取函数负责发起启动信号、等待响应、收集40位数据,并做校验:

#define DHT11_PIN GPIO_PIN_8 #define DHT11_PORT GPIOB int DHT11_Read(float *humi, float *temp) { uint8_t data[5] = {0}; uint16_t timeout; HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); delay_us(20); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); delay_us(20000); // 启动信号,至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); // 释放总线 delay_us(30); if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET) { timeout = 200; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_RESET && timeout--); timeout = 200; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) == GPIO_PIN_SET && timeout--); for (int i = 0; i < 40; i++) { data[i / 8] <<= 1; if (DHT11_ReadBit()) { data[i / 8] |= 0x01; } } if ((uint8_t)(data[0] + data[1] + data[2] + data[3]) == data[4]) { *humi = data[0] + data[1] * 0.1f; *temp = data[2] + data[3] * 0.1f; return 0; } } return -1; }

启动信号之后,第一次判断GPIO_PIN_RESET是在确认DHT11把总线拉低,进入响应状态。之后两个等待循环分别跳过80us低电平和80us高电平。40次循环里,每8位拼成一个字节,正好填充5字节数组。校验通过后转换成浮点温度湿度。

这里有个容易被忽略的坑:delay_us(20000)在72MHz下循环20次万次,如果被中断频繁打断,实际延时可能远大于20ms,甚至导致后续超时判断失效。因此调用DHT11_Read时最好关掉会造成长时间阻塞的中断,或者在临界区外调用并接受偶尔的读取失败。

4.2 驱动里的3个必调参数

DHT11驱动能跑起来容易,跑稳定需要关注下面几个参数。

参数推荐值调参说明
主机拉低时间20ms低于18ms时传感器可能不响应;超过30ms会白白增加功耗
bit采样延时40us延时太短会把0读成1;太长会把1读成0,建议在34us到45us之间试
读取间隔2sDHT11单次转换周期约1秒,连续读太快会拿到上一次缓存值

主机拉低时间决定了DHT11是否进入工作状态。我见过有人把HAL_Delay(1)当成18ms用,结果传感器几乎不响应,偶尔响应也是乱码。开个定时器在IO上抓波形是排查这类问题最快的方法,没示波器就在启动信号前后各翻转一次另一个LED,用肉眼判断延时是否明显过短。

位采样延时是整个驱动里最敏感的参数。如果传感器离MCU很远、线上电容比较大,高电平边沿会变缓,40us不一定是最优点。这时可以写一个循环,从30us到50us扫描采样点,看哪个值下校验通过率最高。

读取间隔也不能省。DHT11内部没有FIFO,上一次读到的数据会一直保持,直到下一次转换完成。如果应用层每秒读50次,你会看到温度湿度完全不变,这是传感器特性,不是驱动死循环。

5. 让dht11温湿度模块读数更稳的3个收尾技巧

5.1 校验失败后延迟重读,而不是把缓存清零

读取失败时直接返回-1很容易让上层把温湿度显示成0,用户一看就认为传感器坏了。常见做法是保持上一次有效值,并在后台重试。重试间隔用2s,避开DHT11的转换周期。

int retry = 0; while (retry < 5 && DHT11_Read(&humi, &temp) != 0) { retry++; HAL_Delay(2000); }

如果连续5次都失败,再上报错误。这样临时插拔线缆、电源抖动导致的一次失败不会影响显示。

5.2 用连续读数做突变过滤

DHT11正常工作时湿度变化不会每秒超过几个百分点。如果相邻两次读数湿度跳了10%以上,温度跳了3℃以上,大概率是受到了电机启停、继电器吸合这类电磁干扰。增加一个简单的判定:

if (fabs(new_humi - last_humi) > 10.0f || fabs(new_temp - last_temp) > 3.0f) { // 丢弃本次数据,保持上次结果 return; }

这个技巧特别适合用在空调控制器、大棚环境采集这些需要平滑曲线的场景,能省掉后面加卡尔曼滤波的麻烦。

5.3 排查前先量上拉和供电

做了一堆软件优化还是偶尔读错,回头量一下数据线空闲电平。DHT11数据线在没通信时应该接近VCC,如果只有2V左右,说明上拉电阻过大或走线太长。把4.7k上拉换成2.2k,或者剪短杜邦线到20cm以内,很多玄学问题直接消失。供电也尽量单独从STM32F1板子的3.3V引脚拉,别和舵机共用一个电源输出。先把上拉电阻换成4.7k再回来调采样延时,通常比反复改代码更管用。

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

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

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

立即咨询