4. I2C设备驱动:客户端注册、读写操作、用户空间接口

好,咱们进入正题。这一章讲的是I2C设备驱动的核心——怎么把设备挂到总线上,怎么读写数据,最后怎么让用户态程序也能用上。说白了,就是打通从硬件寄存器到应用程序的整条链路。

我在做嵌入式项目时,发现很多新手把I2C驱动想得太复杂。其实Linux内核已经把框架搭好了,你要做的就是填对回调函数。嗯,咱们一步步来。

4.1 客户端注册:让内核认识你的设备

设备驱动第一步,就是注册。你得告诉内核:“嘿,我这儿有个I2C设备,挂在哪个地址上,用什么方式匹配。”

4.1.1 设备树匹配(主流方式)

现在做嵌入式,基本都用设备树。你在dts文件里写一段描述,内核启动时就会自动创建对应的i2c_client。

// 设备树片段
&i2c1 {
    status = "okay";
    clock-frequency = <100000>;

    temperature_sensor: tmp102@48 {
        compatible = "ti,tmp102";
        reg = <0x48>;
    };
};

驱动这边,用of_match_table来匹配:

static const struct of_device_id tmp102_of_match[] = {
    { .compatible = "ti,tmp102" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, tmp102_of_match);

4.1.2 传统板级匹配

老项目或者没有设备树的平台,用i2c_device_id:

static const struct i2c_device_id tmp102_id[] = {
    { "tmp102", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, tmp102_id);

4.1.3 probe函数——驱动的入口

匹配成功后,内核调用probe。这里是你初始化硬件、分配资源的地方。

static int tmp102_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct tmp102_data *data;
    int ret;

    // 分配私有数据结构
    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->client = client;
    i2c_set_clientdata(client, data);

    // 检查设备是否存在
    ret = i2c_smbus_read_byte_data(client, TMP102_REG_ID);
    if (ret < 0) {
        dev_err(&client->dev, "device not found\n");
        return -ENODEV;
    }

    // 注册其他子系统(如hwmon、input等)
    // ...

    return 0;
}

个人经验:probe里一定要做设备存在性检查。我遇到过硬件虚焊的情况,板子回来怎么调都不通,最后发现是芯片根本没焊上。加个读ID的校验,能省很多调试时间。

4.2 读写操作:与硬件对话

I2C读写分两种:SMBus接口和普通I2C接口。SMBus是I2C的子集,大部分传感器都支持。我个人习惯优先用SMBus,代码更简洁。

4.2.1 SMBus读写(推荐)

函数 用途 说明
i2c_smbus_read_byte() 读1字节 不带寄存器地址
i2c_smbus_write_byte() 写1字节 不带寄存器地址
i2c_smbus_read_byte_data() 读指定寄存器 最常用
i2c_smbus_write_byte_data() 写指定寄存器 最常用
i2c_smbus_read_word_data() 读2字节 注意字节序
i2c_smbus_read_i2c_block_data() 读多字节 最多32字节

举个例子,读温度传感器的温度值:

static int tmp102_read_temp(struct i2c_client *client, int *temp)
{
    s16 raw;
    int ret;

    ret = i2c_smbus_read_word_data(client, TMP102_REG_TEMP);
    if (ret < 0)
        return ret;

    // TMP102返回的是大端数据,需要转换
    raw = be16_to_cpu((__be16)ret);
    // 12位精度,右移4位
    *temp = raw >> 4;

    return 0;
}

注意:很多I2C设备返回的数据是大端格式。我踩过这个坑——读出来的温度值忽高忽低,查了半天才发现是字节序没转。记住:be16_to_cpu()是你的好朋友。

4.2.2 普通I2C接口(灵活但复杂)

当SMBus不够用时,比如需要连续读写大量数据,就用i2c_transfer():

static int tmp102_bulk_read(struct i2c_client *client,
                            u8 reg, u8 *buf, int len)
{
    struct i2c_msg msgs[2];
    int ret;

    // 第一条消息:写寄存器地址
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;  // 写
    msgs[0].len = 1;
    msgs[0].buf = &reg;

    // 第二条消息:读数据
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;  // 读
    msgs[1].len = len;
    msgs[1].buf = buf;

    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret != 2)
        return -EIO;

    return 0;
}

4.3 用户空间接口:让应用程序访问设备

驱动写好了,怎么让用户态程序用?有三种方式:

  1. sysfs属性文件——简单,适合单个值
  2. 字符设备接口——灵活,适合复杂操作
  3. i2cdev(内核自带)——偷懒,适合调试

