STM32驱动DHT11温湿度传感器:单总线协议与HAL库实战解析
2026/9/9 11:47:12 网站建设 项目流程

很多人第一次接触STM32,做的第一个传感器项目就是DHT11温湿度检测。便宜、够用、代码量适中,还能顺便把GPIO操作、时序协议、串口调试这些基本功都过一遍,确实是入门阶段性价比很高的一块板子。但这个模块有一个特点:它用的是单总线协议,时序要求非常严格,网上教程虽然多,能一次跑通的却不多。我前后在几个项目里都踩过DHT11的坑,从最初的裸机读数据,到后来换成HAL库重写驱动,再到把它接进小型的物联网节点里做周期上报,慢慢把这块小传感器的脾气摸透了。

这篇文章就把我在STM32上驱动DHT11的完整思路整理出来,内容包括DHT11底层工作原理、硬件电路怎么接最稳、HAL库和标准库两种方式下的驱动怎么写、以及实际调试时最容易遇到的几个问题。无论你是刚把开发环境搭好、准备跑第一个外设的新手,还是想把手头项目里温湿度采集部分做得更稳的开发者,这篇内容都可以直接照着操作。

1. DHT11到底是怎么工作的:先搞懂单总线协议

DHT11看起来只是个四脚小器件,但它的通信方式跟I2C、SPI这类标准总线都不一样。它用一根数据线完成双向通信,协议是定制化的单总线时序,主机和设备之间靠严格的电平时间长度来区分“0”和“1”。理解了这套时序,后面写驱动、排查问题都会轻松很多。

1.1 为什么DHT11能这么便宜

DHT11内部集成了一个电阻式湿度传感元件和一个NTC测温元件,再加上一个8位单片机做信号处理和单总线编码,封装在一起后对外只有三个引脚:VCC、GND、DATA。它便宜的原因在于精度本身不高:湿度精度±5%RH,温度精度±2℃,测量范围也有限。但对于大多数消费级、低成本场景,比如室内环境监测、智能鱼缸、机柜温湿度告警这类需求,这个精度完全够用了。

这里要注意,DHT11和DHT22是两代产品。DHT22的精度更高(湿度±2%RH,温度±0.5℃),分辨率也更好,但价格贵好几倍。选型时如果项目对精度要求不高,DHT11就够;如果要做数据记录或者需要小数点后一位的分辨率,直接上DHT22更省事。二者的驱动时序基本兼容,只是数据位定义略有差异,所以先学会DHT11,再切到DHT22的成本很低。

1.2 单总线通信的完整握手过程

DHT11的数据引脚是开漏输出,外部需要接一个上拉电阻(通常4.7kΩ到10kΩ),空闲时数据线保持高电平。一次完整的读取过程分三个阶段:

第一阶段,主机发起起始信号。主机先把数据线拉低,持续至少18ms(我一般拉到20ms以上,确保DHT11能识别),然后释放总线。这个低电平时间不能太短,DHT11内部单片机需要一定时间唤醒;时间太长也没关系,无非是多等一会儿。

第二阶段,DHT11响应。主机释放总线后,DHT11会把数据线拉低80us,再拉高80us,作为应答信号。如果总线上没有设备响应,或者接线有问题,这80us的低电平不会出现,读到的数据就是全1或者全0。

第三阶段,数据输出。响应完成后,DHT11开始连续输出40bit数据,顺序是:湿度整数部分、湿度小数部分、温度整数部分、温度小数部分、校验和,每个字节8位,高位在前。40bit发送完毕,DHT11释放总线,回到空闲状态。

每一位数据的表示方式很有意思:每一位都以50us的低电平开始,表示“开始位”,然后根据高电平的持续时间区分“0”和“1”。高电平持续26到28us表示“0”,持续70us表示“1”。所以读取数据的关键就是测量每bit高电平的宽度。

