4. I2C读写操作:主机写从机、主机读从机、复合格式
I2C 通信说白了就三种模式:主机写、主机读、以及复合传输。很多初学者觉得 I2C 协议复杂,其实拆开看,每个操作都是固定的「套路」。我刚开始调 I2C 时也绕了不少弯路,今天把这三种模式掰开揉碎了讲清楚。
核心要点:I2C 所有操作都遵循「起始信号 → 从机地址 + 读写位 → 数据/应答 → 停止信号」这个基本框架。区别只在于数据流向和中间是否切换方向。
4.1 主机写从机:最基础的操作
主机写从机,就是主机主动往从机里塞数据。流程很简单:
- 主机发送起始信号(S)
- 主机发送从机地址 + 写位(0)
- 等待从机应答(ACK)
- 主机发送寄存器地址(如果是写寄存器)
- 等待从机应答
- 主机发送数据字节,每发一个等一次应答
- 全部发完后,主机发送停止信号(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 主机读从机:方向反转
读操作比写操作多一个步骤——需要先告诉从机你要读哪个寄存器,然后再切换方向去读数据。这就是所谓的「复合格式」的雏形。
流程是这样的:
- 起始信号 → 从机地址 + 写位 → 发送寄存器地址 → 停止信号
- 重新起始信号 → 从机地址 + 读位 → 读取数据 → 主机发送 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 个字节的数据。如果每个字节都发一次起始+停止,效率极低。复合格式可以一次搞定:
- 起始 → 地址+写 → 发送起始寄存器地址 → 重新起始 → 地址+读 → 连续读 10 个字节 → 最后一个发 NACK → 停止
你看,中间没有停止信号,总线一直被主机占用。这样传输效率高,而且时序更紧凑。
复合格式的典型应用场景
| 场景 | 操作序列 | 优势 |
|---|---|---|
| 读单个寄存器 | 写地址 → 写寄存器 → 重新起始 → 读数据 | 标准操作,兼容性好 |
| 读多个连续寄存器 | 写地址 → 写起始寄存器 → 重新起始 → 连续读 N 字节 | 效率高,减少总线开销 |
| 写多个寄存器 | 写地址 → 写起始寄存器 → 连续写 N 字节 | 一次完成批量写入 |
| 先写后读(原子操作) | 写地址 → 写命令 → 重新起始 → 读状态 | 保证操作完整性 |
关键点:复合格式中,每次方向切换都必须使用「重新起始信号」,而不是「停止+起始」。这是 I2C 协议规范,也是很多工程师容易忽略的地方。
4.4 三种操作的波形对比
你想想看,用逻辑分析仪抓波形时,这三种操作的区别一目了然。我画了一张图帮你理解:
4.5 实战中的注意事项
调 I2C 这么多年,我总结了几条铁律:
- 应答检查不能省:每次发送地址或数据后,必须检查从机是否回了 ACK。如果从机没应答,说明地址错了、从机忙或者硬件有问题。我曾经因为没检查应答,调了整整两天才发现是从机地址搞错了。
- 时序要严格:I2C 对时序要求不高,但也不能太离谱。SCL 频率别超过 400kHz(快速模式),否则某些从机跟不上。
- 读操作最后一个字节要发 NACK:这是告诉从机「别再发了,我要停了」。如果发了 ACK,从机会继续发数据,导致总线混乱。
- 复合格式注意方向切换:重新起始信号后,地址的读写位必须正确切换。写→读要发 Sr+地址+R,读→写要发 Sr+地址+W。
调试小技巧:用逻辑分析仪抓波形时,重点关注起始信号、地址和应答位。如果波形看起来没问题但通信失败,大概率是应答处理有问题。我习惯在代码里加个超时机制——等待应答超过 10ms 就报错,这样能快速定位问题。
4.6 三种操作的对比总结
| 操作类型 | 起始信号次数 | 方向切换 | 典型应用 | 注意事项 |
|---|---|---|---|---|
| 主机写从机 | 1 次 | 无 | 配置寄存器、发送命令 | 地址+写位,检查 ACK |
| 主机读从机 | 2 次(含重新起始) | 写→读 | 读取传感器数据、状态寄存器 | 必须用重新起始,最后发 NACK |
| 复合格式 | 2 次或更多 | 可多次切换 | 批量读写、原子操作 | 方向切换必须用重新起始 |