4. I2C子设备注册:摄像头传感器I2C地址配置、子设备驱动绑定、寄存器读写封装

摄像头传感器通常通过I2C总线与主控SoC通信,用于配置分辨率、帧率、增益、曝光等控制寄存器。在Linux内核中,传感器作为I2C子设备存在,需要完成地址配置、驱动绑定以及寄存器读写接口的封装。本章详细拆解这三个关键步骤,并给出典型实现模式。

4.1 I2C子设备地址配置与设备树描述

传感器I2C地址由硬件引脚(如ADDR/CS引脚电平)决定,常见7位地址范围为0x20~0x7F。设备树(Device Tree)是描述硬件拓扑的标准方式,I2C子设备节点必须包含 reg 属性指定地址。

设备树节点示例(OV5640传感器):
i2c@ff610000 {
    status = "okay";
    clock-frequency = <400000>;

    ov5640: camera@3c {
        compatible = "ovti,ov5640";
        reg = <0x3c>;
        clocks = <&clks IMX8MM_CLK_CLKOUT1>;
        clock-names = "xvclk";
        powerdown-gpios = <&gpio1 7 GPIO_ACTIVE_HIGH>;
        reset-gpios = <&gpio1 6 GPIO_ACTIVE_LOW>;
        #address-cells = <1>;
        #size-cells = <0>;
    };
};
属性 说明 典型值
compatible 驱动匹配字符串,与驱动中 of_match_table 对应 "ovti,ov5640"
reg I2C从设备7位地址(左对齐) 0x3c
clocks 传感器主时钟(xvclk)句柄 &clks IMX8MM_CLK_CLKOUT1
powerdown-gpios 电源掉电控制GPIO <&gpio1 7 GPIO_ACTIVE_HIGH>
reset-gpios 硬件复位GPIO <&gpio1 6 GPIO_ACTIVE_LOW>
地址冲突避坑: 同一I2C总线上多个传感器若地址相同,需通过硬件引脚(如ADDR_SEL)切换地址,并在设备树中分别配置不同的 reg 值。否则驱动加载时会因地址冲突导致 -EBUSY 错误。

4.2 子设备驱动绑定:从设备树到驱动实例

内核I2C核心在解析设备树时,会为每个 reg 节点创建 i2c_client 结构体。驱动通过 i2c_driver 注册,利用 id_table 或 of_match_table 完成匹配绑定。

