I2C 时序基础:从空闲到字节传输

各位同学,今天我们来聊聊 I2C 的时序基础。说实话,这部分内容看起来简单,但很多新手甚至老手都在这里栽过跟头。我刚开始做驱动时,就因为起始条件没搞对,折腾了整整一个下午。嗯,咱们一步步来。

1. 空闲状态:总线在“休息”时什么样?

I2C 总线空闲时,SCL(时钟线)和 SDA(数据线)都处于高电平。为什么?因为这两根线都是开漏输出,外部接了上拉电阻。没有设备拉低它们,自然就是高电平。

你想想看,如果总线空闲时不是高电平,那起始条件就没法判断了。我见过有人把上拉电阻焊错了,结果总线一直低电平,主设备死活发不出起始信号。

关键点:空闲状态 = SCL 高 + SDA 高。这是所有 I2C 通信的起点和终点。

2. 起始条件:通信的“发令枪”

起始条件(Start Condition)的定义很简单:SCL 为高电平时,SDA 从高电平切换到低电平。

说白了,就是主设备告诉所有从设备:“注意了,我要开始发数据了!”

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

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

这里有个坑:起始条件必须在 SCL 高电平时产生。我曾经在调试一个温湿度传感器时,发现设备一直没响应。用逻辑分析仪一看,原来是代码里先拉低了 SCL,再拉低 SDA——这根本不是起始条件,从设备根本认不出来。

注意:起始条件后,总线被认为处于“忙”状态。其他主设备看到这个信号,就知道总线被占用了,不会尝试发起通信。

3. 停止条件:通信的“句号”

停止条件(Stop Condition)正好相反:SCL 为高电平时,SDA 从低电平切换到高电平。

实现代码也很直观:

void i2c_stop(void) {
    SDA_LOW();           // 确保 SDA 低
    SCL_HIGH();          // SCL 高
    delay_us(5);         // 保持
    SDA_HIGH();          // SDA 拉高,产生停止条件
    delay_us(5);         // 保持
}

停止条件之后,总线恢复空闲状态。从设备收到停止条件,就知道这次传输结束了。

我记得有一次,一个同事写的驱动在读取数据后忘了发停止条件。结果总线一直处于忙状态,后续所有通信都卡死了。嗯,这种问题用逻辑分析仪一看就明白,但排查起来确实让人头疼。

小技巧:有些从设备支持“重复起始条件”(Repeated Start),即在一次传输中不发送停止条件,直接发新的起始条件。这在读取寄存器数据时非常有用,可以避免总线被其他设备抢占。

4. 数据有效性:什么时候采样数据?

I2C 的数据有效性规则很简单:数据在 SCL 高电平时被采样,在 SCL 低电平时可以变化。

换句话说:

  • SCL 为高时,SDA 必须保持稳定
  • SCL 为低时,SDA 可以变化

为什么这样设计?因为从设备只在 SCL 的上升沿或高电平期间采样 SDA。如果 SDA 在 SCL 高电平时变化,从设备可能采到错误的数据。

我刚开始做 I2C 驱动时,总想着在 SCL 高电平时改变 SDA 来“加快速度”。结果数据全是乱的。后来才明白,时序不是你想快就能快的,必须遵守协议规范。

核心原则:SCL 高电平期间,SDA 不能变。这是 I2C 通信的“铁律”。

5. 字节格式:8 位数据 + 1 位应答

I2C 的字节格式非常固定:每个字节 8 位,高位在前(MSB first),后面跟一个应答位(ACK/NACK)。

传输一个字节的流程如下:

  1. 主设备在 SCL 的每个脉冲中发送 1 位数据(从高位到低位)
  2. 第 8 位发送完后,主设备释放 SDA(拉高)
  3. 主设备产生第 9 个 SCL 脉冲
  4. 从设备在第 9 个脉冲期间拉低 SDA(表示 ACK)或保持高(表示 NACK)

用表格总结一下:

SCL 脉冲序号 作用 SDA 状态
1 发送第 7 位(MSB) 由主设备控制
2-7 发送第 6-1 位 由主设备控制
8 发送第 0 位(LSB) 由主设备控制
9 应答位 由从设备控制

发送一个字节的代码示例:

uint8_t i2c_send_byte(uint8_t data) {
    uint8_t i;
    for (i = 0; i < 8; i++) {
        // 从高位到低位逐位发送
        if (data & 0x80) {
            SDA_HIGH();
        } else {
            SDA_LOW();
        }
        data <<= 1;  // 左移,准备下一位
        SCL_HIGH();    // 产生时钟脉冲
        delay_us(5);
        SCL_LOW();
        delay_us(5);
    }
    // 释放 SDA,等待应答
    SDA_HIGH();
    SCL_HIGH();
    delay_us(5);
    uint8_t ack = SDA_READ();  // 读取应答位
    SCL_LOW();
    return ack;  // 0 表示 ACK,1 表示 NACK
}

注意:应答位是必须的。如果从设备没有应答(NACK),主设备应该发送停止条件终止传输。我见过有人忽略 NACK,继续发数据,结果从设备根本不理会,白白浪费带宽。

6. 时序图:一图胜千言

下面这张 SVG 图把 I2C 的完整时序画出来了。从空闲状态开始,到起始条件,再到数据传输,最后停止条件。你仔细看看,每个细节都在里面。

I2C 完整时序图 I2C 时序图:从空闲到停止 空闲状态 SCL=高, SDA=高 SCL SDA 起始条件 SDA↓ (SCL高) 8 位数据 (MSB first) SCL高时采样,SCL低时变化 应答位 从设备拉低 = ACK 停止条件 SDA↑ (SCL高) 关键时序参数: • tHD:STA (起始条件保持时间) ≥ 4μs (标准模式) • tSU:STO (停止条件建立时间) ≥ 4μs (标准模式) • tHIGH (SCL高电平时间) ≥ 4μs, tLOW (SCL低电平时间) ≥ 4.7μs

7. 避坑指南:我踩过的那些坑

最后,分享几个我实际项目中遇到的坑,希望对你有帮助:

  • 起始条件重复触发:有一次我在循环里连续调用起始条件,没检查总线状态。结果从设备被搞晕了,数据全乱。记住:起始条件只能在空闲状态或停止条件后发送。
  • 应答位超时:有些从设备处理速度慢,需要等待一段时间才会拉低 SDA 应答。我习惯在应答位加一个超时机制,比如等待 1ms,如果还没收到 ACK 就报错。
  • 上拉电阻值不对:I2C 总线的上拉电阻一般在 4.7kΩ 左右。电阻太大,信号上升沿变慢;电阻太小,功耗增加。我见过有人用 10kΩ 电阻,结果 400kHz 模式下波形都变形了。

调试建议:如果你刚开始学 I2C,强烈建议买个逻辑分析仪。几十块钱的东西,能让你看到 SCL 和 SDA 的实时波形。我每次调试 I2C 设备,第一件事就是挂上逻辑分析仪,看看起始条件、数据、应答、停止条件是不是对的。