4、传感器驱动开发(上):DHT22温湿度传感器驱动编写、时序分析、数据校验

各位同学,咱们今天开始啃传感器驱动这块硬骨头。DHT22这个传感器,说实话,在温室项目里用得特别多。便宜、精度还行、单总线通信,省IO口。但坑也不少,我当年第一个温室项目就差点被它搞崩溃。

嗯,咱们先理清楚一件事:DHT22和DHT11啥区别?说白了,DHT22精度更高,测量范围更宽。温度精度±0.5°C,湿度精度±2%RH。DHT11呢?温度±2°C,湿度±5%RH。温室里控温控湿,你想想看,差2度可能苗就蔫了。所以我个人习惯,但凡正经项目,直接上DHT22。

4.1 硬件连接与原理

DHT22只有4个引脚,但实际只用3个:VCC、DATA、GND。第4脚是NC,悬空就行。

引脚 功能 连接
1 VCC 3.3V ~ 5V
2 DATA GPIO + 上拉电阻
3 NC 悬空
4 GND
注意:DATA引脚必须接一个4.7kΩ ~ 10kΩ的上拉电阻到VCC。不接?我试过,数据读出来全是0xFF。别问我怎么知道的,问就是吃过亏。

通信协议是单总线,说白了就是一根线既发又收。主机(MCU)和从机(DHT22)分时复用这根线。时序要求挺严格,微秒级的延迟都得算准。

4.2 通信时序详解

DHT22的通信流程分三步:主机发起、从机响应、数据传输。咱们一步步拆。

4.2.1 主机发起信号

MCU先把DATA线拉低,至少持续18ms。为什么是18ms?因为DHT22要检测到这个低电平信号,才能从休眠中唤醒。我刚开始写的时候只拉了1ms,结果传感器根本不鸟我。

拉低18ms后,MCU释放总线(拉高),然后等待20~40μs。这个等待时间很关键,短了传感器来不及响应,长了就超时了。

// 主机发起信号
void dht22_start(void) {
    // 拉低至少18ms
    gpio_set_mode(DHT22_PIN, GPIO_MODE_OUTPUT);
    gpio_write(DHT22_PIN, 0);
    delay_ms(20);  // 我习惯给20ms,留点余量
    
    // 释放总线,拉高20~40μs
    gpio_write(DHT22_PIN, 1);
    delay_us(30);
    
    // 切换为输入模式,准备接收
    gpio_set_mode(DHT22_PIN, GPIO_MODE_INPUT);
}

4.2.2 从机响应信号

主机释放总线后,DHT22会做两件事:先把总线拉低80μs,再拉高80μs。这就是响应信号。如果没看到这个响应,说明传感器没工作,或者线没接好。

经验之谈:我建议在代码里加个超时判断。如果等待响应超过200μs还没动静,直接返回错误。别死等,嵌入式系统最忌讳死循环。

4.2.3 数据位时序

响应信号之后,就是40位的数据了。每一位的传输方式都一样:

  • 每位开始,DHT22先把总线拉低50μs
  • 然后拉高,拉高的时间长短决定是0还是1
  • 拉高26~28μs表示0
  • 拉高70μs表示1

你想想看,这不就是脉冲宽度调制吗?说白了,就是通过高电平的持续时间来编码。

// 读取一个位
uint8_t dht22_read_bit(void) {
    // 等待低电平结束(50μs)
    while(gpio_read(DHT22_PIN) == 0);
    
    // 延时30μs,在中间采样
    delay_us(30);
    
    // 读取电平状态
    if(gpio_read(DHT22_PIN) == 1) {
        // 高电平还在,说明是1
        while(gpio_read(DHT22_PIN) == 1); // 等待位结束
        return 1;
    } else {
        // 高电平已结束,说明是0
        return 0;
    }
}
核心要点:采样点选在30μs是关键。因为0的高电平只有26~28μs,1的高电平有70μs。30μs时采样,0已经变低了,1还高着。这个时间点我调了好几次才找到最优值。

4.3 数据格式与校验

40位数据怎么分配?看这张表:

字节 位范围 含义
Byte 0 Bit 39 ~ Bit 32 湿度整数部分
Byte 1 Bit 31 ~ Bit 24 湿度小数部分
Byte 2 Bit 23 ~ Bit 16 温度整数部分
Byte 3 Bit 15 ~ Bit 8 温度小数部分
Byte 4 Bit 7 ~ Bit 0 校验和

实际湿度值 = 湿度整数 + 湿度小数/10。温度也一样。比如湿度整数是25,小数是6,那实际湿度就是25.6%RH。

校验和怎么算?简单:Byte0 + Byte1 + Byte2 + Byte3,取低8位。如果等于Byte4,数据有效。否则扔掉重读。

// 数据校验
uint8_t dht22_check_sum(uint8_t data[5]) {
    uint8_t sum = data[0] + data[1] + data[2] + data[3];
    return (sum == data[4]);
}
避坑指南:我曾经遇到过一个情况,校验和每次都错。查了半天,发现是读取时序不对,导致某一位读反了。所以校验失败时,别急着怀疑传感器,先检查你的时序对不对。

4.4 完整驱动代码

把上面这些拼起来,就是一个完整的DHT22驱动。我习惯把读取函数封装成一次完整的事务:发起、等待响应、读40位、校验、返回数据。

// DHT22完整读取函数
int dht22_read(float *humidity, float *temperature) {
    uint8_t data[5] = {0};
    
    // 1. 发起信号
    dht22_start();
    
    // 2. 等待响应(超时200μs)
    uint32_t timeout = 200;
    while(gpio_read(DHT22_PIN) == 1) {
        if(--timeout == 0) return -1; // 无响应
    }
    while(gpio_read(DHT22_PIN) == 0); // 等待低电平结束
    while(gpio_read(DHT22_PIN) == 1); // 等待高电平结束
    
    // 3. 读取40位数据
    for(int i = 0; i < 40; i++) {
        uint8_t bit = dht22_read_bit();
        data[i / 8] = (data[i / 8] << 1) | bit;
    }
    
    // 4. 校验
    if(!dht22_check_sum(data)) return -2; // 校验失败
    
    // 5. 计算实际值
    *humidity = data[0] + data[1] / 10.0;
    *temperature = data[2] + data[3] / 10.0;
    
    return 0; // 成功
}
个人建议:实际项目中,我一般会连续读3次,取平均值。因为单次读数可能有波动,尤其是湿度。另外,两次读取之间至少间隔2秒,DHT22的采样周期就是2秒,读太频繁拿到的都是旧数据。

4.5 常见问题与调试

写驱动时最容易踩的坑,我列几个:

  • 读出来全是0或0xFF:大概率是上拉电阻没接,或者GPIO模式没切对
  • 校验和总错:时序不对,尤其是采样点位置偏了
  • 偶尔读成功偶尔失败:中断干扰了时序。读DHT22时最好关中断,或者用临界区保护
  • 温度湿度值不变:读取间隔太短,传感器没更新数据

嗯,DHT22的驱动开发就讲到这里。下节课咱们继续讲传感器驱动开发的下半部分,重点讲光照传感器和土壤湿度传感器的驱动。到时候会涉及ADC采样和I2C通信,比这个稍微复杂点,但套路是一样的。

记住一句话:传感器驱动,时序是灵魂,校验是底线。时序对了,数据就对了八成。