2、I2C子系统基础:I2C总线协议、I2C核心层、I2C适配器、I2C客户端驱动模型

I2C(Inter-Integrated Circuit)总线是嵌入式Linux系统中连接摄像头传感器、编解码器、触摸控制器等低速外设的核心总线。在V4L2驱动开发中,I2C负责传输寄存器配置、时钟同步参数和中断状态信息。理解I2C子系统是掌握V4L2驱动I2C时钟同步与中断处理协同调试的前提。

2.1 I2C总线协议基础

I2C总线由两根线组成:串行数据线(SDA)和串行时钟线(SCL)。所有设备共享这两根线,通过唯一地址区分。总线上的设备分为主控器(Master)和从设备(Slave),主控器负责产生时钟信号并发起通信。

关键特性: I2C是同步、半双工、多主机的串行通信协议。标准模式100kHz,快速模式400kHz,高速模式3.4MHz。
I2C总线物理拓扑与信号时序 主控器 (Master) 从设备1 (Slave) 从设备2 (Slave) SDA SCL 上拉电阻 I2C数据传输时序 SCL (时钟) SDA (数据) 起始条件 停止条件

I2C总线通信的基本单位是字节(8位),每个字节后跟一个应答位(ACK)。通信流程如下:

  1. 起始条件(START): SCL为高电平时,SDA从高电平切换到低电平。
  2. 发送从设备地址: 主控器发送7位或10位地址,后跟读写位(0=写,1=读)。
  3. 从设备应答: 匹配地址的从设备在第9个SCL周期拉低SDA作为应答。
  4. 数据传输: 每字节8位,MSB先传,每字节后跟应答位。
  5. 停止条件(STOP): SCL为高电平时,SDA从低电平切换到高电平。
调试提示: 使用逻辑分析仪抓取SDA/SCL波形时,重点关注起始条件、地址字节和应答位。无应答(NACK)是I2C调试中最常见的故障信号,通常表示从设备不存在或忙。

2.2 Linux I2C核心层

Linux I2C核心层(i2c-core)位于drivers/i2c/i2c-core.c,提供总线注册、设备枚举、数据传输和驱动模型管理的统一框架。核心层向上对客户端驱动暴露API,向下对适配器驱动提供注册接口。

核心层组件 文件/函数 职责
总线注册 i2c_add_adapter() 注册I2C适配器到系统,创建总线编号
设备枚举 i2c_new_device() 根据板级信息或设备树创建I2C客户端设备
数据传输 i2c_transfer() 执行I2C消息传输,支持组合读写
驱动匹配 i2c_device_match() 通过ID表或设备树兼容性匹配驱动和设备
SMBus兼容 i2c_smbus_xfer() 提供SMBus协议封装,简化单字节读写
Linux I2C核心层架构 用户空间 (i2c-dev / sysfs / i2c-tools) I2C核心层 (i2c-core) i2c_add_adapter | i2c_new_device | i2c_transfer | i2c_smbus_xfer I2C适配器驱动 i2c_algorithm.master_xfer I2C客户端驱动 i2c_driver.probe / i2c_client I2C控制器硬件 I2C从设备硬件 核心层隔离了适配器硬件差异,客户端驱动通过标准API访问任意I2C总线

2.3 I2C适配器驱动模型

I2C适配器(Adapter)对应SoC中的I2C控制器硬件。适配器驱动需要实现struct i2c_algorithm中的master_xfer()函数,该函数负责将struct i2c_msg消息队列转换为硬件时序。

核心数据结构: struct i2c_adapter 包含适配器编号、算法指针、锁、超时设置和DMA能力标志。每个适配器在/dev/i2c-N创建对应的设备节点。
// i2c_adapter 核心结构(简化)
struct i2c_adapter {
    struct module *owner;
    unsigned int class;               // 适配器类别
    const struct i2c_algorithm *algo; // 算法实现
    void *algo_data;                  // 算法私有数据

    int nr;                           // 总线编号
    struct device dev;                // 内核设备
    struct mutex bus_lock;            // 总线访问锁
    unsigned long timeout;            // 传输超时(jiffies)
    unsigned int retries;             // 重试次数
};

// i2c_algorithm 核心结构
struct i2c_algorithm {
    int (*master_xfer)(struct i2c_adapter *adap,
                       struct i2c_msg *msgs, int num);
    int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
                      unsigned short flags, char read_write,
                      u8 command, int size,
                      union i2c_smbus_data *data);
    u32 (*functionality)(struct i2c_adapter *adap);
};

适配器驱动的注册流程:

  1. 分配并初始化struct i2c_adapter实例
  2. 实现struct i2c_algorithm中的master_xfer()
  3. 调用i2c_add_adapter()或i2c_add_numbered_adapter()注册
  4. 核心层自动创建/dev/i2c-N设备节点
