
简介面向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或SPIDHT11输出的每一位是非标准的脉宽编码所以驱动代码不能只判断电平高低还必须测量高电平持续时间才能区分逻辑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个字节比较相等则表示数据有效。比如某次读到湿度整数0x2638%、湿度小数0x00、温度整数0x011°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驱动最常见的问题方向故障现象最可能原因排查手段读回全0xFFGPIO模式配置错、上拉缺失检查开漏模式与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分钟内自动恢复不用手动复位单片机。本文还有配套的精品资源点击获取