4、I2C/SPI传感器驱动开发:I2C协议与Linux I2C子系统、温湿度传感器(SHT30/DHT22)驱动实现、气压传感器(BMP280)驱动实现、用户空间sysfs接口导出

好,咱们进入第四章。这一章我打算跟你聊聊传感器驱动开发。说实话,这是嵌入式Linux项目里最常碰到的活儿。你想想看,气象数据采集系统,核心就是靠传感器嘛。温湿度、气压,这些数据怎么从硬件读到应用层?嗯,这就是本章要解决的问题。

4.1 I2C协议基础与Linux I2C子系统

先说说I2C协议。I2C是个串行通信协议,两根线:SCL(时钟)和SDA(数据)。它属于主从架构,一个总线上可以挂多个设备。每个设备有唯一地址,7位或10位。我个人习惯用7位地址,简单够用。

I2C的通信流程其实不复杂:

  • 主机发送起始条件
  • 然后发送从机地址+读写位
  • 从机应答后,开始传输数据
  • 最后主机发送停止条件

我在项目中遇到过一个问题:总线上两个设备地址冲突了。当时排查了半天,最后发现是芯片手册上写的地址是7位,但我代码里用了8位。嗯,这里要注意,很多数据手册给的是7位地址,左移一位才是你在代码里要用的。

Linux下怎么操作I2C?Linux I2C子系统分三层:

  • I2C核心层:提供通用接口和数据结构
  • I2C总线驱动:处理硬件相关的适配器操作
  • I2C设备驱动:针对具体传感器芯片的驱动

说白了,我们写传感器驱动,主要就是写设备驱动这一层。Linux内核里有个好东西叫i2c-dev,它允许你在用户空间直接操作I2C设备。调试阶段特别方便。

小技巧:调试I2C设备时,先用i2cdetect -y 总线号扫描一下,看看设备地址对不对。我曾经因为没做这步,浪费了半天时间。

4.2 温湿度传感器驱动实现:SHT30与DHT22

温湿度传感器,我选了两个典型代表:SHT30和DHT22。SHT30是数字I2C接口,精度高。DHT22是单总线协议,便宜但时序要求严格。

4.2.1 SHT30驱动实现

SHT30的I2C地址是0x44(7位)。它有个命令集,比如测量命令0x2C06。发送这个命令后,等待一段时间,读取6个字节数据:温度高字节、温度低字节、CRC校验、湿度高字节、湿度低字节、CRC校验。

代码实现大概这样:

/* SHT30驱动核心代码片段 */
#define SHT30_I2C_ADDR  0x44
#define SHT30_CMD_MEAS  0x2C06

static int sht30_read_data(struct i2c_client *client, 
                           int *temp, int *humi)
{
    u8 buf[6];
    int ret;

    /* 发送测量命令 */
    u8 cmd[2] = {0x2C, 0x06};
    ret = i2c_master_send(client, cmd, 2);
    if (ret < 0) return ret;

    msleep(20);  /* 等待测量完成 */

    /* 读取数据 */
    ret = i2c_master_recv(client, buf, 6);
    if (ret < 0) return ret;

    /* 计算温度和湿度 */
    *temp = ((buf[0] << 8) | buf[1]) * 175 / 65535 - 45;
    *humi = ((buf[3] << 8) | buf[4]) * 100 / 65535;

    return 0;
}

这里有个坑:CRC校验。SHT30每个数据后面跟一个CRC字节。我刚开始没做校验,数据偶尔跳变。后来加上CRC校验,问题就解决了。你想想看,工业环境里干扰多,不做校验风险很大。

4.2.2 DHT22驱动实现

DHT22用的是单总线协议,不是标准I2C。但很多开发板会把DHT22接在GPIO上模拟时序。它的数据格式是40位:16位湿度、16位温度、8位校验和。

时序要求很严格:主机拉低至少18ms,然后拉高20-40us。从机响应后,数据位用高低电平宽度表示0或1。

