4. I2C读写操作:主机写从机、主机读从机、复合格式与重复起始条件
I2C 总线上的数据传输,说白了就两种方向:主机写数据给从机,或者主机从从机读数据。但实际项目中,你经常会遇到一种更“拧巴”的场景——先告诉从机我要读哪个寄存器,然后再把数据读回来。这就引出了复合格式和重复起始条件。
我个人习惯把 I2C 的读写操作比作“打电话”。主机是拨号的人,从机是接电话的人。SCL 是时钟,SDA 是话音线路。咱们今天就把这个电话怎么打、怎么挂、怎么转接,彻底聊透。
4.1 主机写从机:最基础的“发指令”
主机写从机,是 I2C 通信里最常用的操作。比如你给一个温度传感器发配置字,或者给一个 DAC 芯片写输出电压值,用的都是这个流程。
完整写操作时序:
- 起始条件(S):SCL 高电平时,SDA 从高拉低。
- 发送从机地址 + 写位(7位地址 + 0):主机在 SCL 的每个高电平期间,在 SDA 上逐位送出地址。第9个 SCL 时钟是应答位(ACK)。
- 等待从机应答(ACK):从机拉低 SDA 表示“收到,继续”。
- 发送寄存器地址/子地址(可选):很多从机设备需要先指定内部寄存器地址。
- 等待从机应答(ACK)
- 发送数据字节(Data):一个或多个字节,每个字节后都有应答。
- 停止条件(P):SCL 高电平时,SDA 从低拉高。
关键点:写操作中,每个字节后的应答位都是由从机拉低的。如果从机没拉低(NACK),说明从机忙、地址不对或者寄存器不存在。这是调试时最常见的故障点。
// 伪代码:主机写一个字节到从机寄存器
I2C_Start(); // 产生起始条件
I2C_SendByte(0xA0 << 1 | 0); // 从机地址 + 写位 (0xA0是典型EEPROM地址)
I2C_WaitAck(); // 等待应答
I2C_SendByte(0x10); // 寄存器地址 (例如0x10)
I2C_WaitAck();
I2C_SendByte(0x55); // 要写入的数据
I2C_WaitAck();
I2C_Stop(); // 产生停止条件
4.2 主机读从机:从设备“取数据”
读操作和写操作在流程上有个关键区别:地址字节的最后一位是 1(读位),而且数据方向反转了——主机变成接收者,从机变成发送者。
完整读操作时序:
- 起始条件(S)
- 发送从机地址 + 读位(7位地址 + 1)
- 等待从机应答(ACK)
- 接收数据字节(Data):主机在 SCL 每个时钟的高电平期间采样 SDA 上的数据。
- 主机发送应答(ACK/NACK):如果还要继续读,主机拉低 SDA 发 ACK;如果这是最后一个字节,主机发 NACK。
- 停止条件(P)
注意:读操作中,应答位的控制权在主机手里。很多新手在这里搞反——以为应答还是从机发。其实从机在发完数据后就会释放 SDA,主机必须主动拉低来应答。如果主机不发 ACK,从机就会认为“主机不要了”,停止发送。
我记得有一次调试一个加速度传感器,读出来的数据全是 0xFF。抓波形一看,主机在读完第一个字节后没发 ACK,从机直接罢工了。嗯,这种坑踩过一次就记住了。
4.3 复合格式(Combined Format):先写后读
实际项目中,大部分传感器、ADC、DAC 芯片都采用寄存器映射架构。你要读某个寄存器的值,必须先告诉芯片“我要读哪个地址”,然后再把数据读回来。这就不能用单纯的读操作或写操作,必须把两个操作拼在一起。
复合格式的标准流程:
- 起始条件(S)
- 从机地址 + 写位:告诉从机“我要跟你说话”。
- 寄存器地址:告诉从机“我要操作哪个寄存器”。
- 重复起始条件(Sr):关键!不产生停止条件,直接再发一次起始条件。
- 从机地址 + 读位:告诉从机“现在我要读数据了”。
- 读取数据字节:一个或多个字节。
- 停止条件(P)
为什么不用“写操作+停止条件+读操作”?因为停止条件会释放总线,其他主机可能抢占总线。而且有些从机在收到停止条件后会进入低功耗模式,需要重新唤醒。重复起始条件保证了总线所有权始终在当前主机手里,原子性更强。
// 伪代码:复合格式——先写寄存器地址,再读数据
I2C_Start(); // 起始条件
I2C_SendByte(0x48 << 1 | 0); // 从机地址 + 写位 (0x48是典型温度传感器地址)
I2C_WaitAck();
I2C_SendByte(0x00); // 寄存器地址 (例如温度寄存器)
I2C_WaitAck();
I2C_RepeatedStart(); // 重复起始条件 (Sr) —— 没有停止条件!
I2C_SendByte(0x48 << 1 | 1); // 从机地址 + 读位
I2C_WaitAck();
uint8_t data = I2C_ReadByte(); // 读取数据
I2C_SendNack(); // 最后一个字节,主机发NACK
I2C_Stop(); // 停止条件
4.4 重复起始条件(Repeated Start):总线上的“无缝切换”
重复起始条件,符号记作 Sr,是 I2C 协议里一个非常巧妙的设计。它本质上就是一个起始条件,但特殊之处在于:它前面没有停止条件。
波形上的表现:
- 正常起始条件(S):总线空闲(SDA、SCL 都是高)→ SDA 拉低。
- 重复起始条件(Sr):上一个数据位或应答位之后(SDA 可能为高或低)→ SCL 还是高时,SDA 先拉高再拉低。
说白了,Sr 就是在不释放总线的情况下,强行把 SDA 拉高再拉低,制造一个“假起始”。从机看到这个波形,就知道“主机还没说完,要继续发地址”。
调试要点:用逻辑分析仪抓复合格式时,注意观察 Sr 的位置。如果 Sr 之后没有立即跟地址字节,或者 Sr 的时序不满足建立时间(tSU:STA),从机可能误判为停止条件+起始条件,导致通信错乱。我曾在某个国产 MCU 上遇到过,它的 I2C 外设在产生 Sr 时,SDA 拉高的时间不够长,结果从机直接复位了。
4.5 四种操作模式对比
| 操作模式 | 起始条件 | 地址方向 | 数据方向 | 应答控制 | 典型应用 |
|---|---|---|---|---|---|
| 主机写从机 | S | 地址 + W (0) | 主机→从机 | 从机发 ACK | 配置寄存器、写 EEPROM |
| 主机读从机 | S | 地址 + R (1) | 从机→主机 | 主机发 ACK/NACK | 读取状态、读取数据 |
| 复合格式(先写后读) | S + Sr | 地址+W → 地址+R | 先写后读 | 混合控制 | 读取传感器寄存器值 |
| 复合格式(多字节读写) | S / Sr | 地址+W 或 地址+R | 连续多字节 | 每字节后应答 | 批量数据传输 |
4.6 知识结构图:I2C读写操作全景
4.7 实战避坑指南
做 I2C 调试这么多年,我总结了几条关于读写操作的“血泪教训”,分享给你:
- 复合格式中忘记发重复起始条件:这是最常见的错误。代码里写了 Stop() 再写 Start(),结果从机复位了,读回来的数据全是错的。我曾经在一个温控项目上排查了整整两天,最后发现是库函数里把 RepeatedStart 写成了 Stop+Start。
- 读操作最后一个字节没发 NACK:如果你还要继续读,就发 ACK;如果这是最后一个字节,必须发 NACK。否则从机会继续发数据,导致总线冲突。
- 地址左移一位搞反:很多新手搞不清 7 位地址和 8 位地址的区别。7 位地址左移一位,最低位是读写位。比如地址 0x48,左移后是 0x90(写)或 0x91(读)。我习惯在代码里写
(addr << 1) | rw,清晰明了。 - 应答超时没处理:如果从机没应答,你的代码不能死等。一定要加超时机制,比如等待 10ms 后还没收到 ACK,就主动发停止条件并报错。
我的调试习惯:每次写 I2C 驱动,我都会先写一个“写寄存器”函数和一个“读寄存器”函数,底层封装好起始、停止、应答处理。上层应用只管调用。这样一旦出问题,我只需要抓一次波形,就能定位是底层时序问题还是上层逻辑问题。