4.3.1 sysfs接口(快速实现)

用DEVICE_ATTR宏创建属性文件:

static ssize_t temp_show(struct device *dev,
                         struct device_attribute *attr, char *buf)
{
    struct i2c_client *client = to_i2c_client(dev);
    int temp, ret;

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

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

static DEVICE_ATTR_RO(temp);

// 在probe中创建
static int tmp102_probe(struct i2c_client *client, ...)
{
    // ... 其他初始化

    ret = device_create_file(&client->dev, &dev_attr_temp);
    if (ret)
        return ret;

    return 0;
}

用户态用起来很简单:

# cat /sys/bus/i2c/devices/1-0048/temp
25

4.3.2 字符设备接口(完整控制)

当需要ioctl、mmap等复杂操作时,注册字符设备:

static int tmp102_open(struct inode *inode, struct file *file)
{
    // 从inode获取i2c_client
    struct tmp102_data *data = container_of(inode->i_cdev,
                                            struct tmp102_data, cdev);
    file->private_data = data;
    return 0;
}

static long tmp102_ioctl(struct file *file, unsigned int cmd,
                         unsigned long arg)
{
    struct tmp102_data *data = file->private_data;

    switch (cmd) {
    case TMP102_GET_TEMP:
        return tmp102_read_temp(data->client, (int __user *)arg);
    case TMP102_SET_LIMIT:
        // 设置温度阈值
        break;
    default:
        return -ENOTTY;
    }
}

static const struct file_operations tmp102_fops = {
    .owner = THIS_MODULE,
    .open = tmp102_open,
    .unlocked_ioctl = tmp102_ioctl,
};

4.3.3 i2cdev(调试利器)

内核配置CONFIG_I2C_CHARDEV后,每个I2C适配器都会生成/dev/i2c-N设备。用户态可以直接用ioctl操作:

// 用户态程序示例
int fd = open("/dev/i2c-1", O_RDWR);
ioctl(fd, I2C_SLAVE, 0x48);  // 设置从机地址

// 读寄存器
__u8 reg = 0x00;
__u8 data;
struct i2c_msg msgs[2] = {
    { .addr = 0x48, .flags = 0, .len = 1, .buf = &reg },
    { .addr = 0x48, .flags = I2C_M_RD, .len = 1, .buf = &data },
};
struct i2c_rdwr_ioctl_data rdwr = {
    .msgs = msgs,
    .nmsgs = 2,
};
ioctl(fd, I2C_RDWR, &rdwr);

我的建议:产品发布时别用i2cdev,它绕过了驱动层的权限控制和电源管理。但调试阶段特别好用——我曾经用i2cdev配合一个Python脚本,半小时就把一个新传感器的寄存器摸透了。

4.4 知识结构总览

下面这张图把整个流程串起来了,你写驱动时按这个思路走,基本不会乱:

I2C设备驱动架构总览 设备注册 设备树 / 板级匹配 probe() 初始化 读写操作 SMBus / i2c_transfer 字节序处理 用户空间接口 sysfs / 字符设备 / i2cdev 文件操作接口 核心数据结构 i2c_client → 代表硬件设备 i2c_adapter → 代表I2C控制器 驱动框架集成 i2c_driver → 注册/注销 file_operations → 用户态交互 用户态应用 ↔ 驱动 ↔ 硬件

4.5 避坑指南

最后,分享几个我实际项目中踩过的坑:

  • 地址左移问题:设备树里写0x48,内核会左移1位变成0x90。别在驱动里再左移一次,否则地址就错了。
  • 时钟延展:有些慢速设备会拉低SCL,主控要支持时钟延展。我遇到过一颗PMIC,读寄存器时偶尔卡死,就是时钟延展没处理好。
  • 原子上下文:I2C传输可能会睡眠,不能在中断上下文或自旋锁里直接调用。用i2c_smbus_read_byte_data前,确认当前上下文允许睡眠。
  • 电源管理:别忘了在suspend/resume里保存恢复寄存器。我有个项目,休眠唤醒后传感器读数全乱,就是因为没做寄存器保存。

调试小技巧:用i2cget、i2cset命令行工具快速验证硬件。比如:i2cget -y 1 0x48 0x00 w,直接读温度值。先确认硬件没问题,再怀疑驱动代码。

嗯,这一章的内容就到这儿。客户端注册、读写操作、用户空间接口,这三块搞明白了,I2C设备驱动的基本框架你就掌握了。剩下的就是根据具体芯片的数据手册,填寄存器地址和协议细节。