4. I2C数据传输格式:单字节读写、多字节读写、组合传输(Repeated Start)

I2C 的数据传输格式,说白了就是主设备和从设备之间怎么「说话」。别看协议简单,里面的门道可不少。我刚开始做驱动时,就因为没搞懂单字节和多字节的区别,折腾了一整个下午。

这一节,咱们把三种最常用的传输格式掰开揉碎了讲。你理解了这些,写驱动时心里就有底了。

4.1 基本传输单元:起始、停止与应答

先看最基础的东西。任何 I2C 传输都离不开这三个信号:

  • 起始条件(S):SCL 高电平时,SDA 从高变低。告诉所有从设备:「我要开始通信了」。
  • 停止条件(P):SCL 高电平时,SDA 从低变高。告诉总线:「我结束了」。
  • 应答位(ACK/NACK):每发送完一个字节,接收方在第 9 个 SCL 周期拉低 SDA 表示应答(ACK),拉高表示非应答(NACK)。

关键点:起始和停止条件只能由主设备产生。从设备永远不能主动发起或终止传输。

嗯,这里要注意:应答位是 I2C 通信的「握手信号」。我曾经在一个项目中,从设备因为忙没来得及拉低 SDA,主设备就认为它挂了,直接发了停止条件。后来加了延时才解决。

4.2 单字节读写:最基础的通信

单字节读写是最简单的操作。你想想看,每次只传一个字节的数据,流程非常清晰。

4.2.1 单字节写

主设备向从设备写入一个字节。流程如下:

  1. 主设备发送起始条件(S)
  2. 主设备发送从设备地址 + 写位(7位地址 + 0)
  3. 从设备应答(ACK)
  4. 主设备发送寄存器地址(1字节)
  5. 从设备应答(ACK)
  6. 主设备发送数据(1字节)
  7. 从设备应答(ACK)
  8. 主设备发送停止条件(P)
// 单字节写时序示意
// S | 0xA0 (地址+写) | ACK | 0x10 (寄存器地址) | ACK | 0xFF (数据) | ACK | P

4.2.2 单字节读

读操作稍微复杂一点,因为需要先告诉从设备要读哪个寄存器,然后再读数据。这里用到了「组合传输」的思想,但咱们先看最简单的实现:

  1. 主设备发送起始条件(S)
  2. 主设备发送从设备地址 + 写位
  3. 从设备应答(ACK)
  4. 主设备发送寄存器地址
  5. 从设备应答(ACK)
  6. 主设备发送重复起始条件(Sr)
  7. 主设备发送从设备地址 + 读位
  8. 从设备应答(ACK)
  9. 从设备发送数据(1字节)
  10. 主设备发送非应答(NACK)
  11. 主设备发送停止条件(P)

个人习惯:我写驱动时,会把单字节读写封装成两个基础函数。所有多字节操作都基于它们来构建。这样代码复用率高,也容易调试。

4.3 多字节读写:批量传输的效率之选

实际项目中,很少只传一个字节。比如读取一个传感器的加速度数据,一次就要读 6 个字节(X、Y、Z 轴各 2 字节)。

多字节读写的核心思想是:地址只发一次,数据连续传。从设备内部会自动递增寄存器地址。

4.3.1 多字节写

和单字节写类似,只是数据部分变成了多个字节:

// 多字节写时序(写3个字节到寄存器0x10开始)
// S | 0xA0+W | ACK | 0x10 | ACK | Data0 | ACK | Data1 | ACK | Data2 | ACK | P

注意:每个数据字节后,从设备都会应答。如果从设备不支持自动地址递增,它会在第二个数据字节时发 NACK。我遇到过这种情况,当时查了半天手册才发现。

4.3.2 多字节读

多字节读和单字节读的区别在于:最后一个数据字节之前,主设备发的是 ACK,只有最后一个字节才发 NACK。

