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 单字节写
主设备向从设备写入一个字节。流程如下:
- 主设备发送起始条件(S)
- 主设备发送从设备地址 + 写位(7位地址 + 0)
- 从设备应答(ACK)
- 主设备发送寄存器地址(1字节)
- 从设备应答(ACK)
- 主设备发送数据(1字节)
- 从设备应答(ACK)
- 主设备发送停止条件(P)
// 单字节写时序示意
// S | 0xA0 (地址+写) | ACK | 0x10 (寄存器地址) | ACK | 0xFF (数据) | ACK | P
4.2.2 单字节读
读操作稍微复杂一点,因为需要先告诉从设备要读哪个寄存器,然后再读数据。这里用到了「组合传输」的思想,但咱们先看最简单的实现:
- 主设备发送起始条件(S)
- 主设备发送从设备地址 + 写位
- 从设备应答(ACK)
- 主设备发送寄存器地址
- 从设备应答(ACK)
- 主设备发送重复起始条件(Sr)
- 主设备发送从设备地址 + 读位
- 从设备应答(ACK)
- 从设备发送数据(1字节)
- 主设备发送非应答(NACK)
- 主设备发送停止条件(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 组合传输的典型场景
最常见的场景就是上面说的「单字节读」和「多字节读」。你看,读操作本质上就是一次组合传输:
- 写阶段:发送寄存器地址
- 重复起始:不释放总线
- 读阶段:读取数据
如果没有重复起始,你就得先发停止条件,再发起始条件。这中间如果有其他主设备插进来,数据就读错了。
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数据传输格式全景
下面这张图帮你理清三种传输格式的关系和适用场景:
4.7 实战建议:驱动开发中的传输选择
写驱动时,怎么选传输格式?我总结了几条经验:
- 配置寄存器:用单字节写。简单,不容易出错。
- 读取状态:用单字节读。但注意,一定要用重复起始。
- 批量数据:用多字节读写。效率高,但先确认从设备支持地址自动递增。
- 复杂命令:用组合传输。比如 EEPROM 的页写入、传感器的多轴数据读取。
我的习惯:在驱动初始化时,先发一个单字节写测试一下通信是否正常。如果从设备不回 ACK,直接报错,省得后面调试浪费时间。