☰
DHT11温湿度传感器实战:从单总线协议到驱动调试避坑指南
2026/10/12 7:15:22 网站建设 项目流程

做单片机项目的人,十有八九都和DHT11这颗温湿度传感器打过照面。它价格便宜、驱动逻辑简单、网上示例代码一抓一大把,但正因为“随便一搜就有”,绝大多数人只是把别人的库函数复制进工程,一旦遇到读数恒为0xFF、校验和反复失败这类经典问题,就完全不知道从哪里下手排查。这篇文章围绕DHT11温湿度传感器的使用写一篇完整的实战记录:从硬件接线、单总线通信协议讲起,再给出一份我实际调试过的源码实现,最后把我踩过的几个坑连同排查思路一起摊开来说。适合刚接触单片机和数字传感器的初学者,也适合被“读不到数据”折磨了一下午的老哥对照自查。

1. 为什么入门温湿度测量先选DHT11:参数、引脚与选型逻辑

老实说,数字温湿度传感器可选的型号并不少,DHT11在精度上没有任何优势,但它有一个别人比不了的特点:便宜、资料多、几乎每个开发板都能直连。对一个刚入门的人来说,先把链路跑通、把协议看懂,比追求高精度重要得多。

1.1 核心参数与适用边界

先看一张参数表,把底数摸清楚。

参数数值说明
温度测量范围0℃ ~ 50℃超出范围读数无意义
温度精度±2℃常规环境够用,做不了精密测量
湿度测量范围20%RH ~ 90%RH低于20%或高于90%时明显失真
湿度精度±5%RH典型值,个体差异较大
分辨率温度1℃、湿度1%RHDHT11整数输出,小数位恒为0
采样周期至少1秒两次读取的时间间隔不能小于1s
供电电压3.3V ~ 5V5V供电最稳,3.3V也能工作

这张表最有价值的地方不是“它能测多少”,而是“它的边界在哪里”。比如你想做冷藏室温度监控,0℃以下它就瞎了;想测空调出风口湿度,湿度经常跌破20%,它也测不了。很多新同学拿DHT11去测冰箱,读回来永远是1℃或者干脆乱跳,就是因为超出测量范围。

那和更高一档的DHT22相比怎么选?我的建议很直接:如果项目对精度没有硬性要求,只需要判断环境的大致舒适度,DHT11足够;如果要搞气象站这类需要小数点后一位、湿度范围更宽的场景,直接上DHT22,别在DHT11上折腾。两者通信协议几乎一样,只是小数位的含义不同,文章后面的源码稍改一下小数位解析就能通用到DHT22上。

1.2 引脚定义与两种接线方式

DHT11有裸芯片和模块两种形态。裸芯片一般是4个引脚,插到面包板上就能用;模块则集成了一颗上拉电阻,引脚也精简成3个(VCC、DATA、GND)或保留4个。

引脚名称说明
1VCC电源,3.3V ~ 5V
2DATA双向数据引脚
3NC悬空,不用接
4GND接地

接线的主要区别在于上拉电阻。如果你买到的是带PCB的模块,板子上已经帮你焊好了上拉电阻,直接把DATA接到单片机的任意GPIO即可。如果买的是裸芯片,就一定要自己加一颗上拉电阻,常见用4.7kΩ到10kΩ,把DATA线经电阻接到电源正极。

这里把上拉电阻的作用说透:单总线协议里,数据线本质上是开漏结构,靠总线空闲时的上拉电平维持高电平。如果没有这颗电阻,DHT11把总线拉低再释放后,数据线会悬浮在一个不确定的电平上,单片机读回来的就是随机值。这个坑我后面还会专门讲,因为很多人的现象是“模块能读、裸芯片读不出”,原因就是少了这颗电阻。

2. 单总线通信协议拆解:起始信号、80μs应答和每一位的判定方法

DHT11用一种非常“朴素”的通信方式:一根数据线既要发又要收,没有独立的时钟线,全靠电平在时间上的宽度传递信息。这种协议叫单总线,简单粗暴,但要求时序精准,而且只能一对一通信,不能像I2C那样挂多个设备编地址。

2.1 一次完整通信的时序流程

把一次完整通信拆开,大致是四个阶段。

  1. 主机把总线拉低,持续至少18ms(我习惯写20ms),相当于告诉DHT11“我要开始读了”。
  2. 主机把总线拉高20μs到40μs,然后把数据线切换成输入模式,释放总线。
  3. DHT11收到起始信号后,先把总线拉低80μs,再拉高80μs,作为应答信号。
  4. 应答完成后,DHT11连续输出40位数据。每一位格式一样:先是50μs的低电平,然后是一段高电平,高电平持续26μs到28μs表示逻辑0,持续约70μs表示逻辑1。

所以每一位的判定核心就一句话:测高电平的持续时间,和阈值比大小。大于阈值判为1,小于阈值判为0。阈值选40μs是因为“0”的高电平在26μs到28μs之间,“1”的高电平在70μs左右,中间留出了将近一倍的裕量,取40μs到60μs之间的任何一个值都安全。我实际更习惯用50μs,原因在后面的实测部分会讲到。

时序上有个经常被忽略的细节:主机拉低总线的时间必须是“至少18ms”。这个时间其实是DHT11内部判定起始信号的门槛,太短了传感器不认账。曾经有人把delay(20)误写成delayMicroseconds(20),看起来只差一个单词,实际差了1000倍,传感器根本不会响应。这种问题在代码里特别难发现,因为编译正常、逻辑看着也对,就是读不出来。

2.2 40位数据的排列与校验规则

DHT11输出的40位数据,按字节可以排成一张表。

字节名称DHT11的实际含义
data[0]湿度整数比如60表示60%RH
data[1]湿度小数DHT11恒为0
data[2]温度整数比如24表示24℃
data[3]温度小数DHT11恒为0
data[4]校验和前四字节相加取低8位

校验和的算法是:前四个字节相加,结果与0xFF取与,然后和第五个字节比较。相等说明这40位传输基本可靠,不相等直接丢弃这帧数据。这个校验逻辑一定要写在代码里,不要省。DHT11的时序依赖微秒级测量,在环境干扰、导线过长、供电不稳的情况下偶尔丢位很正常,校验和就是最后一道防线。

3. 手写驱动源码:从时序到校验一步步实现

网上关于DHT11的库很多,我仍然建议自己手写一遍。原因很简单:用库你只能改参数,手写一遍你才真正理解每一位是怎么进来的,后面遇到任何异常才有排查的方向。

3.1 位读取的核心逻辑

先说最关键的位读取思路。DHT11每一位以50μs低电平开始,然后是一段高电平。我们只要在低电平结束时开始计时,到高电平结束时停止,就拿到了高电平的持续时间,也就知道这位的值了。

// 等待引脚变为指定电平,超时则返回false static bool waitPinLevel(uint8_t level, uint32_t timeoutUs) { uint32_t start = micros(); while (digitalRead(DHT11_PIN) != level) { if (micros() - start > timeoutUs) { return false; } } return true; }

这个waitPinLevel是整个驱动的基础函数。它解决了一个新手常常忽略的问题:如果传感器没接好、没上电、或者数据线断了,digitalRead可能永远等不到目标电平,程序就会卡死在while循环里。加了超时之后,任何异常都会在限定时间内返回false,上层代码可以报错并重试,不会让整个系统死等。

3.2 完整源码与逐段说明

下面这份源码基于常见的开源单片机开发框架编写,用到的都是标准API。移植到别的平台时,只需要把pinMode、digitalWrite、digitalRead、delay、delayMicroseconds这几个函数替换成对应平台的实现即可。

#include <stdint.h> #include <string.h> #define DHT11_PIN 2 static uint8_t data[5]; // 等待引脚变为指定电平,超时返回false static bool waitPinLevel(uint8_t level, uint32_t timeoutUs) { uint32_t start = micros(); while (digitalRead(DHT11_PIN) != level) { if (micros() - start > timeoutUs) { return false; } } return true; } // 读取一次DHT11数据,成功返回true,data数组存放结果 static bool readDHT11(void) { // 1. 主机拉低总线至少18ms,发出起始信号 pinMode(DHT11_PIN, OUTPUT); digitalWrite(DHT11_PIN, LOW); delay(20); // 2. 拉高30us后释放总线,等待DHT11应答 digitalWrite(DHT11_PIN, HIGH); delayMicroseconds(30); pinMode(DHT11_PIN, INPUT); // 3. 应答信号:先低80us,再高80us if (!waitPinLevel(LOW, 100)) return false; if (!waitPinLevel(HIGH, 100)) return false; // 4. 读取连续40位 memset(data, 0, sizeof(data)); for (int i = 0; i < 40; i++) { // 每位以50us低电平开头 if (!waitPinLevel(LOW, 100)) return false; uint32_t t0 = micros(); // 高电平持续时间决定0/1 if (!waitPinLevel(HIGH, 100)) return false; uint32_t highUs = micros() - t0; if (highUs > 40) { // 大于40us判为1 data[i / 8] |= (0x80 >> (i % 8)); } } // 5. 校验和验证 uint8_t sum = data[0] + data[1] + data[2] + data[3]; if (data[4] != sum) { return false; } // 6. 结束通信,把总线拉高恢复空闲状态 pinMode(DHT11_PIN, OUTPUT); digitalWrite(DHT11_PIN, HIGH); return true; } void setup() { Serial.begin(9600); pinMode(DHT11_PIN, OUTPUT); digitalWrite(DHT11_PIN, HIGH); } void loop() { if (readDHT11()) { Serial.print("Temperature: "); Serial.print(data[2]); Serial.print(" C, Humidity: "); Serial.print(data[0]); Serial.println(" %"); } else { Serial.println("DHT11 read failed"); } delay(2000); // 两次读取间隔至少1秒,这里留2秒更稳 }

逐段解释几个关键参数的选择。

第一,起始信号的delay(20)。数据手册要求至少18ms,我用20ms留一点余量。这个延时必须在微秒级和毫秒级之间分清楚,前面已经说过误写delayMicroseconds(20)的后果。

第二,拉高后的delayMicroseconds(30)。这个时间窗口是20μs到40μs,我取中间值30μs。太短可能让DHT11来不及识别起始信号结束,太长又可能让DHT11以为你在传输数据。

第三,每一位判1的阈值用40μs。理论上的“0”高电平最长28μs,“1”最短70μs,40μs是中间偏保守的选择。如果你发现读出来的数据偶尔错位,可以把阈值调到50μs再试,有时会有改善。

第四,第3步和第4步里的所有等待都设了100μs超时。应答信号只有80μs,数据位的低电平也就50μs,100μs足够宽裕;如果超时,说明总线状态异常,立即放弃本次读取。

3.3 移植到其他平台的思路

如果你用的是STM32、ESP32或者其他平台,这套代码的移植点很清楚。

  • pins控制:把pinMode换成对应平台的GPIO模式设置函数,digitalWrite和digitalRead换成对应的拉高、拉低、读电平函数。
  • 延时:delay对应毫秒延时,delayMicroseconds对应微秒延时。STM32的HAL库有HAL_Delay,但微秒延时需要用DWT或者定时器自己实现;ESP32可以直接用ets_delay_us或者esp_rom_delay_us。
  • 微秒计时:micros()在多数平台上对应定时器计数。如果系统跑的是带抢占的实时内核,轮询读取期间可能被中断打断,导致高电平宽度被多算几十微秒,这时需要在读取函数前后临界区保护,或者把读取过程放到定时器中断里执行。

4. 实测常见的三个坑:时序、上拉电阻和供电

这部分是我实际调试中反复遇到的,每一个都有具体现象和完整的排查链路,而不是直接给结论。

4.1 数据全是0xFF或0:先怀疑物理连接,再怀疑代码

我第一次把DHT11模块插到面包板上,烧录后串口一直打印失败,偶尔读到几个0xFF。当时第一反应是代码有问题,反复检查逻辑、换阈值、调延时,折腾了快一个小时。

后来冷静下来,拿万用表一步步量:VCC和GND之间的电压正常,5V没错。然后量DATA引脚对GND的电压,发现问题了——数据线在空闲状态下只有0.4V左右,而不是接近电源电压的高电平。原因就是这颗传感器是裸芯片,没有上拉电阻,数据线悬空后电平被外部干扰拉到了一个不确定的值。

解决方式很简单:DATA到VCC之间接了一颗10kΩ上拉电阻,再上电,一次就读出来了。所以如果你的现象是“完全读不到、全是0xFF”,先用万用表确认三件事:供电有没有到位、DATA空闲时是不是高电平、GPIO有没有选错引脚。物理层没问题之前,不要浪费时间改代码。

4.2 校验和连续失败:导线长度、接触不良和中断打扰

另一个常见现象是数据能偶尔读出来,但checksum经常失败,打印错误率在30%以上。这个阶段的问题往往不在协议逻辑,而在信号质量。

我排查的顺序是这样的。第一步,检查接线。面包板上的跳线如果超过20cm,DHT11的上升沿和下降沿会变得很钝,单片机的电平判断就会漂移。我把导线换短、换成杜邦线直连后,错误率明显下降。第二步,检查接触。面包板的弹片用久了会氧化,传感器震动一下读数就崩,这是面包板通病,最好把引脚直接焊到转接板上。第三步,检查中断。WiFi模块在收发数据时会暂停CPU或者频繁触发中断,如果正好发生在读取DHT11的40位数据期间,micros()的计时会失真,高电平宽度被拉长,位就判错了。