// 多字节读时序(读3个字节从寄存器0x10开始)
// S | 0xA0+W | ACK | 0x10 | ACK | Sr | 0xA0+R | ACK | Data0 | ACK | Data1 | ACK | Data2 | NACK | P

避坑指南:我曾经在读取 MPU6050 时,多字节读的最后一个字节忘了发 NACK,结果从设备继续发数据,导致总线卡死。记住:读操作的最后一个字节必须是 NACK,告诉从设备「够了,别发了」。

4.4 组合传输(Repeated Start):不释放总线的艺术

组合传输,也叫重复起始条件(Repeated Start),是 I2C 协议里最巧妙的设计之一。

它的作用很简单:在不发送停止条件的情况下,重新发起一次传输。为什么需要这个?

  • 原子操作:防止其他主设备在两次传输之间抢占总线
  • 读写切换:读操作必须先写寄存器地址,再读数据,中间不能释放总线
  • 多命令序列:某些设备需要先发命令,再发数据,中间不能被打断

4.4.1 组合传输的典型场景

最常见的场景就是上面说的「单字节读」和「多字节读」。你看,读操作本质上就是一次组合传输:

  1. 写阶段:发送寄存器地址
  2. 重复起始:不释放总线
  3. 读阶段:读取数据

如果没有重复起始,你就得先发停止条件,再发起始条件。这中间如果有其他主设备插进来,数据就读错了。

4.4.2 组合传输的时序

// 组合传输完整时序(读2个字节)
// S | Addr+W | ACK | RegAddr | ACK | Sr | Addr+R | ACK | Data0 | ACK | Data1 | NACK | P
//                                 ^^
//                         重复起始在这里

注意看,重复起始(Sr)和普通起始(S)的区别:Sr 之前没有停止条件。SCL 和 SDA 的时序是连续的。

核心要点:组合传输是 I2C 协议的精华。很多外设(如 EEPROM、传感器)的驱动都依赖它。你写驱动时,一定要确认硬件 I2C 控制器是否支持重复起始。有些低端 MCU 的 I2C 外设不支持,只能用 GPIO 模拟。

4.5 三种传输格式对比

传输格式 起始次数 停止次数 典型应用 注意事项
单字节写 1 1 配置寄存器 简单直接,效率低
单字节读 2(含1次重复起始) 1 读取状态寄存器 必须用重复起始
多字节写 1 1 批量配置、固件升级 注意地址自动递增
多字节读 2(含1次重复起始) 1 读取传感器数据 最后一个字节发NACK
组合传输 2+ 1 复杂命令序列 硬件必须支持重复起始

4.6 知识结构图:I2C数据传输格式全景

下面这张图帮你理清三种传输格式的关系和适用场景:

I2C数据传输格式全景图 I2C数据传输格式全景 单字节读写 多字节读写 组合传输 配置寄存器 状态读取 批量配置 传感器数据 原子操作 读写切换 核心原则 单字节是基础 → 多字节提升效率 → 组合传输保证原子性 典型时序对比 单字节写:S | Addr+W | ACK | Reg | ACK | Data | ACK | P 单字节读:S | Addr+W | ACK | Reg | ACK | Sr | Addr+R | ACK | Data | NACK | P 多字节读:S | Addr+W | ACK | Reg | ACK | Sr | Addr+R | ACK | D0 | ACK | D1 | ACK | D2 | NACK | P

4.7 实战建议:驱动开发中的传输选择

写驱动时,怎么选传输格式?我总结了几条经验:

  • 配置寄存器:用单字节写。简单,不容易出错。
  • 读取状态:用单字节读。但注意,一定要用重复起始。
  • 批量数据:用多字节读写。效率高,但先确认从设备支持地址自动递增。
  • 复杂命令:用组合传输。比如 EEPROM 的页写入、传感器的多轴数据读取。

我的习惯:在驱动初始化时,先发一个单字节写测试一下通信是否正常。如果从设备不回 ACK,直接报错,省得后面调试浪费时间。