简介:面向Arduino、树莓派等微控制器平台的DHT11温湿度传感器驱动代码包,适合智能家居、环境监测及农业自动化等场景的开发者快速集成。压缩包内共3个文件,包含C语言源文件与头文件,另附一个zip归档,整体大小仅2KB,结构紧凑清晰,便于直接嵌入嵌入式工程或二次移植。代码覆盖初始化、单总线通信时序、40位数据读取、校验和验证、温湿度换算以及错误重试等关键环节,既能直接调用获取实时数据,也可作为学习单总线协议与传感器驱动开发的参考实例。已有311人学习下载,对于希望理解DHT11底层时序、封装传感器驱动库或排查通信异常的用户,是一份精简实用的参考资料。
1. 从bsp_dht11.c说起:为什么数字温湿度传感器还要写驱动
DHT11大概是智能家居和嵌入式入门项目里最容易踩坑的传感器之一:看似只有一根数据线,但数据手册上写的"单总线协议"背后是对微秒级延时的严格考验。很多新手把DHT11接到STM32F103的GPIO上,直接调用HAL库的HAL_GPIO_ReadPin去读,结果读回来的永远是0xFF或者数据校验不过。倒不是传感器坏了,而是DHT11的时序要求数据线在主机控制下完成拉低、释放、采样三个动作,每个动作的窗口只有几十微秒,HAL库的冗余操作会把时序完全打乱。这个压缩包里拆出来的bsp_dht11.c和bsp_dht11.h就是一套可以移植到HAL库工程下的裸机驱动。正在做环境监测、智能家居或者课程设计的人,以及想彻底搞懂单总线时序的开发者,都能从这份驱动代码里拿到可直接用的东西。
2. DHT11温湿度传感器单总线协议与40位数据帧结构
2.1 单总线为什么能在一根线上同时收发
DHT11的输出时序和单总线协议有相似之处,但DHT11更简单,总线上只能挂一个传感器,主机与传感器分时占用数据线。初始化时,主机先把数据线拉低至少18ms,再释放总线并保持20到40us高电平,传感器检测到起始信号后主动拉低80us作为应答,接着再拉高80us,然后连续输出40位数据。整个过程中,数据线电平的切换频率远高于普通I2C或SPI,DHT11输出的每一位是非标准的脉宽编码,所以驱动代码不能只判断电平高低,还必须测量高电平持续时间才能区分逻辑0和逻辑1。
理解这一点非常关键。不少网上流传的DHT11驱动读不到数据,问题不在于传感器,而在于代码里缺少对高电平持续时间的判断。有的驱动用delay_us(40)后直接读引脚,把采样点放在逻辑0和逻辑1的分界线上;有的驱动则用超时循环等待引脚变低,再从低到高计时。两种思路各有取舍,后面会展开。
2.2 40位数据帧的排列与校验规则
完成应答后,DHT11会一次性输出40位数据,顺序是:8位湿度整数、8位湿度小数、8位温度整数、8位温度小数、8位校验和。这里有个容易踩的坑:标准DHT11的湿度小数位和温度小数位恒为0,但在驱动里仍然要把这5个字节完整读出来,因为第5个字节是校验和,依赖前四个字节。如果只读前4个字节,校验逻辑就无从谈起。
校验和的算法是前四个字节相加后取低8位,与第5个字节比较,相等则表示数据有效。比如某次读到湿度整数0x26(38%)、湿度小数0x00、温度整数0x01(1°C)、温度小数0x00,那么校验和应该等于0x26加0x00加0x01加0x00,也就是0x27。实际驱动里的判断就是把这个值强制转换成uint8_t再比较,溢出由C语言自然截断处理。
2.2.1 bit编码:26us与70us的长度差异
DHT11每一位的编码是固定的:先拉低50us作为起始电平,然后拉高,拉高的时间长度表示数值。拉高26到28us表示逻辑0,拉高70us表示逻辑1。采样点应当放在高电平中间区间,常见做法是检测到上升沿后延时40us再读引脚,40us刚好大于逻辑0的26到28us、小于逻辑1的70us,所以能稳定区分0和1。
下面是DHT11核心时序参数表,移植驱动时可以直接对照:
| 时序段 | 参数名称 | 持续时间/电平 | 方向 |
|---|---|---|---|
| 起始信号 | 主机拉低总线 | 大于等于18ms | 主机到传感器 |
| 释放总线 | 主机释放上拉 | 20到40us | 主机到传感器 |
| 应答低电平 | 传感器拉低应答 | 80us | 传感器到主机 |
| 应答高电平 | 传感器拉高释放 | 80us | 传感器到主机 |
| 数据位0 | 低电平50us,高电平26到28us | 总时长约76到78us | 传感器到主机 |
| 数据位1 | 低电平50us,高电平70us | 总时长约120us | 传感器到主机 |
需要注意的是,时序参数在不同型号的DHT11模块上会略有偏差,尤其是上拉电阻阻值不同时,高电平的上升沿会变得平缓,实测的26us可能变成30us甚至40us。这也是为什么驱动里不能把采样延时的40us写成固定值,最好定义成一个宏,方便在真机上调。
2.3 用GPIO模拟读取,为什么不用硬件外设
STM32F1没有专门的单总线控制器,DHT11也没有标准的I2C地址,所以只能用GPIO模拟。实验里有一个判断位电平的经典写法,类似下面这样:
static uint8_t dht11_read_bit(void) { uint8_t bit_value = 0; /* 等待数据线被传感器拉低,表示开始输出一位 */ while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); /* 等待引脚从低电平变为高电平,上升沿到来 */ while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_RESET); /* 延时40us后采样,落在逻辑0和逻辑1的分界区域 */ delay_us(40); if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET) { bit_value = 1; } else { bit_value = 0; } /* 等待当前位的剩余高电平结束,再返回 */ while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); return bit_value; }这段代码里的两个while等待循环是识别位边界的核心:第一个循环等引脚变低,说明传感器开始输出一位;第二个循环等引脚变高,说明进入数据位的高电平阶段;然后在40us处采样。delay_us(40)的参数不是拍脑袋定的,而是逻辑0高电平26us和逻辑1高电平70us的中点附近。如果延时太短,逻辑0也可能被误判成1;延时太长,逻辑1在40us后仍为高,但接近70us末端时电平会下降,采样窗口变小。
3. STM32 HAL库驱动DHT11:从bsp_dht11.c移植到实际工程
3.1 文件结构与GPIO开漏配置
压缩包里拆出的bsp_dht11.c和bsp_dht11.h各司其职:dht11.h里定义端口宏、错误码和函数声明,dht11.c里放初始化函数、延时函数和读取函数。移植到STM32CubeMX生成的项目后,把这两个文件加入编译,并在main.c里声明调用即可。
初始化GPIO时,DHT11的数据线需要配置成开漏输出模式,而不是推挽输出。原因是开漏模式下,主机只能主动拉低电平,释放后就由外部上拉电阻把总线拉高。传感器应答时主动拉低总线,如果主机和传感器同时输出相反的推挽电平,引脚上会出现短暂的电平冲突,严重时损坏IO。以下是一个基于HAL库的初始化代码,对应STM32F1系列:
void bsp_dht11_init(void) { GPIO_InitTypeDef gpio_init_struct = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); gpio_init_struct.Pin = DHT11_GPIO_PIN; /* 默认PB11 */ gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD; /* 开漏输出 */ gpio_init_struct.Pull = GPIO_PULLUP; /* 内部上拉辅助 */ gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速翻转 */ HAL_GPIO_Init(DHT11_GPIO_PORT, &gpio_init_struct); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); }开漏输出模式下,STM32读取引脚电平走的是输入数据寄存器,所以HAL_GPIO_ReadPin仍然可以正常工作。内部上拉选择GPIO_PULLUP是辅助性质,最终决定电平恢复速度的是外部上拉电阻。数据手册推荐5K左右的上拉,模块板上通常已经贴好10K,直接连接即可。
3.2 微秒级延时的三种实现方案与选型
DHT11驱动里最难的不是逻辑,而是延时。HAL_Delay最小单位1ms,没法用;普通for循环延时在不同优化等级下结果完全不同,不适合正式项目。下面是三种实际可用的方案对比:
| 延时方案 | 精度 | 资源占用 | 适用场景 |
|---|---|---|---|
| 空循环延时 | 低,受编译器优化影响 | 无 | 临时验证、教学 |
| 定时器延时 | 高,1us稳定 | 占用一个TIM外设 | 团队项目,便于统一管理 |
| DWT延时 | 高,1个CPU时钟周期 | 不占用外设 | 大多数单总线传感器 |
个人推荐DWT方案,原因有三个:精度足够、不占用定时器、初始化代码只有三行。DWT是Cortex-M3内核自带的调试组件,CYCCNT寄存器会随CPU时钟自动递增,不需要外设时钟。下面是配置和延时函数:
/* 启用DWT计数器,必须在读取前调用一次 */ static void dht11_delay_us_init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } /* 延时us微秒,要求主频能整除1000000 */ static void dht11_delay_us(uint32_t us) { uint32_t start_val = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000U); while ((DWT->CYCCNT - start_val) < ticks); }注意在系统时钟不是整数MHz时需要先校准,比如某些F103平台外部晶振配成8MHz再加PLL倍数,SystemCoreClock通常为72MHz,计算后每个微秒等于72个时钟周期。代码里的SystemCoreClock是全局变量,HAL库初始化时会被自动赋值,直接使用即可。
3.3 读取主流程:起始信号、应答检测与40位解析
bsp_dht11.c中读取函数的整体流程是:主机拉低数据线20ms,释放后延时30us,然后等待传感器应答;传感器应答的80us低电平加上80us高电平结束后,按位读取40个数据位;最后做校验和判断,把结果写入传入的参数地址。下面是可直接嵌入工程的读取函数:
uint8_t bsp_dht11_read_data(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] = {0}; uint16_t timeout = 0; /* 1. 起始信号:拉低20ms */ HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); dht11_delay_us(20000); HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); dht11_delay_us(30); /* 2. 等待传感器应答,每个等待都带超时保护 */ timeout = 10000; while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET) { if (--timeout == 0) return 1; /* 无应答 */ } while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); /* 3. 读取40位数据,依次存入data[0]到data[4] */ for (int i = 0; i < 40; i++) { data[i / 8] <<= 1; if (dht11_read_bit() == 1) { data[i / 8] |= 0x01; } } /* 4. 校验和:低8位加和比对 */ if ((uint8_t)(data[0] + data[1] + data[2] + data[3]) != data[4]) { return 2; /* 校验失败 */ } *humidity = data[0]; /* 湿度整数 */ *temperature = data[2]; /* 温度整数 */ return 0; }代码分四步注释了读取流程。第2步里的三个while循环,第一个用于清高电平直到传感器拉低,第二个等待80us低电平结束,第三个等待80us高电平结束。每个循环在正常情况下都会在100us内结束,但第一个循环前面加了timeout,防止传感器未连接时总线始终保持上拉高电平导致死循环。函数返回值0表示成功,1表示传感器无应答,2表示校验和错误。
3.3.1 在FreeRTOS任务中调用时的保护方式
如果DHT11读取函数运行在FreeRTOS的任务里,建议在调用前用taskENTER_CRITICAL()进入临界区,把整个读取过程保护起来,防止任务切换打断40位数据的采样节奏。在STM32F103的主频下,DHT11单次读取大约耗时4到5ms,这个短暂的临界区对大多数应用可以接受。要注意的是,临界区内部不能调用HAL_Delay或者dht11_delay_us之外的阻塞操作,以免把系统时钟节拍拖垮。
4. STM32F1平台实装:接线上拉、信号完整性与故障定位
4.1 原理图设计中容易忽略的上拉问题
最近不少新手在画DHT11原理图时直接照搬模块的参考设计,结果在嘉立创画图阶段就把上拉电阻漏了。DHT11有三种常见形态:四脚裸传感器(VDD、DATA、NC、GND)、三针模块(VCC、DATA、GND)和带PCB的集成模块。裸传感器的原理图里必须在DATA和VDD之间放一个4.7K到10K的上拉电阻,而不是依赖单片机内部上拉。STM32F103的内部上拉约30K到50K,阻值偏大,高电平恢复速度慢,在长走线场景下会出现上升沿过缓的问题。
用嘉立创画完原理图后,建议把DATA线的走线宽度设为10mil以上,长度控制在20cm以内,并远离电感负载和大电流回路。如果模块与MCU的距离超过30cm,最好改用屏蔽线或者把上拉电阻移到靠近MCU的一端,减小寄生电容对上升沿的影响。对DHT11这种微秒级电平判断的传感器,布线寄生电容的影响往往比传感器本身的精度还明显。
4.2 中断环境下的读取稳定方案
在温湿度显示或环境监测项目里,STM32F103通常还承担着串口打印、按键扫描、屏幕刷新等工作。如果系统打开了多个中断,DHT11读取时一旦被高优先级中断打断,采样点的电平持续时间就会被拉长,导致位解析错误。比较常用的保护方式如下:
/* 在非FreeRTOS工程中,读取前后关闭并恢复中断 */ __disable_irq(); uint8_t ret = bsp_dht11_read_data(&humidity, &temperature); __enable_irq();__disable_irq()会把全局中断全部关掉,代价是读取期间按键响应和串口接收都会暂停。更精细的做法是使用BASEPRI寄存器屏蔽优先级低于指定值的中断,保留SysTick和HardFault,这样既能保证DHT11时序,又不至于让系统完全停止响应。STM32F1的BASEPRI只对Cortex-M3有效,具体数值需要根据实际中断优先级分组来定。
4.3 高频故障定位:读回0xFF、数据跳变、校验失败
下面的故障排查表收集了DHT11驱动最常见的问题方向:
| 故障现象 | 最可能原因 | 排查手段 |
|---|---|---|
| 读回全0xFF | GPIO模式配置错、上拉缺失 | 检查开漏模式与4.7K到10K外部上拉 |
| 数据偶尔跳变 | 供电纹波、数据线受干扰 | 加100nF去耦电容,远离电机或PWM线 |
| 校验和反复失败 | 采样延时偏离时序标准 | 示波器抓波形,微调delay_us(40) |
| 长时间运行后无响应 | 传感器状态锁死,代码无超时 | 检查每个while循环的超时保护 |
| 温度湿度恒为0 | 字节解析顺序错误 | 核对湿度整数与温度整数下标位置 |
排查时有一个实用的小技巧:把读取到的5个原始字节打印出来,不要只打印温度和湿度。因为当校验和失败时,前4个字节里可能只有个别位错误,直接对比原始数据能快速定位是采位逻辑问题还是某个字节的延时问题。调试代码可以参考下面这个打印函数:
void dht11_debug_show_raw(uint8_t *data) { printf("RAW: %02X %02X %02X %02X %02X SUM=%02X\r\n", data[0], data[1], data[2], data[3], data[4], (uint8_t)(data[0] + data[1] + data[2] + data[3])); }如果打印结果显示data[0]和data[2]始终成倍漂移,比如湿度从0x22跳到0x11,说明读取位时出现了整体移位,多半是起始位或应答位的边缘检测多等了一个周期。此时应回到dht11_read_bit,检查上升沿检测的while循环是否提前退出了。
5. 进阶:温湿度数据的滤波策略与低功耗采样周期设计
5.1 中值滤波与多次采样窗口的选择
DHT11单次采样的随机误差在正负1°C左右,直接显示在LCD上会出现轻微跳动。连续读5次、每次间隔2秒,排序后取中间值,能消除大部分毛刺,代价是响应速度变慢。对于室内温湿度监控这种慢变量场景,7点中值滤波配合30秒采样周期效果更好。
5.2 低功耗应用里让DHT11只在采样瞬间供电
电池供电的温湿度记录仪里,DHT11不需要一直通电。常见做法是用一颗P-MOS管或负载开关控制传感器VCC,平时断电,RTC定时唤醒后在采样前300ms再上电,让传感器稳定后再发起始信号。这样静态功耗几乎只来自上拉电阻,单节CR2032也能撑数个月。读取完成后记得把GPIO设回低电平并关闭DHT11电源,避免总线悬空漏电。
5.3 失败恢复:连续错误3次后重新初始化
驱动里返回的错误码不要只用来打印日志,对外层应用应该有实际的恢复动作。我会维护一个简单的状态节点,像下面这样:
typedef struct { uint8_t last_error; uint8_t fail_count; uint8_t humidity; int8_t temperature; } dht11_state_t;每次读取返回非0时递增fail_count,达到3次就调用bsp_dht11_init()重新初始化GPIO并延时500ms重试;一旦读取成功就把fail_count清零。这种机制能让传感器在拔插或总线干扰后最多2分钟内自动恢复,不用手动复位单片机。
本文还有配套的精品资源,点击获取