ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

DHT11单总线通信实战:从时序原理到STM32驱动与排错

DHT11单总线通信实战:从时序原理到STM32驱动与排错 很多人在拿到DHT11这块蓝色小模块时第一反应是照着网上的例程把代码抄一遍结果读回来的温湿度不是0就是乱码甚至干脆卡死在等待应答的循环里。我早期调这块传感器的时候也被折腾过几个晚上后来把单总线这条链路上的每个环节彻底搞明白之后才发现DHT11的脾气其实就是电气时序和通信协议的组合问题。这篇文章不打算简单甩一份代码我想把器件原理、单总线协议、时序细节和排错方法放在一起完整拆开讲让你不仅能跑通还能在出问题时自己定位。这篇文章适合正在学习单片机和嵌入式通信的开发者无论你用STM32、51还是其他MCU只要想真正搞懂DHT11乃至单总线通信机制的接下来这部分内容应该都能帮到你。我会从单总线为什么存在讲起一路拆到驱动代码里每个延时和每个判断条件最后把实战中踩过的坑和排查链路全部摊开。1. 单总线不是玄学DHT11凭什么叫单总线1.1 从三条线到一条线省下的引脚是用时序换来的传统数字传感器通信通常需要至少两条线比如IIC的SCL和SDASPI的SCK、MOSI、MISO再加上片选信号。而DHT11这类器件用一根数据线就完成了主机和传感器之间的所有通信这根线就是所谓的单总线1-Wire也叫单总线协议或单线协议。它的基本逻辑很简单所有通信都通过这根线的高低电平变化以及变化的时间长度来表达。发送的是一位数据也好接收的一个应答信号也好本质上都是在约定好的时间段内拉高或拉低这根线然后对方通过采样这根线处于什么电平来解读信息。正因为所有信息都挤在一根线上单总线通信对时序的敏感度非常高。IIC好歹还有时钟线同步设备双方按照SCL的节拍来交换数据而单总线没有独立的时钟线全靠预先约定好的电平持续时间来区分0和1。这就好比两个人约定用敲墙编码交流有人敲得长代表1敲得短代表0但如果敲的人手速不稳或者听的人走神半秒信息就全乱了。1.2 单总线与IIC、SPI的取舍一个引脚换来的代价很多初学者会问单片机引脚又不缺为什么非要选单总线实际上这是成本和应用场景共同决定的结果。单总线最大的优势就是节省IO资源。在一个GPIO紧张的MCU上挂一个DHT11只占一个引脚而且通信速率通常也就几十kbps用于读取温湿度这种变化缓慢的数据完全够用。相比之下SPI需要至少3根线如果只读不写可以省掉MISOIIC也需要两个上拉电阻和两根线对于单纯读一个温湿度来说确实有点杀鸡用牛刀。但单总线的代价也很明显没有时钟同步机制所有时序都必须靠延时函数硬抠通信距离短一般几米以内才稳定抗干扰能力弱线稍长或者环境有电磁干扰就容易失败。IIC和SPI在传输稳定性、速率和灵活度上都要强不少这也是为什么工业级的传感器更多用RS485、Modbus协议而不是单总线。1.3 DHT11是标准1-Wire吗先把概念说清楚严格来说DHT11并不是Maxim公司定义的1-Wire协议。标准的1-Wire协议代表性器件是DS18B20支持在总线上挂接多个设备每个设备有唯一的64位ROM序列号主机会先发起ROM搜索命令来识别总线上有哪些设备然后才能和指定设备通信。而DHT11只是在电气特性和时序上类似单总线它没有ROM ID也不支持多设备寻址一根线上只能挂一个DHT11。所以更准确的说法是DHT11采用单总线通信方式但不是标准1-Wire协议。网上很多文章把这两者混为一谈导致有人试图在一根总线上挂多个DHT11结果自然是失败的。有些文章会用DHT11单总线协议来命名这没问题关键是你要理解它和DS18B20那种标准1-Wire是两回事。写代码的时候DHT11的时序既不能套用DS18B20的驱动也不能直接用Maxim 1-Wire库必须单独实现。2. 动手前的必备认知DHT11内部在做什么2.1 器件引脚与典型电路DHT11常见的有4脚直插封装和模块两种形态。直插封装的引脚定义在正面丝印上有标注一般从左到右依次是VCC、DATA、NC、GND。DATA引脚就是单总线通信的数据线NC为空脚不用连接。模块形态通常已经把上拉电阻和滤波电容做好了甚至有的模块还带了电源指示灯接法非常省心VCC接3.3V或5VGND接地DATA接MCU的一个GPIO。如果你用裸片或者自己画板子一定要记得在DATA引脚上加一个4.7kΩ左右的上拉电阻到VCC。这是因为DHT11的数据线是开漏/集电极开路结构只能主动拉低不能主动拉高高电平完全靠外部上拉电阻提供。缺少上拉电阻的后果就是通信时电平浮空读回来的数据乱七八糟。供电范围方面DHT11标称工作电压是3.3V到5.5V但实际使用中3.3V系统需要特别注意上拉拉到的电压必须和MCU的IO电平兼容。温度在0到50度、湿度在20%到90%RH范围内DHT11还能保证标称精度超出区间误差会明显变大。2.2 感湿与测温的原始思路DHT11内部的感湿元件一般是一种高分子电阻式湿敏元件感湿材料在吸收空气中的水分后电阻值会发生变化通过内部电路转换为湿度数据。温度部分则是用一个热敏电阻或NTC原理同样是电阻随温度变化再经过ADC采样和内部逻辑换算成数字量。因此DHT11输出的温湿度是经过内部校准和量化的数字信号单片机上不需要再做公式换算直接按协议读回数据帧拆出5个字节就行。DHT11的精度并不高湿度精度正负5%RH温度精度正负2摄氏度分辨率分别只有1%RH和1摄氏度小数位通常读出来都是0。很多人在读到类似25.0的结果后会怀疑是不是数据错误其实这就是DHT11的固有分辨率它做不到更高的精度。2.3 为什么时序参数这么关键DHT11通信协议里最关键的就是一系列时间参数。主机起始信号要求低电平至少持续18ms然后释放总线20到40us传感器应答信号是低电平80us再拉高80us每位数据以低电平50us开始之后的高电平持续时间决定这位的值是0还是10的高电平大约持续26到28us1的高电平大约70us。这些数值看起来只是几个延时参数但决定了整个通信的成败。MCU的时钟频率不同、编译器优化等级不同、延时函数写法不同都会导致实际延时和预期偏差。很多人的代码在开发板上能跑换一块板子就不行多半就是延时精度被玄学了。后面代码部分我会专门用一节来讲微秒延时怎么做得靠谱。3. 总线上的一次完整温湿度读取3.1 第一步主机发起起始信号单片机作为主机想从DHT11读取数据时必须先主动发起一个起始信号。过程是这样的主机先把数据线拉低并且保持至少18ms这段时间可以看作是唤醒DHT11的信号DHT11检测到这个足够长的低电平后就会进入准备发送数据的状态。之后主机释放总线也就是把数据线拉高让电平通过上拉电阻回到高电平然后等待20到40us。为什么起始信号要拉低18ms这么久因为DHT11内部逻辑需要时间完成一次采样18ms足够它稳定下来并准备好应答。如果你拉低时间太短传感器可能根本没反应过来太长一般问题不大但会影响读取频率因为DHT11本身采样周期就是1s左右读得再快数据也不会更新。起始信号结束后总线处于高电平主机需要立刻切换为输入模式或者将引脚配置成开漏输出并置高这样可以直接读取引脚电平这在STM32上非常实用等待DHT11的应答。3.2 第二步传感器应答与总线控制权交接DHT11接收到起始信号后会把总线拉低大约80us表示我收到你的请求了然后再释放总线让上拉电阻把电平拉高大约维持80us。主机在这段时间里读到的是一个低80us、高80us的波形这就相当于传感器对主机说准备好我开始发数据了。在这里有一个容易忽略的细节主机释放总线后总线其实处于高电平空闲状态。但DHT11的应答和后续数据都表现为主动拉低一段时间然后释放让上拉电阻拉高所以整个通信过程就是一个不断交替的低电平、高电平序列主机只需要捕捉每次电平转变的持续时间就能解析出数据。如果在起始信号等待应答时总线上一直没有出现低电平脉冲那大概率是DHT11没有正常工作可能是没上电、没加上拉、或者起始信号时序不达标。3.3 第三步40位数据怎么一个个传出来应答信号之后DHT11会连续发送40位数据。每个数据位的格式都一样先拉低总线约50us然后释放总线之后高电平的持续时间决定这一位是0还是1。如果高电平持续26到28us表示这一位是0如果高电平持续大约70us表示这一位是1。主机怎么区分呢常见做法是检测到低电平结束后启动一个计时器或循环计数统计总线保持高电平的时间超过某个阈值就判定为1否则判定为0。这个阈值一般取40us到45us之间因为0的高电平不到30us1的高电平超过70us中间留了足够大的余量。40位数据的排列顺序是固定的前16位是湿度数据其中高8位是湿度整数部分低8位是湿度小数部分中间16位是温度数据高8位是温度整数部分低8位是温度小数部分最后8位是校验和。这里要特别指出DHT11的小数位在大多数情况下是0只有DHT22/AM2302这类高分辨率型号才会用到小数位。如果你强行把DHT11的小数位当作有效数字去精确显示反而会引入无意义的波动。3.4 校验和与数据还原接收完40位数据后主机还需要做一次校验确保传输过程中没有误码。校验规则非常简单把前四个字节加起来取结果的低8位如果等于第五个字节就认为这次通信有效否则丢弃本次数据等待下一次读取。举个例子如果读到的原始数据是字节序号值湿度整数0x1E湿度小数0x00温度整数0x1A温度小数0x00校验和0x38那么前四个字节之和是0x1E 0x00 0x1A 0x00 0x38低8位正好等于0x38校验通过温湿度分别为30%RH和26摄氏度。实际数据还原公式是湿度 湿度整数 湿度小数 / 10温度 温度整数 温度小数 / 10对于DHT11小数位通常为0所以直接读整数值就可以用。4. 手写驱动代码每个函数为什么这样写4.1 微秒延时最容易翻车的环节DHT11的时序对微秒级延时要求很高但MCU上的HAL_Delay、delay_ms这类函数一般只能精确到毫秒级没法处理20us、50us这种短延时。很多人直接把毫秒延时拿来用结果读到的数据全是乱码。最可靠的微秒延时方案是使用DWTData Watchpoint and Trace模块的CYCCNT计数器。它是Cortex-M内核里的一个32位周期计数器每个CPU时钟周期自增一次精度高、不占用定时器资源。配合SystemCoreClock可以非常精确地实现微秒延时。如果你的平台是51单片机那延时方案会更朴素一些。12MHz晶振下一个机器周期是1us实际上8051一个机器周期包含12个时钟周期12MHz时机器周期正好1us所以延时1us约等于执行一条普通指令的时间可以用_nop_函数或几个空循环拼凑。但空循环的实际时长受编译器优化影响很大最好关掉优化或者用示波器实测校准。4.2 GPIO模拟单总线的三种状态切换单总线通信中同一个GPIO既要输出又要输入。常见的处理方式有两种。第一种是模式切换法在主机发送起始信号时把GPIO配置为推挽输出发送完起始信号后切换为浮空输入因为外部有上拉电阻读取数据时保持输入模式。这种方式逻辑清晰但GPIO模式的切换需要时间快速切换可能导致时序抖动。第二种是开漏输出法直接把GPIO配置为开漏输出并开启内部上拉或者依赖外部上拉电阻输出0时引脚拉低输出1时引脚处于高阻状态由外部上拉电阻把电平拉高。这样在开漏模式下GPIO同时也具备输入读取能力HAL库的GPIO_ReadPin可以直接读到当前引脚电平。整个过程不需要切换模式时序一致性更好。我实际用下来STM32上推荐第二种方式。开漏输出置1就相当于释放总线置0就是主动拉低读取数据时直接调ReadPin就能读到外部电路产生的电平非常符合单总线的电气模型。4.3 完整驱动代码与逐段讲解下面以STM32F103 HAL库为例给出一个完整可用的DHT11驱动。GPIO选用PB12采用开漏输出模式。引脚初始化代码void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 GPIO_InitStruct.Pull GPIO_PULLUP; // 内部上拉 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); }微秒延时函数使用DWT周期计数器static void DWT_Delay_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000U); while ((DWT-CYCCNT - start) ticks); }毫秒延时可以复用HAL_Delay或者也用DWT实现。起始信号以及读取间隔需要毫秒级延时直接用HAL_Delay(20)就行。起始信号和应答检测uint8_t DHT11_Start(void) { // 拉低总线至少18ms HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); HAL_Delay(20); // 释放总线等待20~40us HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); delay_us(30); // 此时DHT11应拉低总线作为应答 if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_SET) { return 1; // 总线仍为高说明无应答 } // 等待应答低电平结束约80us带超时保护 uint32_t timeout 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_RESET) { if (--timeout 0) return 2; } // 等待应答高电平结束约80us带超时保护 timeout 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_SET) { if (--timeout 0) return 3; } return 0; }超时保护很有必要。如果DHT11未连接或者损坏主机会一直卡在while循环里整个程序就假死了。所以等待电平变化的循环一定要加超时退出机制否则在调试时非常痛苦。读取一个数据位uint8_t DHT11_ReadBit(void) { uint32_t timeout; uint32_t high_duration 0; // 每个数据位都以50us低电平开始先等低电平结束 timeout 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_RESET) { if (--timeout 0) return 0; } // 测量高电平持续时间用循环计数近似时间 timeout 1000; while (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_12) GPIO_PIN_SET) { if (--timeout 0) break; high_duration; } // 0的高电平约26~28us1的高电平约70us阈值取40次循环计数 return (high_duration 40) ? 1 : 0; }这种读取方式是用空循环的次数来衡量高电平时间不是严格的微秒计时间。它的优点是代码简单配合DWT延时初始化后循环计数的次数和实际时间有一个相对稳定的对应关系缺点是不同优化等级下阈值可能要微调。更精确的做法是在高电平期间读取DWT-CYCCNT的差值然后和微秒阈值比较但这组代码用于学习和大多数工程已经足够了。读取一个字节uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (int i 0; i 8; i) { data 1; data | DHT11_ReadBit(); } return data; }读取完整温湿度数据并校验uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; if (DHT11_Start() ! 0) { return 1; // 起始失败 } for (int i 0; i 5; i) { data[i] DHT11_ReadByte(); } // 校验和 前四字节之和的低8位 if (data[4] ! ((data[0] data[1] data[2] data[3]) 0xFF)) { return 2; // 校验失败 } *humidity data[0]; *temperature data[2]; return 0; }DHT11数据帧格式中data[1]和data[3]分别是湿度和温度的小数部分。对DHT11来说它们基本都是0所以主函数里可以直接读取整数部分。如果你想做更通用的驱动可以把小数部分也返回float humidity data[0] data[1] * 0.1f; float temperature data[2] data[3] * 0.1f;4.4 在main函数里读温湿度的正确姿势DHT11从完成一次采样到数据稳定需要时间官方手册给出的采样周期是1s左右。这意味着你读数据的频率最好不要超过1Hz每次读取之间至少间隔1s以上。频繁读取不仅拿不到新数据还可能因为传感器还在忙而导致通信失败。正确的调用方式是在主循环里加一个1s或者更长的延时每次都重新发起起始信号并读取。很多程序跑着跑着输出就固定不动了往往不是因为死机而是读取频率太快DHT11根本来不及更新数据。另外要注意DHT11上电后需要等待1到2秒才能稳定工作。如果你在系统上电后立刻读取很可能失败一两次。比较稳妥的做法是在初始化完成后先延时2秒然后再进入主循环读取。5. 实战排错读不到数据时的完整排查链路5.1 现象一读回来全是0xFF或0x00如果读回来的字节全是0xFF本质上是主机把所有数据位都判成了1说明在每个数据位的后半段总线一直是高电平或者传感器根本没有拉低总线。常见原因有三个第一上拉电阻缺失。模块一般自带但如果你自己接裸片而漏了上拉总线在传感器释放后会一直处于不确定状态读回高电平的概率很高从而判定为1。解决办法是加一个4.7kΩ上拉到VCC。第二GPIO没有配置成输入或开漏模式。如果你用推挽输出发送完起始信号后仍然保持推挽输出并且输出高电平那传感器拉低总线时就是在和MCU的推挽输出打架电平可能会被强行拉高导致时序完全错乱。第三GPIO模式切换太慢。切换模式时如果引入了额外的us级延迟可能会错过传感器的应答窗口后面读到的自然全是高电平。如果读回来全是0x00那就是所有位都被判成0说明主机在高电平阶段读到了低电平通常是引脚内部配置为下拉或者外部电路把总线强制拉低了。检查一下GPIO的上下拉配置确保测量高电平时引脚是浮空或上拉状态。5.2 现象二只有第一次能读到后续失败这种问题多半是状态没有复位。DHT11的数据读取是一次性的读完40位后总线恢复到空闲状态。如果第二次读取之前没有正确发送起始信号或者起始信号之后没有等足应答时间传感器就不会响应。另一个常见原因是主循环里读取间隔太短。DHT11需要大约1s时间完成一次采样你在500ms内连续读了两次第二次去读时传感器可能还在刷新内部数据应答波形不完整导致校验失败。还有一种隐蔽情况前端读取代码在中断里执行被其他高频中断打断。数据位的高电平窗口只有几十微秒如果刚好在测量高电平时间时被一个优先级更高的中断抢占了几十微秒返回的判断结果就会出错连续读几次之后数据就对不上了。解决思路是在读取DHT11的临界区临时关中断或者把DHT11读取任务放到不会被频繁打断的线程里。5.3 现象三湿度值跳动异常湿度值虽然会随着环境变化但如果反复读到比如20%和60%这样大幅跳动的数据首先怀疑数据校验是否通过。没有通过校验的数据不能被使用有些程序在校验失败后仍然用旧数组里的垃圾数据算温湿度就会产生跳变。排除校验因素后考虑供电和干扰。DHT11对电源纹波比较敏感如果传感器离电机、继电器、电源模块很近或者连接线比较长就容易受到干扰。可以在VCC和GND之间加一个100nF去耦电容并尽量缩短DATA线的长度。如果模块是插在面包板上的检查一下面包板的接触是否可靠。氧化、松动、以及杜邦线内部的断裂都会造成时通时断这类硬件问题往往比软件问题更难排查。5.4 没有示波器时怎么定位逻辑分析仪与排除法最好用的是逻辑分析仪。便宜的逻辑分析仪就能抓到DHT11的完整波形把数据分析仪接到DATA引脚上抓取一次读取过程然后对比协议时序图一眼就能看出是起始信号不够长、应答缺失还是数据位的0/1时长不对。没有逻辑分析仪时可以像剥洋葱一样用排除法先用万用表量DATA引脚在空闲时的电平正常应该接近VCC如果接近0V说明上拉缺失或者MCU引脚被拉低。检查起始信号后能否看到传感器拉低总线的应答可以在读应答处加一个反例判断如果等待低电平超时就打印一个错误码。通过错误码能快速区分是无应答还是数据读出异常。用固定波特率的串口打印每次读到的原始5字节数据把校验和一起打出来。如果数据每次都不同说明通信不稳定如果数据固定但校验不过说明协议解析有误。我调试时最喜欢打印原始数据帧因为只看最终温湿度会被代码逻辑掩盖很多问题原始帧能直接告诉你传感器到底发出来什么。举例来说如果前四个字节和校验和一直是0xFF 0xFF 0xFF 0xFF 0xFF那不用怀疑解析代码直接去查硬件电路。6. 从DHT11出发单总线在传感器选型中的定位6.1 DHT11与DHT22/AM2302别只看价格DHT22也叫AM2302同样是单总线传感器数据格式和DHT11非常像也是5字节前两字节湿度、中间两字节温度、最后一字节校验和。区别在于DHT22的湿度分辨率是0.1%RH温度分辨率也是0.1摄氏度测量范围更大整体精度比DHT11高了一个量级。时序上两者略有差异。DHT22的起始信号同样要求拉低至少1ms一般用10ms也能触发但要用DHT11的18ms时序去读DHT22通常也能正常工作。不过最稳妥的做法还是分别根据手册调参数。驱动函数的改造点主要在起始信号的拉低时间、数据位判定阈值和超时时间上整体数据结构可以复用。价格上DHT11优势非常明显适合对精度要求不高的项目比如简单的室内温湿度显示、孵化箱的粗略监控、智能家居演示板。一旦你开始关心湿度的变化趋势或者需要做相对精确的露点计算至少应该升级到DHT22或者干脆用IIC接口的SHT系列。6.2 单总线、IIC、SPI在传感器应用中的取舍标题里提到了很多通信协议包括SPI、IIC、Modbus等这里可以放在一起做个横向对比帮助你把DHT11和单总线放在整个通信生态里理解。通信方式典型传感器引脚占用传输速率抗干扰能力典型场景单总线DHT11、DHT22、DS18B201根数据线部分需上拉低速kbps级一般板内短距离温湿度采集IICSHT30、BMP280、MPU60502根线SCLSDA100kbps~400kbps较好板内多传感器连接SPI部分传感器、Flash、显示屏3~4根线数十Mbps较好高速数据传输RS485/Modbus工业温湿度变送器2根差分线低速~数Mbps强工业现场远距离通信CAN汽车/工业传感器节点2根差分线最高1Mbps强车载、工控实时网络单总线的优势是省IO劣势同样明显速率上不去、距离拉不长、无法像Modbus那样通过地址挂载多个设备。所以当你看到单总线通信实战的时候一定要理解它是特定场景下的产物不是因为所有传感器都该这么连。6.3 我的选型建议与使用心得从DHT11开始学单总线是很合适的路径因为它协议简单、代码量少、出错时也好定位相比直接上手标准1-Wire协议要友好得多。你在单片机学习阶段花的那些时间并不会白费单总线中通过电平持续时间来编码信息的思想在后续理解红外遥控、数字温度传感器、甚至一些自定义协议时都会反复用到。实际开发中我的建议是临时验证和教学项目用DHT11模块就好便宜又皮实产品原型阶段如果空间允许尽量选择IIC接口的高精度传感器因为它们不需要微秒级时序代码更健壮不依赖具体MCU的主频和延时实现。工业现场则直接考虑RS485和Modbus单总线在这种场景下基本没有生存空间。如果你确实需要在单总线上用DHT11做产品记得在硬件设计阶段就把4.7kΩ上拉电阻、电源去耦电容、ESD保护都加上并且不要让单片机在读取期间处理其他高优先级中断。时序敏感的器件硬件给一点余量软件就少一份玄学。我自己最初调DHT11时最大的教训就是差点被代码不对所以改了三天这个概念迷惑。最后用逻辑分析仪一看波形从头到尾都是对的问题根本出在GPIO配置成了推挽输出导致传感器拉低总线时被单片机强行拉高。从那以后我排问题不再是盯着代码反复改而是先抓波形再从协议和电气上找原因。这种思维比多会几个驱动函数要值钱得多。
返回列表