2. I2C协议精讲:起始条件、停止条件、数据有效性、应答机制、时钟同步与仲裁

好,咱们开始啃I2C协议这块硬骨头。

说实话,I2C协议看起来简单,就两根线——SCL和SDA。但实际调试中,我见过太多工程师在这上面栽跟头。我自己也踩过不少坑,今天把这些经验掰开揉碎了讲给你听。

2.1 起始条件与停止条件

先说说最基础的东西。I2C总线上,什么时候开始传输?什么时候结束?

起始条件(Start Condition)很简单:SCL为高电平时,SDA从高电平跳变到低电平。停止条件(Stop Condition)正好相反:SCL为高电平时,SDA从低电平跳变到高电平

嗯,这里要注意——这两个条件都是主设备产生的。从设备只能被动响应。

关键点:

  • 起始条件:SCL高 → SDA下降沿
  • 停止条件:SCL高 → SDA上升沿
  • 重复起始条件(Repeated Start):不产生停止条件,直接再发一个起始条件

我在项目中遇到过一个问题:某次调试传感器,发现数据总是错位。用示波器一抓,发现从设备在停止条件后还拉着SDA线不放。说白了,就是从设备没正确释放总线。后来加了延时才解决。

2.2 数据有效性

I2C的数据传输有个铁律:数据在SCL高电平时必须稳定。只有在SCL低电平时,SDA才能变化。

你想想看,如果数据在SCL高电平时跳变,那就会被误判成起始或停止条件。这就是为什么I2C协议对时序要求这么严格。

我的习惯:

写I2C驱动时,我总会在SCL低电平中间位置改变SDA。这样留出足够的建立时间和保持时间,避免时序冲突。曾经有个项目,就是因为建立时间不够,导致从设备采样到错误数据,排查了整整两天。

数据是以字节为单位传输的,每个字节8位,高位在前(MSB first)。

2.3 应答机制

这是I2C协议的精髓所在。每发送完一个字节,接收方必须回复一个应答位(ACK)或非应答位(NACK)。

具体怎么操作?发送方在第9个SCL时钟周期释放SDA线,接收方如果拉低SDA就是ACK,保持高电平就是NACK。

应答类型 SDA状态 含义
ACK 低电平 接收成功,继续发送
NACK 高电平 接收失败或传输结束

我曾经调试一个EEPROM驱动,发现写入数据后读出来全是0xFF。用示波器一看,从设备在写入最后一个字节时回复了NACK。查手册才发现,EEPROM在写入内部时需要时间,这段时间不会响应。解决办法是在每次写入后加一个延时,或者轮询应答位直到收到ACK。

避坑指南:

我曾经在批量读取数据时,忘记处理从设备的NACK。结果主设备一直发时钟,从设备一直不响应,总线就卡死了。记住:收到NACK后,主设备必须产生停止条件或重复起始条件,不能继续发时钟。

2.4 时钟同步与仲裁

这两个概念,很多初学者容易搞混。我分开讲。

时钟同步发生在多个主设备同时操作时。每个主设备都有自己的时钟,但总线上的SCL线是线与的——只要有一个主设备拉低SCL,SCL就是低电平。只有所有主设备都释放SCL,SCL才会变高。

说白了,就是慢的从设备或主设备可以拉低SCL来延长时钟周期。这叫时钟拉伸(Clock Stretching)。

仲裁解决的是多个主设备同时发送数据的问题。当两个主设备同时发送起始条件,它们会继续发送数据。如果某个主设备发送高电平,但检测到SDA是低电平(因为另一个主设备在发低电平),它就输了,必须退出。

仲裁规则:

  • 谁先发低电平,谁赢
  • 输的主设备必须释放SDA和SCL
  • 输的主设备不能产生停止条件
  • 仲裁可以在整个传输过程中进行

我记得有一次调试一个多主设备系统,两个MCU都在访问同一个传感器。结果数据经常乱掉。用示波器抓波形,发现两个主设备同时发起了传输,但仲裁机制没处理好。后来检查代码,发现其中一个主设备在仲裁失败后没有正确释放总线。嗯,这个问题花了我一个下午才定位到。

2.5 实际调试中的要点

说了这么多理论,来点实际的。用示波器调试I2C时,我一般这么操作:

  1. 先看起始条件:SCL高时SDA有没有干净的下跳沿?如果有毛刺,可能是上拉电阻问题。
  2. 再看数据字节:每个字节的8位数据在SCL高电平时是否稳定?不稳定的话,检查建立时间。
  3. 检查应答位:第9个时钟后,SDA有没有被拉低?没有的话,从设备可能没收到数据或者正忙。
  4. 看停止条件:SCL高时SDA有没有干净的上跳沿?

我的小技巧:

示波器触发条件设成SDA下降沿触发,然后调整时基到每格10μs左右。这样能稳定抓到起始条件。如果信号不稳定,把触发模式改成普通模式,别用自动模式。

最后说一句:I2C协议虽然简单,但实际调试中坑不少。多动手,多用示波器看波形,慢慢就有感觉了。