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

各位同学,咱们今天聊聊I2C协议最核心的时序部分。说实话,搞嵌入式驱动开发这么多年,我见过太多人在时序上翻车了。你想想看,协议搞懂了,代码写起来才顺手,否则调试的时候能把你逼疯。

2.1 起始条件(START Condition)

先说说起始条件。I2C总线上,所有通信都是从起始条件开始的。怎么产生?很简单:SCL为高电平时,SDA从高电平切换到低电平

关键点:起始条件由主机产生,从机只能被动响应。我刚开始做项目时,总以为从机也能主动发起通信,结果被坑了一次——从机拉低SDA,主机直接懵了。

用代码表示一下起始条件的产生(假设是GPIO模拟I2C):

void i2c_start(void) {
    SDA_OUT();          // SDA设为输出
    SDA_HIGH();         // SDA拉高
    SCL_HIGH();         // SCL拉高
    delay_us(5);        // 保持一段时间
    SDA_LOW();          // SDA拉低,产生起始条件
    delay_us(5);        // 保持
    SCL_LOW();          // SCL拉低,准备传输数据
}

嗯,这里要注意:起始条件之后,总线就被主机占用了。其他设备别想插嘴。

2.2 停止条件(STOP Condition)

有开始就有结束。停止条件正好反过来:SCL为高电平时,SDA从低电平切换到高电平

我个人习惯在每次通信结束后,都检查一下总线是否真的释放了。曾经有一次,从机没及时释放SDA,导致下一次起始条件失败,查了半天才发现是硬件上拉电阻没焊好。

void i2c_stop(void) {
    SDA_OUT();          // SDA设为输出
    SDA_LOW();          // SDA拉低
    SCL_HIGH();         // SCL拉高
    delay_us(5);
    SDA_HIGH();         // SDA拉高,产生停止条件
    delay_us(5);
}

避坑指南:我曾经在停止条件后立即发送起始条件,结果从机没反应过来。建议停止后至少等待5μs再发起新的通信,给从机一点喘息时间。

2.3 数据有效性(Data Validity)

数据什么时候才算有效?I2C协议规定:SCL为高电平时,SDA上的数据必须稳定。只有在SCL为低电平时,SDA才能变化。

说白了,就是SCL高电平的时候,你别乱动SDA。否则从机读到的数据就是错的。我见过有人用中断去改SDA电平,结果时序全乱了。

规则总结:

  • SCL高电平 → SDA数据稳定(采样点)
  • SCL低电平 → SDA数据可以变化(准备下一个bit)
  • 起始条件和停止条件除外(它们是在SCL高电平时改变SDA)

2.4 字节格式(Byte Format)

I2C传输数据是以字节为单位的。每个字节8位,高位在前(MSB first)。传输完一个字节后,紧接着就是ACK/NACK位。

举个例子,你要发送0x55(二进制01010101),顺序是:

  1. 先发bit7:0
  2. 再发bit6:1
  3. ...依次类推
  4. 最后发bit0:1

我刚开始写驱动时,总搞不清MSB和LSB的顺序。后来养成了习惯:每次写代码前,先把要发的数据用二进制写出来,标上bit7到bit0,再对着写时序。这个方法推荐给你们。

void i2c_send_byte(uint8_t data) {
    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);
    }
}

2.5 ACK/NACK机制

这是I2C协议里最巧妙的设计。每发送完一个字节,接收方必须回应一个ACK(应答)或NACK(非应答)。

ACK怎么产生?发送方在第9个SCL时钟周期释放SDA(设为输入),接收方拉低SDA表示ACK,保持高电平表示NACK。

ACK/NACK的含义:

场景 ACK NACK
主机发送地址后 从机存在,可以通信 从机不存在或忙
主机发送数据后 从机接收成功 从机无法接收(缓冲区满等)
主机接收数据后 主机还要继续读 主机不再需要数据

我曾经遇到过一个坑:从机在ACK时拉低SDA的时间太短,主机采样时已经释放了。结果主机以为收到了NACK,直接终止通信。后来在从机端加了延时才解决。

uint8_t i2c_wait_ack(void) {
    uint8_t ack;
    SDA_IN();           // SDA设为输入
    SCL_HIGH();         // 第9个时钟
    delay_us(5);
    ack = SDA_READ();   // 读取SDA电平
    SCL_LOW();
    return ack;         // 0=ACK, 1=NACK
}

注意事项:

  • 主机发送完最后一个字节后,从机必须返回NACK,否则主机不知道传输结束
  • 如果从机一直不拉低SDA(总线卡死),建议加超时处理,我一般设5ms超时
  • 多主机环境下,ACK冲突可能导致总线仲裁,需要额外处理

2.6 完整时序图

下面我用SVG画一个完整的I2C读写时序图,把起始、停止、数据、ACK都串起来。你们可以对照着看,理解整个流程。

I2C完整读写时序图 SCL SDA 起始 地址(7位) + R/W ACK 数据(8位) NACK 停止 数据有效性说明 • SCL高电平时,SDA数据必须稳定(采样点) • SCL低电平时,SDA数据可以变化 • 起始条件:SCL高电平时,SDA从高→低 • 停止条件:SCL高电平时,SDA从低→高

2.7 实际项目中的注意事项

最后,结合我自己的经验,给你们几个实用建议:

  • 时序参数别卡太死:I2C标准规定SCL最高100kHz(标准模式)或400kHz(快速模式),但实际项目中我习惯留20%余量。曾经有个项目,SCL跑到380kHz就出错了,降到320kHz稳如老狗。
  • ACK超时处理:我一般设5ms超时,超过就认为从机无响应。别死等,否则系统卡死。
  • 起始条件重复:有些从机支持重复起始条件(Repeated START),可以在不释放总线的情况下切换通信方向。这个后面章节会细讲。
  • 调试工具:逻辑分析仪是调试I2C的神器。我每次调I2C驱动,第一件事就是挂逻辑分析仪看波形。肉眼看到的时序,比代码里猜的靠谱多了。

个人小技巧:写I2C驱动时,先把起始、停止、发送字节、接收字节、等待ACK这几个基础函数写好并测试通过。上层读写函数就简单了,组合调用就行。我所有I2C项目都这么干,从来没出过问题。

好了,I2C协议的时序部分就讲到这里。这些基础打牢了,后面写驱动就是水到渠成的事。记住:时序是I2C的灵魂,搞懂了时序,你就掌握了I2C。

专注资料整理