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总线通信的基本单位是字节(8位),每个字节后跟一个应答位(ACK)。通信流程如下:
- 起始条件(START): SCL为高电平时,SDA从高电平切换到低电平。
- 发送从设备地址: 主控器发送7位或10位地址,后跟读写位(0=写,1=读)。
- 从设备应答: 匹配地址的从设备在第9个SCL周期拉低SDA作为应答。
- 数据传输: 每字节8位,MSB先传,每字节后跟应答位。
- 停止条件(STOP): SCL为高电平时,SDA从低电平切换到高电平。
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协议封装,简化单字节读写 |
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);
};
适配器驱动的注册流程:
- 分配并初始化
struct i2c_adapter实例 - 实现
struct i2c_algorithm中的master_xfer() - 调用
i2c_add_adapter()或i2c_add_numbered_adapter()注册 - 核心层自动创建
/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驱动关键字段) |
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;
}
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() |
读中断状态寄存器 | 中断处理协同 |