4. I2C协议基础:从物理层到通信细节

各位同学,今天我们来聊聊I2C总线。说实话,I2C是我在嵌入式开发中用得最多的总线之一。它简单、灵活,两根线就能搞定很多外设。但越是简单的东西,越容易在细节上翻车。我刚开始做驱动时,就因为在I2C时序上差了那么几个微秒,折腾了整整两天。

好,我们正式开始。这一章我会从物理层讲起,再到协议细节,最后聊聊地址和仲裁。你跟着我的思路走,保证能把这些基础吃透。

4.1 I2C总线物理层:两根线的艺术

I2C总线只用两根线:SDA(数据线)和SCL(时钟线)。嗯,就这么简单。但这两根线背后有不少讲究。

开漏输出与上拉电阻

I2C总线的输出级是开漏结构。什么叫开漏?说白了,就是芯片只能把线拉低,不能主动拉高。拉高的工作交给上拉电阻来完成。

上拉电阻的取值很关键。我个人习惯:

  • 标准模式(100kHz):4.7kΩ 左右
  • 快速模式(400kHz):2.2kΩ 左右
  • 高速模式(3.4MHz):1kΩ 左右
注意:上拉电阻太小,功耗大;太大,信号上升沿变缓,容易导致通信失败。我曾经在一个项目中用了10kΩ上拉跑400kHz,结果数据老是出错。换成2.2kΩ后,问题立刻解决。

总线电容

I2C总线有电容限制,一般不超过400pF。总线越长、挂的设备越多,电容越大。你想想看,电容大了,信号上升沿就变慢,时序就乱了。

我建议:

  • 同一总线上挂的设备不要超过8个
  • 总线长度控制在30cm以内
  • 如果必须长距离传输,考虑使用I2C总线扩展器

4.2 I2C通信协议:起始、停止与数据帧

I2C的通信协议其实很优雅。它用两个特殊状态来标记通信的开始和结束,中间传输数据。

起始条件(START)

当SCL为高电平时,SDA从高电平跳变到低电平。这就是起始条件。

// 起始条件的时序要求(标准模式)
// SCL高电平保持时间 > 4.7μs
// SDA建立时间 > 4.7μs
// SDA保持时间 > 4.0μs

停止条件(STOP)

当SCL为高电平时,SDA从低电平跳变到高电平。这就是停止条件。

关键点:起始和停止条件都是由主机产生的。从机只能被动响应,不能主动发起。

数据帧格式

I2C的数据传输以字节为单位,每个字节8位,后面跟一个应答位(ACK/NACK)。

数据传输的规则:

  • 数据在SCL低电平时变化
  • 数据在SCL高电平时采样
  • 高位在前(MSB first)

一个完整的数据帧长这样:

| 起始 | 7位地址 | R/W | ACK | 8位数据 | ACK | ... | 8位数据 | ACK/NACK | 停止 |

我刚开始学的时候,总觉得这个格式有点绕。后来我画了个时序图,一下子就明白了。你可以在纸上画一下SCL和SDA的波形,跟着数据走一遍,效果很好。

4.3 I2C地址:7位还是10位?

I2C地址分为7位地址和10位地址两种格式。7位地址最多支持112个设备(0x00和0x01保留),10位地址最多支持1024个设备。

7位地址格式

主机发送的第一个字节:高7位是地址,最低位是R/W位。

7 6 5 4 3 2 1 0
含义 设备地址(A6-A0) R/W

10位地址格式

10位地址需要两个字节来传输:

  • 第一个字节:11110 + 地址高2位 + R/W
  • 第二个字节:地址低8位

经验之谈:大多数常用外设(如温度传感器、EEPROM、RTC)都使用7位地址。10位地址主要用于需要大量设备的工业场景。我个人建议,除非必要,优先使用7位地址,省事。

地址冲突

这是实际开发中最常见的问题。两个设备用了同一个地址,总线就乱了。

解决办法:

  1. 检查设备的数据手册,看是否有地址配置引脚
  2. 使用I2C多路复用器(如PCA9548)
  3. 换用不同地址的设备

我曾经在一个项目中,用了两个同样型号的传感器,地址都是0x48。后来发现其中一个传感器的ADDR引脚可以接VCC改变地址。嗯,这就是不看数据手册的代价。

4.4 I2C仲裁:多主机如何不打架?

I2C支持多主机通信。但多个主机同时发送数据时,怎么办?答案是:仲裁。

仲裁原理

I2C的仲裁是基于线与逻辑的。因为总线是开漏结构,谁拉低谁就赢。

具体过程:

  • 多个主机同时发送数据
  • 每个主机都会监测SDA线的电平
  • 如果某个主机发送了高电平,但SDA线是低电平,说明有其他主机在拉低总线
  • 这个主机就输了,退出竞争

仲裁的规则

  1. 仲裁在SCL高电平时进行
  2. 仲裁不丢失数据
  3. 输掉仲裁的主机必须释放总线
  4. 输掉仲裁的主机可以在总线空闲后重试

重要:仲裁不仅发生在数据位,也发生在地址位和应答位。也就是说,两个主机可以同时访问同一个从机,但最终只有一个主机能成功。

实际开发中的仲裁问题

说实话,在QNX驱动开发中,多主机仲裁的场景并不多见。大多数情况下,一个I2C总线上只有一个主机(我们的处理器),其他都是从机。

但如果你真的需要多主机,要注意:

  • 所有主机必须支持仲裁
  • 总线空闲检测时间要一致
  • 建议使用硬件I2C控制器,不要用GPIO模拟

4.5 避坑指南:我踩过的那些坑

最后,分享几个我在I2C开发中遇到的典型问题:

  • 时钟拉伸(Clock Stretching):有些从机在SCL低电平后,会拉低SCL不放,表示自己还没准备好。主机必须等待。我遇到过一款传感器,时钟拉伸时间长达1ms,差点以为是死锁。
  • 应答位超时:从机不发送ACK,可能是地址错误、设备忙或者设备不存在。我建议设置超时时间,一般5ms就够了。
  • 总线锁死:如果从机在传输过程中复位,可能会把SDA拉低不放。解决办法是发送9个时钟脉冲,让从机释放总线。

我的调试习惯:先用逻辑分析仪抓波形,确认时序没问题。然后再看代码。很多I2C问题,看波形一眼就能看出来,比盯着代码瞎猜强多了。

好了,这一章的内容就到这里。I2C协议看起来简单,但实际开发中坑不少。你把物理层、协议格式、地址和仲裁这几个基础打牢了,后面写驱动就会顺手很多。下一章我们开始实战,在QNX上写一个I2C驱动,到时候你会用到今天学的这些知识。