调试陷阱: master_xfer()返回值为成功传输的消息数量。返回负值表示错误,返回0表示无应答。V4L2驱动中I2C传输失败会导致传感器寄存器配置丢失,表现为图像异常或时钟失锁。

2.4 I2C客户端驱动模型

I2C客户端驱动(Client Driver)对应具体的I2C从设备,如摄像头传感器ov5640、视频解码器adv7180等。客户端驱动通过struct i2c_driver注册到核心层,核心层在设备枚举时调用probe()函数完成初始化。

数据结构 字段/函数 说明
struct i2c_driver probe() 设备匹配成功时调用,初始化硬件和数据结构
struct i2c_driver remove() 设备移除时调用,释放资源
struct i2c_driver id_table 支持的设备ID列表,用于匹配
struct i2c_client addr 7位I2C从设备地址
struct i2c_client adapter 指向所属适配器的指针
struct i2c_client irq 从设备中断号(V4L2驱动关键字段)
I2C客户端驱动注册与匹配流程 设备树/板级信息 i2c@xxx { ov5640@3c } I2C核心层枚举 i2c_new_device() 客户端驱动注册 i2c_add_driver() 匹配成功 → i2c_driver.probe(client, id) 被调用 读取设备ID i2c_smbus_read_byte() 初始化寄存器 i2c_master_send() 注册V4L2子设备 v4l2_i2c_subdev_init() 请求中断 devm_request_irq() V4L2驱动中的I2C客户端关键点 1. client->irq 在probe中获取,用于中断处理注册 2. v4l2_i2c_subdev_init() 将i2c_client与v4l2_subdev绑定

2.5 I2C消息传输与SMBus封装

客户端驱动通过两种方式与从设备通信:

  • 原始消息传输: 使用i2c_transfer()发送struct i2c_msg数组,支持组合读写(重复起始条件)。
  • SMBus封装: 使用i2c_smbus_read_byte_data()、i2c_smbus_write_word_data()等函数,简化单寄存器读写。
// I2C消息结构
struct i2c_msg {
    __u16 addr;    // 从设备地址(7位或10位)
    __u16 flags;   // 标志:I2C_M_RD(读)、I2C_M_TEN(10位地址)
    __u16 len;     // 数据长度
    __u8 *buf;     // 数据缓冲区
};

// 示例:向传感器寄存器0x30写入值0x01
static int sensor_write_reg(struct i2c_client *client, u8 reg, u8 val)
{
    u8 buf[2] = {reg, val};
    struct i2c_msg msg = {
        .addr = client->addr,
        .flags = 0,          // 写操作
        .len = 2,
        .buf = buf,
    };
    int ret = i2c_transfer(client->adapter, &msg, 1);
    return (ret == 1) ? 0 : -EIO;
}

// 示例:读取传感器寄存器0x30
static int sensor_read_reg(struct i2c_client *client, u8 reg)
{
    struct i2c_msg msgs[2] = {
        {
            .addr = client->addr,
            .flags = 0,      // 写寄存器地址
            .len = 1,
            .buf = ®,
        },
        {
            .addr = client->addr,
            .flags = I2C_M_RD, // 读数据
            .len = 1,
            .buf = &val,
        },
    };
    int ret = i2c_transfer(client->adapter, msgs, 2);
    return (ret == 2) ? val : -EIO;
}
性能建议: 在V4L2驱动中,批量配置寄存器时优先使用i2c_transfer()发送多消息数组,避免多次总线仲裁开销。对于单寄存器读写,使用SMBus封装函数更简洁。

2.6 I2C与V4L2的绑定关系

在V4L2驱动框架中,I2C客户端驱动通过v4l2_i2c_subdev_init()将i2c_client与v4l2_subdev关联。这使得V4L2桥接驱动可以通过子设备接口间接操作I2C从设备。

V4L2子设备操作 底层I2C操作 典型应用场景
s_stream() 写寄存器启动/停止视频流 摄像头传感器流控制
s_ctrl() 写寄存器设置曝光、增益 图像参数调节
g_frame_interval() 读寄存器获取帧率 时钟同步参数获取
s_frame_interval() 写寄存器设置帧率 时钟同步参数配置
interrupt_service_routine() 读中断状态寄存器 中断处理协同
V4L2与I2C客户端驱动的绑定关系 V4L2桥接驱动 video_device / media_entity v4l2_subdev subdev_ops / v4l2_ctrl_handler i2c_client addr / adapter / irq v4l2_i2c_subdev_init() I2C从设备硬件 桥接驱动通过v4l2_subdev调用子设备操作,子设备内部通过i2c_client完成寄存器读写 中断处理中通过i2c_client读取状态寄存器,实现时钟同步状态反馈