2. I2C控制器驱动架构:Linux/RTOS下的I2C子系统、设备树配置、驱动框架

好,我们进入第二章。这一章我打算把I2C控制器的驱动架构彻底讲透。不管你用的是Linux还是RTOS,底层逻辑其实是相通的。说白了,就是「怎么让CPU里的I2C硬件模块,乖乖听你使唤」。

我个人习惯,拿到一个新平台的I2C控制器,第一件事不是写代码,而是先看它的寄存器手册和设备树。为什么?因为驱动框架再花哨,最终落地的还是那几个控制寄存器。你想想看,如果连时钟分频、从机地址、中断标志位在哪都不知道,代码写得再漂亮也是空中楼阁。

2.1 Linux I2C子系统:三层架构

Linux的I2C子系统,我把它拆成三层来看:

  • I2C核心层:提供统一的API,比如i2c_transfer()、i2c_add_adapter()。这部分你基本不用动,内核已经写好了。
  • I2C控制器驱动层:这是我们要写的。负责操作具体的I2C硬件寄存器,实现master_xfer()回调。
  • I2C设备驱动层:挂载在I2C总线上的外设驱动,比如eeprom、传感器。它们通过核心层提供的接口,跟控制器驱动打交道。

这三层的关系,我画了张图,你看一眼就明白了。

Linux I2C 子系统三层架构 用户空间应用 (APP) I2C 设备驱动层 i2c_driver / i2c_client / 设备节点 I2C 核心层 i2c-core / i2c-dev / 算法抽象 I2C 控制器驱动层 i2c_adapter / master_xfer / 硬件寄存器操作 I2C 硬件控制器 (SoC) 核心工作:实现 master_xfer 回调,完成 I2C 总线上的读写时序

这张图里,最核心的其实是控制器驱动层。你写的master_xfer()函数,就是整个I2C通信的「发动机」。我在项目中遇到过,有人把设备驱动写得花里胡哨,结果master_xfer里一个时序错误,整条总线都卡死。嗯,调试起来那叫一个酸爽。

2.2 设备树配置:把硬件信息告诉内核

在Linux下,设备树是描述硬件信息的「户口本」。I2C控制器的设备树节点,通常长这样:

i2c1: i2c@40012000 {
    compatible = "st,stm32f4-i2c";
    reg = <0x40012000 0x400>;
    interrupts = <31 0>;
    clocks = <&rcc 0 43>;
    clock-frequency = <100000>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";

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

这里有几个关键字段,我列个表,你对照着看:

字段 含义 避坑点
compatible 匹配驱动,告诉内核「我是谁」 必须跟驱动中的of_match_table一致,大小写敏感
reg 寄存器基地址和映射范围 地址别写错,否则ioremap会失败
interrupts 中断号及触发方式 我曾经因为中断号配错,导致中断一直不触发
clock-frequency I2C总线时钟频率,单位Hz 标准模式100K,快速模式400K,别超了
子节点reg 从机设备地址(7位) 注意是7位地址,不是8位!左移一位的坑我踩过
⚠️ 重要提醒: 设备树里的reg地址,跟芯片手册里的I2C从机地址,经常有「7位 vs 8位」的混淆。Linux内核里用的是7位地址,但有些芯片手册写的是8位(左移了一位)。我建议你拿到外设手册后,先确认地址格式,再填设备树。

2.3 驱动框架:从probe到master_xfer

一个标准的I2C控制器驱动,核心流程是这样的:

  1. probe():解析设备树,获取资源(寄存器地址、中断号、时钟)。
  2. 初始化硬件:设置时钟分频、配置GPIO为I2C功能、使能控制器。
  3. 注册i2c_adapter:调用i2c_add_adapter(),把控制器注册到核心层。
  4. 实现master_xfer():这是最关键的,负责发送起始信号、地址、数据、停止信号。
  5. 中断处理:处理传输完成、仲裁丢失、NACK等中断事件。

我写一个简化的master_xfer框架,你看下核心逻辑:

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 = 0;

    for (i = 0; i < num; i++) {
        // 1. 发送起始信号
        i2c_dev_write_reg(i2c_dev, I2C_CR1, START_BIT);

        // 2. 发送从机地址 + 读写位
        u8 addr = (msgs[i].addr << 1) | (msgs[i].flags & I2C_M_RD);
        i2c_dev_write_reg(i2c_dev, I2C_DR, addr);

        // 3. 等待ACK
        if (!wait_for_ack(i2c_dev)) {
            ret = -ENXIO;
            goto out;
        }

        // 4. 读写数据
        if (msgs[i].flags & I2C_M_RD) {
            // 读操作
            for (int j = 0; j < msgs[i].len; j++) {
                msgs[i].buf[j] = i2c_dev_read_reg(i2c_dev, I2C_DR);
            }
        } else {
            // 写操作
            for (int j = 0; j < msgs[i].len; j++) {
                i2c_dev_write_reg(i2c_dev, I2C_DR, msgs[i].buf[j]);
                wait_for_ack(i2c_dev);
            }
        }
    }

out:
    // 5. 发送停止信号
    i2c_dev_write_reg(i2c_dev, I2C_CR1, STOP_BIT);
    return ret ? ret : num;
}
💡 个人经验: 写master_xfer时,超时处理一定要加。我曾经在一个项目里,从机设备偶尔会不响应ACK,结果master_xfer死等,整个系统卡死。后来加了超时机制,问题就解决了。建议用wait_for_completion_timeout(),别用while(1)死等。

2.4 RTOS下的I2C驱动:轻量级实现

在RTOS里,没有Linux那么复杂的设备树和分层架构。但核心思想是一样的:操作寄存器,实现时序。我通常的做法是:

  • 写一个i2c_handle_t结构体,封装寄存器基地址、锁、配置参数。
  • 提供i2c_master_send()和i2c_master_recv()两个API。
  • 用信号量做互斥访问,防止多任务同时操作I2C总线。

RTOS下的驱动,说白了就是「去掉中间层,直接撸寄存器」。但要注意,中断处理和任务同步的配合,比Linux更考验功底。这部分我们后面章节会专门讲。

2.5 调试要点:三板斧

最后,我分享几个调试I2C控制器驱动的经验:

  1. 先看时钟:用示波器量SCL引脚,看有没有时钟信号。没有?检查时钟配置和GPIO复用。
  2. 再看数据:发送一个简单的写操作,抓SDA波形,看地址和数据对不对。
  3. 最后看中断:如果轮询模式能通,中断模式不通,大概率是中断标志位没清或者中断号配错了。

🔑 核心总结: I2C控制器驱动,不管在Linux还是RTOS下,本质就是「寄存器操作 + 时序控制」。设备树只是把硬件信息告诉内核,驱动框架只是帮你搭好架子。真正硬核的,还是master_xfer里那几行控制寄存器的代码。