static const struct i2c_device_id ov5640_id[] = {
    { "ov5640", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, ov5640_id);

static const struct of_device_id ov5640_of_match[] = {
    { .compatible = "ovti,ov5640" },
    { }
};
MODULE_DEVICE_TABLE(of, ov5640_of_match);

static struct i2c_driver ov5640_i2c_driver = {
    .driver = {
        .name = "ov5640",
        .of_match_table = ov5640_of_match,
    },
    .probe    = ov5640_probe,
    .remove   = ov5640_remove,
    .id_table = ov5640_id,
};
module_i2c_driver(ov5640_i2c_driver);

绑定流程如下:

  1. 设备树解析: I2C核心遍历总线节点,提取 compatible 和 reg。
  2. 驱动匹配: 遍历已注册的 i2c_driver,比对 of_match_table 或 id_table。
  3. probe调用: 匹配成功后调用驱动的 probe 函数,传入 i2c_client 指针。
  4. 资源获取: 在probe中通过 devm_clk_get、devm_gpiod_get 获取时钟和GPIO资源。
最佳实践: 使用 devm_* 系列API(设备资源管理)可自动释放资源,避免probe失败或remove时的内存泄漏。推荐在probe中优先使用 devm_kzalloc、devm_i2c_new_dummy_device 等。

4.3 寄存器读写封装:原子操作与错误处理

传感器寄存器通常为16位地址 + 8/16位数据。I2C读写需要封装为高内聚、低耦合的辅助函数,支持单次读写和批量读写,并处理NACK、总线仲裁等异常。

struct ov5640_device {
    struct i2c_client *client;
    struct mutex lock;
    u8 chip_id;
};

/* 单字节写:16位地址 + 8位数据 */
static int ov5640_write_reg(struct ov5640_device *dev, u16 reg, u8 val)
{
    struct i2c_client *client = dev->client;
    u8 buf[3] = { (reg >> 8) & 0xFF, reg & 0xFF, val };
    int ret;

    mutex_lock(&dev->lock);
    ret = i2c_master_send(client, buf, 3);
    mutex_unlock(&dev->lock);

    if (ret < 0) {
        dev_err(&client->dev, "write reg 0x%04x failed: %d\n", reg, ret);
        return ret;
    }
    return 0;
}

/* 单字节读:16位地址 + 8位数据 */
static int ov5640_read_reg(struct ov5640_device *dev, u16 reg, u8 *val)
{
    struct i2c_client *client = dev->client;
    u8 addr_buf[2] = { (reg >> 8) & 0xFF, reg & 0xFF };
    int ret;

    mutex_lock(&dev->lock);
    ret = i2c_master_send(client, addr_buf, 2);
    if (ret < 0) {
        mutex_unlock(&dev->lock);
        dev_err(&client->dev, "send addr failed: %d\n", ret);
        return ret;
    }
    ret = i2c_master_recv(client, val, 1);
    mutex_unlock(&dev->lock);

    if (ret < 0) {
        dev_err(&client->dev, "read reg 0x%04x failed: %d\n", reg, ret);
        return ret;
    }
    return 0;
}
批量读写优化: 对于连续寄存器(如增益、曝光参数),使用 i2c_transfer 组合消息(struct i2c_msg)可减少总线往返次数,提升配置速度。
static int ov5640_write_array(struct ov5640_device *dev,
                              const struct ov5640_regval *regs, int count)
{
    int i, ret;
    for (i = 0; i < count; i++) {
        ret = ov5640_write_reg(dev, regs[i].reg, regs[i].val);
        if (ret)
            return ret;
    }
    return 0;
}

4.4 I2C子设备注册与数据流协同架构

下图展示了从设备树解析到传感器寄存器读写的完整软件架构,突出I2C子设备在V4L2框架中的位置。

I2C子设备注册与数据流协同架构 设备树 (Device Tree) i2c@ff610000 → ov5640@3c (compatible, reg, clocks, gpios) I2C核心 (i2c-core) of_i2c_register_devices() → i2c_new_client_device() → i2c_client 驱动匹配 (i2c_driver) of_match_table → probe() → 分配ov5640_device结构体 寄存器读写封装 (ov5640_write_reg / ov5640_read_reg) i2c_master_send / i2c_master_recv / i2c_transfer + mutex保护 V4L2子设备 (v4l2_subdev) 数据流:V4L2 → 寄存器 → I2C

4.5 常见问题与调试手段

问题现象 可能原因 排查方法
probe未调用 compatible字符串不匹配 / 设备树未加载 检查 /sys/bus/i2c/devices/ 下是否有对应设备;使用 dtc 反编译设备树验证
I2C读写返回 -EIO 地址错误 / 传感器未上电 / 总线时钟不匹配 用 i2cdetect -y <bus> 扫描地址;示波器测量SCL/SDA波形
寄存器写入无响应 传感器处于复位状态 / 电源时序错误 检查reset/powerdown GPIO电平;确认xvclk时钟频率
批量配置部分失败 I2C消息长度超限 / 总线被占用 拆分消息为单次不超过32字节;增加mutex超时重试
关键提醒: 传感器寄存器读写必须考虑时序约束。部分传感器在写入后需要等待若干毫秒(如PLL锁定时间),此时应使用 usleep_range 或 msleep 延迟,不可忙等。同时,I2C操作应全程使用 mutex 保护,避免中断上下文与用户空间配置并发冲突。

通过以上步骤,摄像头传感器作为I2C子设备成功注册到内核,驱动完成绑定,寄存器读写接口具备原子性和错误恢复能力。这为后续V4L2子设备初始化、中断响应与数据流协同奠定了坚实的硬件控制基础。