4、I2C总线协议详解:从物理层到仲裁机制

各位同学,今天我们来聊聊I2C总线。说实话,I2C这玩意儿在FPGA开发中太常见了——SFP模块的DDM信息读取、温度传感器配置、EEPROM读写,哪样都离不开它。我最早接触I2C是在一个光模块项目里,当时被那个应答时序坑了一整天,后来才明白,细节都在协议里。

4.1 物理层:两条线,一个故事

I2C总线物理上就两条线:SCL(时钟线)SDA(数据线)。嗯,就这么简单。但别小看这两条线,它们都是开漏输出,需要外接上拉电阻。

关键点:开漏结构意味着任何设备都可以拉低总线,但不能主动拉高。高电平全靠上拉电阻实现。这就是为什么I2C支持多主设备——谁都能控制总线。

我在项目中遇到过一个问题:上拉电阻选大了,信号上升沿太慢,高速模式下通信失败。后来换成4.7kΩ的电阻,问题就解决了。你想想看,电阻值的选择直接影响通信速率。

模式 最大速率 推荐上拉电阻
标准模式 100 kbit/s 4.7kΩ - 10kΩ
快速模式 400 kbit/s 1kΩ - 4.7kΩ
高速模式 3.4 Mbit/s 0.5kΩ - 1kΩ

个人经验:我习惯在FPGA的I2C接口上预留可调电阻位置,调试时先用10kΩ,再根据波形调整。这样能快速定位问题。

4.2 起始与停止条件:总线的开关

I2C通信的开始和结束,靠的是SCL和SDA的特定时序组合。说白了,就是两个特殊信号。

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

为什么会这样设计?因为数据在SCL低电平时变化,高电平时采样。起始和停止条件利用了SCL高电平时的SDA变化,这样就不会和数据传输混淆。我曾经在写FPGA驱动时,把起始条件的时序搞反了,结果设备死活不响应。后来用示波器一看,原来SDA下降沿早了半个时钟周期。

避坑指南:我曾经在连续传输时忘记发送停止条件,导致总线一直被占用。记住:每次传输结束,一定要发停止条件释放总线。

4.3 数据帧格式:8位数据,1位应答

I2C的数据帧格式很规整。每个字节8位,后面跟一个应答位。数据传输时,MSB(最高位)先发。

// 数据帧格式示例
起始条件 | 7位地址 + R/W | 应答 | 8位数据 | 应答 | ... | 停止条件

// 写操作示例
S | 0xA0 (0) | ACK | 0x00 (寄存器地址) | ACK | 0x55 (数据) | ACK | P

// 读操作示例
S | 0xA0 (0) | ACK | 0x00 (寄存器地址) | ACK | 
Sr | 0xA0 (1) | ACK | 0x55 (读取数据) | NACK | P

注意看,读操作有个重复起始条件(Sr)。这是I2C的一个巧妙设计——不释放总线就能切换读写方向。我个人习惯在FPGA代码里把起始和重复起始分开处理,这样逻辑更清晰。

4.4 应答机制:确认收到

每个字节传输完后,接收方必须发送应答信号。应答位在SCL的第9个时钟周期采样。

  • ACK(应答):SDA被拉低,表示「收到,请继续」
  • NACK(非应答):SDA保持高,表示「收完了,停止」或「出错了」

应答机制是I2C可靠性的基础。我记得有一次调试SFP模块,读取DDM信息时总是返回0xFF。后来发现是从设备在发送NACK,因为主设备发送的寄存器地址不对。嗯,从那以后我每次写驱动都会先验证地址是否正确。

重要规则:主设备在接收完最后一个字节后,必须发送NACK,然后发停止条件。这是告诉从设备:「别再发了,我要结束了」。

4.5 时钟同步与仲裁:多主设备的智慧

I2C支持多个主设备共享总线。这听起来很美好,但实现起来需要两个机制:时钟同步和仲裁。

时钟同步

多个主设备同时驱动SCL时,谁拉低SCL,SCL就是低电平。谁先释放,SCL才能变高。说白了,就是「慢的说了算」。我刚开始理解这个时觉得挺绕,后来画了个时序图就明白了。

时钟同步示例: SCL1 SCL2 SCL SCL1拉低 SCL2拉低 同步后低电平延长

仲裁机制

多个主设备同时发送数据时,谁先拉低SDA,谁就赢得仲裁。输的一方自动退出,等总线空闲后再重试。仲裁在SDA上进行,SCL只负责同步。

仲裁规则:发送低电平的设备会赢得仲裁。因为开漏结构下,低电平会覆盖高电平。所以,发送「0」的设备优先级高于发送「1」的设备。

我在FPGA里实现I2C主设备时,仲裁逻辑是这么处理的:发送完每个位后,检查SDA的实际电平。如果发现SDA被拉低而自己发送的是高电平,说明仲裁失败,立即释放总线。

// 仲裁检测伪代码
if (sda_out == 1 && sda_in == 0) begin
    // 仲裁失败
    arbitration_lost <= 1;
    // 释放总线
    sda_out <= 1'bz;
    scl_out <= 1'bz;
end

实用建议:在FPGA设计中,我建议把仲裁检测放在状态机里单独处理。这样代码结构清晰,调试时也容易定位问题。

小结

I2C协议看似简单,但每个细节都有它的道理。物理层的开漏结构决定了多主设备能力,起始/停止条件保证了传输的完整性,应答机制提供了可靠性,时钟同步和仲裁则让多主设备和谐共处。你想想看,一个1980年代提出的协议,到现在还在广泛使用,说明它的设计确实精妙。

下次我们聊I2C的FPGA驱动实现,我会分享一些实际项目中的代码和调试技巧。嗯,到时候见。


公众号:蓝海资料掘金营,微信deep3321