4. I2C协议基础:从物理层到通信细节
各位同学,今天我们来聊聊I2C总线。说实话,I2C是我在嵌入式开发中用得最多的总线之一。它简单、灵活,两根线就能搞定很多外设。但越是简单的东西,越容易在细节上翻车。我刚开始做驱动时,就因为在I2C时序上差了那么几个微秒,折腾了整整两天。
好,我们正式开始。这一章我会从物理层讲起,再到协议细节,最后聊聊地址和仲裁。你跟着我的思路走,保证能把这些基础吃透。
4.1 I2C总线物理层:两根线的艺术
I2C总线只用两根线:SDA(数据线)和SCL(时钟线)。嗯,就这么简单。但这两根线背后有不少讲究。
开漏输出与上拉电阻
I2C总线的输出级是开漏结构。什么叫开漏?说白了,就是芯片只能把线拉低,不能主动拉高。拉高的工作交给上拉电阻来完成。
上拉电阻的取值很关键。我个人习惯:
- 标准模式(100kHz):4.7kΩ 左右
- 快速模式(400kHz):2.2kΩ 左右
- 高速模式(3.4MHz):1kΩ 左右
总线电容
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位地址,省事。
地址冲突
这是实际开发中最常见的问题。两个设备用了同一个地址,总线就乱了。
解决办法:
- 检查设备的数据手册,看是否有地址配置引脚
- 使用I2C多路复用器(如PCA9548)
- 换用不同地址的设备
我曾经在一个项目中,用了两个同样型号的传感器,地址都是0x48。后来发现其中一个传感器的ADDR引脚可以接VCC改变地址。嗯,这就是不看数据手册的代价。
4.4 I2C仲裁:多主机如何不打架?
I2C支持多主机通信。但多个主机同时发送数据时,怎么办?答案是:仲裁。
仲裁原理
I2C的仲裁是基于线与逻辑的。因为总线是开漏结构,谁拉低谁就赢。
具体过程:
- 多个主机同时发送数据
- 每个主机都会监测SDA线的电平
- 如果某个主机发送了高电平,但SDA线是低电平,说明有其他主机在拉低总线
- 这个主机就输了,退出竞争
仲裁的规则
- 仲裁在SCL高电平时进行
- 仲裁不丢失数据
- 输掉仲裁的主机必须释放总线
- 输掉仲裁的主机可以在总线空闲后重试
重要:仲裁不仅发生在数据位,也发生在地址位和应答位。也就是说,两个主机可以同时访问同一个从机,但最终只有一个主机能成功。
实际开发中的仲裁问题
说实话,在QNX驱动开发中,多主机仲裁的场景并不多见。大多数情况下,一个I2C总线上只有一个主机(我们的处理器),其他都是从机。
但如果你真的需要多主机,要注意:
- 所有主机必须支持仲裁
- 总线空闲检测时间要一致
- 建议使用硬件I2C控制器,不要用GPIO模拟
4.5 避坑指南:我踩过的那些坑
最后,分享几个我在I2C开发中遇到的典型问题:
- 时钟拉伸(Clock Stretching):有些从机在SCL低电平后,会拉低SCL不放,表示自己还没准备好。主机必须等待。我遇到过一款传感器,时钟拉伸时间长达1ms,差点以为是死锁。
- 应答位超时:从机不发送ACK,可能是地址错误、设备忙或者设备不存在。我建议设置超时时间,一般5ms就够了。
- 总线锁死:如果从机在传输过程中复位,可能会把SDA拉低不放。解决办法是发送9个时钟脉冲,让从机释放总线。
我的调试习惯:先用逻辑分析仪抓波形,确认时序没问题。然后再看代码。很多I2C问题,看波形一眼就能看出来,比盯着代码瞎猜强多了。
好了,这一章的内容就到这里。I2C协议看起来简单,但实际开发中坑不少。你把物理层、协议格式、地址和仲裁这几个基础打牢了,后面写驱动就会顺手很多。下一章我们开始实战,在QNX上写一个I2C驱动,到时候你会用到今天学的这些知识。