1. I2C协议基础:物理层、协议层、时序图解析、起始条件与停止条件

各位同学,咱们今天正式开讲I2C。说实话,I2C这协议看起来简单,但坑不少。我最早接触I2C是在一个温湿度传感器项目上,当时觉得不就是两根线嘛,结果调试了整整两天才把数据读出来——后来发现是起始条件没给对。嗯,咱们今天就把这些细节掰开揉碎了讲清楚。

1.1 物理层:就两根线,但别小看它们

I2C总线物理上只需要两根线:SCL(时钟线)和SDA(数据线)。这两根线都是开漏输出,外部接上拉电阻。为什么用开漏?说白了,就是为了实现多设备共用总线——任何设备都可以把线拉低,但没人能主动拉高。

核心要点:开漏结构 + 上拉电阻 = 线与逻辑。谁拉低谁说了算,这就是I2C仲裁的基础。

我在项目中遇到过一个问题:上拉电阻选大了,总线上升沿太慢,高速模式下通信失败;选小了,功耗又高。一般4.7kΩ是万金油,100kHz标准模式没问题。400kHz快速模式建议用2.2kΩ左右。

参数 标准模式 (100kHz) 快速模式 (400kHz) 高速模式 (3.4MHz)
上拉电阻推荐值 4.7kΩ ~ 10kΩ 1.5kΩ ~ 4.7kΩ 0.5kΩ ~ 1.5kΩ
总线电容上限 400pF 400pF 100pF
最大从设备数量 理论127个 理论127个 受限于电容

我的习惯:做原型时先用4.7kΩ,调试稳定后再根据实际总线长度和设备数量微调。别一上来就追求高速,先把数据读对再说。

1.2 协议层:地址、读写、应答,三板斧

I2C的协议层其实就三个核心概念:设备地址、读写方向、应答机制。你想想看,总线上挂了一堆设备,主设备怎么找到目标?靠的就是7位或10位地址。

一个典型的I2C通信帧长这样:

起始条件 | 7位地址 | R/W位 | 应答位 | 数据字节 | 应答位 | ... | 停止条件

这里有个细节:地址是7位,但发送时是8位——高7位是地址,最低位是读写标志。0表示写,1表示读。所以0x50这个地址,发送时其实是0xA0(写)或0xA1(读)。

我曾经踩过的坑:某款加速度传感器的数据手册上写的地址是0x1D,但那是7位地址。我直接拿0x1D去发,结果设备死活不应答。后来才发现,发送时要左移一位,变成0x3A。记住:数据手册上的地址通常是7位,发送时要左移1位加上R/W位。

1.3 时序图解析:别怕,其实就几个关键点

很多同学看到时序图就头疼。我教你一个方法:只看电平变化和时钟边沿。I2C的时序说白了就是SCL和SDA在不同时间点的组合状态。

咱们用一张SVG图把核心时序画出来:

I2C 完整通信时序图 SCL SDA 起始条件 7位从机地址 R/W ACK 8位数据 ACK 停止条件 时序关键点说明: 1. SCL高电平时,SDA从高到低跳变 = 起始条件 2. SCL高电平时,SDA从低到高跳变 = 停止条件 3. 数据在SCL低电平时变化,高电平时采样(稳定读取) 4. 每个字节后跟一个ACK/NACK位,由接收方控制

1.4 起始条件与停止条件:通信的开关

这两个条件是I2C通信的"开门"和"关门"动作。我刚开始学的时候总觉得这俩条件很玄乎,其实记住一句话就行:SCL高电平时,看SDA的跳变方向。

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

为什么要在SCL高电平时判断?因为数据采样也是在SCL高电平时进行的。起始和停止条件本质上是一种"特殊的数据位",它们违反了正常数据在SCL低电平时变化的规则,所以能被总线上的所有设备识别出来。

实战要点:模拟I2C时,起始条件和停止条件的时序必须严格满足。我曾经在一个项目中,起始条件后SDA保持低电平的时间不够,导致从设备误判了地址位。后来加了2μs的延时才稳定。

1.5 数据有效性:SCL高电平时别乱动

I2C的数据有效性规则很简单:SCL高电平时,SDA上的数据必须稳定;SCL低电平时,SDA可以变化。这意味着,你要在SCL低电平时准备好下一个数据位,然后在SCL高电平时让从设备去读。

用代码模拟一下这个过程:

// 模拟I2C发送一个字节
void i2c_send_byte(uint8_t data) {
    for (int i = 0; i < 8; i++) {
        // SCL低电平,设置SDA
        SCL_LOW();
        if (data & 0x80)
            SDA_HIGH();
        else
            SDA_LOW();
        delay_us(1);
        
        // SCL高电平,从设备采样
        SCL_HIGH();
        delay_us(1);
        
        data <<= 1;  // 准备下一位
    }
    // 释放SDA,等待应答
    SCL_LOW();
    SDA_INPUT();
    SCL_HIGH();
    // 读取ACK位...
}

我的经验:模拟I2C时,延时别用死等,最好用硬件定时器或者nop指令计数。不同MCU主频不同,同样的延时函数效果可能差很多。我习惯先写一个基准延时函数,然后用逻辑分析仪实测校准。

1.6 本章小结

I2C协议基础就这些内容。说白了,物理层就是两根开漏线加上拉电阻;协议层就是地址+读写+应答三板斧;时序上抓住起始条件、停止条件和数据有效性三个关键点。你把这些搞清楚了,后面模拟I2C实现传感器驱动就是水到渠成的事。