I2C协议层:起始条件、停止条件、数据帧

讲I2C协议,很多人一上来就背时序图。我觉得没必要。
你只要抓住三个核心:怎么开始、怎么结束、中间怎么传数据。
说白了,这就是一场“总线上的对话”,规则非常固定。

起始条件(S)

SCL高电平时,SDA从高变低。就这么简单。
嗯,这是总线的“发令枪”。只有主设备能发出这个信号。

我个人习惯:写驱动时,先把起始条件和停止条件封装成两个函数。后面所有读写操作都调用它们,不容易出错。

为什么SCL必须是高?因为总线空闲时SCL和SDA都是高。
一旦检测到SCL高、SDA下降沿,所有从设备就知道:要开始干活了。

停止条件(P)

SCL高电平时,SDA从低变高。和起始条件正好相反。
我见过不少新手把时序画反了,结果总线一直卡死。

避坑:我曾经在一个项目中,软件模拟I2C时把停止条件的延时加错了。导致从设备收不到停止位,一直死等。排查了整整一个下午。后来我用逻辑分析仪一抓,波形不对。所以啊,做I2C一定要上逻辑分析仪,光靠眼瞅代码根本看不出来。

起始和停止条件都可以由主设备随时发送。
注意:如果主设备在传输过程中发了一个新的起始条件(重复起始),中间不需要停止条件。这是多字节读写时的高效用法。

数据帧格式

数据帧非常规整。每帧9个时钟周期:8位数据 + 1位应答。
SCL低电平时数据变,高电平时数据锁存。

数据字节传输顺序:MSB在前(最高位先发)。
你想想看,I2C是从高位到低位,跟SPI的默认设置一样。别搞混了。

基本传输过程

  1. 主设备发起始条件
  2. 主设备发7位从机地址 + 1位读写位(0写、1读)
  3. 从设备在第9个时钟周期拉低SDA作为应答(ACK)
  4. 主设备发送/接收一个字节数据
  5. 每字节后接收方必须给出ACK(除非是最后字节故意给出NACK)
  6. 主设备发停止条件结束通信

关键点:地址字节的8位数据中,前7位是从机地址,最低位是R/W。比如地址0x50(写)实际上是0xA0(地址左移一位+0)。很多初学者把原始地址和通信地址搞混,导致设备没响应。我建议你写代码时直接使用7位地址,由库函数去移位处理,省心。

应答机制(ACK/NACK)

每一字节后,接收方必须在第9个SCL时钟周期内拉低SDA表示ACK,或者保持高表示NACK。
主设备发送数据时,从设备回复ACK表示“收到了,继续发”。
主设备接收数据时,主设备回复ACK表示“再发一个”,回复NACK表示“够了,停止”。

场景发送方接收方应答位含义
主写 (发送地址+数据)主设备从设备ACK: 从设备存在且就绪;NACK: 从设备忙或地址错误
主读 (接收数据)从设备主设备ACK: 主设备还要继续读;NACK: 主设备要结束读操作

这里有个小陷阱:主设备读最后一个字节前必须提前发NACK。
为什么?因为从设备在收到NACK后才知道不用再发送下一字节,然后主设备发停止位。我曾经在这里被坑过——主设备发了ACK后直接发停止位,从设备以为还要发数据,结果总线乱套了。

完整数据帧时序图(SVG)

下面这张图把刚才讲的内容串起来:起始条件 → 地址+读写位 → ACK → 数据 → ACK/NAK → 停止条件。

I2C 数据帧结构(单字节写传输) SCL SDA 起始 地址 + R/W(8位) ACK 数据字节(8位) ACK/NAK 停止 每个字节传输都是 8 个数据位 + 1 个应答位,共 9 个 SCL 时钟周期

时钟同步与仲裁(简单提一下)

I2C允许多个主设备同时发起通信。靠什么?SCL线与和SDA仲裁。
谁先拉低SDA,谁就赢了。输了的主设备要释放总线,等待下一次机会。

我建议你在做多主设备系统时,给每个主设备分配不同的优先级(通过地址或等待时间)。否则两个主设备同时发相同地址和数据的概率虽低,但一旦发生,也没事——因为数据完全一样,仲裁不会失败。但数据不同时,仲裁失败的那一方就丢了总线。

实用技巧:如果只用单主设备,时钟同步和仲裁可以忽略。所有从设备都是“听命”的。但如果你用STM32做主机,同时外接一个SBC(系统基础芯片)也做主机,那就得认真处理多主场景。我记得有次客户要求双主备份,结果因为仲裁没处理好,导致偶尔通信失败。后来加了硬件延时和软件重试机制才搞定。

总结关键点

  • 起始条件:SCL高时SDA下降沿
  • 停止条件:SCL高时SDA上升沿
  • 数据有效性:SCL高时数据必须稳定,只能在SCL低时变化
  • 应答位:每字节后必须有一个ACK或NACK
  • 终止读操作:主设备必须在最后一个字节之前发NACK

掌握了这些,I2C协议层基本就通了。剩下的就是写代码实现时序,不外乎就是IO口翻转 + 延时控制。但别小看时序容差——我曾经用很粗糙的延时循环也能跑通,但换到高速模式下就翻车了。下回咱们聊怎么用硬件I2C外设和DMA,那才是真正的高效方案。