3. I2C从设备驱动开发:从设备驱动模型、probe流程、数据传输接口

好,咱们进入第三章。这一章我打算聊聊I2C从设备驱动到底怎么写。很多刚入行的朋友总觉得写驱动就是填几个结构体,注册一下就完事了。其实不然,驱动模型、probe流程、数据传输接口,这三块是环环相扣的。任何一个环节出问题,设备都跑不起来。

我个人习惯把从设备驱动拆成三个层次来看:驱动模型层(怎么注册)、probe流程层(怎么匹配)、数据传输层(怎么读写)。咱们一个一个说。

3.1 从设备驱动模型:结构体与注册

在Linux内核里,I2C从设备驱动主要围绕两个结构体展开:i2c_driver 和 i2c_client。前者代表驱动本身,后者代表设备实例。说白了,驱动是“会干活的人”,设备是“需要被干活的硬件”。

结构体 作用 关键字段
i2c_driver 描述驱动能力,注册到I2C核心 probe, remove, id_table
i2c_client 描述一个具体的I2C从设备 addr, adapter, name
i2c_device_id 驱动与设备的匹配表 name, driver_data

注册驱动的代码模板大概是这样的:

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

static struct i2c_driver mydevice_driver = {
    .driver = {
        .name = "my_i2c_sensor",
        .of_match_table = of_match_ptr(mydevice_of_match),
    },
    .probe    = mydevice_probe,
    .remove   = mydevice_remove,
    .id_table = mydevice_id,
};

module_i2c_driver(mydevice_driver);

这里有个细节:module_i2c_driver 是个宏,它帮你完成了 i2c_add_driver 和 i2c_del_driver 的注册与注销。我刚开始写驱动时,总喜欢手动调用这两个函数,后来发现用宏更干净,还不会漏掉错误处理。

小提示: 如果你的设备支持设备树,记得加上 of_match_table。否则probe可能不会被调用。我曾经在一个项目里忘了加,结果设备树配得再完美,驱动就是纹丝不动。

3.2 probe流程:从匹配到初始化

probe函数是驱动的“入口”。当I2C核心发现一个 i2c_client 与你的 i2c_driver 匹配时,就会调用probe。匹配的依据通常是设备树里的 compatible 字符串,或者ACPI表,或者板级代码里的 i2c_board_info。

probe里一般做三件事:

  1. 获取资源:从设备树或平台数据里读取配置参数。
  2. 初始化硬件:通过I2C读写寄存器,设置设备工作模式。
  3. 注册子系统:比如注册为输入设备、传感器、或者字符设备。

一个典型的probe函数长这样:

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

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

    i2c_set_clientdata(client, data);

    // 2. 读取设备树属性
    if (of_property_read_u32(client->dev.of_node, "vref-mv", &data->vref))
        data->vref = 3300; // 默认值

    // 3. 初始化硬件
    ret = mydevice_hw_init(client);
    if (ret)
        return ret;

    // 4. 注册到子系统
    ret = mydevice_register_subsystem(data);
    if (ret)
        return ret;

    dev_info(&client->dev, "probe success, addr=0x%02x\n", client->addr);
    return 0;
}
注意: probe里不要做耗时操作,比如msleep(1000)。因为probe是在系统启动或设备热插拔时调用的,阻塞太久会影响系统启动速度。如果确实需要延时,考虑用工作队列或定时器推迟执行。

我曾经遇到过一个坑:probe里调用了 i2c_smbus_read_byte_data,但此时I2C控制器还没完全准备好,结果返回了-ENXIO。后来我在probe开头加了一个 msleep(10) 才稳定。嗯,硬件有时就是这么不讲道理。

3.3 数据传输接口:SMBus vs. 原生I2C

数据传输是驱动里最核心的部分。Linux内核提供了两套API:SMBus兼容接口 和 原生I2C消息接口。SMBus接口更简单,但功能有限;原生接口更灵活,但代码量稍大。

接口类型 函数 适用场景
SMBus读字节 i2c_smbus_read_byte_data(client, reg) 单寄存器读取
SMBus写字节 i2c_smbus_write_byte_data(client, reg, val) 单寄存器写入
SMBus读块 i2c_smbus_read_block_data(client, cmd, buf) 连续读取多个字节
原生I2C消息 i2c_transfer(adapter, msgs, num) 复杂传输,如重复起始条件

我个人建议:能用SMBus就用SMBus。代码简洁,可读性强。但如果你需要发送重复起始条件(Repeated Start),或者需要同时读写不同长度的数据,那就得上原生接口了。

原生I2C传输的例子:

static int mydevice_read_block(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 = ®

    // 第二条消息:读数据
    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 < 0)
        return ret;

    return 0;
}
关键点: 原生接口的 i2c_transfer 返回的是成功传输的消息数量,不是字节数。所以判断错误时,要检查返回值是否等于消息数(这里是2),而不是大于0。

3.4 知识结构图:从设备驱动核心流程

下面这张图把整个从设备驱动的核心流程串起来了。从驱动注册到probe,再到数据传输,一目了然。

I2C从设备驱动核心流程 驱动注册 module_i2c_driver 设备匹配 id_table / of_match probe流程 初始化硬件 分配私有数据 读取设备树属性 硬件初始化 注册子系统 数据传输接口 SMBus / 原生I2C消息 错误处理与重试 -EIO / -ENXIO / -ETIMEDOUT 关键说明: 1. 驱动注册后,I2C核心自动进行设备匹配。 2. probe成功返回0,失败返回负错误码。 3. 数据传输接口选择:简单读写用SMBus,复杂场景用原生接口。 4. 错误处理要区分总线错误和设备无应答。

3.5 避坑指南:我踩过的几个雷

写I2C从设备驱动,有几个地方特别容易出问题。我把自己踩过的坑列出来,你遇到了可以少走弯路。

  • 地址不匹配:设备树里写的地址是7位还是10位?很多传感器手册给的是8位地址(含读写位),但内核用的是7位地址。记得右移一位。我曾经因为这个折腾了一下午。
  • probe重复调用:如果设备树里配了多个节点,或者驱动支持多个设备ID,probe可能会被调用多次。确保你的初始化代码是幂等的,或者做好引用计数。
  • 数据传输超时:I2C总线有默认超时时间(通常1秒)。如果你的从设备响应慢,考虑用 i2c_transfer 的 I2C_M_IGNORE_NAK 标志,或者调整时钟频率。
  • 忘记释放资源:probe里用了 devm_kzalloc 可以自动释放,但如果你手动申请了GPIO、中断或DMA,记得在remove里释放。否则热插拔几次,系统就崩了。
严重警告: 不要在中断上下文里调用 i2c_transfer!I2C传输可能睡眠,而中断上下文不允许睡眠。如果非要在中断里读写,考虑使用 threaded IRQ 或 workqueue。