3. I2C驱动框架

好,我们进入第三章。这一章是Sensor驱动开发的核心基础——I2C驱动框架。说实话,我见过不少工程师,写Sensor驱动时直接照搬别人的模板,一旦遇到I2C通信异常就抓瞎了。说白了,就是没搞懂Linux I2C子系统到底是怎么工作的。

这一章,我会带你从架构到实战,把I2C驱动框架彻底讲透。

3.1 Linux I2C子系统架构

先看整体架构。Linux I2C子系统分为三层:

  • I2C核心层:提供总线注册、设备注册、驱动注册等通用接口
  • I2C总线驱动层:也就是适配器驱动,直接操作硬件I2C控制器
  • I2C设备驱动层:我们写Sensor驱动主要待的地方

我刚开始接触这个框架时,总觉得分层太多,有点绕。后来在项目中调试一个温度传感器,发现只要把这三层的职责搞清楚,定位问题就快多了。

核心思想:I2C核心层负责"牵线搭桥",总线驱动负责"怎么发",设备驱动负责"发什么"。

下面这张图,我建议你多看几遍,它把整个I2C子系统的数据流和依赖关系都画出来了。

Linux I2C 子系统架构图 用户空间 (User Space) 内核空间 I2C 设备驱动层 i2c_driver / i2c_client I2C 核心层 i2c-core / i2c-dev I2C 总线驱动层 i2c_adapter / algorithm 硬件层 (I2C Controller) 关键数据结构 • i2c_driver • i2c_client • i2c_adapter • i2c_algorithm • i2c_msg • i2c_transfer • smbus_xxx 注册/匹配 管理/调度 硬件操作 数据流向:用户空间 → 设备驱动 → 核心层 → 总线驱动 → 硬件 I2C Sensor I2C Sensor

3.2 I2C设备驱动注册流程

写一个I2C设备驱动,核心就是注册一个i2c_driver结构体。我习惯把这个流程拆成四步:

  1. 定义i2c_driver结构体:填充probe、remove、id_table等回调
  2. 实现probe函数:在这里初始化设备、注册字符设备或输入设备
  3. 实现remove函数:释放资源、注销设备
  4. 注册/注销驱动:module_i2c_driver宏一键搞定

我的习惯:probe函数里一定要先检查i2c_check_functionality,确认适配器支持你需要的功能。我曾经在一个项目里没做这个检查,结果适配器不支持SMBus块读,调试了整整两天。

来看一个典型的Sensor驱动框架代码:

#include <linux/i2c.h>
#include <linux/module.h>

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

    dev_info(&client->dev, "Sensor probe start\n");

    // 1. 检查适配器功能
    if (!i2c_check_functionality(client->adapter, 
                                 I2C_FUNC_SMBUS_BYTE_DATA |
                                 I2C_FUNC_SMBUS_I2C_BLOCK)) {
        dev_err(&client->dev, "Adapter不支持所需功能\n");
        return -ENODEV;
    }

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

    // 3. 保存client指针
    data->client = client;
    i2c_set_clientdata(client, data);

    // 4. 初始化设备(读ID、配置寄存器等)
    ret = sensor_init_hw(client);
    if (ret)
        return ret;

    // 5. 注册其他子系统(如IIO、输入子系统等)
    // ...

    dev_info(&client->dev, "Sensor probe success\n");
    return 0;
}

static int sensor_remove(struct i2c_client *client)
{
    struct sensor_data *data = i2c_get_clientdata(client);

    // 1. 注销其他子系统
    // 2. 关闭设备
    sensor_power_down(client);

    dev_info(&client->dev, "Sensor removed\n");
    return 0;
}

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

