2. I2C协议时序:起始条件、停止条件、数据有效性、字节格式与ACK/NACK机制、时钟同步与仲裁

各位同学,咱们今天聊点实在的。I2C协议看着简单,就两根线——SCL和SDA。但真要在项目里把它调稳了,你得把它的时序吃透。我刚开始用I2C的时候,总觉得不就是发个起始信号、传几个字节、再发个停止信号嘛。结果呢?从设备就是不响应,折腾了两天才发现是起始条件没满足时序要求。嗯,咱们今天就把它掰开揉碎了讲清楚。

2.1 起始条件与停止条件

I2C总线上所有的通信,都从起始条件(S)开始,以停止条件(P)结束。这两个信号由主设备产生,从设备只能被动响应。

信号 SCL状态 SDA变化 说明
起始条件(S) 高电平 从高→低跳变 SCL为高时,SDA由高变低
停止条件(P) 高电平 从低→高跳变 SCL为高时,SDA由低变高

说白了,起始和停止就是SCL在高电平期间,SDA来一个反向跳变。我习惯在代码里这样写:

// 起始条件:SCL高时,SDA从高拉低
void i2c_start(void) {
    SDA_HIGH();
    SCL_HIGH();
    delay_us(5);      // 保持建立时间
    SDA_LOW();
    delay_us(5);      // 保持保持时间
    SCL_LOW();        // 准备传输数据
}

// 停止条件:SCL高时,SDA从低拉高
void i2c_stop(void) {
    SDA_LOW();
    SCL_HIGH();
    delay_us(5);
    SDA_HIGH();
    delay_us(5);
}
注意:起始条件和停止条件之间的时间间隔不能太短。标准模式下,停止条件到下一个起始条件至少要有4.7μs的总线空闲时间。我曾经在100kHz下把间隔压到2μs,结果从设备直接罢工了。

2.2 数据有效性

I2C的数据采样有个铁律:SCL高电平时,SDA必须稳定;SCL低电平时,SDA才能变化。你想想看,如果SCL高的时候SDA还在跳变,那到底是数据还是起始/停止条件?协议就乱套了。

具体来说:

  • SCL为低电平时,发送器可以改变SDA的电平
  • SCL为高电平时,接收器采样SDA的电平
  • 每个SCL时钟脉冲传输1位数据

我在调试一个温湿度传感器时遇到过一个问题:传感器偶尔返回错误数据。用逻辑分析仪一看,发现是MCU的SCL高电平时间太短,传感器还没来得及采样SDA就变了。后来把SCL高电平的占空比调到50%以上,问题就解决了。

2.3 字节格式与传输顺序

I2C每次传输一个字节(8位),高位在前(MSB first)。每个字节后面紧跟着一个应答位(ACK/NACK)。

一个完整的字节传输流程是这样的:

  1. 发送器在SCL低电平时逐位设置SDA(从MSB到LSB)
  2. SCL每来一个高电平脉冲,接收器采样一位
  3. 第8位(LSB)传输完成后,发送器释放SDA
  4. 第9个SCL脉冲,接收器拉低SDA表示ACK,或保持高表示NACK
// 发送一个字节,并读取应答位
uint8_t i2c_send_byte(uint8_t data) {
    uint8_t ack;
    for (int i = 7; i >= 0; i--) {
        if (data & (1 << i))
            SDA_HIGH();
        else
            SDA_LOW();
        SCL_HIGH();
        delay_us(5);
        SCL_LOW();
        delay_us(5);
    }
    // 第9个时钟:释放SDA,读取ACK
    SDA_HIGH();          // 释放总线
    SCL_HIGH();
    delay_us(3);
    ack = SDA_READ();    // 0=ACK, 1=NACK
    SCL_LOW();
    return ack;
}

2.4 ACK/NACK机制

ACK/NACK是I2C协议的握手信号。每个字节传输完成后,接收器必须给出回应:

应答 SDA电平(第9个SCL) 含义
ACK(应答) 低电平 接收成功,可以继续发送下一字节
NACK(非应答) 高电平 接收失败,或接收器不想继续接收

这里有个坑:谁拉低SDA谁就是ACK的发送方。发送器发送完字节后必须释放SDA,由接收器拉低。如果接收器没拉低(比如地址不匹配、设备忙、或者根本没这个设备),SDA保持高电平,就是NACK。

经验之谈:我调试I2C设备时,第一个检查的就是ACK。如果发送从设备地址后收到NACK,90%的情况是地址写错了,或者从设备没上电。别急着怀疑时序,先拿万用表量一下SDA和SCL的上拉电阻有没有焊好。

2.5 时钟同步与仲裁

这两个机制是I2C多设备通信的基石。很多同学觉得I2C只能一主多从,其实它支持多主设备,靠的就是时钟同步和仲裁。

时钟同步

多个主设备同时驱动SCL时,谁拉低SCL,SCL就是低电平;谁先释放,SCL也不会立即变高,要等所有主设备都释放了,SCL才变高。这叫线与(Wired-AND)。

说白了,SCL的低电平时间由拉低时间最长的主设备决定,高电平时间由拉高时间最短的主设备决定。这样所有主设备的时钟就同步了。

仲裁

仲裁发生在SDA线上。多个主设备同时发送数据时,如果某个主设备想拉高SDA,但另一个主设备在拉低SDA,那么SDA实际是低电平。拉高失败的那个主设备检测到SDA与自己发送的电平不一致,就知道自己仲裁失败了,立即释放总线。

仲裁的规则是:低电平优先。谁先发送0,谁就赢得总线控制权。

关键点:仲裁过程中不会丢失数据。赢得仲裁的主设备继续传输,失败的主设备自动转为从设备模式,等待下一次机会。这也是I2C协议优雅的地方——不需要额外的总线仲裁器。

我曾在项目中用两个MCU通过I2C互传数据,两个MCU都配置为主设备。一开始担心会冲突,后来发现只要处理好仲裁,两个主设备可以轮流占用总线,完全没问题。当然,前提是每个主设备都要有仲裁失败后的重试机制。

2.6 本章知识结构图

下面这张图把I2C协议时序的核心知识点串起来了,方便你对照理解:

I2C协议时序核心知识体系 起始条件 (S) 停止条件 (P) 数据有效性 字节格式 (MSB first) ACK / NACK 时钟同步 总线仲裁 多设备可靠通信的基础 说明:起始/停止条件定义传输边界,数据有效性和字节格式规范传输内容, ACK/NACK提供握手反馈,时钟同步与仲裁支撑多主设备共存。