1. 为什么DHT11至今还在被大量使用
如果你最近在做一个环境监测的小项目,比如温室大棚监控、机房温湿度记录、或者家里养了爬宠需要盯着温湿度,搜索一圈之后你大概率会买几颗DHT11回来。这颗传感器从上市到现在已经超过十五年,参数放在今天看并不亮眼:湿度精度±5%RH,温度精度±2℃,采样间隔不能低于1秒,单总线协议对时序要求还挺苛刻。但它的销量始终没怎么跌过,原因很实在——便宜、够用、资料多、随便一个单片机都能驱动。
我手头几个项目里,DHT11出现的场景通常是这样的:需求只需要知道“大概湿度够不够”“温度有没有超过阈值”,对精度要求不高,成本又压得很死。这时候用一颗几块钱的DHT11,比上一颗高精度I2C传感器省下来的钱可能不多,但省下来的调试时间和外围电路设计时间是真金白银。尤其是新手入门阶段,用DHT11练手单总线时序、练手GPIO方向切换、练手微秒级延时,是一个非常好的教学载体。
不过,问题也恰恰出在这里。很多人把DHT11当成一个“插上就能用”的模块,结果在STM32F1上折腾半天读不出数据,或者在嘉立创EDA里画原理图时随手一画,板子打回来发现数据乱跳。这篇内容就是想把DHT11从原理到驱动、从画图到排查的整条链路讲清楚,然后再聊聊当项目升级时,新一代数字温湿度传感器该怎么选、怎么迁移。
适合读这篇内容的人:正在用STM32F1或者类似MCU驱动DHT11的嵌入式初学者;准备自己画板子把DHT11集成进去的硬件爱好者;以及那些DHT11用着还行、但隐约觉得该升级又不知道往哪升的人。
注意:本文所有代码基于STM32F1系列和HAL库编写,其他平台思路一致,时序参数需要根据主频重新计算。
2. DHT11到底是怎么工作的
2.1 内部结构拆解:一颗传感器里装了什么
DHT11的外观是一个四针或者三针的塑料壳子,里面其实封装了两套独立的感应结构。测湿部分用的是一种高分子感湿电阻,湿度变化时它的阻值会跟着变;测温部分用的是一颗NTC热敏电阻,温度升高阻值下降。这两路模拟信号不会直接输出,而是先送到内部一颗8位单片机做ADC采样和校准,校准系数出厂时已经烧录在OTP内存里,用户改不了。
这个结构决定了几个关键特性。第一,DHT11输出的是数字信号,不需要外部再加ADC,省了一路模拟电路。第二,因为内部有MCU做处理,所以它需要一定的响应时间,数据手册里写的采样周期不低于1秒就是这个原因。第三,校准系数固定,意味着它的精度天花板在出厂那一刻就定死了,后期没法通过软件补偿大幅提升。
我拆过一颗坏掉的DHT11,里面就是一块很小的PCB,上面贴了感湿膜和热敏电阻,再加一颗COB封装的控制芯片。结构简单到让人怀疑它凭什么卖这个价,但反过来想,正因为简单,它的可靠性在正确使用的前提下其实还不错。
2.2 单总线协议:一根线怎么完成双向通信
DHT11最让人又爱又恨的就是它的通信方式。它只有一根数据线,主机和从机轮流控制这根线的高低电平来传递信息。没有时钟线,没有片选线,全靠严格的时序约定。
一次完整的通信流程是这样的:主机先把数据线拉低至少18毫秒,然后释放,再拉低20到40微秒,接着释放。这组动作是告诉DHT11“我要开始读数据了”。DHT11收到之后,会先拉低80微秒作为应答,再拉高80微秒,表示“我准备好了”。接下来它连续输出40位数据,每一位都是先拉低50微秒作为起始,然后用高电平的持续时间来区分是0还是1。高电平持续26到28微秒表示0,持续70微秒表示1。
40位数据分成五组:第一组是湿度的整数部分,第二组是湿度的小数部分(DHT11固定为0),第三组是温度的整数部分,第四组是温度的小数部分(同样固定为0),第五组是校验和,等于前四组相加取低八位。
这套协议看起来不复杂,但实操时会发现两个坑。第一个坑是微秒级延时的精度,HAL库自带的HAL_Delay只能到毫秒,用不了。第二个坑是GPIO方向切换的速度,在读数据阶段需要快速在输出和输入模式之间切换,切换太慢就会错过位起始信号。
提示:DHT11的高电平持续时间是用来区分0和1的唯一依据,所以读取时的采样点必须落在正确的时间窗口内。一般建议在位起始后的30到40微秒之间采样。
2.3 为什么时序容错这么差
很多人抱怨DHT11难驱动,根本原因是它的时序窗口太窄。以区分0和1为例,0的高电平是26到28微秒,1的高电平是70微秒,中间有超过40微秒的间隔。理论上容错空间不小,但问题出在起始信号的检测上。如果主机在拉低18毫秒之后释放,DHT11需要在20到40微秒内响应,这个时间窗口对中断干扰极其敏感。
我试过在跑着RTOS的工程里直接调用DHT11读取函数,失败率超过一半。原因是任务切换和中断会打断微秒级延时,导致时序偏移。后来改成读之前关中断,读完再开,成功率立刻上到95%以上。所以如果你在F1上读DHT11经常失败,先检查是不是有其他中断在捣乱。
3. 原理图设计:嘉立创EDA上画DHT11电路的那些细节
3.1 引脚连接不是随便接上就行
DHT11通常有四个引脚:VCC、DATA、NC、GND。三针版本把NC去掉了。VCC接3.3V还是5V?数据手册写的是3.3V到5.5V,但实测下来3.3V供电时信号幅度也是3.3V,和STM32F1的IO电平匹配得很好。如果接5V,DATA线的高电平就是5V,虽然F1的很多引脚标称容忍5V,但为了保险,建议统一用3.3V供电。
DATA引脚需要接一个上拉电阻,典型值4.7k到10k。这个电阻的作用是在总线释放时把电平拉高,因为没有它的话总线就是浮空状态,读到的数据全是乱的。我见过有人在嘉立创EDA里忘了加上拉电阻,板子打回来之后数据偶尔能读出来但极不稳定,查了半天才找到原因。
NC引脚悬空就行,不用管。GND老老实实接地,别想着省一根线。
3.2 去耦电容和走线布局的实战经验
DHT11的供电引脚旁边建议放一颗0.1微法的去耦电容,位置越靠近传感器越好。这个电容的作用是滤掉电源上的高频噪声,因为DHT11内部的模拟电路对电源质量比较敏感。我试过在电机驱动的板子上不加去耦电容,DHT11读出来的湿度值会在电机启动瞬间跳变十几个百分点,加上电容之后就稳了。
走线方面,DATA线尽量短,不要和电机驱动线、继电器控制线平行走。如果板子上有大电流开关器件,DHT11最好放在远离它们的位置。嘉立创EDA里做DRC检查的时候,注意DATA线的网络别和其他信号线靠得太近,间距至少保持0.3毫米以上。
还有一个细节:如果DHT11是外接的,通过排线连到板子上,排线长度不要超过20厘米。太长了分布电容会变大,上升沿变缓,读出来的位宽度会失真。我试过用50厘米的杜邦线连DHT11,成功率直接掉到30%以下,换成15厘米的排线就恢复正常了。
3.3 一个可直接参考的原理图方案
如果你在嘉立创EDA里画图,可以参考这个连接方式:DHT11的VCC接3.3V,同时并联一颗0.1微法电容到地;DATA引脚接一个4.7k上拉电阻到3.3V,然后连到STM32F1的某个GPIO,比如PA0;GND接地。如果板子上有多个DHT11,每个都要独立上拉,不能共用一根DATA线。
注意:有些人为了省IO口,想把多个DHT11的DATA线接到同一个引脚上,通过不同地址区分。DHT11不支持地址寻址,这样做只会导致总线冲突,读出来的数据全是错的。
4. STM32F1 + HAL库驱动DHT11的完整实现
4.1 微秒级延时的三种实现方式
HAL库默认的HAL_Delay只能做到毫秒级,驱动DHT11必须自己实现微秒延时。我试过三种方式,各有优劣。
第一种是用系统滴答定时器Systick做计数。把Systick的 reload 值设成1微秒对应的计数值,然后轮询计数标志。这种方式精度最高,但会干扰HAL库本身的毫秒计时,需要小心处理。
第二种是用一个通用定时器,比如TIM2,配置成1微秒计数一次,读CNT寄存器做延时。这种方式不干扰Systick,精度也不错,是我最推荐的做法。配置的时候注意预分频值要算对,F1主频72MHz的话,预分频设成71,计数器时钟就是1MHz,每个计数就是1微秒。
第三种是用空循环做粗略延时,根据主频和循环次数估算。这种方式最省事但精度最差,受编译器优化影响很大,不推荐用在正式项目里。
我一般用TIM2做微秒延时,代码大概长这样:
void delay_us(uint16_t us) { __HAL_TIM_SET_COUNTER(&htim2, 0); while (__HAL_TIM_GET_COUNTER(&htim2) < us); }调用之前确保TIM2已经初始化并且启动,否则计数器不动,会死循环。
4.2 GPIO方向切换的正确姿势
DHT11的DATA线在通信过程中需要多次切换方向。主机发送起始信号时是输出模式,释放总线后要切成输入模式来读取DHT11的应答和数据。切换速度直接影响读取成功率。
HAL库提供的HAL_GPIO_Init函数可以重新配置引脚方向,但它的执行时间比较长,大概几十微秒,用在位读取的间隙里会错过时序。我的做法是直接操作寄存器,修改CRL或CRH寄存器的对应位,速度快很多。
具体来说,把PA0配置成输出模式时,CRL的对应位设成0x3(通用推挽输出,50MHz);配置成输入模式时,设成0x4(浮空输入)或者0x8(上拉输入)。因为外部已经有上拉电阻了,用浮空输入就行。
#define DHT11_SET_OUT() do { GPIOA->CRL &= ~(0xF << 0); GPIOA->CRL |= (0x3 << 0); } while(0) #define DHT11_SET_IN() do { GPIOA->CRL &= ~(0xF << 0); GPIOA->CRL |= (0x4 << 0); } while(0) #define DHT11_OUT_H() (GPIOA->BSRR = GPIO_PIN_0) #define DHT11_OUT_L() (GPIOA->BRR = GPIO_PIN_0) #define DHT11_IN() (GPIOA->IDR & GPIO_PIN_0)这套宏定义直接操作寄存器,切换时间在纳秒级,完全不影响时序。如果你的DHT11接在其他引脚上,把GPIOA和对应的位改掉就行。
4.3 完整读取函数的逐步拆解
读取DHT11的完整流程可以分为五个阶段:起始信号、等待应答、读取40位数据、校验、返回结果。我把它写成一个函数,返回0表示成功,返回1表示失败。
起始阶段:主机拉低至少18毫秒,然后拉高20到40微秒,接着切成输入模式等待DHT11响应。
DHT11_SET_OUT(); DHT11_OUT_L(); HAL_Delay(20); // 拉低20ms,满足最小18ms要求 DHT11_OUT_H(); delay_us(30); // 拉高30us DHT11_SET_IN(); // 切换为输入,等待DHT11应答等待应答阶段:DHT11会先拉低80微秒,再拉高80微秒。我们需要检测这两个电平变化,如果在超时时间内没等到就返回失败。
uint32_t timeout = 0; while (DHT11_IN() && timeout < 100) { timeout++; delay_us(1); } if (timeout >= 100) return 1; timeout = 0; while (!DHT11_IN() && timeout < 100) { timeout++; delay_us(1); } if (timeout >= 100) return 1; timeout = 0; while (DHT11_IN() && timeout < 100) { timeout++; delay_us(1); } if (timeout >= 100) return 1;读取数据阶段:循环40次,每次等待位起始的低电平结束,然后延时30到40微秒采样高电平,如果还是高电平就说明这一位是1,否则是0。
for (int i = 0; i < 40; i++) { timeout = 0; while (!DHT11_IN() && timeout < 100) { timeout++; delay_us(1); } delay_us(35); if (DHT11_IN()) { data[i / 8] |= (1 << (7 - (i % 8))); } timeout = 0; while (DHT11_IN() && timeout < 100) { timeout++; delay_us(1); } }校验阶段:把前四个字节加起来,取低八位,和第五个字节比较,相等则数据有效。
uint8_t sum = data[0] + data[1] + data[2] + data[3]; if (sum != data[4]) return 2;最终返回湿度和温度值,DHT11的小数字节固定为0,所以直接用整数部分就行。
4.4 调用时需要注意的坑
第一个坑是连续读取的间隔。两次读取之间至少要间隔1秒,否则DHT11内部还没完成上一次转换,读出来的数据可能是旧的或者错误的。我试过500毫秒读一次,前几次正常,后面就开始出错了,改成1.5秒之后很稳定。
第二个坑是读取期间关中断。前面说过,中断会打断微秒延时,导致时序偏移。我的做法是在读取函数开头调用__disable_irq(),读完再__enable_irq()。这样做的副作用是系统会丢失这段时间的中断,所以读取频率不能太高。如果项目里有对实时性要求很高的中断,可以考虑只关掉优先级低于某个阈值的中断。
第三个坑是上电后的稳定时间。DHT11刚上电的一秒内读出来的数据是不准确的,建议上电后延时1到2秒再开始第一次读取。
5. DHT11的局限和常见问题排查
5.1 那些参数表上看不到的短板
DHT11的数据手册写得很清楚:湿度精度±5%RH,温度精度±2℃,量程湿度20%到90%RH,温度0到50℃。这些数字看起来还能接受,但实际用起来会发现几个手册没写的问题。
第一是长期漂移。DHT11的感湿膜在潮湿环境里用久了会老化,半年到一年之后读数会慢慢偏高。我有一批放在温室里的DHT11,用了八个月之后和标准表对比,湿度读数普遍高了7到10个百分点。这个漂移是不可逆的,只能换新的。
第二是响应速度慢。从低湿环境移到高湿环境,DHT11需要几分钟才能稳定到新的读数。如果项目需要快速跟踪湿度变化,DHT11完全不够用。
第三是低温失效。虽然手册写量程从0℃开始,但接近0℃时读数会明显不准,零下之后干脆不工作。北方户外项目千万别用DHT11。
5.2 读取失败排查速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全读不到数据,超时返回 | 接线错误或传感器损坏 | 检查VCC和GND是否接反,换一颗传感器试试 |
| 偶尔能读偶尔失败 | 上拉电阻缺失或阻值不合适 | 确认DATA线有4.7k到10k上拉,示波器看波形上升沿 |
| 数据全是0或全是1 | 时序偏差太大 | 检查微秒延时是否准确,示波器测位宽 |
| 湿度值明显偏高 | 传感器老化或受潮 | 和新传感器对比,确认是否已经漂移 |
| 温度值正常湿度值乱跳 | 电源噪声干扰 | 加0.1微法去耦电容,远离电机驱动线 |
| 读取成功率低于50% | 中断干扰或延时不准 | 读取期间关中断,用定时器做微秒延时 |
5.3 几个我从踩坑里总结的技巧
技巧一:读之前先发一个起始信号再丢弃。有时候DHT11处于不确定状态,先读一次不管结果,等1秒后再读第二次,成功率会提高。这个操作相当于给传感器一个复位。
技巧二:如果DATA线比较长,可以在靠近单片机的一端再加一个100欧姆的串联电阻,抑制反射。虽然DHT11速率很低,反射影响不大,但在一些干扰强的环境里这个电阻能明显改善波形。
技巧三:不要在DHT11的供电线上接其他大功率器件。我试过把DHT11和一颗无线模块共用一路3.3V,无线模块发射时DHT11读数就会跳,分开供电之后问题消失。
6. 新一代数字温湿度传感器的技术演进
6.1 从DHT11到DHT22:同一路线的升级
DHT22也叫AM2302,是DHT11的同门升级版。封装差不多,协议兼容,但参数全面提升:湿度精度±2%RH,温度精度±0.5℃,量程扩展到0到100%RH和-40到80℃。价格大概是DHT11的三到四倍。
如果你的项目本来用DHT11,想升级但不想改代码,DHT22是最省事的选择。时序基本一致,只需要把读取间隔从1秒改成2秒,因为DHT22的转换时间更长。我试过同一套驱动代码直接读DHT22,改一下延时参数就能用,数据明显更稳。
但DHT22也没有解决根本问题:依然是单总线,依然对时序敏感,依然有长期漂移。它只是把DHT11的短板补了一部分,没有换赛道。
6.2 SHT3x和SHT4x:I2C接口带来的质变
SHT3x系列是Sensirion出的数字温湿度传感器,I2C接口,湿度精度±2%RH,温度精度±0.2℃,量程覆盖0到100%RH和-40到125℃。SHT4x是更新的一代,精度和长期稳定性进一步提升。
I2C接口的好处是显而易见的。第一,时序由硬件外设保证,不需要微秒级延时,也不怕中断干扰。第二,可以挂多个传感器在同一组I2C总线上,通过不同地址区分,省IO口。第三,通信速率高,读取一次只要几毫秒,采样率可以做到很高。
我用SHT30做过一个冷链运输的温湿度记录仪,每秒读一次,连续跑了三个月,数据漂移在0.1℃以内。同样的场景用DHT11根本做不到。
缺点是价格。SHT3x大概是DHT11的十倍以上,SHT4x更贵。而且I2C需要上拉电阻和更规范的PCB布局,对新手来说门槛稍高。
6.3 选型对比:什么场景该用什么
| 传感器 | 接口 | 湿度精度 | 温度精度 | 采样率 | 价格区间 | 适用场景 |
|---|---|---|---|---|---|---|
| DHT11 | 单总线 | ±5%RH | ±2℃ | 1Hz | 极低 | 教学、低成本监测 |
| DHT22 | 单总线 | ±2%RH | ±0.5℃ | 0.5Hz | 低 | 一般环境监测 |
| SHT30 | I2C | ±2%RH | ±0.2℃ | 10Hz以上 | 中 | 工业、冷链、高精度 |
| SHT40 | I2C | ±1.8%RH | ±0.2℃ | 10Hz以上 | 中高 | 高端监测、长期部署 |
选型的核心逻辑是:先看精度要求,再看采样率要求,最后看预算。如果精度要求±5%RH以内、采样间隔大于1秒,DHT11够用。如果要求±2%RH、需要快速响应,直接上I2C传感器,不要在单总线上继续折腾。
7. 从DHT11迁移到新一代传感器的实操路径
7.1 硬件改板的注意事项
从DHT11换到I2C传感器,硬件上要做几处改动。第一,DHT11的4.7k上拉电阻去掉,换成I2C的4.7k上拉电阻,接在SCL和SDA上。第二,供电电压确认,SHT3x支持2.4V到5.5V,和DHT11一样可以用3.3V。第三,如果板子上已经画了DHT11的封装,换传感器意味着重新画板,所以建议在新项目里直接预留两种传感器的兼容封装。
嘉立创EDA里有现成的SHT3x封装库,直接调用就行。注意SHT3x的引脚间距和DHT11不一样,布局的时候留够空间。
7.2 驱动层抽象:让代码兼容两种传感器
如果项目之前写了DHT11的驱动,想平滑迁移,可以在驱动层加一层抽象。定义一个统一的接口结构体:
typedef struct { uint8_t (*init)(void); uint8_t (*read)(float *temp, float *humi); } sensor_driver_t;DHT11和SHT3x各自实现这两个函数,上层业务代码只调用接口,不关心底层是哪种传感器。这样迁移的时候只需要换一个驱动文件,业务逻辑完全不用动。
我在一个环境监测项目里就是这么做的,最早用DHT11,后来换成SHT30,上层的数据记录和报警逻辑一行没改,只替换了驱动实现。
7.3 数据校准和验证方法
换传感器之后,建议做一次对比验证。把旧传感器和新传感器放在同一个环境里,每隔一分钟记录一次,连续记录几个小时,然后对比两组数据。如果差异在旧传感器的精度范围内,说明新传感器工作正常。
如果有条件,找一个标准温湿度计做参考。我用过一款校准过的温湿度记录仪做基准,对比下来SHT30的读数偏差在0.3℃和1.5%RH以内,DHT11偏差到了1.8℃和6%RH。这个差距在要求不高的场景里无所谓,但在精密监测里就是能不能用的区别。
最后分享一个小经验:不管用哪种传感器,远离发热源和气流死角。我见过有人把DHT11装在单片机旁边,读出来的温度永远比环境高3到5℃,因为单片机和稳压芯片在发热。传感器要装在板子边缘,或者用排线引出来,离热源越远越好。