【Linux驱动开发】第18天:I2C驱动深度解析

一、Linux I2C驱动整体架构

Linux I2C驱动有三层架构:

  1. 核心层(i2c-core) :内核提供的通用框架,负责管理I2C适配器(Adapter)和I2C设备(Client),提供注册、匹配、传输等通用接口。
  2. 适配器驱动(Adapter Driver):对应硬件的I2C控制器(如RK3399的I2C控制器),实现控制器的硬件初始化、数据收发、中断处理等,向上对接核心层。
  3. 设备驱动(Client Driver):对应挂在I2C总线上的外设(如温湿度传感器、EEPROM),向下通过核心层调用适配器驱动的接口完成数据交互。

RK3399的I2C驱动核心代码位于内核源码的 drivers/i2c/busses/i2c-rockchip.c(不同内核版本路径可能略有差异),以下围绕该文件结合I2C驱动核心知识拆解。

二、RK3399 I2C适配器驱动核心实现

1. 驱动注册:平台驱动框架

RK3399的I2C控制器属于平台设备(Platform Device) (由设备树描述硬件信息),因此驱动通过platform_driver框架注册,核心结构体和注册逻辑如下:

c 复制代码
// 1. 定义设备树匹配表(匹配RK3399的I2C控制器节点)
static const struct of_device_id rockchip_i2c_of_match[] = {
    { .compatible = "rockchip,rk3399-i2c", }, // 匹配设备树中compatible属性
    { /* Sentinel */ },
};
MODULE_DEVICE_TABLE(of, rockchip_i2c_of_match);

// 2. 定义平台驱动结构体
static struct platform_driver rockchip_i2c_driver = {
    .probe = rockchip_i2c_probe,    // 设备匹配成功后执行的初始化函数
    .remove = rockchip_i2c_remove,  // 驱动卸载时执行的清理函数
    .driver = {
        .name = "rockchip-i2c",
        .of_match_table = rockchip_i2c_of_match, // 设备树匹配表
        .pm = &rockchip_i2c_pm_ops, // 电源管理(可选)
    },
};

// 3. 驱动入口/出口函数
module_platform_driver(rockchip_i2c_driver);
MODULE_LICENSE("GPL");

关键说明:

  • 设备树匹配是RK3399(ARM64架构)驱动的核心方式,设备树中会定义I2C控制器的基地址、中断号、时钟等信息,驱动通过compatible属性匹配对应的硬件节点。
  • platform_driver是Linux针对片上外设的标准驱动框架,probe函数是适配器初始化的核心入口。
2. Probe函数:I2C适配器初始化

rockchip_i2c_probe是驱动的核心初始化函数,完成硬件资源申请、控制器初始化、I2C适配器注册,核心逻辑拆解:

c 复制代码
static int rockchip_i2c_probe(struct platform_device *pdev)
{
    struct rockchip_i2c *i2c;
    struct i2c_adapter *adap;
    struct resource *res;
    int ret;

    // 1. 分配私有数据结构体(rockchip_i2c是RK3399 I2C的私有结构体)
    i2c = devm_kzalloc(&pdev->dev, sizeof(*i2c), GFP_KERNEL);
    if (!i2c) return -ENOMEM;

    // 2. 申请IO内存(I2C控制器的寄存器基地址)
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    i2c->base = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(i2c->base)) return PTR_ERR(i2c->base);

    // 3. 申请中断(I2C控制器的中断号)
    i2c->irq = platform_get_irq(pdev, 0);
    ret = devm_request_irq(&pdev->dev, i2c->irq, rockchip_i2c_irq_handler,
                           IRQF_SHARED, dev_name(&pdev->dev), i2c);
    if (ret) return ret;

    // 4. 初始化硬件(时钟、控制器寄存器)
    i2c->clk = devm_clk_get(&pdev->dev, "i2c_clk"); // 获取I2C时钟
    clk_prepare_enable(i2c->clk);                   // 使能时钟
    rockchip_i2c_hw_init(i2c);                      // 初始化I2C控制器寄存器(如速率、模式)

    // 5. 初始化I2C适配器结构体(对接i2c-core)
    adap = &i2c->adap;
    adap->owner = THIS_MODULE;
    adap->class = I2C_CLASS_HWMON | I2C_CLASS_SPD;  // 适配器分类
    adap->algo = &rockchip_i2c_algo;                // 核心:I2C传输算法(数据收发逻辑)
    adap->dev.parent = &pdev->dev;
    strscpy(adap->name, "rockchip-i2c", sizeof(adap->name));
    i2c_set_adapdata(adap, i2c);                    // 绑定私有数据

    // 6. 注册适配器到I2C核心层
    ret = i2c_add_adapter(adap);
    if (ret) goto err_clk_disable;

    platform_set_drvdata(pdev, i2c); // 保存私有数据
    return 0;

