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、数据和停止。建议大家对照着看,比光看文字直观得多。
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 写操作流程
- 主机发送起始条件
- 主机发送从设备地址 + 写位(0)
- 从设备回复ACK
- 主机发送寄存器地址(1字节或多字节)
- 从设备回复ACK
- 主机发送数据字节
- 从设备回复ACK
- 重复步骤6~7,直到数据发完
- 主机发送停止条件
1.5.2 读操作流程
- 主机发送起始条件
- 主机发送从设备地址 + 写位(0)
- 从设备回复ACK
- 主机发送要读取的寄存器地址
- 从设备回复ACK
- 主机发送重复起始条件(Sr)
- 主机发送从设备地址 + 读位(1)
- 从设备回复ACK
- 从设备发送数据字节,主机回复ACK
- 重复步骤9,直到数据收完
- 主机回复NACK,然后发送停止条件
经验之谈:读操作里的重复起始条件(Sr)是个关键点。很多新手会先发停止再发起始,这样会导致总线释放,可能被其他主机抢走。用Sr可以保持总线控制权。我在写驱动时,一律用Sr,从不发停止再发起始。
1.6 多主机仲裁:谁抢到算谁的
多主机仲裁是I2C最巧妙的设计之一。说白了,就是多个主机同时抢总线时,谁先拉低SDA谁赢。
仲裁过程是这样的:
- 多个主机同时发送起始条件,都拉低SDA
- 然后开始发送地址位,每个主机都监测SDA电平
- 如果某个主机想发高电平,但SDA被另一个主机拉低了,它就检测到冲突
- 检测到冲突的主机立即退出,释放总线
- 赢得仲裁的主机继续通信,从设备完全感知不到发生过仲裁
重要提醒:仲裁只在SDA上发生,SCL由所有主机共同驱动(线与逻辑)。仲裁失败的主机不会丢失数据,因为它会在检测到冲突时停止发送。我曾经在一个双主机的项目里,因为两个主机发送的地址完全相同,导致仲裁一直不成功。后来改了其中一个主机的地址才解决。
1.7 常见问题与调试方法
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 总线一直忙 | 从设备未释放SDA | 发送9个SCL脉冲强制复位 |
| 第一个字节NACK | 地址错误或设备未上电 | 检查地址、电源、上拉电阻 |
| 数据偶尔出错 | 总线电容过大或干扰 | 减小上拉电阻、缩短走线 |
| 时钟拉伸超时 | 从设备处理慢 | 增加超时时间或降低频率 |