3. I2C控制器驱动:适配器注册、消息传输、中断处理

好,咱们进入I2C控制器驱动的核心环节。说白了,这部分就是让CPU里的I2C外设真正跑起来,能跟外设芯片对话。我刚开始写I2C驱动时,总觉得适配器注册就是个模板代码,后来踩了坑才明白——这里每一步都有讲究。

3.1 适配器注册——让内核认识你的I2C总线

适配器(Adapter)在Linux I2C子系统里,代表一条物理I2C总线。每个I2C控制器对应一个struct i2c_adapter。注册适配器,就是告诉内核:“嘿,我这有条I2C总线,叫I2C-0,能收发消息。”

核心数据结构长这样:

struct i2c_adapter {
    struct module *owner;
    unsigned int class;               // 适配器支持的设备类
    const struct i2c_algorithm *algo; // 算法指针,最关键
    struct device dev;
    struct i2c_bus_recovery_info *bus_recovery_info;
    // ... 其他成员
};

注册流程我习惯分三步走:

  1. 分配并初始化适配器——填充owner、class、algo等字段。
  2. 设置算法接口——实现master_xfer和functionality回调。
  3. 调用i2c_add_adapter()——把适配器挂入内核I2C总线。

关键点: i2c_add_adapter() vs i2c_add_numbered_adapter()。前者让内核自动分配总线号,后者你指定号。我建议产品中用后者,避免重启后总线号漂移。

举个例子,一个简化的注册片段:

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

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

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

    adap->owner = THIS_MODULE;
    adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
    adap->algo = &my_i2c_algo;
    adap->dev.parent = &pdev->dev;
    strscpy(adap->name, "my_i2c_adapter", sizeof(adap->name));

    ret = i2c_add_adapter(adap);
    if (ret)
        return ret;

    platform_set_drvdata(pdev, adap);
    return 0;
}

嗯,这里要注意:devm_kzalloc自动管理内存,但i2c_add_adapter失败后要手动释放吗?不用,devm_系列会在probe失败时自动清理。但如果你用普通kzalloc,记得在错误路径加kfree。

3.2 消息传输——master_xfer的实现艺术

master_xfer是适配器驱动的灵魂。内核I2C核心层调用它,传入一个struct i2c_msg数组,你要把这些消息通过硬件发出去。

消息结构体:

struct i2c_msg {
    __u16 addr;    // 从设备地址(7位或10位)
    __u16 flags;   // I2C_M_RD 等标志
    __u16 len;     // 数据长度
    __u8 *buf;     // 数据缓冲区
};

实现master_xfer时,我一般按这个流程走:

  • 解析消息数组——判断是读还是写,地址是否匹配。
  • 构造硬件传输序列——把消息转换成控制器能理解的启动条件、地址、数据、停止条件。
  • 轮询或中断等待完成——短消息用轮询,长消息用中断。
  • 返回传输的消息数——成功返回正数,失败返回负错误码。

个人经验: 我遇到过一种情况——从设备在发送地址后不响应ACK。这时master_xfer要能检测到NACK,并立即发送停止条件,然后返回负值。否则总线会一直挂住,后续所有传输都卡死。

一个简化的轮询式传输示例:

static int my_i2c_master_xfer(struct i2c_adapter *adap,
                              struct i2c_msg *msgs, int num)
{
    struct my_i2c_dev *i2c_dev = i2c_get_adapdata(adap);
    int i, ret;

    for (i = 0; i < num; i++) {
        // 发送启动条件和从地址
        my_i2c_send_start(i2c_dev);
        ret = my_i2c_send_addr(i2c_dev, msgs[i].addr, msgs[i].flags);
        if (ret < 0)
            goto err;

        if (msgs[i].flags & I2C_M_RD) {
            // 读操作
            my_i2c_read_bytes(i2c_dev, msgs[i].buf, msgs[i].len);
        } else {
            // 写操作
            my_i2c_write_bytes(i2c_dev, msgs[i].buf, msgs[i].len);
        }
    }

    my_i2c_send_stop(i2c_dev);
    return i;

err:
    my_i2c_send_stop(i2c_dev);
    return ret;
}

你想想看,如果消息数组里有多个消息,且第一个是写、第二个是读,那就需要重复启动条件(Repeated Start)。很多新手在这里翻车——在两个消息之间错误地发送了停止条件,导致从设备退出传输。

3.3 中断处理——别让CPU空等

轮询方式在短传输时没问题,但一旦数据量大了,CPU就一直在那傻等。我建议产品中都用中断方式。

中断处理的核心逻辑:

  • 中断服务程序(ISR)——读取状态寄存器,判断是发送完成、接收完成、还是错误。
  • 状态机驱动——每个中断触发一次状态迁移,直到整个消息传输完成。
  • 完成量唤醒——传输完成后,用complete()唤醒等待的master_xfer。

状态机设计我习惯用枚举:

enum my_i2c_state {
    STATE_IDLE,
    STATE_START,
    STATE_ADDR,
    STATE_WRITE,
    STATE_READ,
    STATE_STOP,
    STATE_ERROR,
};

ISR里根据当前状态和硬件事件,决定下一步操作。举个例子:

static irqreturn_t my_i2c_isr(int irq, void *dev_id)
{
    struct my_i2c_dev *i2c_dev = dev_id;
    u32 status = readl(i2c_dev->base + REG_STATUS);

    if (status & STATUS_TX_EMPTY) {
        // 发送缓冲区空,可以继续发数据
        if (i2c_dev->msg_idx < i2c_dev->msg->len) {
            writel(i2c_dev->msg->buf[i2c_dev->msg_idx++],
                   i2c_dev->base + REG_DATA);
        } else {
            // 数据发完,发停止条件
            my_i2c_send_stop(i2c_dev);
            complete(&i2c_dev->done);
        }
    }

    if (status & STATUS_NACK) {
        dev_err(i2c_dev->dev, "NACK received\n");
        i2c_dev->state = STATE_ERROR;
        my_i2c_send_stop(i2c_dev);
        complete(&i2c_dev->done);
    }

    return IRQ_HANDLED;
}

我曾经踩过的坑: 中断处理里直接调用了i2c_transfer()。这是大忌!i2c_transfer会尝试获取总线锁,而中断上下文不能睡眠。正确的做法是:ISR只操作硬件寄存器,用complete或工作队列通知上半部。

3.4 知识结构总览

下面这张图把适配器注册、消息传输、中断处理的关系串起来了:

I2C控制器驱动核心流程 1. 适配器注册 分配 i2c_adapter 填充 algo / class i2c_add_adapter() → 挂入 i2c 总线 2. 消息传输 master_xfer 被调用 解析 i2c_msg 数组 发送 Start + 地址 读写数据 发送 Stop / Repeated Start 3. 中断处理 ISR 读取状态 状态机迁移 操作数据寄存器 complete() 唤醒 → 返回传输结果 ⚠ 关键设计原则 中断上下文不做耗时操作 | 状态机要覆盖所有异常路径 | 超时机制防止总线死锁

3.5 关键参数对比

不同传输方式的选择,直接影响系统实时性。我整理了一张对比表:

特性 轮询方式 中断方式 DMA方式
CPU占用率 高(忙等) 低(仅中断时) 极低(硬件搬运)
延迟确定性 高(无调度抖动) 中(受中断优先级影响) 中(DMA完成中断)
适用场景 短消息(< 10字节) 常规传输(10~256字节) 大数据块(> 256字节)
实现复杂度 低 中 高(需DMA引擎支持)
典型错误 超时未处理 ISR中调用阻塞函数 DMA缓冲对齐问题

我的建议: 在RTOS环境下,如果I2C传输频率不高(比如每秒几十次),中断方式足够。但如果你的系统里I2C是高频数据通道(比如传感器数据流),考虑用DMA+双缓冲,能大幅降低CPU负载。

3.6 避坑指南——我踩过的三个坑

做I2C控制器驱动这些年,有些坑印象特别深:

  • 总线锁死恢复——我曾经遇到过从设备异常拉低SCL,导致总线卡死。后来加了bus_recovery_info,用GPIO模拟9个时钟脉冲强制恢复。这个机制一定要在适配器注册前配好。
  • 中断共享冲突——有些SoC里多个外设共享一个中断号。ISR里必须先判断中断是否来自本设备,不是就返回IRQ_NONE。否则会干扰其他驱动。
  • 时钟延展处理——有些慢速从设备会拉低SCL延长时钟。控制器必须支持时钟延展,否则传输会超时。我建议在master_xfer里加一个超时计数器,避免死等。

嗯,这部分内容就到这里。适配器注册是骨架,消息传输是血肉,中断处理是神经——三者配合好,I2C控制器驱动才算真正立住了。