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

在音频 Codec 驱动开发中,I2C 总线是配置内部寄存器(如采样率、增益、混音器路由)的主要通道。理解四种基本读写操作的时序,是编写正确驱动代码、排查通信故障的前提。本章将逐一拆解单字节写、单字节读、多字节写(页写)和多字节读(顺序读)的 SCL/SDA 波形与协议流程。

4.1 四种操作的核心差异

下表从传输方向、数据长度、起始/停止条件数量三个维度对比四种操作,帮助建立整体认知:

操作类型 主机发送 → 从机 从机发送 → 主机 数据字节数 起始/停止条件 典型用途
单字节写 从机地址(W) + 寄存器地址 + 数据 无 1 字节 1 Start + 1 Stop 配置单个寄存器
单字节读 从机地址(W) + 寄存器地址 + 重复起始 + 从机地址(R) 1 字节数据 1 字节 1 Start + 1 Repeated Start + 1 Stop 读取单个寄存器值
多字节写(页写) 从机地址(W) + 起始寄存器地址 + N 字节数据 无 N 字节(通常 ≤ 16) 1 Start + 1 Stop 批量初始化寄存器组
多字节读(顺序读) 从机地址(W) + 起始寄存器地址 + 重复起始 + 从机地址(R) N 字节数据 N 字节 1 Start + 1 Repeated Start + 1 Stop 读取连续寄存器块
关键概念: 多字节写/读的“自动地址递增”特性由从机内部逻辑实现。主机只需发送起始寄存器地址,从机在每接收/发送一个字节后自动将地址加 1,无需主机逐字节发送地址。

4.2 单字节写(Single Byte Write)

单字节写是最基础的 I2C 写操作,用于向音频 Codec 的某个寄存器写入一个配置值。完整时序包含以下 7 个阶段:

  1. 起始条件(S): SCL 高电平时,SDA 由高变低。
  2. 从机地址 + 写位: 主机发送 7 位从机地址 + 第 8 位为 0(写方向)。
  3. ACK(从机应答): 从机在第 9 个 SCL 周期将 SDA 拉低,表示确认。
  4. 寄存器地址: 主机发送 8 位寄存器地址(如 0x02 表示 ALC 控制寄存器)。
  5. ACK(从机应答): 从机再次拉低 SDA 确认。
  6. 数据字节: 主机发送 8 位数据(如 0x3F 表示增益值)。
  7. ACK(从机应答) + 停止条件(P): 从机应答后,主机在 SCL 高电平时将 SDA 由低变高,结束传输。
单字节写时序(Single Byte Write) S 从机地址 (7bit) W ACK 寄存器地址 (8bit) ACK 数据 (8bit) ACK P SDA SCL ACK S/P
驱动代码示例(Linux I2C 子系统):
// 单字节写:向 Codec 寄存器 0x02 写入 0x3F
int codec_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,
    };
    return i2c_transfer(client->adapter, &msg, 1);
}

4.3 单字节读(Single Byte Read)

单字节读需要两次传输:先写寄存器地址,再读数据。关键特征是使用 重复起始条件(Repeated Start, Sr) 代替 Stop + Start,避免总线释放后被其他主机抢占。

  • 阶段 1(写地址段): Start → 从机地址(W) → ACK → 寄存器地址 → ACK
  • 阶段 2(读数据段): Repeated Start → 从机地址(R) → ACK → 数据字节 → NACK(主机非应答)→ Stop
常见错误: 初学者容易在写地址段结束后发送 Stop 条件,再发 Start 进行读操作。这会导致总线释放,在多主系统中可能被其他设备插入。正确做法是使用 Repeated Start,保持总线控制权。
单字节读时序(Single Byte Read with Repeated Start) S 从机地址(W) ACK 寄存器地址 ACK Sr 从机地址(R) ACK 数据 NACK P
驱动代码示例:
// 单字节读:从 Codec 寄存器 0x02 读取当前值
int codec_read_reg(struct i2c_client *client, u8 reg, u8 *val) {
    struct i2c_msg msgs[2] = {
        {
            .addr = client->addr,
            .flags = 0,          // 写:发送寄存器地址
            .len = 1,
            .buf = ®,
        },
        {
            .addr = client->addr,
            .flags = I2C_M_RD,   // 读:接收数据
            .len = 1,
            .buf = val,
        },
    };
    return i2c_transfer(client->adapter, msgs, 2);
}

