资讯动态

DHT11单总线协议深度拆解:从时序原理到STM32实战驱动

发布时间:2026/9/8 6:57:57 来源:尧图企业网站定制
最近在做一个环境监测的小项目需要在单片机上读取温湿度数据。翻了一圈传感器最后又用回了DHT11。说实话这颗芯片在工程师圈子里口碑两极分化很严重有人觉得它精度一般、时序敏感有人觉得它便宜好用、单根线就能通信。我的实际体验是只要把它的协议真正吃透DHT11在低成本、低复杂度的场景里依然是非常能打的选择。这篇博文就从器件原理、单总线协议细节到代码实现完整拆一遍我在实战中总结出来的方案希望能帮你少踩几个坑。先说清楚这篇文章适合谁手头有STM32或者类似单片机想用DHT11但读出来的数据偶尔是0或者干脆没反应的人以及刚接触单总线通信想理解“一根线怎么既发命令又收数据”的初学者。如果你只是调包侠拿着现成库一顿复制粘贴那可以关掉了因为这篇文章不讲“怎么抄”讲的是“为什么这么做”。1. 内容整体设计与思路拆解1.1 为什么选DHT11而不是DHT22或SHT30很多人在选型时会纠结同价位附近有DHT22、SHT30这些看起来参数更漂亮的传感器。我的看法是选型首先要看场景。DHT11的核心优势是单总线通信只需要一根数据线主控IO资源极度节省价格极其便宜批量采购一两块钱一颗软件模拟协议成熟不需要额外的硬件I2C或SPI外设。缺点是精度一般湿度误差±5%RH温度误差±2℃测量范围也窄0到50℃20%到90%RH更新速率只有1Hz。如果你的项目是智能家居里的大棚环境监测、机房温湿度记录这种对精度要求不高的场景DHT11完全够用。但如果你做的是医疗设备、精密仪器校准那直接上SHT30或者BME280别在DHT11上浪费时间。这就像买车DHT11是代步小车SHT30是性能车先想清楚用途再掏钱。1.2 单总线通信的核心思路一根线如何完成双向通信单总线1-Wire协议的名字很直白就是一根数据线完成主机和从机之间的双向通信。DHT11用的就是这种通信方式和DS18B20属于同一个家族思路。它的工作方式可以类比成两个人用一根绳子传纸条某个人先拉一下绳子主机拉低总线告诉对方“我要开始说话了”然后松开绳子拉高释放总线轮到对方回应。DHT11接收到主机的起始信号后会发回一串脉冲主机通过测量每个脉冲的宽度来判断数据是0还是1。一根线双向通信的关键是“时分复用”和“开漏输出”。DHT11的数据引脚属于开漏结构外部需要接一个上拉电阻通常是4.7kΩ到10kΩ默认状态下总线被上拉到高电平。主机想发送信号时把引脚配置为输出模式拉低总线想接收数据时把引脚配置为输入模式释放总线让上拉电阻把电平拉高然后检测从机拉低总线的时刻。这个设计的好处是节省引脚坏处是时序要求严格稍有偏差就通信失败。下面第二章节详细讲协议层的每个时间参数。2. 单总线协议深度拆解从时序图读懂DHT112.1 DHT11的数据帧结构和校验机制先看通信数据格式。DHT11一次完整传输40位数据顺序是湿度整数部分8位湿度小数部分8位温度整数部分8位温度小数部分8位校验和8位需要特别说明的是DHT11的“小数部分”在实际应用中基本是0因为它的分辨率就是1%RH和1℃。但协议里依然保留了小数位读取的时候要当成有效数据来处理校验和的计算是把前四个字节加起来取低8位。如果算出来和校验字节不一致这帧数据就得丢弃。举个例子如果读到湿度整数0x3250%RH、湿度小数0x00、温度整数0x1A26℃、温度小数0x00、校验和0x4C那校验和就等于0x320x000x1A0x000x4C说明数据有效。2.2 主机起始信号和DHT11响应时序通信的第一步是主机发起起始信号。具体操作是主机把数据引脚拉低持续时间至少18ms。我实测下来18ms是下限用20ms更稳妥因为有些DHT11批次对18ms响应不够干脆。主机释放总线拉高然后等待20到40us。此时DHT11会主动拉低总线80us作为响应信号然后再拉高80us准备发送数据。从主机的视角看这个响应信号是“一个低脉冲接一个高脉冲”。如果主机发送起始信号后等了好久都没有看到这串脉冲大概率是接线问题或者传感器坏了。时序参数在这个阶段有一个很容易被忽略的细节主机拉低的时间不能太长也不能太短。太长会让DHT11误判为持续低电平故障太短则DHT11根本没反应过来。我之前有一次用延时函数不精准实际拉低了30多msDHT11偶尔能响应偶尔不能折腾了很久才发现是延时过长的问题。2.3 数据位“0”和“1”的区分方式起始信号之后DHT11开始发送40位数据。每一位数据的结构都一样先拉低总线50us然后拉高总线高电平持续的时间决定了这一位是0还是1。关键的时间标准在这里高电平持续26到28us表示“0”高电平持续70us左右表示“1”读取数据的代码思路就很清晰了等DHT11拉低总线每一位开始的50us低电平然后等总线拉高再测量从拉高到再次拉低之间的时间。如果高电平时间小于50us判为0如果大于50us判为1。这里要注意的是不同厂家的DHT11时序参数会有微小差异判0/1的阈值不要卡得太死。从26us到70us之间差距很大取一个50us的中间值作为阈值容错性最好。有些教程里用40us作为阈值也能用但我觉得50us更稳。2.4 通信结束与总线释放40位数据全部发送完毕后DHT11会释放总线由上拉电阻把电平拉高。此时一次完整的通信过程结束总线回到空闲状态。空闲状态的高电平很重要。很多人在写完读取代码后发现第二次读取失败就是因为上一次通信结束后总线没有恢复到正确状态。DHT11的采样周期是1秒也就是两次起始信号之间至少间隔1秒否则传感器可能还在处理上一次的数据不会正常响应。实际使用中我习惯把读取周期设在1.5秒到2秒留出足够余量。3. 实战代码全拆解STM32 HAL库下的DHT11驱动3.1 硬件准备和最小电路写代码之前先把硬件接对。DHT11通常有3个引脚有的模块是4脚其中一个悬空VCC接3.3V或5V都可以DHT11的工作电压范围是3.3V到5.5VGND接地DATA数据线接单片机的一个普通GPIO同时通过一个4.7kΩ上拉电阻接到VCC如果你用的是现成的DHT11模块大部分模块上已经集成了上拉电阻和滤波电容直接接杜邦线就行。如果是裸的传感器别偷懒上拉电阻一定要加否则总线浮空通信必然失败。在主控选择上我以STM32F103系列配合HAL库为例。为什么选这个组合因为STM32F1是市面上保有量最大的MCU之一HAL库也是现在CubeMX默认生成的代码框架通用性最强。你用其他型号或者标准库逻辑是完全一样的。3.2 微秒级延时函数的实现DHT11时序操作对延时精度要求很高20us、50us、70us这些时间参数普通单片机自带的延时函数很难做到精确。所以第一步要搞定微秒级延时。在STM32上我推荐两种方案。第一种是用定时器配置一个1us递增的定时器延时时候读取计数器值做忙等。第二种是用DWTData Watchpoint and Trace模块这是Cortex-M3/M4内核自带的周期计数器精度高且不占用额外的定时器资源。HAL库下用DWT实现延时的代码很简洁#include stm32f1xx_hal.h static volatile uint32_t *DWT_CYCCNT (uint32_t *)0xE0001004; static volatile uint32_t *DWT_CONTROL (uint32_t *)0xE0001000; static volatile uint32_t *SCB_DEMCR (uint32_t *)0xE000EDFC; void DWT_Delay_Init(void) { *SCB_DEMCR | 0x01000000; // 使能DWT访问 *DWT_CYCCNT 0; // 清零周期计数器 *DWT_CONTROL | 1; // 使能周期计数器 } void DWT_Delay_us(uint32_t us) { uint32_t start *DWT_CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((*DWT_CYCCNT - start) ticks); }这段代码的核心思路是利用CPU的机器周期计数1us对应的周期数等于SystemCoreClock除以1000000。如果你的系统时钟是72MHz那1us就是72个周期。注意减法的无符号运算特性即使计数器回绕也能正确计算差值所以不用担心溢出问题。3.3 GPIO模式的动态切换DHT11通信过程中主机的GPIO要不断在输出模式和输入模式之间切换发送起始信号时是输出模式读取响应和数据时是输入模式。HAL库下将GPIO从输出切换到输入最简单的办法是用HAL_GPIO_WritePin配合GPIO_MODE_OUTPUT同时修改GPIO初始化结构体。但频繁调用HAL_GPIO_Init会带来不小的开销时序上容易出问题。更推荐直接用寄存器操作修改CRL或CRH寄存器。STM32F103的PA0到PA7对应CRL寄存器PA8到PA15对应CRH寄存器。每个引脚占用4位其中MODEx位控制模式CNFy位控制配置类型。下面这个函数可以把指定引脚切换为推挽输出或浮空输入void DHT11_Pin_Output(void) { GPIOA-CRL ~(GPIO_CRL_CNF0_Msk); // 清CNF位 GPIOA-CRL | (0 2); // CNF00推挽输出 GPIOA-CRL ~(GPIO_CRL_MODE0_Msk); // 清MODE位 GPIOA-CRL | (3 0); // MODE1150MHz输出 } void DHT11_Pin_Input(void) { GPIOA-CRL ~(GPIO_CRL_CNF0_Msk); // 清CNF位 GPIOA-CRL | (1 2); // CNF01浮空输入 GPIOA-CRL ~(GPIO_CRL_MODE0_Msk); // MODE00输入模式 }这里以PA0为例如果你的引脚不是PA0按同样的思路修改对应的位偏移即可。用寄存器操作的好处是切换速度极快几个机器周期就搞定了不会破坏时序。3.4 完整读取时序的代码实现在DWT延时和GPIO动态切换都准备好之后就可以开始写读取函数了。整体流程分四步主机拉低总线20ms发送起始信号释放总线切换为输入模式等待DHT11响应检测80us低电平响应信号和80us高电平就绪信号依次读取40位数据最后做校验直接上代码uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0, 0, 0, 0, 0}; uint8_t i, j; // 主机发送起始信号 DHT11_Pin_Output(); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); DWT_Delay_us(20000); // 拉低20ms HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); DWT_Delay_us(30); // 释放总线等待30us // 切换输入模式等待响应 DHT11_Pin_Input(); // 等待DHT11拉低总线响应信号开始 uint16_t timeout 0; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { if (timeout 1000) return 0; // 超时退出 } // 等待DHT11拉高总线80us低电平结束 timeout 0; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { if (timeout 1000) return 0; } // 等待DHT11再次拉低总线80us高电平结束即将发送数据 timeout 0; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { if (timeout 1000) return 0; } // 读取40位数据 for (j 0; j 5; j) { for (i 0; i 8; i) { // 等待50us低电平结束 timeout 0; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_RESET) { if (timeout 1000) return 0; } // 延时30us避开0和1高电平的交界区 DWT_Delay_us(30); // 如果30us后仍然是高电平说明是1否则是0 if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { data[j] | (0x80 i); } // 等待高电平结束进入下一位的50us低电平 timeout 0; while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) GPIO_PIN_SET) { if (timeout 1000) return 0; } } } // 校验 if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) { return 0; } *humidity data[0]; *temperature data[2]; return 1; }这套代码的读位思路是“延时判电平”低电平结束即50us低电平的最后时刻后延时30us如果此时总线仍然是高电平就判定为数据位1。因为数据位0的高电平只有26到28us30us之后已经变低数据位1的高电平有70us30us之后依然维持高电平。这个技巧比“等待高电平结束再测量宽度”要快得多也避免了测量高电平宽度时的额外开销。不过要注意30us这个延时值有讲究不同主频下需要微调。72MHz的STM32F103上我实测30us是最佳值偏低或偏高都会导致误判。3.5 容易忽略的中断和优先级问题还有一个特别容易踩的坑是中断。如果在读取DHT11的过程中来一个UART中断或者定时器中断中断处理函数执行时间超过10us那整个时序就乱了。数据位0的高电平只有26us左右一个高优先级中断就能把这个窗口直接吃掉导致误判。解决办法有几种如果项目里中断比较多在读取DHT11之前用__disable_irq()关闭全局中断读完再恢复。但这样会影响系统实时性不适合中断密集的应用。把DHT11的读取放到一个高优先级的定时器中断里执行并且中断服务函数里只做读取操作不做数据处理。使用RTOS时把DHT11读取放在一个紧急任务里并临时挂起其他任务。我的建议是除非你的系统特别简单否则不要关全局中断尽量预留一个专用定时器中断来处理DHT11的读取。这个思路在后续做多传感器采集时尤其重要因为每个传感器都会占用一段不可被打断的时间窗口。4. 常见问题与排查技巧实录4.1 读取数据一直为0或校验失败这是我被问得最多的问题。数据一直为0通常不是传感器坏了而是主机压根没等到响应信号。排查顺序按照从易到难先量电压。确认DHT11的VCC引脚电压在3.3V到5.5V之间GND确实接地。检查上拉电阻。数据线上拉电阻是否焊接好阻值是不是4.7kΩ到10kΩ之间。阻值太小会增大功耗阻值太大会让电平变化变慢影响时序。用示波器或者逻辑分析仪抓数据引脚波形。主机起始信号有没有发出去DHT11有没有响应脉冲。没有示波器的话可以写一段代码让DHT11引脚翻转点灯观察是否正常。校验失败说明数据传回来了但内容不对通常是延时精度不够。检查你的微秒延时函数在72MHz下是否准确用示波器实测一下引脚高低电平时长对比协议标准。另一个常见原因是供电电压太低有些主板3.3V电源纹波大DHT11工作不稳定换成5V供电试试。4.2 DHT11数据偶尔跳变或长时间无响应如果数据大部分时候正常偶尔跳变优先怀疑干扰。DHT11的数据线如果和电源线、电机驱动线平行走线电磁干扰会直接影响时序判断。解决办法是让数据线尽量短远离大电流线路必要时在传感器的VCC和GND之间加一个100nF的去耦电容。如果出现“长时间无响应”的问题时间间隔超过10秒大概率是传感器进入了错误状态。这种状态下即使重新发送起始信号也没用需要给传感器断电重启。代码层面可以做一个自动恢复机制连续读取失败N次后主动切断DHT11供电几十毫秒再上电强制复位。这是个很实用的招我在户外环境监测的项目里靠这个办法把故障率降了一个量级。4.3 常见问题速查表问题现象可能原因解决办法读取超时无响应接线错误或上拉电阻缺失检查VCC/GND/DATA接线补焊4.7kΩ上拉电阻数据为0但偶有成功起始信号延时不足拉低时间从18ms增加到20ms以上校验总是失败微秒延时不准或中断干扰用DWT或定时器实现延时读取期间关中断或提高优先级读取值明显偏高/偏低测量环境有热源或湿度干扰检查传感器周围是否有发热元件避免紧贴PCB铜皮连续多次读取失败后稳定失败传感器进入错误状态断电几十毫秒强制复位或降低读取频率每次读取周期必须大于1秒采样周期限制两次读取之间间隔1.5秒以上4.4 实战中使用频率控制的经验最后一个建议DHT11不适合像读取内部寄存器那样高频轮询。它的物理响应时间决定了1Hz是极限超过这个频率只会让总线上堆满无效请求还会增加传感器进入异常状态的概率。我一般采取的策略是主循环里每2秒读取一次DHT11每次读取完成后立即处理数据、刷新LCD或上报上位机并且在两次读取之间让MCU进入低功耗模式。这个策略既保证了数据的实时性又不会让传感器过载。如果项目里需要更高频率的温湿度数据那就不是DHT11的活儿了换个I2C接口的数字传感器读取速度能快到几百赫兹。5. 基于HAL库的完整工程整合思路5.1 工程文件的组织方式我习惯把DHT11的驱动单独做成两个文件dht11.c和dht11.h。模块化之后主程序文件保持干净业务逻辑和传感器驱动分离后续要换传感器或者移植到其他平台也方便。在dht11.h中对外提供以下接口#ifndef __DHT11_H #define __DHT11_H #include main.h #define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_0 uint8_t DHT11_Init(void); uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature); void DHT11_Delay_Init(void); #endifdht11.c文件里再补充引脚切换和读取实现这部分代码在上面已经给出了完整逻辑。唯一要注意的是DHT11_Init函数只需要调用一次DWT_Delay_Init不要每次读取都重新初始化延时模块。5.2 主循环中的调用示例主程序里的调用逻辑很简单但有一件事千万别做错读取DHT11前不要用阻塞式的延时等待传感器稳定DHT11不像I2C设备那样上电后立即可以通信它需要1秒左右完成上电自检。上电后立刻读取大概率失败建议系统上电延时500ms以上再发起第一次读取。下面是一个完整的调用示例#include dht11.h #include stdio.h char uart_buf[64]; uint8_t hum, temp; int main(void) { HAL_Init(); SystemClock_Config(); DHT11_Delay_Init(); DHT11_Init(); HAL_Delay(500); while (1) { if (DHT11_ReadData(hum, temp)) { sprintf(uart_buf, Humidity: %d.%d%% Temperature: %d.%dC\r\n, hum 4, hum 0x0F, temp 4, temp 0x0F); HAL_UART_Transmit(huart1, (uint8_t *)uart_buf, strlen(uart_buf), 1000); } else { HAL_UART_Transmit(huart1, (uint8_t *)DHT11 read failed\r\n, 20, 1000); } HAL_Delay(1500); } }温度的小数部分处理值得单独说一下。DHT11返回的温度低位数据通常在小数部分但实际分辨率是1℃所以小数部分大多数情况是0。用temp 4取高位作为整数部分temp 0x0F取低位作为小数部分打印出来格式比较直观。不过要注意某些批次的DHT11在湿度上会返回0x80这种低位值表示0.8%这时候直接忽略小数部分或者按协议解析都可以。我建议在主控里保留小数位的解析逻辑毕竟数据都已经读回来了。5.3 移植到其他单片机的要点DHT11驱动移植到ESP32、GD32、AT32等其他平台时核心逻辑不用改需要调整的只有两处GPIO输出/输入切换的方式和微秒级延时函数。ESP32用Arduino框架的话可以用pinMode切换GPIO模式delayMicroseconds做微秒延时速度也够。GD32本身兼容STM32的寄存器操作代码几乎可以无缝移植。AT32同理内核同样是Cortex-M系列DWT延时函数也能直接用。有一点要提醒不同MCU的GPIO翻转速度不一样同样的延时参数跑在不同主频上结果可能截然不同。换平台之后务必用示波器检查一下起始信号和数据位的实际波形再做微调。不要想当然认为代码一样就能直接跑时序这个东西和硬件强相关。6. 从DHT11到单总线协议的通用能力迁移6.1 单总线协议家族器件的共性学会了DHT11再去看DS18B20或者其他单总线器件会发现协议骨架非常相似主机先发起复位脉冲从机回响应脉冲然后按位传输数据。区别主要在每个器件的数据长度、位定义和命令序列不同。DS18B20比DHT11复杂一些多了ROM命令、存储器操作等环节但底层读“0”和读“1”的方式是一样的都是测量高电平宽度。所以DHT11很适合作为理解单总线协议的敲门砖。把这个协议读透了后续接触任何单总线器件都会轻松很多。这也是我在这篇文章里花大量篇幅讲协议时序而不是直接丢代码的原因。代码终究是表象协议才是灵魂。6.2 多传感器挂载时的总线仲裁思路DHT11的单总线协议本身不支持多设备挂载因为它在数据发送阶段不会做总线仲裁。但是实践中可以利用GPIO模拟的方式把多个DHT11分别接在不同的引脚上轮询读取。这个方案在引脚充足时完全可行。如果引脚紧张可以考虑用一个模拟多路开关比如CD4051来切换多个DHT11的数据线。这时候要注意一个问题CD4051的导通电阻会影响信号边沿导致时序变化所以切换之后要加一个短暂延时让总线稳定再发起始信号。读取频率也要相应调整两个传感器之间的间隔至少500ms不然传感器可能响应不过来。这类方案虽然看起来原始但在低成本、多节点的场景下比上I2C总线的传感器要省不少钱。6.3 结合现有项目的实际扩展思路如果你已经在做一个嵌入式项目DHT11读取功能可以作为一个独立的任务模块通过消息队列把温湿度数据发给其他任务。比如用FreeRTOS的环境里可以创建一个DHT11_Task优先级设为中等每2秒读取一次读取结果通过队列发送给显示任务和上报任务。这样做的好处是DHT11的时序敏感问题被隔离在单一任务里其他任务不会干扰它显示任务也不会因为等待DHT11而卡死。如果你用的是乐鑫ESP8266或者ESP32做物联网项目DHT11的数据读取方式也一样只是GPIO操作和延时函数换成对应SDK的接口。我自己试过在ESP8266上用软件模拟的单总线读取DHT11刷新率1Hz连续跑了一周多没有出现卡死稳定性还是很可靠的。7. 写在最后的实战心得DHT11这个传感器在社区里被吐槽最多的就是“时序太烂”但我实际用下来发现大部分人的问题不是出在传感器本身而是出在对协议的理解不够透彻。你清楚每个时序参数的含义和容差范围之后写驱动就像是照着图纸拼乐高每一步都很确定根本不需要瞎试。如果让我给刚上手的人一个建议那就是先拿逻辑分析仪或者示波器把波形抓出来看一遍。不用看太久一次正常的通信过程就够了你会看到起始信号、响应脉冲、40位数据的完整波形那一刻对协议的理解会超过看十篇文章。没有示波器的话退而求其次用LED翻转法通过延时调整观察读取结果的变化也能定位大部分问题。我自己的项目里DHT11的驱动代码从第一次写到现在已经用了快四年换过STM32F103、GD32F303、ESP32三种平台核心逻辑几乎没有变过。这一方面说明协议本身很稳定另一方面也说明把原理吃透比背代码重要得多。最后分享一个小技巧如果你的产品需要在低温环境下用DHT11比如低于0℃的冷库DHT11的温度测量精度会明显下降而且容易导致内部晶振频率偏移影响通信时序。这种场景下我建议在硬件上留一个加热电阻的焊盘位置低温时给传感器轻微加热保证它在规格书标称的工作范围内工作。这个设计一开始可能用不上但等到现场温度跌破0℃的时候就知道这个预留方案省了多少麻烦了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价