1. I2C协议深度解析:物理层、协议层、时序图、寻址方式、读写操作、多主机仲裁

各位同学,咱们今天正式开讲I2C。说实话,做了这么多年嵌入式,I2C是我用得最多的总线之一。它简单、灵活,两根线就能搞定一堆外设。但越是简单的东西,坑往往越深。我记得刚入行那会儿,就因为I2C时序没调好,折腾了整整三天。嗯,今天咱们就把I2C从头到尾捋一遍,把那些容易踩的坑都给它填上。

1.1 物理层:就两根线,但别小看它

I2C总线物理上只需要两根线:SCL(时钟线)和SDA(数据线)。这两根线都是开漏输出,必须接上拉电阻。

核心要点:开漏结构意味着任何设备都可以把总线拉低,但拉高要靠上拉电阻。这是I2C多主机仲裁的基础,也是很多新手容易忽略的地方。

上拉电阻怎么选?我个人的经验是:

  • 标准模式(100kHz):4.7kΩ 比较稳妥
  • 快速模式(400kHz):2.2kΩ 左右
  • 高速模式(3.4MHz):1kΩ 甚至更小

为什么会这样?说白了,总线电容是固定的,频率越高,你需要更小的电阻来保证信号上升沿够快。我曾经在一个项目里用了10kΩ上拉电阻跑400kHz,结果波形圆得像馒头,从设备根本不认。

参数 标准模式 快速模式 高速模式
SCL频率 100 kHz 400 kHz 3.4 MHz
上拉电阻(典型) 4.7 kΩ 2.2 kΩ 1.0 kΩ
最大总线电容 400 pF 400 pF 100 pF
最大从设备数量 理论127个 理论127个 受限于电容

注意:总线电容不要超过400pF,否则信号质量会急剧下降。我曾经在一个大板上挂了8个I2C设备,线长加起来半米多,结果通信时好时坏。后来用示波器一看,上升沿都快赶上时钟周期了。

1.2 协议层:起始、停止、数据、应答

I2C的协议层其实就四个基本动作:起始条件、停止条件、数据传送、应答信号。你想想看,所有复杂的读写操作,都是这四个动作的组合。

1.2.1 起始条件和停止条件

  • 起始条件(S):SCL高电平时,SDA从高变低
  • 停止条件(P):SCL高电平时,SDA从低变高

这两个条件由主机独占,从设备不能发起。我见过有人把起始和停止搞反了,结果总线一直卡在忙状态。嗯,这里要注意:每次通信必须以起始开始,以停止结束。

1.2.2 数据传送规则

数据在SCL低电平时变化,高电平时采样。每个字节8位,高位在前。每传完一个字节,接收方要回一个ACK(低电平)或NACK(高电平)。

小技巧:调试I2C时,我最常用的方法就是用逻辑分析仪抓起始条件后的第一个字节。如果地址对了,ACK也对了,那后面基本没问题。如果第一个字节就NACK,先检查地址对不对,再查上拉电阻。

1.3 时序图:用眼睛看,别光靠脑子想

下面这张图是我手绘风格的I2C完整读写时序,包含了起始、地址、读写位、ACK、数据和停止。建议大家对照着看,比光看文字直观得多。

I2C完整读写时序图 SCL SDA S 从设备地址 (7位) R/W ACK 数据字节 (8位) ACK 数据字节 (8位) NACK P 时序说明: 1. SCL高电平时SDA下降沿 → 起始条件(S) 2. 主机发送7位从地址 + 1位读写位(0=写,1=读) 3. 从设备在第9个SCL周期拉低SDA → ACK 4. 数据字节传送,每字节后跟ACK;最后NACK + 停止条件(P)

1.4 寻址方式:7位地址够用吗?

I2C标准寻址是7位,理论上支持112个从设备(地址0x00保留给通用呼叫,0x01~0x7F可用)。但实际项目中,同一总线上挂4~5个设备就差不多了,因为要考虑总线电容和地址冲突。

10位寻址是扩展方案,用得不多。我个人习惯:能用7位就别用10位,兼容性好,代码也简单。

避坑指南:很多I2C芯片的地址不是完全固定的,比如AT24C02的地址是1010XXX,其中XXX由A0/A1/A2引脚电平决定。我曾经因为没注意A0引脚悬空,导致两个EEPROM地址冲突,写数据时互相覆盖。嗯,教训深刻。

1.5 读写操作:主机说了算

I2C的读写操作由主机控制,从设备只能被动响应。咱们来看两种典型操作:

1.5.1 写操作流程

  1. 主机发送起始条件
  2. 主机发送从设备地址 + 写位(0)
  3. 从设备回复ACK
  4. 主机发送寄存器地址(1字节或多字节)
  5. 从设备回复ACK
  6. 主机发送数据字节
  7. 从设备回复ACK
  8. 重复步骤6~7,直到数据发完
  9. 主机发送停止条件

1.5.2 读操作流程

  1. 主机发送起始条件
  2. 主机发送从设备地址 + 写位(0)
  3. 从设备回复ACK
  4. 主机发送要读取的寄存器地址
  5. 从设备回复ACK
  6. 主机发送重复起始条件(Sr)
  7. 主机发送从设备地址 + 读位(1)
  8. 从设备回复ACK
  9. 从设备发送数据字节,主机回复ACK
  10. 重复步骤9,直到数据收完
  11. 主机回复NACK,然后发送停止条件

经验之谈:读操作里的重复起始条件(Sr)是个关键点。很多新手会先发停止再发起始,这样会导致总线释放,可能被其他主机抢走。用Sr可以保持总线控制权。我在写驱动时,一律用Sr,从不发停止再发起始。

1.6 多主机仲裁:谁抢到算谁的

多主机仲裁是I2C最巧妙的设计之一。说白了,就是多个主机同时抢总线时,谁先拉低SDA谁赢。

仲裁过程是这样的:

  • 多个主机同时发送起始条件,都拉低SDA
  • 然后开始发送地址位,每个主机都监测SDA电平
  • 如果某个主机想发高电平,但SDA被另一个主机拉低了,它就检测到冲突
  • 检测到冲突的主机立即退出,释放总线
  • 赢得仲裁的主机继续通信,从设备完全感知不到发生过仲裁

重要提醒:仲裁只在SDA上发生,SCL由所有主机共同驱动(线与逻辑)。仲裁失败的主机不会丢失数据,因为它会在检测到冲突时停止发送。我曾经在一个双主机的项目里,因为两个主机发送的地址完全相同,导致仲裁一直不成功。后来改了其中一个主机的地址才解决。

1.7 常见问题与调试方法

现象 可能原因 解决方法
总线一直忙 从设备未释放SDA 发送9个SCL脉冲强制复位
第一个字节NACK 地址错误或设备未上电 检查地址、电源、上拉电阻
数据偶尔出错 总线电容过大或干扰 减小上拉电阻、缩短走线
时钟拉伸超时 从设备处理慢 增加超时时间或降低频率