3. I2C总线驱动开发:I2C核心层API、I2C适配器驱动、I2C设备驱动编写

好,咱们进入I2C总线驱动开发这一章。I2C这玩意儿,在机顶盒里太常见了——调电压的PMIC、读温度的传感器、控制HDMI的EDID,几乎都靠它。我个人习惯把I2C驱动分成三层来看:核心层、适配器层、设备层。搞懂了这三层,写驱动就跟搭积木一样。

3.1 I2C核心层API:你离不开的那些函数

核心层是Linux内核帮你封装好的。说白了,你不用管底层怎么发START信号、怎么ACK,直接用API就行。我刚开始做驱动时,总想自己操作寄存器,后来发现——嗯,内核大佬们已经搞定了,别重复造轮子。

常用的核心API就这几个:

  • i2c_add_adapter() / i2c_del_adapter():注册和注销I2C适配器。适配器就是你的I2C控制器硬件。
  • i2c_new_device() / i2c_unregister_device():动态添加或移除I2C设备。设备就是挂在总线上的从机。
  • i2c_transfer():最底层的传输函数。一次调用可以发多个消息(读+写组合)。
  • i2c_master_send() / i2c_master_recv():封装好的单次发送和接收。适合简单场景。
  • i2c_smbus_read_byte_data() / i2c_smbus_write_byte_data():SMBus风格的读写。很多传感器都支持这种协议。

核心要点:i2c_transfer() 是万能钥匙。其他API底层都调它。你如果遇到奇葩设备,直接拼struct i2c_msg数组,用i2c_transfer()搞定。

举个例子,我写一个温度传感器的读操作:

#include <linux/i2c.h>

int temp_sensor_read(struct i2c_client *client, u8 reg, u8 *val)
{
    struct i2c_msg msg[2];
    int ret;

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

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

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret < 0)
        return ret;

    return 0;
}

你看,代码很简洁。核心就是填充msg结构体,然后交给i2c_transfer。我在项目中遇到过一个问题:有些从机在写地址后需要一点延时才能读数据。这时候你可以在两条消息之间加个delay,或者用SMBus的读写组合函数。

3.2 I2C适配器驱动:让硬件跑起来

适配器驱动,说白了就是你的I2C控制器驱动。机顶盒里常见的I2C控制器有DesignWare、SUNXI、Rockchip等。适配器驱动的核心是填充一个struct i2c_algorithm结构体。

这个结构体里最重要的两个回调:

  • master_xfer():实现I2C总线的数据传输。你要在这里操作控制器的寄存器,发送START、发送地址、读写数据、发送STOP。
  • functionality():返回适配器支持的功能。比如是否支持10位地址、是否支持SMBus等。

我写一个简化的适配器驱动框架:

static int my_i2c_master_xfer(struct i2c_adapter *adap,
                              struct i2c_msg *msgs, int num)
{
    int i;
    // 遍历每个消息
    for (i = 0; i < num; i++) {
        // 发送START信号
        // 发送从机地址 + 读写位
        // 发送或接收数据
        // 发送STOP信号
        // 处理错误和重试
    }
    return num;
}

static u32 my_i2c_functionality(struct i2c_adapter *adap)
{
    return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}

static struct i2c_algorithm my_i2c_algo = {
    .master_xfer   = my_i2c_master_xfer,
    .functionality = my_i2c_functionality,
};

注意:master_xfer() 里一定要处理好超时和错误重试。我曾经遇到一个坑——某款机顶盒的I2C控制器在总线忙时不会自动重试,导致系统卡死。后来我在xfer里加了忙等待和超时退出,才解决问题。

适配器注册的完整流程:

  1. 分配并初始化struct i2c_adapter结构体
  2. 设置algo指针指向你的算法结构体
  3. 设置dev.parent指向你的平台设备
  4. 调用i2c_add_adapter()注册

代码示例:

static int my_i2c_probe(struct platform_device *pdev)
{
    struct i2c_adapter *adap;
    int ret;

    adap = kzalloc(sizeof(*adap), GFP_KERNEL);
    if (!adap)
        return -ENOMEM;

    adap->owner = THIS_MODULE;
    adap->class = I2C_CLASS_DEPRECATED;
    adap->algo = &my_i2c_algo;
    adap->dev.parent = &pdev->dev;
    snprintf(adap->name, sizeof(adap->name), "my_i2c_adapter");

    ret = i2c_add_adapter(adap);
    if (ret) {
        kfree(adap);
        return ret;
    }

    platform_set_drvdata(pdev, adap);
    return 0;
}

3.3 I2C设备驱动:跟从机对话

设备驱动就是你的从机驱动。比如温度传感器、EEPROM、音频Codec。设备驱动的核心是struct i2c_driver结构体。

关键字段:

  • probe():设备匹配成功后调用。在这里初始化硬件、注册字符设备或输入设备。
  • remove():设备移除时调用。释放资源。
  • id_table:设备ID表。告诉内核这个驱动支持哪些设备。

我写一个温度传感器驱动的probe函数:

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

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

    data = devm_kzalloc(&client->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

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

    // 注册一个输入设备或者字符设备
    // ...

    dev_info(&client->dev, "temp sensor probed\n");
    return 0;
}

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

static struct i2c_driver temp_sensor_driver = {
    .driver = {
        .name = "temp_sensor",
    },
    .probe    = temp_sensor_probe,
    .remove   = temp_sensor_remove,
    .id_table = temp_sensor_id,
};
module_i2c_driver(temp_sensor_driver);

小技巧:在probe里先读一个寄存器确认设备是否存在。我遇到过设备地址冲突导致probe成功但读写失败的情况。加个验证,能省很多调试时间。

3.4 设备树配置:让驱动和设备匹配

现在的机顶盒基本都是设备树(Device Tree)了。你需要在dts里描述I2C控制器和挂载的设备。

适配器节点示例:

&i2c0 {
    status = "okay";
    clock-frequency = <100000>;  // 100KHz标准模式

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

    eeprom@50 {
        compatible = "atmel,24c02";
        reg = <0x50>;
        pagesize = <8>;
    };
};

驱动里通过of_match_table来匹配:

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

static struct i2c_driver temp_sensor_driver = {
    .driver = {
        .name = "temp_sensor",
        .of_match_table = temp_sensor_of_match,
    },
    // ...
};

3.5 调试与避坑指南

调试I2C驱动,我常用的工具:

  • i2cdetect:扫描总线上的设备。看看地址对不对。
  • i2cget / i2cset:手动读写寄存器。验证硬件是否正常。
  • /sys/kernel/debug/i2c/:查看适配器和设备信息。
  • 逻辑分析仪:抓波形。这是终极手段。

避坑指南:我曾经遇到一个很隐蔽的问题——某款机顶盒的I2C总线在系统休眠后时钟线被拉低,导致唤醒后总线卡死。后来发现是某个从机在休眠时没释放总线。解决办法是在resume回调里重新初始化控制器,发送一个STOP信号恢复总线。

常见问题汇总:

现象 可能原因 解决办法
i2cdetect扫不到设备 地址错误、上拉电阻缺失、控制器未使能 检查硬件连接,确认时钟频率
读写返回-ETIMEDOUT 从机无应答、总线被占用 加超时重试,检查从机电源
数据错位 字节序问题、从机协议理解错误 用逻辑分析仪对比波形

嗯,I2C驱动开发其实不难。核心就是理解三层架构,把适配器驱动写对,设备驱动写稳。你想想看,机顶盒里那么多I2C设备,只要掌握了这套方法,换什么芯片都不怕。下一章咱们聊聊SPI总线,那个比I2C快,但协议更灵活。