3、I2C总线协议:起始条件、停止条件、数据有效性、ACK/NACK机制

I2C总线,说白了就是两根线(SCL时钟线、SDA数据线)搞定一切通信。很多刚入行的工程师觉得它简单,不就是拉高拉低嘛。但我在项目中吃过不少亏,发现越是看似简单的协议,越容易在时序细节上翻车。今天咱们就把I2C最核心的四个机制掰开揉碎了讲清楚。

3.1 起始条件(Start Condition)

先说说起始条件。这是I2C通信的“发令枪”。

定义:当SCL为高电平时,SDA从高电平切换到低电平。

你想想看,为什么要在SCL高电平时切换?因为I2C规定,数据在SCL低电平时变化,高电平时采样。起始条件故意打破这个规则,就是为了让所有从机知道:注意,要开始通信了!

时序要求:

  • SDA下降沿必须在SCL高电平期间完成
  • 起始条件建立时间(tSU:STA):至少4.7μs(标准模式100kHz)
  • 起始条件保持时间(tHD:STA):至少4.0μs

我个人习惯在代码里这样实现起始条件:

void i2c_start(void) {
    SDA_HIGH();          // 先确保SDA为高
    delay_us(5);         // 留点余量
    SCL_HIGH();          // SCL拉高
    delay_us(5);         // 等待SCL稳定
    SDA_LOW();           // SDA拉低,产生起始条件
    delay_us(5);         // 保持时间
    SCL_LOW();           // 拉低SCL,准备传输数据
}

我在项目中遇到过一个问题:某次用GPIO模拟I2C时,起始条件后紧接着发送地址,结果从机一直没响应。查了半天,发现是起始条件保持时间不够,从机还没反应过来我就开始发数据了。嗯,这里要注意,起始条件后的第一个SCL时钟一定要给足时间。

3.2 停止条件(Stop Condition)

有开始就有结束。停止条件就是通信的“休止符”。

定义:当SCL为高电平时,SDA从低电平切换到高电平。

和起始条件正好相反。停止条件告诉所有从机:总线释放了,你们可以继续监听下一次通信了。

小技巧:停止条件后,SDA和SCL都应该被释放为高电平(通过上拉电阻)。我习惯在停止条件后加一个短延时,确保总线完全释放,避免下一个起始条件被误判。

代码实现也很直观:

void i2c_stop(void) {
    SDA_LOW();           // 先确保SDA为低
    delay_us(5);
    SCL_HIGH();          // SCL拉高
    delay_us(5);
    SDA_HIGH();          // SDA拉高,产生停止条件
    delay_us(5);         // 总线释放时间
}

我曾经犯过一个低级错误:在停止条件后立刻重新发送起始条件,中间没有留总线释放时间。结果从机认为还在上一次通信中,直接无视了新的起始条件。避坑指南:停止条件后至少等待4.7μs再发起新的起始条件。

3.3 数据有效性(Data Validity)

这是I2C协议里最容易忽略但又最关键的部分。

核心规则:

  • SDA上的数据必须在SCL低电平时变化
  • SDA上的数据必须在SCL高电平时被采样(保持稳定)

说白了,就是SCL高电平时SDA不能变。你想想看,如果SCL高电平时SDA变了,从机会把它当成起始条件或停止条件来处理,通信就乱套了。

注意:数据变化只允许在SCL低电平期间进行。SCL高电平期间,SDA必须保持稳定。这个规则对主从双方都适用。

我见过不少工程师在写I2C驱动时,为了追求速度,在SCL上升沿刚过就改变SDA。结果就是数据采样错误,通信时好时坏。我的建议是:在SCL低电平的中间段改变SDA,留足建立时间和保持时间。

数据有效性的时序参数:

参数 标准模式(100kHz) 快速模式(400kHz)
数据建立时间 tSU:DAT ≥ 250ns ≥ 100ns
数据保持时间 tHD:DAT ≥ 0ns(实际建议≥ 300ns) ≥ 0ns(实际建议≥ 100ns)

这里有个坑:数据保持时间理论上可以是0ns,但实际项目中我建议至少留300ns。为什么?因为从机的输入滤波器、信号传播延迟都会吃掉你的时序余量。我曾经在快速模式下只留了50ns保持时间,结果从机偶尔会采样到错误数据,排查了整整两天才找到原因。

3.4 ACK/NACK机制

ACK/NACK是I2C的“握手信号”,用来确认数据是否被正确接收。

工作机制:

  • 每发送完一个字节(8位数据),发送方释放SDA
  • 接收方在第9个SCL时钟周期拉低SDA表示ACK(确认)
  • 接收方保持SDA为高表示NACK(不确认)

你可能会问:为什么要搞这么个机制?说白了,就是为了让发送方知道数据有没有被正确接收。如果没有ACK,发送方可以决定重发或者终止通信。

ACK/NACK的典型场景:

  • 主机发送地址后:从机ACK表示地址匹配,NACK表示无此从机或从机忙
  • 主机发送数据后:从机ACK表示数据已接收,NACK表示无法处理(如接收缓冲区满)
  • 主机接收数据后:主机ACK表示还要继续接收,NACK表示够了,停止传输

代码实现ACK检测:

uint8_t i2c_wait_ack(void) {
    uint8_t timeout = 100;
    SDA_INPUT();         // 释放SDA,设置为输入
    SCL_HIGH();          // 产生第9个时钟
    delay_us(5);
    
    while (SDA_READ() == 1) {  // 等待SDA被拉低
        if (--timeout == 0) {
            SCL_LOW();
            return 1;    // 超时,返回NACK
        }
    }
    
    SCL_LOW();
    return 0;            // 收到ACK
}

我在项目中遇到过一种情况:从机明明存在,但发送地址后总是收到NACK。后来用示波器一看,发现从机的ACK信号太短,只有几百纳秒,而我的采样点刚好错过了。嗯,这里要注意,ACK检测时SCL高电平的保持时间要足够长,确保能采样到从机的ACK信号。

避坑指南:我曾经在批量生产时发现,同一批次的从机芯片,有的ACK响应时间快,有的慢。后来在代码里加了ACK超时检测,超时后重试3次,问题就解决了。建议大家在写驱动时,永远不要假设从机一定会及时响应。

最后总结一下这四个机制的关系:起始条件开启通信,停止条件结束通信,数据有效性保证数据正确传输,ACK/NACK确保传输可靠。这四个机制环环相扣,任何一个出问题,整个通信都会失败。下次调试I2C时,不妨先用示波器抓一下这四个信号,往往能快速定位问题。