4. I2C读写操作:单字节写、单字节读、多字节写(页写)、多字节读(顺序读)时序图

在音频Codec驱动调试中,I2C总线是配置寄存器最常用的接口。理解四种基本读写操作的时序,是定位通信失败、寄存器值异常等硬件问题的核心技能。本章通过时序图与步骤分解,逐一拆解单字节写、单字节读、多字节写(页写)和多字节读(顺序读)的完整流程。

4.1 四种操作的核心差异

操作类型 传输方向 数据量 典型应用场景 关键时序特征
单字节写 主机 → 从机 1字节数据 配置单个寄存器(如设置Codec采样率) 地址+数据,一次Stop
单字节读 主机 ← 从机 1字节数据 读取单个寄存器状态(如查询中断标志) 写地址→重启动→读数据
多字节写(页写) 主机 → 从机 2~N字节数据 批量初始化寄存器组(如配置EQ滤波器系数) 地址+连续数据,一次Stop
多字节读(顺序读) 主机 ← 从机 2~N字节数据 批量读取寄存器组(如读取Codec状态寄存器块) 写地址→重启动→连续读
调试提示: 在音频Codec驱动中,单字节写用于关键寄存器的精确配置,多字节写常用于上电初始化序列。读操作多用于调试阶段的状态回读与故障诊断。

4.2 单字节写时序

单字节写是最基础的I2C操作,主机向从机发送设备地址、寄存器地址和一个数据字节。时序分为三个阶段:启动与设备寻址、寄存器地址发送、数据发送与停止。

单字节写时序图 S + 设备地址 W + ACK 寄存器地址 ACK 数据字节 ACK P S=Start条件 W=写位(0) ACK=从机应答 P=Stop条件
单字节写步骤:
  1. 主机产生Start条件(SDA在SCL高电平时由高变低)。
  2. 主机发送7位设备地址 + 写位(0),共8位。
  3. 从机在第9个SCL周期拉低SDA作为ACK应答。
  4. 主机发送8位寄存器地址,指定要写入的寄存器。
  5. 从机再次ACK应答。
  6. 主机发送8位数据字节,即要写入寄存器的值。
  7. 从机ACK应答。
  8. 主机产生Stop条件(SDA在SCL高电平时由低变高),结束传输。

4.3 单字节读时序

单字节读需要先写入寄存器地址,然后重新启动总线,切换为读方向,从从机读取一个字节。这种“写地址→重启动→读数据”的模式是I2C读操作的标准范式。

单字节读时序图 S + 设备地址 W + ACK 寄存器地址 ACK Sr 设备地址 R + ACK 数据 Sr=重启动条件 R=读位(1) 主机在第9周期发送NACK后产生Stop 操作步骤: ① Start → 设备地址+W → ACK → 寄存器地址 → ACK ② 重启动(Sr) → 设备地址+R → ACK → 从机发送数据 ③ 主机发送NACK(非应答) → Stop
常见调试陷阱: 单字节读操作中,如果主机在读取最后一个字节后没有发送NACK就直接产生Stop,从机可能继续发送数据,导致总线冲突。务必在最后一个字节的第9个SCL周期保持SDA为高(NACK),再产生Stop。

4.4 多字节写(页写)时序

多字节写允许主机在一次传输中连续写入多个寄存器,无需为每个字节单独发送Start和Stop。音频Codec的批量初始化(如配置多个滤波器系数)通常使用页写来提升效率。

多字节写(页写)时序图 S+地址+W ACK 起始寄存器地址 ACK 数据1 ACK 数据2 ACK P … 页写特点:一次Start→连续发送N个数据字节→一次Stop,地址自动递增 关键约束: • 数据字节数不能超过从机支持的页写长度(常见为8/16/32字节) • 寄存器地址自动递增,但不可跨页边界(如从0x0F写到0x10可能回卷)
页写效率对比: 假设要写入8个寄存器,单字节写需要8次Start-Stop序列(约16字节总线开销),而页写仅需1次Start-Stop(约10字节总线开销),效率提升约37%。在音频Codec初始化阶段,页写能显著缩短启动时间。

4.5 多字节读(顺序读)时序

顺序读用于连续读取多个寄存器的值,常用于读取Codec的状态寄存器块或音频数据缓冲区。与单字节读类似,先写寄存器地址,再重启动后连续读取多个字节。

多字节读(顺序读)时序图 S+地址+W ACK 起始寄存器地址 ACK Sr 地址+R ACK 数据1 ACK 数据2 ACK … 数据N NACK P 顺序读特点:最后一个字节主机发送NACK,通知从机停止发送,然后产生Stop 顺序读与单字节读的关键区别: • 单字节读:主机在收到1个字节后发送NACK+Stop • 顺序读:主机在收到前N-1个字节时发送ACK,仅在最后一个字节发送NACK+Stop

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 = &reg_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,
        },