4. I2C读写操作:主机写从机、主机读从机、复合格式

I2C 通信说白了就三种模式:主机写、主机读、以及复合传输。很多初学者觉得 I2C 协议复杂,其实拆开看,每个操作都是固定的「套路」。我刚开始调 I2C 时也绕了不少弯路,今天把这三种模式掰开揉碎了讲清楚。

核心要点:I2C 所有操作都遵循「起始信号 → 从机地址 + 读写位 → 数据/应答 → 停止信号」这个基本框架。区别只在于数据流向和中间是否切换方向。

4.1 主机写从机:最基础的操作

主机写从机,就是主机主动往从机里塞数据。流程很简单:

  1. 主机发送起始信号(S)
  2. 主机发送从机地址 + 写位(0)
  3. 等待从机应答(ACK)
  4. 主机发送寄存器地址(如果是写寄存器)
  5. 等待从机应答
  6. 主机发送数据字节,每发一个等一次应答
  7. 全部发完后,主机发送停止信号(P)

嗯,这里要注意:从机地址是 7 位,加上读写位正好凑成 1 个字节。比如地址 0x3C,左移一位变成 0x78,写操作就是 0x78,读操作就是 0x79。

我在项目中遇到过一个问题:写数据时从机一直不回 ACK。查了半天发现是从机地址搞错了——7 位地址和 8 位地址混用,这种坑特别容易踩。

代码示例:主机写从机(寄存器模式)

// 往从机地址 0x3C 的寄存器 0x10 写入 0xAB
uint8_t slave_addr = 0x3C << 1;  // 左移一位,写操作
uint8_t reg_addr = 0x10;
uint8_t data = 0xAB;

I2C_Start();                    // 起始信号
I2C_SendByte(slave_addr | 0);   // 地址 + 写位
I2C_WaitAck();                  // 等待应答
I2C_SendByte(reg_addr);         // 寄存器地址
I2C_WaitAck();
I2C_SendByte(data);             // 数据
I2C_WaitAck();
I2C_Stop();                     // 停止信号
个人习惯:我写 I2C 驱动时,会把「发送地址+等待应答」封装成一个函数。这样代码干净,调试时也容易定位问题。

4.2 主机读从机:方向反转

读操作比写操作多一个步骤——需要先告诉从机你要读哪个寄存器,然后再切换方向去读数据。这就是所谓的「复合格式」的雏形。

流程是这样的:

  1. 起始信号 → 从机地址 + 写位 → 发送寄存器地址 → 停止信号
  2. 重新起始信号 → 从机地址 + 读位 → 读取数据 → 主机发送 NACK → 停止信号

为什么要有「重新起始信号」?因为 I2C 协议规定,方向切换必须重新发送起始信号。你不能在同一个传输里突然从写变成读,必须「断开重连」。

我曾经踩过的坑:读操作时忘记发「重新起始信号」,直接发了停止再发起始。虽然也能工作,但时序上不标准,某些从机(比如某些传感器)会直接罢工。后来我全部改用「重新起始信号」,再没出过问题。

代码示例:主机读从机

// 从从机地址 0x3C 的寄存器 0x10 读取 1 个字节
uint8_t slave_addr = 0x3C << 1;
uint8_t reg_addr = 0x10;
uint8_t received_data;

I2C_Start();
I2C_SendByte(slave_addr | 0);   // 写方向
I2C_WaitAck();
I2C_SendByte(reg_addr);         // 告诉从机要读哪个寄存器
I2C_WaitAck();

I2C_Start();                    // 重新起始信号(重点!)
I2C_SendByte(slave_addr | 1);   // 读方向
I2C_WaitAck();
received_data = I2C_ReadByte(); // 读取数据
I2C_SendNack();                 // 主机发送非应答
I2C_Stop();

4.3 复合格式:组合传输

复合格式,说白了就是一次传输里包含多个读写操作,中间用重新起始信号连接,不释放总线。这种模式在需要连续读写多个寄存器时特别有用。

举个例子:你要从传感器读取 10 个字节的数据。如果每个字节都发一次起始+停止,效率极低。复合格式可以一次搞定:

  1. 起始 → 地址+写 → 发送起始寄存器地址 → 重新起始 → 地址+读 → 连续读 10 个字节 → 最后一个发 NACK → 停止

你看,中间没有停止信号,总线一直被主机占用。这样传输效率高,而且时序更紧凑。

复合格式的典型应用场景

场景 操作序列 优势
读单个寄存器 写地址 → 写寄存器 → 重新起始 → 读数据 标准操作,兼容性好
读多个连续寄存器 写地址 → 写起始寄存器 → 重新起始 → 连续读 N 字节 效率高,减少总线开销
写多个寄存器 写地址 → 写起始寄存器 → 连续写 N 字节 一次完成批量写入
先写后读(原子操作) 写地址 → 写命令 → 重新起始 → 读状态 保证操作完整性
关键点:复合格式中,每次方向切换都必须使用「重新起始信号」,而不是「停止+起始」。这是 I2C 协议规范,也是很多工程师容易忽略的地方。

4.4 三种操作的波形对比

你想想看,用逻辑分析仪抓波形时,这三种操作的区别一目了然。我画了一张图帮你理解:

I2C 三种读写操作波形对比 主机写从机: S 地址+W ACK 寄存器地址 ACK 数据 ACK P 主机读从机: S 地址+W ACK 寄存器地址 ACK Sr 地址+R ACK 数据 NACK P 复合格式(连续读多字节): S 地址+W ACK 起始寄存器 ACK Sr 地址+R ACK 数据1 ACK 数据2 ACK 数据N NACK P 图例说明: 起始信号 地址帧 应答 寄存器地址 数据 停止/NACK

4.5 实战中的注意事项

调 I2C 这么多年,我总结了几条铁律:

  • 应答检查不能省:每次发送地址或数据后,必须检查从机是否回了 ACK。如果从机没应答,说明地址错了、从机忙或者硬件有问题。我曾经因为没检查应答,调了整整两天才发现是从机地址搞错了。
  • 时序要严格:I2C 对时序要求不高,但也不能太离谱。SCL 频率别超过 400kHz(快速模式),否则某些从机跟不上。
  • 读操作最后一个字节要发 NACK:这是告诉从机「别再发了,我要停了」。如果发了 ACK,从机会继续发数据,导致总线混乱。
  • 复合格式注意方向切换:重新起始信号后,地址的读写位必须正确切换。写→读要发 Sr+地址+R,读→写要发 Sr+地址+W。
调试小技巧:用逻辑分析仪抓波形时,重点关注起始信号、地址和应答位。如果波形看起来没问题但通信失败,大概率是应答处理有问题。我习惯在代码里加个超时机制——等待应答超过 10ms 就报错,这样能快速定位问题。

4.6 三种操作的对比总结

操作类型 起始信号次数 方向切换 典型应用 注意事项
主机写从机 1 次 无 配置寄存器、发送命令 地址+写位,检查 ACK
主机读从机 2 次(含重新起始) 写→读 读取传感器数据、状态寄存器 必须用重新起始,最后发 NACK
复合格式 2 次或更多 可多次切换 批量读写、原子操作 方向切换必须用重新起始