校验规则也很简单:前四个字节相加,低8位等于第五个字节,就说明数据有效。比如湿度整数0x34、湿度小数0x05、温度整数0x18、温度小数0x01,那校验和应该是0x34 + 0x05 + 0x18 + 0x01 = 0x52。校验不对的时候,建议直接丢弃这次数据,不要拿错误数据去做控制逻辑。

2. 硬件连接与电路设计:别小看这颗上拉电阻

DHT11的硬件连接看起来简单到只需要三根杜邦线,但在实际项目中,电路设计的好坏直接影响读取成功率。尤其当数据线比较长、或者模块供电电压不稳的时候,问题会非常明显。

2.1 引脚连接方式

DHT11模块通常有三种形态:裸芯片(四脚直插)、小板模块(带焊盘和上拉电阻)、以及封装好的防水探头。最常见的开发板配套模块,板上已经集成了上拉电阻和去耦电容,直接用杜邦线连STM32即可。引脚对应关系如下:

DHT11引脚STM32引脚说明
VCC3.3V或5VDHT11供电范围3.3V~5.5V
DATA任意GPIO(本文以PA0为例)需要配置为开漏输出或推挽输出+外部上拉
GNDGND共地必须接好
NC悬空空脚,不需要连接

如果是裸芯片,必须在DATA引脚和VCC之间接一个4.7kΩ上拉电阻。有些同学直接用推挽输出模式驱动,不加上拉电阻,短距离测试也能工作,但这是不稳定的。因为DHT11的数据引脚是开漏输出,如果没有外部上拉,设备拉高电平的能力基本没有,只能靠STM32侧的推挽输出来维持逻辑高电平,这会带来两个问题:一是通信时电平翻转的沿不够陡峭,时序容错变差;二是如果引脚配置切换不及时,总线状态可能不确定。稳妥的做法是:MCU侧GPIO用开漏输出模式,配合外部上拉电阻;或者用推挽输出模式,但必须保证外部电路有上拉。

2.2 供电与滤波细节

DHT11对供电纹波不算敏感,但如果你用STM32开发板上的3.3V供电,且板上还有其他大电流外设(比如ESP8266、蜂鸣器、舵机),DHT11的读数可能会偶发异常。这是因为Wi-Fi模块或电机启动瞬间会把电源电压拉低,导致DHT11内部逻辑混乱,输出无效数据。

我实测下来,给DHT11单独供电或者从电源模块引一路干净的3.3V/5V,读取成功率会有明显改善。另外,在VCC和GND之间并联一个0.1uF去耦电容,放置在靠近DHT11供电引脚的位置,能有效抑制高频噪声。这个电容在很多模块上已经焊好了,但你如果自己画PCB,一定要记得加。

2.3 数据线长度与线序问题

DHT11的单总线协议对时序要求虽然严格,但对于20cm以内的杜邦线,问题不大。如果数据线超过50cm,寄生电容会拉慢电平转换速度,导致MCU采样到的脉冲宽度发生偏差,可能出现一部分bit读成0、一部分bit读成1,最终校验失败。

另外,我之前遇到过一种诡异现象:模块正常、代码正常,但插上某个特定杜邦线后数据就一直超时。排查了半天,发现是那根杜邦线内部接触不良,时通时断。所以排查DHT11问题时,第一步永远先换线、换供电、换模块,排除硬件接触问题,再去查代码。

3. 软件设计与驱动实现:HAL库和标准库都能跑

DHT11的驱动不依赖任何标准外设库,本质上就是“GPIO输出控制+微秒级延时+GPIO输入捕获”。HAL库或标准库的差异只体现在GPIO初始化和电平读写函数的调用上,时序部分完全一样。这部分我给出HAL库版本的实现,并对关键代码做逐段说明。标准库版本只需要把GPIO操作函数替换成GPIO_SetBits、GPIO_ResetBits、GPIO_ReadInputDataBit即可。

3.1 微秒级延时的正确姿势

