4、传感器驱动开发(I):温湿度传感器(SHT30/DHT22)数据手册解读、I2C时序分析与驱动代码实现

好,咱们今天来啃一块硬骨头——温湿度传感器驱动。说实话,嵌入式开发里,传感器驱动这块是绕不开的坎。你想想看,一个气象站要是连温湿度都测不准,那还叫气象站吗?

这节课我选了市面上最常用的两款传感器:SHT30和DHT22。一个走I2C,一个走单总线。我个人的习惯是,先啃数据手册,再动手写代码。别急着上电,先把手册看透了,能省下后面一大半调试时间。

4.1 先聊聊SHT30和DHT22的区别

这两款传感器,说白了就是两个时代的产物。DHT22是经典款,便宜、皮实,但精度和稳定性一般。SHT30是后来者,走I2C,精度高,还带校准。

参数 SHT30 DHT22
接口类型 I2C 单总线(One-Wire)
温度精度 ±0.3°C ±0.5°C
湿度精度 ±2% RH ±2-5% RH
采样速率 最高 10 Hz 最高 0.5 Hz
工作电压 2.4V - 5.5V 3.3V - 5.5V
典型价格 约 15-25 元 约 5-10 元

我的建议:如果项目预算允许,优先选SHT30。我在一个农业大棚项目里吃过DHT22的亏——湿度一高,数据就开始飘。后来换成SHT30,稳得很。

4.2 数据手册解读——SHT30篇

拿到SHT30的数据手册,别从头翻到尾。我教你一个方法:先看引脚定义,再看通信协议,最后看寄存器。

4.2.1 引脚定义

SHT30一共8个引脚,但常用的就4个:VDD、GND、SCL、SDA。还有个ADDR引脚,用来设置I2C地址。默认地址是0x44,如果把ADDR拉高,就变成0x45。

小技巧:如果你在一条I2C总线上挂多个SHT30,可以通过ADDR引脚区分。我一般习惯用0x44作为默认地址,省事。

4.2.2 I2C时序分析

SHT30的I2C时序,说白了就是标准的I2C协议。但有几个坑要注意:

  • 起始条件:SCL高电平时,SDA从高变低
  • 停止条件:SCL高电平时,SDA从低变高
  • 数据有效性:SCL低电平时改变SDA,SCL高电平时采样
  • 应答信号:每发送完一个字节,从机拉低SDA表示应答

为什么会这样?因为I2C协议就是这么规定的。你想想看,要是没有这些时序约束,总线上的设备不就乱套了吗?

4.2.3 命令集

SHT30的命令都是16位的。最常用的就这几个:

命令 代码 说明
测量命令(高重复性) 0x2400 默认模式,精度最高
测量命令(中重复性) 0x240B 平衡精度和速度
测量命令(低重复性) 0x2416 速度最快,精度最低
读取状态寄存器 0xF32D 用于诊断
软复位 0x30A2 复位传感器

注意:发送命令时,一定要先发送高字节,再发送低字节。我曾经在这里翻过车,发送顺序搞反了,传感器死活没反应。

4.3 数据手册解读——DHT22篇

DHT22的数据手册就简单多了,总共没几页。但越简单的东西,坑往往越多。

4.3.1 单总线协议

DHT22用的是单总线协议,说白了就是一根线既当时钟又当数据。主机通过拉低总线来发起通信。

时序要求是这样的:

  • 起始信号:主机拉低总线至少18ms,然后释放
  • 响应信号:DHT22拉低总线80μs,再拉高80μs
  • 数据位'0':低电平50μs,高电平26-28μs
  • 数据位'1':低电平50μs,高电平70μs

嗯,这里要注意。DHT22的时序要求非常严格,尤其是起始信号的18ms。我刚开始做的时候,用了个10ms的延时,结果数据死活读不出来。后来一查手册,才发现是延时不够。

4.3.2 数据格式

DHT22一次传输40位数据:

  • 高16位:湿度数据(乘以0.1得到实际湿度)
  • 低16位:温度数据(乘以0.1得到实际温度)
  • 最后8位:校验和

校验方法:将前4个字节相加,取低8位,应该等于校验和。如果不相等,说明数据有误,直接丢弃。

4.4 驱动代码实现——SHT30

好,理论讲完了,咱们来写代码。我习惯用HAL库,但原理是一样的。

// SHT30驱动代码片段
#define SHT30_ADDR         0x44 << 1
#define SHT30_CMD_MEASURE  0x2400

