2. DHT22温湿度传感器驱动:从时序到实战

DHT22这颗传感器,我用了不下五年。说实话,第一次调它的时候差点被时序搞疯——单总线协议看着简单,但坑是真不少。今天咱们就把它的底裤扒干净,从通信协议到代码实现,一步到位。

2.1 DHT22通信协议解析

DHT22用的是单总线协议,说白了就是一根数据线既当输入又当输出。主机和传感器之间靠拉高拉低电平来传递信息。我刚开始做项目时总觉得单总线比I2C简单,后来发现恰恰相反——时序要求太严格了。

先看数据格式。DHT22一次传输40位数据,结构如下:

数据位 内容 说明
bit 31-24 湿度整数部分 0-100% RH
bit 23-16 湿度小数部分 精确到0.1%
bit 15-8 温度整数部分 -40~80°C
bit 7-0 温度小数部分 精确到0.1°C
bit 39-32 校验和 前4字节之和的低8位

举个例子:如果收到 0000 0010 1001 0010 0000 0001 1011 1100 1010 0010,拆开看就是:

  • 湿度 = 0x02 0x92 = 658 → 65.8% RH
  • 温度 = 0x01 0xBC = 444 → 44.4°C
  • 校验和 = 0xA2
  • 验证:0x02 + 0x92 + 0x01 + 0xBC = 0x151,取低8位 = 0x51 ≠ 0xA2 → 数据无效
注意:DHT22的温度最高位是符号位。如果温度整数部分大于0x80,说明是负温度。比如0x80 0x00代表-0.0°C,0x81 0x02代表-10.2°C。这个坑我踩过,读出来温度忽高忽低,查了半天才发现是符号位没处理。

2.2 时序图解读

时序图这东西,很多人一看就头大。其实你把它拆成几个阶段就好理解了。DHT22的通信过程分四步:

  1. 主机发起信号:主机拉低总线至少18ms,然后拉高20-40μs
  2. 传感器响应:传感器拉低80μs,再拉高80μs
  3. 数据传输:40位数据,每位由低电平50μs+高电平决定
  4. 结束:传感器拉低50μs后释放总线

关键在第三步。每个数据位都是先拉低50μs,然后拉高。高电平持续26-28μs代表"0",持续70μs代表"1"。说白了就是看高电平的时间长短。

我的经验:实际测量时,26μs和70μs的边界没那么清晰。我一般取中间值50μs作为判断阈值。高电平超过50μs算"1",否则算"0"。这个阈值在大多数MCU上都能稳定工作。

下面这张图把整个时序流程画清楚了:

DHT22 单总线通信时序 主机拉低≥18ms 主机拉高20-40μs 传感器拉低80μs 传感器拉高80μs 40位数据传输(每位50μs低+高电平) 位"0":低50μs + 高26-28μs 高电平短 → 判为0 位"1":低50μs + 高70μs 高电平长 → 判为1 传感器拉低50μs释放总线 💡 时序要点 • 主机拉低必须≥18ms,太短传感器不响应,太长会误判为复位 • 每位数据从低电平开始,读高电平宽度判断0/1 • 两次读取间隔建议≥2秒,DHT22采样频率有限 • 数据线必须接上拉电阻(4.7kΩ典型值),否则通信不稳定

2.3 使用Adafruit_DHT库读取数据

自己写时序代码太折腾了,尤其是不同平台移植起来很麻烦。我推荐直接用Adafruit_DHT库,它把底层时序封装好了,支持树莓派、Jetson Nano等主流平台。

安装很简单:

pip install Adafruit_DHT

然后看个完整示例:

import Adafruit_DHT
import time

# 定义传感器类型和引脚
SENSOR = Adafruit_DHT.DHT22
PIN = 4  # GPIO4,对应物理引脚7

def read_dht22():
    """
    读取DHT22温湿度数据
    返回:(humidity, temperature) 或 (None, None)
    """
    humidity, temperature = Adafruit_DHT.read_retry(SENSOR, PIN)
    return humidity, temperature

def main():
    print("DHT22温湿度读取程序启动...")
    print("按 Ctrl+C 退出\n")
    
    while True:
        humidity, temperature = read_dht22()
        
        if humidity is not None and temperature is not None:
            # 温度单位是摄氏度,湿度是百分比
            print(f"温度: {temperature:.1f}°C, 湿度: {humidity:.1f}%")
        else:
            print("读取失败,请检查接线")
        
        # DHT22建议读取间隔2秒以上
        time.sleep(2)

