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);
绑定流程如下:
- 设备树解析: I2C核心遍历总线节点,提取
compatible和reg。 - 驱动匹配: 遍历已注册的
i2c_driver,比对of_match_table或id_table。 - probe调用: 匹配成功后调用驱动的
probe函数,传入i2c_client指针。 - 资源获取: 在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框架中的位置。
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子设备初始化、中断响应与数据流协同奠定了坚实的硬件控制基础。