// 读取温湿度
uint8_t SHT30_ReadTempHum(float *temp, float *hum) {
    uint8_t buf[6];
    uint16_t raw_temp, raw_hum;
    
    // 发送测量命令
    uint8_t cmd[2] = {0x24, 0x00};
    HAL_I2C_Master_Transmit(&hi2c1, SHT30_ADDR, cmd, 2, 100);
    
    // 等待测量完成(至少15ms)
    HAL_Delay(20);
    
    // 读取6字节数据
    HAL_I2C_Master_Receive(&hi2c1, SHT30_ADDR, buf, 6, 100);
    
    // 解析数据
    raw_hum = (buf[0] << 8) | buf[1];
    raw_temp = (buf[3] << 8) | buf[4];
    
    // 转换为实际值
    *hum = 100.0f * raw_hum / 65535.0f;
    *temp = 175.0f * raw_temp / 65535.0f - 45.0f;
    
    return 0;
}

注意:转换公式是数据手册里给的。我建议你直接复制手册里的公式,别自己推导,容易出错。

4.5 驱动代码实现——DHT22

DHT22的驱动就麻烦一些,因为要精确控制时序。我一般用GPIO模拟,配合定时器。

// DHT22驱动代码片段
#define DHT22_GPIO_PORT    GPIOB
#define DHT22_GPIO_PIN     GPIO_PIN_0

uint8_t DHT22_ReadTempHum(float *temp, float *hum) {
    uint8_t data[5] = {0};
    uint8_t i, j;
    
    // 主机发送起始信号
    HAL_GPIO_WritePin(DHT22_GPIO_PORT, DHT22_GPIO_PIN, GPIO_PIN_RESET);
    HAL_Delay(20);  // 至少18ms
    HAL_GPIO_WritePin(DHT22_GPIO_PORT, DHT22_GPIO_PIN, GPIO_PIN_SET);
    
    // 等待DHT22响应
    delay_us(40);
    if (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_GPIO_PIN) == GPIO_PIN_RESET) {
        // 等待80μs低电平结束
        while (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_GPIO_PIN) == GPIO_PIN_RESET);
        // 等待80μs高电平结束
        while (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_GPIO_PIN) == GPIO_PIN_SET);
        
        // 读取40位数据
        for (j = 0; j < 5; j++) {
            for (i = 0; i < 8; i++) {
                // 等待低电平结束
                while (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_GPIO_PIN) == GPIO_PIN_RESET);
                delay_us(40);
                if (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_GPIO_PIN) == GPIO_PIN_SET) {
                    data[j] |= (1 << (7 - i));
                }
                // 等待高电平结束
                while (HAL_GPIO_ReadPin(DHT22_GPIO_PORT, DHT22_GPIO_PIN) == GPIO_PIN_SET);
            }
        }
        
        // 校验
        uint8_t checksum = data[0] + data[1] + data[2] + data[3];
        if (checksum == data[4]) {
            *hum = ((data[0] << 8) | data[1]) * 0.1f;
            *temp = ((data[2] << 8) | data[3]) * 0.1f;
            return 0;
        }
    }
    return 1;  // 读取失败
}

避坑指南:我曾经在DHT22的时序上栽过跟头。那个40μs的延时,用普通的for循环延时根本不准。一定要用定时器或者SysTick来精确延时。否则,数据读出来全是0xFF。

4.6 调试技巧与常见问题

写完了驱动,怎么调试?我分享几个经验:

  1. 先读ID寄存器:SHT30有个状态寄存器,读一下看看能不能正常通信。能读到0xFFFF以外的值,说明I2C通了。
  2. 用逻辑分析仪:别靠猜。把SCL和SDA挂到逻辑分析仪上,看看波形对不对。我每次调试I2C设备,第一件事就是抓波形。
  3. 注意上拉电阻:I2C总线需要上拉电阻,一般是4.7kΩ。没有上拉电阻,通信会时好时坏。
  4. DHT22的采样间隔:DHT22要求两次读取间隔至少2秒。读得太快,传感器会罢工。

我的经验:调试传感器驱动,最忌讳的就是「我觉得没问题」。一定要用工具验证。逻辑分析仪、示波器,该上就上。省下来的时间,够你喝好几杯咖啡了。

4.7 小结

这节课我们讲了SHT30和DHT22的驱动开发。从数据手册解读,到I2C和单总线时序分析,再到代码实现。说白了,传感器驱动开发就是三个步骤:看懂手册、写对时序、做好校验。

下节课,我们会讲气压传感器和风速传感器的驱动。到时候你会发现,有了这节课的基础,后面的内容就轻松多了。

好,今天就到这里。有什么问题,咱们在群里聊。