4. I2C总线驱动:从控制器到从设备的完整链路

好,咱们今天聊I2C总线驱动。说实话,I2C这玩意儿在嵌入式系统里太常见了——传感器、触摸屏、音频Codec、电源管理芯片,十有八九都挂在I2C总线上。MTK8678这颗芯片也不例外,它内部集成了多个I2C控制器,每个控制器可以挂载多个从设备。

我个人习惯把I2C驱动分成三层来看:控制器驱动(也就是适配器驱动)、核心层(Linux内核帮你搞定的通用框架)、以及从设备驱动(你真正需要写的业务代码)。今天咱们重点讲控制器驱动和从设备驱动怎么写,核心层我简单带过,因为内核已经封装得很好了。

4.1 I2C控制器驱动框架:Linux是怎么管的?

Linux内核的I2C子系统,说白了就是一套总线-设备-驱动模型。你想想看,I2C控制器是硬件,它需要被抽象成一个struct i2c_adapter;挂在总线上的从设备,被抽象成struct i2c_client;而驱动代码,就是struct i2c_driver

这三者的关系,我画个简图你感受一下:

I2C Core (核心层)
    ├── i2c_adapter (控制器/适配器)
    │   ├── 算法: struct i2c_algorithm
    │   │   ├── master_xfer()    // 主模式传输
    │   │   └── functionality()  // 查询支持的功能
    │   └── 物理设备: platform_device / device tree node
    │
    ├── i2c_client (从设备)
    │   ├── 地址: 7位或10位
    │   └── 挂载在哪个adapter上
    │
    └── i2c_driver (从设备驱动)
        ├── probe() / remove()
        └── id_table 匹配规则

嗯,这里要注意:控制器驱动负责实现底层的硬件操作,比如怎么发START信号、怎么产生时钟、怎么读写数据。而从设备驱动只管调用i2c_transfer()接口,不用关心底层时序细节。这种分层设计,让驱动开发变得清爽很多。

核心要点:控制器驱动提供"怎么发",从设备驱动只管"发什么"。两者通过i2c_adapteri2c_algorithm解耦。

4.2 MTK I2C适配器实现:从寄存器到框架

好,咱们来点硬核的。MTK8678的I2C控制器,硬件上长什么样?

每个I2C控制器有一组寄存器,包括控制寄存器、状态寄存器、数据寄存器、时钟分频寄存器等等。我挑几个关键的说说:

寄存器名称 偏移地址 作用
I2C_CON 0x0000 控制寄存器:使能、中断、ACK/NACK设置
I2C_STA 0x0004 状态寄存器:传输完成、总线忙、错误标志
I2C_DAT 0x0008 数据寄存器:读写数据都走这里
I2C_CLK_DIV 0x000C 时钟分频:决定SCL频率
I2C_SLAVE_ADDR 0x0010 从设备地址寄存器

在MTK平台上,I2C控制器通常被注册为platform_driver。驱动入口长这样:

static const struct of_device_id mtk_i2c_of_match[] = {
    { .compatible = "mediatek,mt8678-i2c", },
    { /* sentinel */ }
};

static struct platform_driver mtk_i2c_driver = {
    .probe  = mtk_i2c_probe,
    .remove = mtk_i2c_remove,
    .driver = {
        .name = "mtk_i2c",
        .of_match_table = mtk_i2c_of_match,
    },
};

module_platform_driver(mtk_i2c_driver);

probe()函数里,我们要做几件事:

  1. 获取硬件资源:从设备树里拿到寄存器基地址、中断号、时钟。
  2. 初始化硬件:复位控制器、设置时钟分频、使能中断。
  3. 填充i2c_adapter:设置算法、名称、设备节点。
  4. 注册适配器:调用i2c_add_adapter(),让内核知道这个控制器存在。

我贴一段核心代码,你感受一下:

static int mtk_i2c_probe(struct platform_device *pdev)
{
    struct mtk_i2c *i2c;
    struct i2c_adapter *adap;
    int ret;

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

    // 1. 获取寄存器基地址
    i2c->base = devm_platform_ioremap_resource(pdev, 0);
    if (IS_ERR(i2c->base))
        return PTR_ERR(i2c->base);

    // 2. 获取时钟
    i2c->clk = devm_clk_get(&pdev->dev, "main");
    if (IS_ERR(i2c->clk))
        return PTR_ERR(i2c->clk);

    clk_prepare_enable(i2c->clk);

    // 3. 硬件初始化:设置100KHz标准模式
    mtk_i2c_set_clk_div(i2c, 100000);

    // 4. 填充适配器
    adap = &i2c->adap;
    adap->owner = THIS_MODULE;
    adap->class = I2C_CLASS_DEPRECATED;
    adap->algo = &mtk_i2c_algorithm;
    adap->dev.of_node = pdev->dev.of_node;
    adap->nr = pdev->id;
    snprintf(adap->name, sizeof(adap->name), "MTK I2C %d", pdev->id);

    // 5. 注册适配器
    ret = i2c_add_numbered_adapter(adap);
    if (ret) {
        dev_err(&pdev->dev, "failed to add i2c adapter\n");
        clk_disable_unprepare(i2c->clk);
        return ret;
    }

    platform_set_drvdata(pdev, i2c);
    return 0;
}

个人经验:我在MTK平台上调试I2C时,踩过最大的坑是时钟分频算错了。MTK的时钟树比较复杂,APB总线时钟和I2C模块时钟可能不同源。我建议你在mtk_i2c_set_clk_div()里加个调试打印,把实际算出来的SCL频率打出来,用示波器量一下确认。

接下来是算法实现,也就是struct i2c_algorithm里的两个回调:

