我最早接触DHT11是在帮一个学弟调试毕业设计的时候,他的温湿度数据读出来永远是0,整块板子连初始化都过不去。当时我第一反应是"这模块不是有手就行?",结果连我自己也栽了一个下午。后来发现,DHT11看着简单,实际上它那个单总线协议对时序极度敏感,稍微有点偏差就全军覆没。这篇教程就是把我从硬件选型、原理图设计、底层驱动到踩坑排查的完整经验整理出来,给正在用STM32做温湿度采集的开发者,尤其是用标准库或HAL库写驱动时总被时序问题折磨的人,一条能直接走通的路。
1. 为什么2024年还在用DHT11:先从选型逻辑说起
市面上温湿度传感器多得是,SHT30、AHT20、BME280,随便拉一个出来精度都比DHT11强。那为什么DHT11至今还是教学板、毕业设计、DIY小项目里的常客?我自己的看法是:它的定位从来就不是"性能王者",而是"学习曲线最平滑的入门传感器"。
先看一张我常用的对比表:
| 型号 | 精度(温度/湿度) | 通信方式 | 典型价格 | 驱动难度 |
|---|---|---|---|---|
| DHT11 | ±2°C / ±5%RH | 单总线 | 3-5元 | 低 |
| DHT22 | ±0.5°C / ±2%RH | 单总线 | 10-15元 | 低 |
| SHT30 | ±0.3°C / ±2%RH | I2C | 6-10元 | 中 |
| AHT20 | ±0.3°C / ±2%RH | I2C | 3-6元 | 中 |
| BME280 | ±1°C / ±3%RH | I2C/SPI | 10-20元 | 中高 |
I2C传感器确实更简单——主机发地址,从机回数据,时序标准,不用微秒级掐表。但DHT11的"单总线协议"反而更能锻炼人对数字时序的敏感度。你在DHT11上学到的电平翻转、延时窗口、起始信号、响应时序,后面玩DS18B20、红外遥控、甚至是自定义单总线协议时全部用得上。
还有一个非常现实的原因:DHT11模块版便宜到可以忽略成本,坏了直接换,不需要重新画板子。做产品选型我不会推荐DHT11,但做学习项目、实验室原型验证、或者给学生练手,它依然是最顺手的那个。
另外一个常被忽略的点:DHT11的单总线协议对MCU的GPIO操作能力有很好的测试作用。如果你的SPI、I2C经常莫名失败,先用DHT11把引脚操作练熟,再回头排查硬件问题,往往事半功倍。
2. 硬件设计的三件小事:上拉电阻、电源去耦和布线细节
很多新手直接买现成的DHT11模块,插上杜邦线就开始写代码。但我建议至少要知道模块上那几个元件是干嘛的,因为你迟早要自己画板子。
2.1 上拉电阻:为什么必须接,阻值怎么选
DHT11的数据引脚是开漏输出结构——它本身只能把电平拉低,不能主动输出高电平。要让总线在空闲时处于高电平,必须在数据线上接一个上拉电阻到VCC。
模块版上通常已经焊好了上拉电阻,一般4.7kΩ或10kΩ。如果你用的是散装DHT11传感器,裸芯片只有四个引脚(VCC、DATA、GND、还有一个NC空脚),这时上拉电阻必须自己加。
阻值选4.7k还是10k?DHT11的单总线通信速率不高,边沿要求也不苛刻,4.7k、5.1k、10k都能正常工作。我习惯用4.7k,理由有两个:一是抗干扰能力比10k更强,二是DHT11数据线稍长(超过20cm)时,4.7k的上升沿陡峭度更好,读取成功率更高。
2.2 电源去耦:一个104电容解决大问题
单独给DHT11供电的场合,VCC和GND之间跨接一个0.1μF(104)的瓷片电容,再并联一个10μF电解电容会更好。这个不是玄学——DHT11在发出数据时,内部电路会在每个bit的边沿产生短暂的电流冲击。如果电源纹波大,这个冲击可能影响内部RC振荡器的稳定性,导致输出的脉冲宽度漂移。脉冲宽度漂移的直接后果就是MCU误判bit是0还是1。
特别是当你把DHT11和电机、继电器、蜂鸣器这类感性负载共用电源时,不加去耦电容,读取乱码的概率会明显上升。
2.3 3.3V还是5V:宽电压不是免死金牌
模块版的DHT11(蓝色那种)内部通常带了一个电平转换电路,可以工作在3.3V或5V。但如果你用的是散装传感器,情况就不一样了。
散装DHT11虽然标称是3.3V~5.5V宽电压供电,但市面上不少批次实际上更适应5V逻辑。我实测过一批散装的DHT11,在3.3V供电下,输出高电平大约只有2.4V左右。STM32的GPIO输入高电平阈值是0.7×VDD,也就是3.3V供电下需要超过2.31V才算高电平——2.4V勉强越过阈值,但余量很小,线一长就出错。
我的建议是:散装DHT11优先用5V供电,数据线通过电阻分压(比如1k+2k)或者电平转换芯片降到3.3V再接STM32,这样最稳。模块版则无所谓,3.3V直接干就行。
2.4 布线长度和走线位置的影响
传感器和MCU之间的杜邦线尽量控制在20cm以内。超过这个距离,建议把上拉电阻的阻值适当调小到2.2k~3.3k,补偿线缆电容对上升沿的拖累。
还有个实战中容易踩的坑:DHT11的数据线不要和SPI时钟线、USB差分线平行走太长距离。强耦合会让DHT11的时序出现毛刺,单片机读到错误bit后依然可能通过校验(因为校验位只有8位,碰撞概率1/256),导致温度突然跳变几十度。这种故障最难查,因为绝大多数时间数据是正常的,偶尔抽风一次。
3. 通信协议拆解:DHT11的单总线时序到底在说什么
网上关于DHT11的时序分析文章不少,但很多都只说"拉低18ms然后读数据",没有讲清楚协议的精髓。这里我从底层把整个通信过程完整拆一遍。
3.1 一次完整通信的四个阶段
DHT11的单总线通信以主机发起起始信号为开始,整个过程分成四段:
主机发送起始信号:主机把总线拉低至少18ms(通常我拉低20ms),然后释放总线(拉高)。这个低电平时间必须足够长,让DHT11的内部电路检测到并进入工作状态。
DHT11应答信号:主机释放总线后,DHT11会在20~40μs内把总线拉低,持续约80μs,表示"我准备好了";随后释放总线,总线被上拉电阻拉高,再持续约80μs,准备发送数据。
数据位传输:40位数据,按序发送,每位都由一个50μs左右的低电平起始,然后是高电平。高电平持续时间的长短决定了该位是0还是1。
总线释放:40位数据发送完后,DHT11释放总线,回到空闲状态,等待下一次起始信号。
3.2 位0和位1的区别:一切都在于高电平宽度
这是整个DHT11协议最核心也最容易搞混的点。每一位数据的格式是:50μs低电平 + 一定的的高电平。
- 逻辑0:高电平持续约26~28μs
- 逻辑1:高电平持续约70μs
MCU怎么判断收到的是0还是1?没有捷径,只能是:先把低电平等完(因为每一位都以50μs低电平开头,但实际长度有波动,26到38μs都算正常),然后开始计时高电平的时间。如果高电平持续超过某个阈值(通常取40~50μs),判为1;否则判为0。
这里有一个很多教程没提到的关键点:低电平的起始时刻并不精确,所以不能等到低电平结束才采样,而是在检测到下降沿后要等足够的时间跨过低电平区间,再开始测量高电平宽度。
3.3 40位数据布局与校验逻辑
DHT11一次上报40位数据,顺序是:
- 湿度整数部分(8位)
- 湿度小数部分(8位)
- 温度整数部分(8位)
- 温度小数部分(8位)
- 校验和(8位)
所有数据都是高位在前(MSB first)。校验和数据等于前四个字节之和的低8位。
比如你读到的40位是:
- 湿度整数:0x1F(十进制的31)
- 湿度小数:0x00
- 温度整数:0x1C(十进制的28)
- 温度小数:0x00
- 校验和:0x3B
验证一下:0x1F + 0x00 + 0x1C + 0x00 = 0x3B,校验和一致,数据有效。如果校验失败,说明本次读取过程中有时序错误,直接丢弃重读即可。DHT11的更新周期是1Hz(0.5Hz到2Hz不等),重读间隔至少要等1秒以上,否则大概率还是读到旧数据或错误数据。
3.4 为什么DHT11的时序对延时函数这么敏感
这里要解释一个问题:为什么同样一份代码,在别人的板子上跑得好好的,在你的板子上就死机、卡死或者数据全零?
因为DHT11协议要求的延时是微秒级的(26μs、50μs、80μs),这取决于:
- 系统时钟频率(8MHz还是72MHz,精确延时数值完全不同)
- 编译器优化等级
- MCU执行循环的指令周期数
- 中断是否干扰了时序
很多人直接用了一个不精确的Delay函数,比如for(i=0; i<30; i++);,这个循环在不同优化级别下耗时差异巨大。我在-O2优化下写的延时函数,换到-O0就完全跑不通,就是这个原因。
4. 标准库实战:从发送起始信号到校验完整驱动
下面给一套我实测稳定的标准库驱动代码。开发环境是Keil MDK5 + STM32F103C8T6(蓝色Pill板),系统时钟72MHz。核心逻辑是发送起始信号、读取响应、逐位采集40位数据、校验和确认。
如果你的硬件配置和我不同,重点看延时函数怎么改,不要照抄延时数值。
4.1 引脚配置与宏定义
#include "stm32f10x.h" #include "delay.h" // 自己实现的延时函数,提供 delay_us 和 delay_ms #define DHT11_GPIO_PORT GPIOB #define DHT11_GPIO_CLK RCC_APB2Periph_GPIOB #define DHT11_GPIO_PIN GPIO_Pin_0 // 两行关键宏:切换输入输出模式 #define DHT11_OUT_MODE() { GPIOB->CRL &= 0xFFFFFFF0; GPIOB->CRL |= 0x00000003; } // PB0 推挽输出50MHz #define DHT11_IN_MODE() { GPIOB->CRL &= 0xFFFFFFF0; GPIOB->CRL |= 0x00000004; } // PB0 浮空输入 #define DHT11_SET_HIGH() GPIO_SetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN) #define DHT11_SET_LOW() GPIO_ResetBits(DHT11_GPIO_PORT, DHT11_GPIO_PIN) #define DHT11_READ() GPIO_ReadInputDataBit(DHT11_GPIO_PORT, DHT11_GPIO_PIN)这里有个细节:DHT11的数据线在通信过程中需要切换方向——主机发送起始信号时要输出模式,读取应答和数据时要切换为输入模式。GPIO方向切换是关键,不要偷懒。
在标准库中,我通常直接操作CRL寄存器而不是调用GPIO_Init,因为GPIO_Init函数体较长,在需要快速切换模式的场景下效率偏低,而且寄存器操作指令数固定,时序更可控。
4.2 微秒级延时函数:不要裸用for循环
我在delay.h里自己实现了微秒延时,基于SysTick定时器,精度在±2μs以内。DHT11对50μs级别的窗口要求没那么变态,±2μs完全够用。这里给出一种基于SysTick的实现方式:
void delay_us(uint32_t nus) { uint32_t temp; SysTick->LOAD = nus * (SystemCoreClock / 1000000); // 设置重装载值 SysTick->VAL = 0; // 清空计数器 SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; // 使能SysTick do { temp = SysTick->CTRL; } while ((temp & SysTick_CTRL_ENABLE_Msk) && !(temp & SysTick_CTRL_COUNTFLAG_Msk)); SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; // 关闭SysTick SysTick->VAL = 0; }注意,SystemCoreClock在标准库的system_stm32f10x.c中定义,72MHz主频下就是72000000。这个延时函数会被频繁调用,如果觉得每次都要开关SysTick开销太大,也可以用DWT计数器或者裸机while递减配合校准。我主要图它直观、好移植。
4.3 DHT11完整的读取函数
uint8_t DHT11_ReadData(uint8_t *humidity_int, uint8_t *humidity_dec, uint8_t *temp_int, uint8_t *temp_dec) { uint8_t buf[5] = {0, 0, 0, 0, 0}; uint8_t i, j; uint8_t retry = 0; // 1. 主机发送起始信号:拉低至少18ms DHT11_OUT_MODE(); DHT11_SET_LOW(); delay_ms(20); // 2. 释放总线,模式切为输入 DHT11_SET_HIGH(); delay_us(30); // 释放后等待一小段时间,让上拉电阻把电平拉高 DHT11_IN_MODE(); // 3. 等待DHT11应答:先低后高各约80us retry = 0; while (DHT11_READ() == 1 && retry < 100) { // 等待低电平(应答开始) retry++; delay_us(1); } if (retry >= 100) return 1; // 超时,传感器未响应 retry = 0; while (DHT11_READ() == 0 && retry < 100) { // 等待低电平结束 retry++; delay_us(1); } if (retry >= 100) return 1; retry = 0; while (DHT11_READ() == 1 && retry < 100) { // 等待高电平结束,准备读数据 retry++; delay_us(1); } if (retry >= 100) return 1; // 4. 读取40位数据 for (j = 0; j < 5; j++) { for (i = 0; i < 8; i++) { // 跳过50us低电平 retry = 0; while (DHT11_READ() == 0 && retry < 80) { retry++; delay_us(1); } // 测量高电平宽度 retry = 0; while (DHT11_READ() == 1 && retry < 80) { retry++; delay_us(1); } // 如果高电平持续超过40us,判为逻辑1 if (retry > 40) { buf[j] |= (0x80 >> i); // 注意高位在前 } } } // 5. 校验 if ((uint8_t)(buf[0] + buf[1] + buf[2] + buf[3]) != buf[4]) { return 2; // 校验失败 } *humidity_int = buf[0]; *humidity_dec = buf[1]; *temp_int = buf[2]; *temp_dec = buf[3]; return 0; }这段代码里的两个关键点:
为什么用retry > 40而不是retry > 35判1?因为我的delay_us(1)实际耗时可能略大于1μs,加上while循环的判断开销,实际循环周期大约是1.5μs。如果我阈值设得太小,逻辑0的高电平(约26μs)可能也被判成1。用40作为阈值,实测在72MHz下区分度为:逻辑0约17次循环,逻辑1约46次循环,非常清晰。
为什么每个等待循环都有超时上限?如果传感器没接好、损坏或者总线被拉死,没有超时的while会变成死循环,程序直接卡死。这也是很多人说"DHT11卡死程序"的根本原因。加超时是所有单总线驱动的基本素养。
4.4 调用示例
int main(void) { uint8_t hum_i, hum_d, temp_i, temp_d; uint8_t ret; delay_init(); // 时钟初始化 GPIO_Config(); // 端口时钟使能和引脚配置 while (1) { ret = DHT11_ReadData(&hum_i, &hum_d, &temp_i, &temp_d); if (ret == 0) { printf("Humidity: %d.%d%%RH Temp: %d.%d°C\r\n", hum_i, hum_d, temp_i, temp_d); } else { printf("DHT11 read error: %d\r\n", ret); } delay_ms(2000); // 注意:间隔至少1秒,DHT11更新速率限制 } }5. HAL库移植:从标准库思维切换到HAL库的三个变化
现在STM32CubeMX已经成为主流初始化工具,很多人的工程直接从HAL库起步。HAL库封装了底层寄存器操作,但这几个点会让DHT11的HAL实现踩坑。
5.1 GPIO方向切换:HAL函数的开销问题
HAL库切换GPIO输入输出方向的标准写法是:
GPIO_InitTypeDef GPIO_InitStruct = {0}; GPIO_InitStruct.Pin = GPIO_PIN_0; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);问题在于:HAL_GPIO_Init函数非常庞大,它会重设整个GPIO配置寄存器的所有位,每次调用耗时几十微秒。而DHT11协议里,起始信号释放后只有20~40μs的应答窗口,你用HAL_GPIO_Init切一次模式,窗口就错过了。
我的HAL库方案是:初始化时用HAL正常配置,切换方向时直接操作寄存器。在HAL库工程里,GPIOB的CRL寄存器地址依然相同,直接改寄存器完全兼容。
#define DHT11_OUT_MODE() { GPIOB->CRL &= 0xFFFFFFF0; GPIOB->CRL |= 0x00000003; } #define DHT11_IN_MODE() { GPIOB->CRL &= 0xFFFFFFF0; GPIOB->CRL |= 0x00000004; }这种写法在HAL库工程里完全合法,因为HAL库也是基于同一套寄存器映射的。我用它在STM32F407上验证过,没问题。
5.2 HAL_Delay的精度陷阱
HAL提供的HAL_Delay()是基于SysTick中断实现的毫秒级延时。如果你要微秒级延时,不要尝试用HAL_Delay拆——它的节拍就是1ms,拆不出微秒精度。
在HAL库工程里,我依然使用自己实现的SysTick微秒延时。但要注意:如果HAL已经占用了SysTick(HAL_Init默认会),你自己再用SysTick会冲突。解决办法有两种:
- 改HAL的时基源:在CubeMX的SYS配置里,把Timebase Source改成TIM6或其他定时器,把SysTick腾出来给你的微秒延时用。
- 用DWT计数器实现延时:DWT是Cortex-M内核的调试组件,有一个32位的CYCCNT寄存器,专门用来记录CPU周期数,不依赖SysTick,也不受HAL影响:
void delay_us_DWT(uint32_t us) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; uint32_t cycles = us * (SystemCoreClock / 1000000); uint32_t start = DWT->CYCCNT; while (DWT->CYCCNT - start < cycles); }我后来在F4上长期使用DWT方案,稳定性很好,推荐直接把这段代码封装到你的bsp层里。
5.3 中断优先级:为什么你的数据偶尔跳变
在标准库时代,我很少遇到DHT11读取被中断干扰的情况,因为F103本身没有多少中断源。但换到F4、H7后,外设丰富,如果DHT11读取过程中被UART中断、定时器中断打断几百微秒,时序窗口就被破坏了。
一个最典型的场景:你用串口每秒打印一次温湿度,同时DHT11每次读取都失败。原因是USART的TXE中断在DHT11读取的50μs高电平时间内频繁触发,导致循环变长,阈值判断全部失真。
解决办法:
- DHT11读取函数整体放进临界区:读取前
__disable_irq(),读完后__enable_irq()。这个粗暴但有效,因为DHT11一次完整读取只有几毫秒,关中断对系统影响很小。 - 如果你要读取的同时还要保证串口不丢数据,把DHT11读取放到定时器中断里,并且把该定时器中断优先级设为最低。
- 最优雅的解法:用DMA传输串口数据,让串口中断根本不触发。
6. 实测问题排查:六种翻车现场与修复链路
6.1 数据全为0或校验永远失败
这是最常见的问题。排查链路应该是:
- 先确认接线:VCC是3.3还是5V?DATA接对引脚没有?GND是不是共地?如果模块输出电压低于STM32高电平阈值,数据线就用示波器或万用表量高电平幅值。
- 再查上拉:裸传感器没有上拉电阻时,数据线空闲状态是浮空的,读回的值随机,几乎必然失败。
- 然后用逻辑分析仪或示波器看起始信号:主机有没有正常拉低20ms?释放后DHT11有没有拉低80μs作为应答?如果完全没看到应答波形,大概率传感器已经损坏或者供电异常。
- 最后检查延时:你用的延时准确吗?用
delay_us(50)实际延时了多少?有时我直接用HAL_Delay(1)想"顺便延时1微秒",结果延时了1毫秒,整个时序全部错乱。
6.2 第一次读取卡死
很多人写的主循环第一次调用读取函数就死机,这是因为起始信号发出后,DHT11没有响应,程序在等待低电平的while(DHT11_READ()==1)里死循环了。前面也提过,所有等待循环必须加超时退出。没有超时机制的DHT11驱动代码,网上能找到一大把,抄的时候务必自己加上。
6.3 一直返回超时错误,但传感器是好的
还有一种情况:传感器在第一次成功读取后,第二次读取必须间隔1秒以上。如果你在循环里50ms就调用一次读取,DHT11根本来不及刷新数据,会一直处在"忙"状态,不响应起始信号。
解决方案:把读取间隔设为2秒。如果需要更快的刷新率,考虑DHT22或SHT30,它们在高速轮询下表现好得多。
6.4 温度湿度偶发乱跳
温度从25°C突然跳到80°C,然后又跳回来,这种偶发故障通常是电磁干扰或者电源毛刺。
我曾经遇到过一个小项目,DHT11数据线走到了一块开关电源附近,只要电源启动,温湿度就开始乱跳。后面加了一根屏蔽线(把信号线用外皮接地包裹),并在线路入口加了一个100nF滤波电容,故障消失。
另一个排查点:检查你选择的GPIO是不是被其他外设复用。比如STM32F103的PB0在默认情况下是ADC的通道8输入引脚。如果你初始化了ADC外设但没有配置引脚复用,DHT11读到的是被ADC内部电路干扰后的电平,数据就会时好时坏。
6.5 使用JTAG引脚导致的下载失败
我见过一个典型案例:有人把DHT11接在PA13(JTMS引脚)上,然后发现程序下载不进去,或者下载一次后再也连不上调试器。原因是PA13/PA14默认被JTAG占用,你配置成普通GPIO后,调试口被禁用,SWD也就失效了。
解决办法:在代码里加上这个:
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);然后按复位键的同时点下载,抢在用户程序运行前把芯片擦掉。或者干脆换一个不带调试功能的GPIO引脚,别给自己挖坑。
6.6 板子能跑但读出来是恒定的28°C 60%RH
这个温度恒定地异常"合理",大概率是传感器没有真正更新数据——除了1秒间隔问题外,还有可能是因为DHT11的DATA引脚和VCC之间短路了,或者传感器内部晶振损坏。用另一块已知正常的模块交叉测试,能快速定位问题。
7. 进阶玩法:一条总线上挂多个DHT11与低功耗设计
DHT11的单总线协议从原理上讲,一条总线上可以挂多个传感器,因为它每个bit都有明确的电平窗口。但实际上很少人这么干,原因有二:一是DHT11没有芯片地址,所有挂在同一总线上的传感器会同时响应,数据互相冲突;二是DHT11没有冲突检测机制,不能像DS18B20那样通过序列号寻址。
所以如果你想采集多个位置的温湿度,我的建议是:
- 每个DHT11独占一个GPIO引脚,读取逻辑完全独立。STM32的引脚资源充足,几个传感器还占得起。
- 引脚紧张时,可以用一个8选1模拟开关(比如CD4051)轮流把不同DHT11的数据线接到同一个GPIO。注意切换后要给传感器留足够的稳定时间(至少100ms),避免电平冲突。
- 如果你追求极低功耗,DHT11本身就不是好选择——它的上拉电阻一直在耗电。低功耗场景我更推荐把DHT11的VCC引脚也接到MCU的GPIO上,读取前拉高供电,读完再拉低,这样待机电流几乎为零。不过要提醒:上电后DHT11需要1秒左右的稳定时间才能可靠通信,这个特性决定了它做不了高频采集。
8. 两个实战经验补充:关于移植和PCB设计
8.1 关于"国产替代"芯片的移植注意事项
最近看到有人用APM32、GD32替代STM32跑DHT11驱动。这些芯片在引脚和寄存器层次上兼容STM32F1,标准库代码基本可以直接用,但有一个被我同事踩过的坑:
有一批GD32F103的GPIO速度档位和STM32定义不完全一样,如果照搬标准库的GPIO初始化,引脚速度配置可能没生效,导致GPIO翻转速度不够,DHT11起始信号的下降沿不够陡,传感器完全收不到起始信号。
解决办法很简单:把GPIO速度档位强制设为最高档(在标准库中是GPIO_Speed_50MHz,在HAL库中是GPIO_SPEED_FREQ_HIGH)。不要用LOW或MEDIUM,DHT11对边沿斜率有隐性要求。
另外,如果碰到"起始信号发出后传感器一直无响应"的情况,先把时间参数整体放大到1.5倍试试。部分国产芯片的指令执行周期和ST原厂有细微差异,延时会偏短。
8.2 PCB设计时给DHT11留的位置建议
如果你在画板子,给DHT11留位置时注意三点:
- 传感器开口方向要朝外,方便空气流通,不要在它正上方放遮挡物或大功率发热元件。
- 数据线走线远离电源开关管和电感类器件,实在躲不开就在数据线上串一个330Ω电阻,限制高频干扰灌入。
- 在DATA和GND之间加一个1nF的滤波电容。虽然这个电容会略微拖慢上升沿,但只要总线长度不长(小于10cm),没影响。我在产品原型中验证过,这个电容能显著减少数据跳变。
9. 从DHT11到DHT22:一个驱动函数的通用化改造
最后说一个实用技巧:如果你以后要升级到DHT22,DHT11的驱动代码可以复用90%。DHT22的通信协议结构与DHT11几乎一致,不同的地方在于:
- 数据位逻辑1的高电平宽度更长,约75μs
- 温湿度分辨率是16位,前两个字节是湿度,按0.1%RH为单位;接下来两个字节是温度,最高位是符号位,按0.1°C为单位
- 校验和同样是前四个字节之和的低8位
所以你可以把读取函数抽象成:
typedef struct { float humidity; float temperature; } DHT_Data_t; uint8_t DHT_GenericRead(DHT_Data_t *data);在读取完40位原始数据后,根据传感器类型(用宏或参数区分)做不同的数据解码。我当年在项目里就是靠这个抽象,把DHT11/DHT22的驱动维护成本降到了零。
这套改造做完,你的温湿度模块基本就到了"可复用到下一个项目"的程度了。从最基础的时序原理,到标准库和HAL库两套实现,再到干扰排查和PCB设计注意事项,DHT11这条线吃透之后,再去碰其他单总线设备或者I2C传感器,都会顺手很多。