I2C协议层:起始条件、停止条件、数据帧
讲I2C协议,很多人一上来就背时序图。我觉得没必要。
你只要抓住三个核心:怎么开始、怎么结束、中间怎么传数据。
说白了,这就是一场“总线上的对话”,规则非常固定。
起始条件(S)
SCL高电平时,SDA从高变低。就这么简单。
嗯,这是总线的“发令枪”。只有主设备能发出这个信号。
我个人习惯:写驱动时,先把起始条件和停止条件封装成两个函数。后面所有读写操作都调用它们,不容易出错。
为什么SCL必须是高?因为总线空闲时SCL和SDA都是高。
一旦检测到SCL高、SDA下降沿,所有从设备就知道:要开始干活了。
停止条件(P)
SCL高电平时,SDA从低变高。和起始条件正好相反。
我见过不少新手把时序画反了,结果总线一直卡死。
避坑:我曾经在一个项目中,软件模拟I2C时把停止条件的延时加错了。导致从设备收不到停止位,一直死等。排查了整整一个下午。后来我用逻辑分析仪一抓,波形不对。所以啊,做I2C一定要上逻辑分析仪,光靠眼瞅代码根本看不出来。
起始和停止条件都可以由主设备随时发送。
注意:如果主设备在传输过程中发了一个新的起始条件(重复起始),中间不需要停止条件。这是多字节读写时的高效用法。
数据帧格式
数据帧非常规整。每帧9个时钟周期:8位数据 + 1位应答。
SCL低电平时数据变,高电平时数据锁存。
数据字节传输顺序:MSB在前(最高位先发)。
你想想看,I2C是从高位到低位,跟SPI的默认设置一样。别搞混了。
基本传输过程
- 主设备发起始条件
- 主设备发7位从机地址 + 1位读写位(0写、1读)
- 从设备在第9个时钟周期拉低SDA作为应答(ACK)
- 主设备发送/接收一个字节数据
- 每字节后接收方必须给出ACK(除非是最后字节故意给出NACK)
- 主设备发停止条件结束通信
关键点:地址字节的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仲裁。
谁先拉低SDA,谁就赢了。输了的主设备要释放总线,等待下一次机会。
我建议你在做多主设备系统时,给每个主设备分配不同的优先级(通过地址或等待时间)。否则两个主设备同时发相同地址和数据的概率虽低,但一旦发生,也没事——因为数据完全一样,仲裁不会失败。但数据不同时,仲裁失败的那一方就丢了总线。
实用技巧:如果只用单主设备,时钟同步和仲裁可以忽略。所有从设备都是“听命”的。但如果你用STM32做主机,同时外接一个SBC(系统基础芯片)也做主机,那就得认真处理多主场景。我记得有次客户要求双主备份,结果因为仲裁没处理好,导致偶尔通信失败。后来加了硬件延时和软件重试机制才搞定。
总结关键点
- 起始条件:SCL高时SDA下降沿
- 停止条件:SCL高时SDA上升沿
- 数据有效性:SCL高时数据必须稳定,只能在SCL低时变化
- 应答位:每字节后必须有一个ACK或NACK
- 终止读操作:主设备必须在最后一个字节之前发NACK
掌握了这些,I2C协议层基本就通了。剩下的就是写代码实现时序,不外乎就是IO口翻转 + 延时控制。但别小看时序容差——我曾经用很粗糙的延时循环也能跑通,但换到高速模式下就翻车了。下回咱们聊怎么用硬件I2C外设和DMA,那才是真正的高效方案。