4、I2C协议详解(上):I2C物理层特性、起始信号与停止信号、数据有效性

各位同学,今天我们来啃I2C协议的上半部分。说实话,I2C这玩意儿看起来简单,两根线嘛,但坑是真不少。我早年做传感器采集项目时,就因为没搞懂起始信号和停止信号的时序,导致OLED屏幕死活不亮,折腾了两天才发现是信号毛刺问题。所以这一章,咱们把物理层和基本信号彻底讲透。

4.1 I2C物理层特性

I2C总线就两条线:SCL(时钟线)和SDA(数据线)。但你别小看这两根线,它们的电气特性决定了整个通信的可靠性。

特性 说明 我的经验
总线拓扑 多主从结构,所有设备挂在同一总线上 挂载超过8个设备时,总线电容会明显增加
信号电平 开漏输出,需要外部上拉电阻 上拉电阻选4.7kΩ还是10kΩ,直接影响上升沿时间
最大速率 标准模式100kHz,快速模式400kHz STM32F103的I2C外设支持400kHz,但实际建议跑200kHz左右
设备地址 7位或10位地址,7位最常用 OLED屏通常是0x3C或0x3D,记得看数据手册

这里有个关键点:开漏输出是什么意思?说白了,就是芯片只能把引脚拉低到GND,不能主动拉高。拉高靠外部上拉电阻。所以总线空闲时,SCL和SDA都是高电平。

核心原则:I2C总线在任何时刻,只能由一个主设备控制SCL时钟,从设备只能拉低SDA(用于应答)或拉低SCL(用于时钟拉伸)。

4.2 起始信号与停止信号

这两个信号是I2C通信的“开关”。没有它们,总线上的设备根本不知道什么时候开始、什么时候结束。

4.2.1 起始信号(START Condition)

起始信号的时序很简单:SCL为高电平时,SDA从高电平跳变到低电平。

我习惯用示波器抓这个信号。有一次我发现起始信号之后SDA没拉低到位,结果从设备根本没响应。后来查出来是上拉电阻太大,上升沿太缓,导致主设备误判了电平。

// 起始信号的软件模拟(GPIO模拟I2C)
void I2C_Start(void) {
    SDA_HIGH();          // 确保SDA为高
    SCL_HIGH();          // SCL拉高
    delay_us(5);         // 保持一段时间
    SDA_LOW();           // SDA拉低,产生起始信号
    delay_us(5);         // 保持
    SCL_LOW();           // SCL拉低,准备传输数据
}

注意:起始信号必须在总线空闲时产生。如果总线上有其他设备正在通信,强行发送起始信号会导致总线冲突。我曾经在中断里直接调用I2C_Start(),结果和主循环的通信撞车了,屏幕花屏。后来加了互斥锁才解决。

4.2.2 停止信号(STOP Condition)

停止信号和起始信号正好相反:SCL为高电平时,SDA从低电平跳变到高电平。

// 停止信号的软件模拟
void I2C_Stop(void) {
    SDA_LOW();           // 先确保SDA为低
    SCL_HIGH();          // SCL拉高
    delay_us(5);         // 保持
    SDA_HIGH();          // SDA拉高,产生停止信号
    delay_us(5);         // 保持
}

你想想看,停止信号之后总线恢复空闲状态,其他设备才能发起新的通信。如果停止信号没发完整,从设备会一直等待,整个总线就卡死了。

小技巧:在发送停止信号之前,最好先确认从设备已经释放了SDA线(即SDA被拉高)。如果从设备还在拉低SDA做应答,你强行发停止信号,SDA根本拉不上去。我遇到过这种情况,后来在停止信号前加了一个超时检测,问题就解决了。

4.3 数据有效性

I2C协议规定:数据在SCL的上升沿被采样,在SCL的低电平期间变化。换句话说,SDA上的数据必须在SCL高电平期间保持稳定。

这个规则看似简单,但实际写代码时很容易忽略。我刚开始写OLED驱动时,在SCL高电平期间改变了SDA,结果数据全错。后来查了协议才明白,数据变化只能在SCL低电平时进行。

数据有效性规则:

  • SCL高电平期间:SDA必须保持稳定(数据被采样)
  • SCL低电平期间:SDA可以变化(准备下一位数据)
  • 起始信号和停止信号是例外,它们在SCL高电平时改变SDA

我们来看一个字节传输的时序图,我用SVG画出来,方便你理解:

I2C字节传输时序图(数据有效性示意) SCL(时钟线) SDA(数据线) 数据变化区(SCL低) 数据变化区(SCL低) 数据采样点(SCL高) D7 D6 D5 D4 D3 D2 D1 D0

从图上你能看到:SCL高电平时,SDA保持稳定,主设备在这个时刻采样数据。SCL低电平时,SDA可以变化,主设备准备好下一位数据。一个字节传输完成后,从设备会在第9个SCL周期拉低SDA作为应答。

4.4 实际应用中的注意事项

讲完理论,咱们聊聊实际开发中容易踩的坑:

  • 上拉电阻选择:100kHz时用10kΩ没问题,400kHz时建议用4.7kΩ甚至2.2kΩ。我试过用10kΩ跑400kHz,上升沿太缓,数据采样出错。
  • 总线电容:长距离走线或挂载多个设备会增加总线电容,导致信号变形。STM32F103的I2C外设有滤波功能,但别完全依赖它。
  • 起始信号重复:I2C支持重复起始信号(Restart),用于改变数据传输方向。比如读操作时,先发写地址,再发Restart,然后发读地址。这个我们下一节会详细讲。

避坑指南:我曾经在OLED初始化时,连续发送多个起始信号而没有停止信号,结果从设备状态机混乱,屏幕显示异常。后来严格按照“起始-数据-停止”的流程来写,问题就消失了。记住:每个通信事务都必须以停止信号结束,除非你明确使用重复起始信号。