HAL_Delay()只能做到毫秒级,而DHT11时序里面大量用到几十微秒的延时,所以必须自己实现us级延时。常见方案有两种:使用SysTick定时器,或者使用DWT(Data Watchpoint and Trace)模块的周期计数器。

我更推荐DWT方案,因为它不需要占用SysTick中断,也不会影响HAL库原本的时基。核心代码如下:

static void DWT_Delay_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } static void DWT_Delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000U); while ((DWT->CYCCNT - start) < ticks); }

这段代码的原理是:使能DWT的周期计数器,让它在每个内核时钟周期自动加1,然后通过计算两个时刻之间的差值来延时。SystemCoreClock是内核时钟频率,例如STM32F103在72MHz主频下,SystemCoreClock / 1000000 = 72,也就是延时1us需要72个周期。需要注意,如果主频改了,延时时间也要跟着变,所以用SystemCoreClock动态计算,而不是写死一个数字。

为什么强调不要用普通的for循环做延时?因为编译器优化等级不同,同样的循环次数在不同优化级别下延时差异非常大。代码在-O0下调试正常,一开-O2优化,时序就乱了,这种问题排查起来特别头疼。

3.2 复位与应答检测

初始化DHT11时,先让MCU拉低数据线,保持20ms,然后拉高并释放总线。这里有个容易踩的坑:GPIO方向切换要及时。在HAL库中,如果用推挽输出模式,释放总线就是把引脚置高,但设备应答时会把总线拉低,如果此时引脚还是输出模式,就会出现总线争用问题。所以读取数据前要把GPIO切换到输入模式。

#define DHT11_GPIO_PORT GPIOA #define DHT11_GPIO_PIN GPIO_PIN_0 #define DHT11_GPIO_CLK_ENABLE() __HAL_RCC_GPIOA_CLK_ENABLE() void DHT11_Start(void) { GPIO_InitTypeDef GPIO_InitStruct = {0}; HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_RESET); DWT_Delay_us(20000); // 拉低至少18ms,这里用20ms HAL_GPIO_WritePin(DHT11_GPIO_PORT, DHT11_GPIO_PIN, GPIO_PIN_SET); DWT_Delay_us(30); // 释放总线后保持30us GPIO_InitStruct.Pin = DHT11_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); }

为什么释放总线后要延时30us?因为DHT11接收到起始信号后,需要一小段时间准备应答。这个时间不能太长,否则会错过应答信号的前沿。30us是一个比较安全的值,既给设备留了响应时间,又不会错过80us低电平的开始。

3.3 数据位读取的核心函数

读取每一位的关键是判断高电平持续时间。标准做法是:先等待低电平结束(也就是50us低电平的尾部),然后开始计时高电平宽度。