#ifdef CONFIG_OF
static const struct of_device_id sensor_of_match[] = {
    { .compatible = "vendor,sensor-xxx" },
    { }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
#endif

static struct i2c_driver sensor_driver = {
    .driver = {
        .name   = "sensor_xxx",
        .of_match_table = of_match_ptr(sensor_of_match),
    },
    .probe    = sensor_probe,
    .remove   = sensor_remove,
    .id_table = sensor_id,
};

module_i2c_driver(sensor_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("I2C Sensor Driver");

注意:of_match_table和id_table至少要提供一个。设备树匹配优先于id_table匹配。如果两个都提供了,内核会先查设备树。

3.3 i2c_transfer与SMBus接口

驱动注册好了,接下来就是怎么和Sensor通信了。Linux I2C子系统提供了两套API:

接口类型 核心函数 适用场景 特点
i2c_transfer i2c_transfer(adapter, msgs, num) 任意I2C通信,支持复合消息 灵活,可组合读写,需要构造i2c_msg
SMBus接口 i2c_smbus_read_byte_data()等 寄存器读写、简单命令 封装好,使用简单,但功能受限

我个人更倾向于在Sensor驱动里用SMBus接口,因为它封装了大部分细节。但遇到一些特殊时序要求时,就得用i2c_transfer了。

3.3.1 SMBus接口实战

SMBus接口其实就是对i2c_transfer的封装。最常用的几个:

  • i2c_smbus_read_byte_data(client, reg):读一个8位寄存器
  • i2c_smbus_write_byte_data(client, reg, value):写一个8位寄存器
  • i2c_smbus_read_word_data(client, reg):读一个16位寄存器
  • i2c_smbus_read_i2c_block_data(client, reg, len, buf):块读
  • i2c_smbus_write_i2c_block_data(client, reg, len, buf):块写

经验之谈:很多Sensor的数据手册会写"支持I2C快速模式",但实际用SMBus接口完全没问题。SMBus是I2C的子集,时序更严格。如果你的Sensor能跑SMBus,那I2C肯定也能跑。

看一个实际读Sensor ID的例子:

static int sensor_read_id(struct i2c_client *client)
{
    int ret;
    u8 chip_id;

    ret = i2c_smbus_read_byte_data(client, SENSOR_REG_CHIP_ID);
    if (ret < 0) {
        dev_err(&client->dev, "读取芯片ID失败: %d\n", ret);
        return ret;
    }

    chip_id = ret & 0xFF;
    dev_info(&client->dev, "芯片ID: 0x%02x\n", chip_id);

    if (chip_id != EXPECTED_CHIP_ID) {
        dev_err(&client->dev, "芯片ID不匹配: 期望0x%02x, 实际0x%02x\n",
                EXPECTED_CHIP_ID, chip_id);
        return -ENODEV;
    }

    return 0;
}

3.3.2 i2c_transfer高级用法

什么时候必须用i2c_transfer?我遇到过两种情况:

  1. 需要组合消息:比如先发命令再读数据,中间不产生STOP信号
  2. 需要自定义时序:比如某些Sensor要求先写寄存器地址,再写数据,但地址和数据之间要有特殊延迟

来看一个组合消息的例子:

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

    // 消息0:写寄存器起始地址
    msgs[0].addr  = client->addr;
    msgs[0].flags = 0;  // 写
    msgs[0].len   = 1;
    msgs[0].buf   = &start_reg;

    // 消息1:读数据
    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) {
        dev_err(&client->dev, "i2c_transfer失败: %d\n", ret);
        return ret < 0 ? ret : -EIO;
    }

    return 0;
}

避坑指南:i2c_transfer返回的是成功传输的消息数量,不是字节数。如果返回1,说明第一条写成功了,但第二条读失败了。我曾经在这里踩过坑,一直以为返回值是字节数,结果数据全是错的。

3.4 调试技巧与常见问题

最后分享几个我在项目中积累的调试经验:

  • 先确认硬件地址:用i2cdetect工具扫描总线,确认Sensor的I2C地址是否正确。我遇到过好几次,原理图上写的地址和实际焊的地址不一样。
  • 检查适配器功能:i2c_check_functionality一定要调用,否则后面调用SMBus接口可能返回-EOPNOTSUPP。
  • 善用dev_dbg:在probe和读写函数里加dev_dbg打印,动态调试时打开,能快速定位问题。
  • 注意时序:有些Sensor对I2C时序很敏感,尤其是上电后需要等待一段时间才能通信。我习惯在probe里加一个msleep。

我的调试三板斧:1) i2cdetect看设备在不在;2) i2cget读寄存器看返回值;3) 示波器抓波形。这三步走完,90%的问题都能定位。

好了,这一章的内容就到这里。I2C驱动框架是Sensor驱动开发的基础,把这三层架构和两种通信接口搞明白,后面写具体Sensor驱动时就会顺手很多。