注意:DHT22对时序非常敏感。我曾经在中断频繁的系统上读DHT22,数据经常出错。解决办法是关中断或者用高优先级任务来读。
/* DHT22读取时序示例 */
static int dht22_read_data(int gpio, int *temp, int *humi)
{
    /* 主机发起信号 */
    gpio_direction_output(gpio, 0);
    msleep(20);  /* 拉低至少18ms */
    gpio_direction_output(gpio, 1);
    udelay(30);  /* 拉高20-40us */

    /* 等待从机响应 */
    gpio_direction_input(gpio);
    while(gpio_get_value(gpio) == 1);  /* 等待从机拉低 */
    while(gpio_get_value(gpio) == 0);  /* 等待从机拉高 */

    /* 读取40位数据 */
    for(i = 0; i < 40; i++) {
        while(gpio_get_value(gpio) == 0);  /* 等待数据位开始 */
        udelay(40);
        if(gpio_get_value(gpio) == 1)
            data |= (1 << (39 - i));
        while(gpio_get_value(gpio) == 1);  /* 等待数据位结束 */
    }

    /* 解析数据 */
    *humi = (data >> 24) & 0xFFFF;
    *temp = (data >> 8) & 0xFFFF;
    /* 校验和验证 */
    ...
}

4.3 气压传感器BMP280驱动实现

BMP280是博世的气压传感器,I2C接口。它内部有校准参数,存储在寄存器0x88到0xA1。每次读取原始数据后,要用这些校准参数做补偿计算。

BMP280的I2C地址是0x76(SDO接地)或0x77(SDO接VDD)。

驱动实现要点:

  • 初始化时读取校准参数
  • 配置测量模式(比如正常模式、强制模式)
  • 读取原始温度和压力数据
  • 用校准参数计算实际值
/* BMP280校准参数结构体 */
struct bmp280_calib {
    u16 dig_T1;
    s16 dig_T2;
    s16 dig_T3;
    u16 dig_P1;
    s16 dig_P2;
    s16 dig_P3;
    s16 dig_P4;
    s16 dig_P5;
    s16 dig_P6;
    s16 dig_P7;
    s16 dig_P8;
    s16 dig_P9;
};

/* 温度补偿计算 */
static s32 bmp280_compensate_temp(s32 adc_temp, 
                                   struct bmp280_calib *calib)
{
    s32 var1, var2, T;
    var1 = ((((adc_temp >> 3) - ((s32)calib->dig_T1 << 1))) *
            ((s32)calib->dig_T2)) >> 11;
    var2 = (((((adc_temp >> 4) - ((s32)calib->dig_T1)) *
              ((adc_temp >> 4) - ((s32)calib->dig_T1))) >> 12) *
            ((s32)calib->dig_T3)) >> 14;
    T = (var1 + var2) >> 5;
    return T;  /* 单位:0.01摄氏度 */
}

说实话,BMP280的补偿计算有点绕。我第一次看数据手册时也懵了。后来我写了个测试程序,把原始数据和校准参数打印出来,对照手册一步步算,才搞明白。

4.4 用户空间sysfs接口导出

驱动写好了,怎么让应用层读到数据?Linux下最常用的方法就是sysfs。sysfs是个虚拟文件系统,挂载在/sys目录下。我们可以把传感器数据导出成文件,应用层直接cat就能读到。

实现方法:在驱动里创建attribute,然后注册到sysfs。

/* sysfs接口导出示例 */
static ssize_t temp_show(struct device *dev,
                         struct device_attribute *attr,
                         char *buf)
{
    struct i2c_client *client = to_i2c_client(dev);
    int temp, humi;
    int ret;

    ret = sht30_read_data(client, &temp, &humi);
    if (ret < 0)
        return ret;

    return sprintf(buf, "%d\n", temp);
}

static DEVICE_ATTR(temp, S_IRUGO, temp_show, NULL);

/* 在probe函数中创建sysfs文件 */
static int sht30_probe(struct i2c_client *client,
                       const struct i2c_device_id *id)
{
    /* ... 其他初始化 ... */
    device_create_file(&client->dev, &dev_attr_temp);
    device_create_file(&client->dev, &dev_attr_humi);
    return 0;
}

这样,应用层就能这样读数据:

# cat /sys/bus/i2c/devices/0-0044/temp
2567  # 表示25.67摄氏度
经验之谈:sysfs接口设计时,我建议一个文件只导出一个数据。别搞成"一个文件包含所有数据",那样应用层解析起来麻烦。简洁就是美。

还有个细节:权限设置。S_IRUGO表示所有用户可读。如果数据需要保密,可以设成S_IRUSR(仅所有者可读)。

嗯,这一章的内容就这些。从I2C协议到具体传感器驱动,再到sysfs接口导出,一条线串下来了。下一章我们会聊聊数据采集的调度策略和缓存管理。到时候见。