uint8_t DHT11_ReadByte(void) { uint8_t data = 0; for (int i = 0; i < 8; i++) { // 等待50us低电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_RESET); DWT_Delay_us(40); // 延时40us后采样 if (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET) { data |= (0x80 >> i); } // 等待高电平结束 while (HAL_GPIO_ReadPin(DHT11_GPIO_PORT, DHT11_GPIO_PIN) == GPIO_PIN_SET); } return data; }

这段代码的思路是:每个bit开始时,数据线会先出现50us的低电平,此时不断循环等待,直到变高。变高之后延时40us,然后采样引脚电平。如果采样到高电平,说明高电平持续时间超过了40us,那就是“1”;如果采样到低电平,说明“0”的短高电平(26~28us)已经结束,那就是“0”。

判断逻辑其实很粗暴,但足够可靠。为什么要采样两次而不是直接计时?因为用DWT延时40us后采样,比频繁调用HAL_GPIO_ReadPin去轮询更稳定,也更容易理解。如果MCU主频较低,或代码有其他中断干扰,轮询方式的误差会变大。

3.4 数据校验与完整读取流程

40bit数据读取完成后,需要做校验。完整的读取函数如下:

uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] = {0}; // 重新初始化GPIO为输出模式 GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = DHT11_GPIO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DHT11_GPIO_PORT, &GPIO_InitStruct); // 发送起始信号 DHT11_Start(); // 等待应答信号:先低后高 uint32_t 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) { if (--timeout == 0) return 1; } // 读取40bit数据 for (int i = 0; i < 5; i++) { data[i] = DHT11_ReadByte(); } // 校验 uint8_t checksum = data[0] + data[1] + data[2] + data[3]; if ((checksum & 0xFF) != data[4]) { return 2; // 校验错误 } *humidity = data[0]; // 湿度整数部分 *temperature = data[2]; // 温度整数部分 return 0; }

这里返回的湿度和温度都是整数。DHT11的小数部分在很多应用中可以忽略,因为它的精度本身只有±5%RH和±2℃,小数位基本没有参考价值。但如果你用DHT22,建议把小数位也解析出来,因为DHT22的分辨率是0.1。

主循环里调用的频率要注意:DHT11的数据更新周期是1秒到2秒,所以读取间隔不能小于1秒。如果连续快速读取,第二次读取大概率失败。我一般在主循环里用一个1秒的软件定时标志,保证两次读取之间至少间隔1秒。

4. 常见问题与排查技巧实录

DHT11的坑大多数集中在读取失败、数据异常、以及编译环境相关这三类问题上。下面把我在实际项目中遇到过的典型问题和排查思路整理出来。

4.1 读取一直超时,总线没有响应

现象:不管怎么调延时,DHT11_ReadData的返回值永远是1(超时无应答)。排查顺序如下:

第一,测硬件。用万用表量DHT11的VCC和GND之间是否有稳定的3.3V或5V电压。再测DATA引脚空闲时的电平,正常应该是高电平。如果是0V,说明上拉电阻没接或者模块损坏。

第二,查接线。DATA引脚是否真的连接到了正确的GPIO?杜邦线是否插紧?我遇到过一次GPIO初始化用的是PA0,但杜邦线插到了PA1,查了两小时才发现。

第三,确认GPIO配置。开漏输出模式下,必须先设置输出寄存器为高,再切换成输入模式,否则释放总线后引脚是低电平。有些代码库在GPIO_Init之后没有把ODR置位,导致总线一直被拉低,DHT11永远收不到合法的起始信号。

第四,确认延时是否准确。如果DWT_Delay_Init没有调用,或者SystemCoreClock的值不对,20ms延时会严重偏移。建议在调试时加一个引脚翻转示波器看波形,确认起始信号的拉低时间确实大于18ms。

4.2 数据能读到,但校验一直失败

现象:DHT11_ReadData返回2,读出来的五字节数据变化毫无规律,或者某一位总是读错。

这个问题通常出在位读取的延时精度上。DHT11的“0”高电平时间是26~28us,“1”高电平时间是70us,读bit时40us的采样点是关键。如果延时不准确(偏长超过10us以上),就会导致“1”被误判成“0”,因为采样点落到了高电平结束之后。

另外一个容易被忽略的原因:中断干扰。如果项目里开了定时器中断、串口中断,而且中断处理函数耗时较长,可能导致读bit过程中的40us延时被拉长,进而采样错乱。解决方法是:在读取DHT11数据这段时间里暂时屏蔽优先级不高的中断(临界区保护),或者把DHT11读取放在一个中断频率较低的任务里。

还有一种情况:GPIO的翻转速度配置太低。如果用HAL库初始化GPIO为输入模式时设置了GPIO_SPEED_FREQ_LOW,会影响输入采样电路的响应速度,导致边沿检测发生偏移。建议GPIO速度至少设置成GPIO_SPEED_FREQ_HIGH。

4.3 读取失败,烧录时报错“error: no stm32 target found”

这个错误虽然和DHT11本身无关,但在调试DHT11项目时特别常见,因为很多人会手滑把DHT11的DATA线接到SWDIO引脚(PA13)或者SWCLK引脚(PA14)上。接上去之后,调试器无法和STM32建立连接,于是在烧录时报错:

error: no stm32 target found! if your product embeds debug authentication, please check whatever debug authentication is enabled

遇到这个问题,先把DHT11的所有接线拔掉,再尝试连接ST-Link/J-Link。如果拔线后能正常连接,说明是引脚冲突;如果仍然连不上,检查调试器接线、目标板供电、以及BOOT0引脚电平。STM32F103等芯片如果BOOT0被拉高,会进入系统存储器启动模式,此时内核不执行用户程序,SWD仍然可用,但有些情况下调试连接也会异常。

另外,如果程序中初始化GPIO时不小心把SWDIO引脚(PA13、PA14)复用成了普通GPIO输出,并且把电平拉低,就会导致调试器无法连接。这种情况下只能按住复位键的同时点击烧录,或者用串口ISP方式擦除芯片。

4.4 电脑无法识别串口,设备管理器出现感叹号

DHT11项目里经常要加串口打印数据,新手使用国产STM32开发板时,可能会遇到USB转串口芯片驱动问题。如果设备管理器里出现感叹号,设备名类似“STM32 Virtual COM Port”,有两种情况:

一种是板载的USB转串口芯片(比如CH340、CP2102)驱动没有安装,下载对应厂商的驱动安装即可。另一种是STM32芯片本身支持USB虚拟串口(比如STM32F103C8T6、F407),但你烧录的程序里集成了USB库,此时电脑会枚举出一个“STM32 Virtual COM Port”,如果驱动有问题或者固件异常,就会显示感叹号。这个和DHT11没有直接关系,但会影响你观察串口打印的温湿度数据。

排查思路:先确认板子上有没有独立的USB转串口芯片,如果有,检查它的驱动;如果没有,说明需要用STM32的USB接口实现串口通讯,这时需要检查USB库的版本和中断配置。常见的问题是USB库版本和HAL库版本不匹配,或者USB时钟配置错误。

4.5 开机第一次读取正常,之后一直失败

这也是DHT11很经典的“脾气”:模块上电后需要1秒左右的稳定时间,如果上电后立刻读取,大概率超时。另外,DHT11内部的湿敏电容需要一定时间完成充放电稳定,频繁读取会导致它内部状态没恢复。

解决办法很简单:初始化完成之后,延时2秒再开始第一次读取;之后每次读取间隔至少1秒。如果项目里用RTOS,可以单独创建一个DHT11任务,以2秒周期挂起唤醒读取,效果比较稳定。这里补充一个细节:即使读取失败,也不要连续快速重试,否则容易打乱DHT11内部的状态机。失败时建议直接等到下一个周期再试。

5. 应用场景与进阶扩展:DHT11能玩出的花样

DHT11虽然只是个基础传感器,但把它和STM32的不同外设组合起来,能做出不少有意思的东西。这里分享几个我已经落地过的场景,给正在纠结“接下来做什么”的读者一些参考。

5.1 智能鱼缸环境监测

我之前帮朋友做过一个STM32鱼缸控制器,DHT11负责测量环境温湿度,DS18B20测水温,再加上一个水泵继电器和LED补光灯。DHT11的数据用OLED屏实时显示,温湿度超过设定阈值时,触发蜂鸣器报警。

这个项目里最值得注意的一点是:鱼缸周围湿度常年偏高,DHT11探头不能直接放在鱼缸盖正上方,否则水汽会直接附着在传感器表面,导致湿度读数长期处于饱和状态。建议把传感器放在离水面有一定距离、通风的位置。另外,如果鱼缸用了加热棒,环境温度会受加热棒影响,测温数据要和水温参数配合来判断,不能只凭DHT11一个数据做决策。

5.2 宿舍/家居智能控制

配合ESP8266或机智云模块,STM32把DHT11采集的温湿度数据通过串口发送给Wi-Fi模块,再上传到云平台,实现手机远程查看。这个场景也是热词里“stm32 8266 宿舍控制灯开发 实战”比较典型的延伸。

实际开发中有个坑:STM32的串口发送是阻塞式的,如果DHT11读取数据和串口发送同时进行,串口中断可能会打断DHT11的时序。建议在架构上把两个任务错开:先读完DHT11,把结果缓存到全局变量,再进行串口发送,避免在DHT11读取过程中占用过多的CPU时间。如果必须并发,可以在DHT11读取时屏蔽串口中断,或者把DHT11读取的优先级提到最高。

5.3 与K210等AI芯片配合做数据感知

最近不少人在做K210与STM32通讯的项目,比如用K210做图像识别,用STM32做传感器采集和控制。这种异构架构里,DHT11通常挂在STM32上,STM32把温湿度数据通过串口或SPI/I2C发给K210,K210再结合视觉信息做综合决策。

这种场景下要注意通讯协议的设计,不能简单地把温湿度作为裸数据发送,建议定义一个简单的帧格式,比如帧头+设备ID+湿度+温度+校验,这样K210解析起来更稳。还有一个细节:如果STM32和K210的供电不是同一个电源,串口通讯两端必须共地,否则可能出现数据乱码或者通讯不稳定。

5.4 数据处理的小技巧:滤波与迟滞

DHT11读数本身有一定波动,尤其湿度值,相邻两次读取可能跳变几个百分点。如果直接拿这个值去控制继电器或者触发电机,设备会频繁启停。

我的做法是在应用层加一个简单的滑动滤波:保留最近5次湿度值,每次取平均作为当前湿度。如果要做阈值控制,再加一点迟滞。比如设定湿度低于40%开加湿器,高于45%才关加湿器,这样能避免设备在临界点反复切换。

温度数据相对来说稳定一些,但如果你发现温度读数在1℃范围内来回跳,也可以用同样的平均值滤波处理。需要注意的是,DHT11的温度测量响应速度比较慢,不要期望它能快速捕捉温度突变。

6. 踩坑总结与个人体会

DHT11这个模块,代码量不大,难度也主要在时序控制上,但实际调试中暴露出来的问题,往往来自硬件细节、环境干扰、以及工程配置,而不是协议本身。我把这几年涉及DHT11的项目经验浓缩成几条建议,大家可以直接参考。

第一,一定要给DHT11留足“静默时间”。上电稳定1秒、两次读取间隔至少1秒,这条如果做不到,后续所有问题都会被放大。

第二,GPIO方向转换要果断。输出模式释放总线后要立即切换到输入模式,中间不要插入额外的延时。有的同学在切换模式前还用HAL_Delay延时几毫秒,这会导致总线悬空状态持续时间过长,增加误码概率。

第三,数据校验必须写。即使你的项目只是demo,也建议养成读取数据后做校验的习惯。校验失败就抛掉这一帧,等下一周期再读。这个习惯能让你在后续接DHT22、甚至接其他单总线传感器时省下大量排查时间。

第四,示波器或逻辑分析仪是真的神器。调试DHT11时,用示波器抓一次起始信号和应答信号的波形,比盲调代码高效十倍。先看主机信号是否正常,再看设备应答是否出现,最后看每个bit的高低电平宽度是否符合协议,问题出在哪一层一目了然。

DHT11不是精度最高的传感器,但它的单总线通信机制是一个非常好的学习样本。搞懂了它,再去读DHT22、DS18B20这类同样基于单总线的传感器,你会发现套路都差不多:都是主机拉低触发、设备应答、然后按bit输出数据。把DHT11的驱动写稳了,后面很多外设驱动都能触类旁通。

另外顺便说一句,如果你用的是APM32这类国产替代芯片,DHT11驱动代码基本可以直接复用,因为它们的内核和GPIO寄存器结构兼容性很好。但切芯片前最好对照一下数据手册确认GPIO开漏模式和延时函数所需的内核时钟设置,别默认万事大吉。

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

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

立即咨询