static const struct i2c_algorithm mtk_i2c_algorithm = {
    .master_xfer      = mtk_i2c_master_xfer,
    .functionality    = mtk_i2c_functionality,
};

master_xfer()是核心函数,它接收一个struct i2c_msg数组,每个msg描述了一次传输的方向、地址、数据和长度。MTK控制器的硬件逻辑是:写一个字节到数据寄存器,然后等待传输完成中断,再写下一个字节。读操作类似,只是方向反过来。

我简化一下实现逻辑:

static int mtk_i2c_master_xfer(struct i2c_adapter *adap,
                               struct i2c_msg *msgs, int num)
{
    struct mtk_i2c *i2c = i2c_get_adapdata(adap);
    int i, ret;

    for (i = 0; i < num; i++) {
        // 设置从设备地址
        writel(msgs[i].addr << 1, i2c->base + I2C_SLAVE_ADDR);

        if (msgs[i].flags & I2C_M_RD) {
            // 读操作
            ret = mtk_i2c_read_bytes(i2c, msgs[i].buf, msgs[i].len);
        } else {
            // 写操作
            ret = mtk_i2c_write_bytes(i2c, msgs[i].buf, msgs[i].len);
        }

        if (ret < 0)
            return ret;
    }

    return num;
}

注意:MTK的I2C控制器在传输完成后,需要检查状态寄存器里的NACK标志位。如果从设备没有应答,硬件会置位NACK位。我曾经遇到一个情况:从设备地址写错了,但代码没检查NACK,结果一直读到0xFF,排查了半天才发现是地址问题。

4.3 从设备驱动编写:以温度传感器为例

好了,控制器驱动写好了,现在该写从设备驱动了。假设我们挂了一个温度传感器,比如LM75,它的I2C地址是0x48(7位地址)。

从设备驱动的套路很固定:

  1. 定义struct i2c_driver,填充probe()remove()id_table
  2. probe()里,拿到struct i2c_client,然后注册一个字符设备或者输入设备。
  3. 实现读写函数,调用i2c_smbus_read_byte_data()i2c_transfer()

代码示例:

static const struct i2c_device_id lm75_ids[] = {
    { "lm75", 0 },
    { }
};

static int lm75_probe(struct i2c_client *client)
{
    struct lm75_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);

    // 读取温度寄存器(0x00),验证设备是否存在
    ret = i2c_smbus_read_word_swapped(client, 0x00);
    if (ret < 0) {
        dev_err(&client->dev, "failed to read temperature\n");
        return ret;
    }

    // 注册一个sysfs接口或者字符设备
    // ...

    return 0;
}

static struct i2c_driver lm75_driver = {
    .driver = {
        .name = "lm75",
    },
    .probe    = lm75_probe,
    .remove   = lm75_remove,
    .id_table = lm75_ids,
};

module_i2c_driver(lm75_driver);

这里有个细节:i2c_smbus_read_word_swapped()是SMBus协议里的函数,它自动处理了字节序。LM75的温度寄存器是16位,高字节在前,低字节在后,所以用swapped版本刚好。

我个人的习惯:probe()里先做一次读操作,验证设备是否真的在线。如果读不到数据,直接返回-ENODEV,避免后面操作一个不存在的设备。这个习惯帮我省了不少调试时间。

4.4 设备树配置:把硬件描述清楚

在MTK平台上,I2C控制器和从设备的信息都在设备树里描述。控制器节点长这样:

&i2c0 {
    pinctrl-names = "default";
    pinctrl-0 = <&i2c0_pins>;
    status = "okay";
    clock-frequency = <100000>;

    lm75@48 {
        compatible = "lm75";
        reg = <0x48>;
    };
};

注意reg = <0x48>,这个地址是7位地址左移0位后的值。有些平台要求左移1位,但Linux内核里i2c_client的地址是7位原始地址,不需要移位。嗯,这个细节容易搞混,我建议你查一下内核文档Documentation/i2c/instantiating-devices.rst

4.5 调试技巧:没有示波器怎么搞?

说实话,I2C调试最靠谱的工具是示波器或逻辑分析仪。但有时候手边没有,怎么办?

我分享几个软件层面的调试方法:

  • 打开内核I2C调试:编译内核时开启CONFIG_I2C_DEBUG_BUSCONFIG_I2C_DEBUG_ALGO,内核会打印每次传输的细节。
  • 使用i2c-tools:在用户空间用i2cdetect -y 0扫描总线,用i2cgeti2cset直接读写寄存器。这个工具在MTK平台上可以直接用。
  • 加调试打印:在master_xfer()里打印每个msg的地址、长度、方向和数据。我一般会加个动态调试开关,避免影响性能。

避坑指南:我曾经遇到一个I2C总线锁死的问题——从设备拉低了SCL线,导致总线一直忙。排查后发现是从设备进入了异常状态,需要给它一个复位信号。从那以后,我在每个I2C控制器的probe()里都加了总线恢复逻辑:先拉高SCL 9个时钟脉冲,让从设备释放总线。

4.6 小结

I2C总线驱动,说白了就是三件事:

  • 控制器驱动:操作寄存器,实现master_xfer(),把硬件能力暴露给内核。
  • 从设备驱动:调用i2c_transfer(),实现具体的设备功能。
  • 设备树:描述硬件拓扑,让驱动和设备匹配起来。

在MTK8678上,I2C控制器驱动已经由MTK官方提供了,大部分情况下你只需要写从设备驱动。但理解底层实现,能帮你更快地定位问题。下次遇到I2C通信失败,你知道该查时钟分频、查NACK标志、还是查设备树地址——这就是系统思维的价值。

好,这一章就到这儿。下一章咱们聊SPI总线驱动,那个比I2C稍微复杂一点,但套路是相通的。