err_clk_disable:
    clk_disable_unprepare(i2c->clk);
    return ret;
}

核心知识点:

  • rockchip_i2c:RK3399 I2C的私有结构体,包含控制器基地址、中断号、时钟、I2C适配器结构体、传输状态等,是驱动中贯穿硬件操作的核心载体。
  • i2c_adapter:Linux I2C核心层定义的适配器结构体,驱动需填充该结构体并通过i2c_add_adapter注册到核心层,完成"硬件控制器"到"内核I2C总线"的映射。
  • 硬件初始化(rockchip_i2c_hw_init):会配置I2C控制器的工作模式(主模式)、通信速率(如100KHz/400KHz)、ACK使能、清空FIFO等寄存器。
3. 核心:I2C传输算法(master_xfer)

i2c_adapter的algo成员(rockchip_i2c_algo)是适配器的核心,实现master_xfer(I2C主模式数据传输),这是I2C核心层调用的核心接口,RK3399的实现逻辑如下(简化版):

c 复制代码
// I2C算法结构体(对接i2c-core)
static const struct i2c_algorithm rockchip_i2c_algo = {
    .master_xfer = rockchip_i2c_xfer, // 主模式数据传输函数
    .functionality = rockchip_i2c_func, // 适配器功能(如支持的速率、模式)
};

// 核心传输函数:处理I2C消息收发
static int rockchip_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg msgs[], int num)
{
    struct rockchip_i2c *i2c = i2c_get_adapdata(adap);
    int ret;

    // 1. 加锁(防止多线程并发访问控制器)
    mutex_lock(&i2c->lock);

    // 2. 初始化传输状态(清空待传输消息、长度、状态)
    i2c->msgs = msgs;
    i2c->msg_num = num;
    i2c->msg_ptr = 0;
    i2c->msg_idx = 0;
    i2c->status = 0;

    // 3. 触发硬件传输(启动I2C控制器,发送起始位、设备地址)
    rockchip_i2c_start_xfer(i2c);

    // 4. 等待传输完成(中断驱动方式:等待中断处理完成)
    ret = wait_event_timeout(i2c->wait, (i2c->status != 0), HZ);
    if (ret == 0) {
        ret = -ETIMEDOUT; // 超时
    } else {
        ret = (i2c->status == 1) ? num : -EIO; // 传输成功返回消息数,失败返回错误
    }

    // 5. 解锁并返回
    mutex_unlock(&i2c->lock);
    return ret;
}

关键说明:

  • i2c_msg:Linux I2C核心层定义的消息结构体,包含外设地址、读写方向、数据长度、数据缓冲区等,是I2C数据传输的基本单元。
  • RK3399的I2C传输采用中断驱动 :rockchip_i2c_start_xfer启动硬件传输后,驱动通过wait_event_timeout等待中断触发,中断处理函数(rockchip_i2c_irq_handler)完成实际的数据收发、ACK检测、停止位发送等。
4. 中断处理:rockchip_i2c_irq_handler

中断是I2C传输的核心,RK3399的I2C控制器会在"地址发送完成、数据收发完成、ACK错误、传输完成"等场景触发中断,中断处理函数逻辑如下(简化版):