if __name__ == "__main__":
    main()

我的习惯:实际项目中我不会直接用read_retry(),因为它会无限重试。我一般自己写个带超时的循环,比如重试3次就放弃。这样系统不会因为传感器故障而卡死。

2.4 数据校验与异常处理

数据校验这块,很多人觉得DHT22自带校验和就万事大吉了。其实不然。我在项目中遇到过好几次校验和通过但数据明显异常的情况——比如湿度突然跳到120%,温度飙到99°C。

为什么会这样?因为校验和只能检测传输错误,不能检测传感器本身故障。所以我们需要加几层防护:

2.4.1 校验和验证

Adafruit_DHT库内部已经做了校验和验证,但如果你自己写驱动,一定要手动校验:

def verify_checksum(data):
    """
    data: 长度为5的字节数组 [hum_H, hum_L, temp_H, temp_L, checksum]
    返回 True/False
    """
    if len(data) != 5:
        return False
    total = (data[0] + data[1] + data[2] + data[3]) & 0xFF
    return total == data[4]

2.4.2 范围检查

DHT22的官方参数范围:

参数 最小值 最大值
温度 -40°C 80°C
湿度 0% RH 100% RH

超出这个范围的数据,直接扔掉。我一般还会加个变化率限制——比如相邻两次读取的温度变化超过5°C,大概率是传感器出问题了。

2.4.3 超时处理

DHT22的响应时间一般在20-200ms之间。如果超过200ms还没收到响应,直接超时退出。别死等。

import time

def read_with_timeout(sensor, pin, timeout=0.5):
    """
    带超时的DHT22读取
    """
    start = time.time()
    while time.time() - start < timeout:
        humidity, temperature = Adafruit_DHT.read(sensor, pin)
        if humidity is not None:
            return humidity, temperature
        time.sleep(0.05)  # 50ms轮询一次
    return None, None  # 超时返回None
避坑指南:我曾经在一个项目里连续读取DHT22,间隔只有100ms。结果读出来的数据全是乱的。后来查手册才发现,DHT22每次读取后需要至少2秒的稳定时间。你想想看,传感器内部还在测量呢,你急着读肯定拿不到正确数据。所以两次读取间隔一定要≥2秒。

2.5 完整驱动封装

把上面这些整合起来,写一个健壮的驱动类:

import Adafruit_DHT
import time

class DHT22Driver:
    def __init__(self, pin=4, retry=3, timeout=0.5, interval=2):
        self.pin = pin
        self.sensor = Adafruit_DHT.DHT22
        self.retry = retry
        self.timeout = timeout
        self.interval = interval
        self.last_read = 0
        
    def read(self):
        """带异常处理的读取方法"""
        # 检查读取间隔
        if time.time() - self.last_read < self.interval:
            return None, None
        
        for attempt in range(self.retry):
            try:
                humidity, temperature = Adafruit_DHT.read(
                    self.sensor, self.pin
                )
                
                if humidity is not None and temperature is not None:
                    # 范围检查
                    if -40 <= temperature <= 80 and 0 <= humidity <= 100:
                        self.last_read = time.time()
                        return round(temperature, 1), round(humidity, 1)
                    else:
                        print(f"数据超范围: T={temperature}, H={humidity}")
                else:
                    print(f"第{attempt+1}次读取失败")
                    
            except Exception as e:
                print(f"读取异常: {e}")
            
            time.sleep(0.1)
        
        return None, None

# 使用示例
dht = DHT22Driver(pin=4)
temp, hum = dht.read()
if temp:
    print(f"温度: {temp}°C, 湿度: {hum}%")

总结一下:DHT22驱动看似简单,但要做好异常处理并不容易。我的经验是——校验和检查是底线,范围检查是常规操作,超时和重试机制是保命符。这三层防护做好了,传感器基本不会拖垮整个系统。

嗯,到这里DHT22的驱动就讲完了。代码可以直接拿去用,但建议你根据实际硬件平台微调一下时序参数。不同MCU的主频不同,延时函数精度也不一样,跑一遍测试就知道该调多少了。