没有示波器时,怎么量化这种问题?可以在代码里统计一下校验失败的次数,连续读取100次打印一次失败率。如果失败率在10%以下且偶尔发生,大多数是环境干扰;如果失败率一直很高,多半是接线或供电问题。

4.3 读到的温湿度不变或变化极慢:采样周期和探头位置

还有一种“假故障”:读到的数据合法,但温度始终不变,湿度也纹丝不动。这个其实是DHT11的物理特性决定的,内部测温元件是NTC热敏电阻,感湿元件是高分子电容,两者的响应速度本来就慢,不是一秒钟就能跟上的。

更关键的是,DHT11两次测量之间必须隔1秒以上。你如果每100ms读一次,读到的大部分是上次的旧值,而且频繁的起始信号会干扰传感器内部的状态机,可能导致后续读数越来越不准。我的建议是主循环里至少delay(2000),给传感器留足刷新时间。

还有一次我在做一个小机箱项目,把传感器直接固定在PCB附近,读到的温度比环境高2℃到3℃。原因是传感器贴着发热器件,箱内热量堆积。把探头用延长线引到机箱外侧之后,读数才接近真实环境温度。测温湿度,探头摆放和散热设计的影响往往比传感器本身的精度误差还大。

5. 进阶思路:滤波、周期采样和多点测量

跑通基础读取之后,如果你是做实际项目,还会遇到几个进阶问题。这里给出我常用的处理方式。

5.1 滑动平均与中值滤波

DHT11的读数是整数,偶尔会有一两个跳变值,比如温度突然从24跳到26再跳回24。简单粗暴的做法是连续读5次取平均,但平均算法对“离群值”不敏感,一个26会被平均成24.4,还是不准。

更稳的做法是中值滤波:连续读5次,排序后取中间值。一颗异常的跳变值会被直接丢掉。代价是每次要等5个采样周期,也就是至少5秒出一个值,对温湿度这种缓变信号完全够用。

#define SAMPLE_COUNT 5 // 返回5次采样中的中间值,丢弃跳变点 uint8_t medianFilter(uint8_t* samples) { for (uint8_t i = 0; i < SAMPLE_COUNT - 1; i++) { for (uint8_t j = i + 1; j < SAMPLE_COUNT; j++) { if (samples[j] < samples[i]) { uint8_t tmp = samples[i]; samples[i] = samples[j]; samples[j] = tmp; } } } return samples[SAMPLE_COUNT / 2]; }

如果你的数据抖动不明显,滑动平均就够用;如果抖动明显,直接上中值滤波。别两个都用,会让响应变得迟钝。

5.2 多传感器的接法和注意点

DHT11的单总线协议没有设备地址,不能像I2C那样把多个传感器挂到同一条数据线上。每颗传感器必须独占一个GPIO,所以要用多少颗,就得准备多少个引脚。

接多颗时另有三个细节。第一,每一路都要有自己的上拉电阻,不能共用一个。第二,传感器离主板远了,供电线要适当加粗,避免线阻造成电压跌落。DHT11在工作瞬间会有一个电流脉冲,如果供电线太细,这个压降会让传感器进入欠压状态。第三,读取顺序上不要两个传感器同时读,DHT11的应答时序本身就很紧,串行轮流读取比并行读取稳得多。

5.3 什么时候该换DHT22或者数字温度芯片

用DHT11做了几个项目之后,你大概会开始嫌弃它的精度。说实话,±2℃的温度精度在大多数室内环境都能接受,真正不够用的是湿度,±5%RH的偏差对需要精确控湿的场景基本等于没有。

这时候别犹豫,直接切DHT22。它的协议和DHT11几乎一样,只是小数位有实际意义,把这部分拼上去就能得到0.1℃和0.1%RH的分辨率。代码改动量不超过十行,就是把data[1]和data[3]从“忽略”改成“拼进结果”。如果连湿度都不需要,只测温度,那还有更便宜的单总线温度芯片方案,读取方式更简单,精度还更好。

最后分享一个我的个人习惯:无论多简单的传感器,我都会先写一版裸驱动,跑通之后再考虑引入现成库。DHT11这种协议清晰的器件,花一个晚上把时序吃透,后面再碰其他依赖时序的传感器,基本上半天就能上手。数据读不出来的时候也别急着改代码,先拿万用表量电源、量电平、检查接线,多数问题根本不是逻辑问题,而是物理连接问题。

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

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

立即咨询