4. I2C读写操作:单字节写、单字节读、多字节写(页写)、多字节读(顺序读)时序图
在音频Codec驱动调试中,I2C总线是配置寄存器最常用的接口。理解四种基本读写操作的时序,是定位通信失败、寄存器值异常等硬件问题的核心技能。本章通过时序图与步骤分解,逐一拆解单字节写、单字节读、多字节写(页写)和多字节读(顺序读)的完整流程。
4.1 四种操作的核心差异
| 操作类型 | 传输方向 | 数据量 | 典型应用场景 | 关键时序特征 |
|---|---|---|---|---|
| 单字节写 | 主机 → 从机 | 1字节数据 | 配置单个寄存器(如设置Codec采样率) | 地址+数据,一次Stop |
| 单字节读 | 主机 ← 从机 | 1字节数据 | 读取单个寄存器状态(如查询中断标志) | 写地址→重启动→读数据 |
| 多字节写(页写) | 主机 → 从机 | 2~N字节数据 | 批量初始化寄存器组(如配置EQ滤波器系数) | 地址+连续数据,一次Stop |
| 多字节读(顺序读) | 主机 ← 从机 | 2~N字节数据 | 批量读取寄存器组(如读取Codec状态寄存器块) | 写地址→重启动→连续读 |
调试提示: 在音频Codec驱动中,单字节写用于关键寄存器的精确配置,多字节写常用于上电初始化序列。读操作多用于调试阶段的状态回读与故障诊断。
4.2 单字节写时序
单字节写是最基础的I2C操作,主机向从机发送设备地址、寄存器地址和一个数据字节。时序分为三个阶段:启动与设备寻址、寄存器地址发送、数据发送与停止。
单字节写步骤:
- 主机产生Start条件(SDA在SCL高电平时由高变低)。
- 主机发送7位设备地址 + 写位(0),共8位。
- 从机在第9个SCL周期拉低SDA作为ACK应答。
- 主机发送8位寄存器地址,指定要写入的寄存器。
- 从机再次ACK应答。
- 主机发送8位数据字节,即要写入寄存器的值。
- 从机ACK应答。
- 主机产生Stop条件(SDA在SCL高电平时由低变高),结束传输。
4.3 单字节读时序
单字节读需要先写入寄存器地址,然后重新启动总线,切换为读方向,从从机读取一个字节。这种“写地址→重启动→读数据”的模式是I2C读操作的标准范式。
常见调试陷阱: 单字节读操作中,如果主机在读取最后一个字节后没有发送NACK就直接产生Stop,从机可能继续发送数据,导致总线冲突。务必在最后一个字节的第9个SCL周期保持SDA为高(NACK),再产生Stop。
4.4 多字节写(页写)时序
多字节写允许主机在一次传输中连续写入多个寄存器,无需为每个字节单独发送Start和Stop。音频Codec的批量初始化(如配置多个滤波器系数)通常使用页写来提升效率。
页写效率对比: 假设要写入8个寄存器,单字节写需要8次Start-Stop序列(约16字节总线开销),而页写仅需1次Start-Stop(约10字节总线开销),效率提升约37%。在音频Codec初始化阶段,页写能显著缩短启动时间。
4.5 多字节读(顺序读)时序
顺序读用于连续读取多个寄存器的值,常用于读取Codec的状态寄存器块或音频数据缓冲区。与单字节读类似,先写寄存器地址,再重启动后连续读取多个字节。
4.6 四种操作的代码实现参考
以下代码展示了在Linux内核I2C子系统下,四种操作的典型实现方式。注意多字节操作中i2c_msg数组的构造差异。
/* 单字节写:向reg_addr写入1字节数据 */
int i2c_write_byte(struct i2c_client *client, u8 reg_addr, u8 data)
{
u8 buf[2] = {reg_addr, data};
struct i2c_msg msg = {
.addr = client->addr,
.flags = 0, /* 写操作 */
.len = 2,
.buf = buf,
};
return i2c_transfer(client->adapter, &msg, 1);
}
/* 单字节读:从reg_addr读取1字节数据 */
int i2c_read_byte(struct i2c_client *client, u8 reg_addr, u8 *data)
{
struct i2c_msg msgs[2] = {
{
.addr = client->addr,
.flags = 0, /* 写寄存器地址 */
.len = 1,
.buf = ®_addr,
},
{
.addr = client->addr,
.flags = I2C_M_RD, /* 读数据 */
.len = 1,
.buf = data,
},
};
return i2c_transfer(client->adapter, msgs, 2);
}
/* 多字节写(页写):从start_addr开始连续写入len字节 */
int i2c_write_page(struct i2c_client *client, u8 start_addr,
u8 *data, int len)
{
u8 *buf = kmalloc(len + 1, GFP_KERNEL);
if (!buf)
return -ENOMEM;
buf[0] = start_addr;
memcpy(buf + 1, data, len);
struct i2c_msg msg = {
.addr = client->addr,
.flags = 0,
.len = len + 1,
.buf = buf,
};
int ret = i2c_transfer(client->adapter, &msg, 1);
kfree(buf);
return ret;
}
/* 多字节读(顺序读):从start_addr开始连续读取len字节 */
int i2c_read_sequential(struct i2c_client *client, u8 start_addr,
u8 *data, int len)
{
struct i2c_msg msgs[2] = {
{
.addr = client->addr,
.flags = 0,
.len = 1,
.buf = &start_addr,
},
{
.addr = client->addr,
.flags = I2C_M_RD,
.len = len,
.buf = data,
},