4.4 多字节写(页写 / Page Write)

音频 Codec 初始化时通常需要连续配置多个寄存器(如时钟树、数字接口格式)。多字节写允许主机在一次传输中发送起始地址和多个数据字节,从机内部自动递增地址,大幅提高效率。

时序特点:

  • 只使用 1 个 Start 和 1 个 Stop。
  • 寄存器地址之后连续发送 N 个数据字节,每个字节后跟一个 ACK。
  • 从机内部地址指针在每接收一个字节后自动加 1。
  • 页写长度受从机限制:常见 Codec(如 WM8960、TLV320AIC3x)页写上限为 16 字节,超出部分行为未定义。
阶段 主机发送 从机响应 说明
1 Start + 从机地址(W) ACK 开始写传输
2 起始寄存器地址 ACK 设置内部地址指针
3 数据字节 1 ACK 写入地址 N,指针 → N+1
4 数据字节 2 ACK 写入地址 N+1,指针 → N+2
… … … …
N+2 数据字节 N ACK 写入地址 N+(N-1)
N+3 Stop — 结束传输
页写 vs 单字节写性能对比: 假设要初始化 8 个寄存器,单字节写需要 8 次 Start/Stop 序列,总开销约 8 × (2.5 字节协议开销) = 20 字节;页写只需 1 次 Start/Stop,总开销约 2.5 + 8 = 10.5 字节,效率提升近 50%。
多字节写(页写)时序 一次 I2C 传输(1 Start + 1 Stop) S 从机地址(W) ACK 起始地址 ACK 数据 1 ACK … 数据 N P
驱动代码示例:
// 多字节写(页写):从起始寄存器地址开始连续写入 len 个字节
int codec_page_write(struct i2c_client *client, u8 start_reg,
                     const u8 *data, int len) {
    u8 buf[256]; // 确保足够大,实际使用需动态分配或限制长度
    buf[0] = start_reg;
    memcpy(&buf[1], data, len);

    struct i2c_msg msg = {
        .addr = client->addr,
        .flags = 0,
        .len = len + 1,  // 寄存器地址 + 数据
        .buf = buf,
    };
    return i2c_transfer(client->adapter, &msg, 1);
}

4.5 多字节读(顺序读 / Sequential Read)

顺序读用于从 Codec 中读取连续寄存器块,例如读取整个 EQ 系数表或 ALC 状态。其结构与单字节读类似,但在读数据段中主机连续接收多个字节,除最后一个字节外均发送 ACK。

关键规则:

  • 主机在接收完倒数第二个字节后发送 ACK,通知从机继续发送。
  • 主机在接收完最后一个字节后发送 NACK,通知从机停止发送。
  • 从机内部地址指针在每发送一个字节后自动加 1。
阶段 方向 内容 应答
1 主机→从机 Start + 从机地址(W) ACK
2 主机→从机 起始寄存器地址 ACK
3 主机→从机 Repeated Start + 从机地址(R) ACK
4 从机→主机 数据字节 1(地址 N) 主机 ACK
5 从机→主机 数据字节 2(地址 N+1) 主机 ACK
… 从机→主机 … 主机 ACK
N+3 从机→主机 数据字节 N(地址 N+N-1) 主机 NACK
N+4 主机→从机 Stop —
调试陷阱: 如果主机在顺序读的最后一个字节误发了 ACK 而不是 NACK,从机会认为主机还想继续接收,从而将 SDA 保持为输出状态。此时主机发送 Stop 条件时,SDA 可能被从机拉低,导致 Stop 条件无法形成,总线被锁死。这是 I2C 调试中最常见的挂死原因之一。
多字节读(顺序读)时序 S 地址(W) A 起始地址 A Sr 地址(R) A 数据 1 A ← 上一章 📖 返回目录 下一章 →