c 复制代码
static irqreturn_t rockchip_i2c_irq_handler(int irq, void *dev_id)
{
    struct rockchip_i2c *i2c = dev_id;
    u32 stat, ctrl;

    // 1. 读取中断状态寄存器
    stat = readl(i2c->base + ROCKCHIP_I2C_INT_STATUS);
    if (!stat) return IRQ_NONE;

    // 2. 处理不同中断类型
    if (stat & ROCKCHIP_I2C_INT_NACK) { // NACK错误
        i2c->status = -EIO;
        rockchip_i2c_stop_xfer(i2c);    // 发送停止位
    } else if (stat & ROCKCHIP_I2C_INT_COMPLETE) { // 单条消息传输完成
        i2c->msg_ptr++;
        if (i2c->msg_ptr >= i2c->msgs[i2c->msg_idx].len) { // 当前消息传输完
            i2c->msg_ptr = 0;
            i2c->msg_idx++;
            if (i2c->msg_idx >= i2c->msg_num) { // 所有消息传输完
                i2c->status = 1;               // 标记传输成功
                rockchip_i2c_stop_xfer(i2c);
            } else {
                rockchip_i2c_start_xfer(i2c); // 继续传输下一条消息
            }
        } else {
            rockchip_i2c_send_byte(i2c); // 发送下一个字节(写)/读取下一个字节(读)
        }
    }

    // 3. 清除中断状态
    writel(stat, i2c->base + ROCKCHIP_I2C_INT_CLEAR);
    // 4. 唤醒等待队列(通知xfer函数传输完成)
    wake_up(&i2c->wait);
    return IRQ_HANDLED;
}

核心逻辑:

  • 中断处理函数负责逐字节收发数据、检测错误(如NACK)、切换消息、最终触发"传输完成"的状态更新,并唤醒rockchip_i2c_xfer中的等待队列。
  • 硬件寄存器操作(readl/writel)是直接操作RK3399 I2C控制器的物理寄存器,需严格匹配芯片手册的寄存器定义。

三、RK3399 I2C设备驱动(Client)示例

适配器驱动完成后,挂在I2C总线上的外设(如AHT20温湿度传感器)需要编写Client驱动,核心是通过i2c_driver注册,并调用i2c_transfer(核心层接口)完成数据交互:

c 复制代码
// 1. 设备匹配表(匹配I2C外设地址/名字)
static const struct i2c_device_id aht20_id[] = {
    { "aht20", 0 },
    { /* Sentinel */ },
};
MODULE_DEVICE_TABLE(i2c, aht20_id);

// 2. I2C设备驱动结构体
static struct i2c_driver aht20_driver = {
    .driver = {
        .name = "aht20",
        .owner = THIS_MODULE,
    },
    .probe = aht20_probe,
    .remove = aht20_remove,
    .id_table = aht20_id,
};

// 3. Probe函数:外设初始化+数据读取
static int aht20_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    struct i2c_msg msgs[2];
    u8 tx_buf[3] = {0xAC, 0x33, 0x00}; // AHT20触发测量指令
    u8 rx_buf[6];

    // 构造I2C消息:先写指令,再读数据
    msgs[0].addr = client->addr; // 外设地址(如0x38)
    msgs[0].flags = 0;           // 写操作
    msgs[0].len = 3;
    msgs[0].buf = tx_buf;

    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;    // 读操作
    msgs[1].len = 6;
    msgs[1].buf = rx_buf;

    // 调用核心层接口,最终触发适配器的master_xfer
    if (i2c_transfer(client->adapter, msgs, 2) != 2) {
        return -EIO;
    }

    // 解析rx_buf中的温湿度数据(略)
    return 0;
}

module_i2c_driver(aht20_driver);

关键说明:

  • i2c_client:由I2C核心层根据设备树/板级信息创建,包含外设地址、对应的适配器指针等。
  • i2c_transfer:核心层接口,会遍历适配器的master_xfer函数完成实际硬件传输,是Client驱动和Adapter驱动的桥梁。

四、核心总结

  1. RK3399的I2C驱动分为适配器驱动(控制器) 和设备驱动(外设),适配器驱动是硬件层实现,设备驱动是业务层实现,通过I2C核心层解耦。
  2. 适配器驱动基于platform_driver,通过设备树匹配硬件,核心是实现i2c_algorithm的master_xfer(中断驱动的数收发)。
  3. 中断是RK3399 I2C高效传输的核心,负责逐字节处理、错误检测、传输状态更新。
  4. Client驱动通过i2c_driver注册,调用i2c_transfer对接核心层,无需关心底层硬件细节,实现跨平台复用。
相关推荐
雯宝6 小时前
mnt_init 函数
linux
律宏阔12 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔12 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴12 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千12 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3712 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3712 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei3712 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh12 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW12 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux