1. I2C 总线协议概述

聊 I2C 之前,我先问你一个问题:你手头同时有好几个芯片要通信,板子上线又少,你会选什么总线?对我而言,I2C 就是那个最顺手的选择。两根线,多设备,还带应答——简直是为嵌入式量身定做的。别小看它,很多工程师栽的坑,根源恰恰是从这“两根线”开始的。

1.1 什么是 I2C

I2C(Inter-Integrated Circuit)是 Philips 在 80 年代发明的同步串行总线,现在满大街都是:传感器、EEPROM、ADC、RTC……几乎每块板上都能找到它。它只有两根信号线:SDA(数据线)和 SCL(时钟线),采用主从通信结构。一个主设备发起传输,一个或多个从设备响应。我见过不少新人以为主设备只能有一个,其实多主设备也能共存,靠的是仲裁机制。

我的经验:I2C 最吸引我的地方是芯片管脚少——MCU 用两个 GPIO 就能挂一箩筐外设。但省了引脚,不代表省了心。时钟拉伸、总线锁死、噪声干扰……都是这根线上埋的雷。

1.2 物理层:两根线与上拉电阻

SDA 和 SCL 都是开漏(Open-Drain)输出。什么叫开漏?就是芯片只能把线拉到低电平,拉高要靠外部的上拉电阻。所以你看到 I2C 总线上挂一个 4.7kΩ 或 10kΩ 的电阻,就是这个用处。

为什么用开漏?好处是多个设备可以共线——任意一个设备都能把总线拉低,不会短路。坏处?信号上升沿靠电阻充电,速度上不去。尤其是总线电容大或者上拉电阻选高了,波形就歪了,时钟拉伸故障就容易冒出来。

注意:上拉电阻不是随便选的。我在一个项目里用过 100kΩ 的电阻,结果 400kHz 模式根本跑不动,SCL 上升到逻辑高花了大半个周期——这就是时钟拉伸的早期预警。

简化的参数参考:

速度模式最大频率典型上拉电阻总线电容上限
标准模式100 kHz4.7kΩ – 10kΩ~200 pF
快速模式400 kHz1.5kΩ – 4.7kΩ~200 pF
快速模式+1 MHz1kΩ – 2kΩ~100 pF
高速模式3.4 MHz固定电流源~100 pF

1.3 通信协议:起始、停止、数据与应答

I2C 的时序看着简单,但每个细节都精打细算。我们先来过一次完整的数据搬砖过程:

  1. 起始条件(START):SCL 高电平时,SDA 由高变低。这叫“总线不再空,我要说话”。
  2. 发送从设备地址 + 读写位:通常是 7 位地址(也有 10 位),后面跟 1 位 R/W。这 8 位数据在 SCL 的每个高电平期间采样。
  3. 等待应答(ACK/NACK):第 9 个 SCL 时钟,从设备如果拉低 SDA 表示“收到”,不拉低就是“没空(NACK)”。我曾遇到过从设备上电慢了,主设备发地址后一直等不到 ACK,总线就卡在那里——这就是时钟拉伸故障的一种表现。
  4. 数据传输:数据字节也是 8 位 + 1 ACK。主设备发送时,SCL 由主设备驱动;主设备接收时,SCL 仍然由主设备产生。
  5. 停止条件(STOP):SCL 高电平时,SDA 由低变高。总线释放,给下一个设备用。
一句话记住:SDA 在 SCL 低电平时才能变,高电平必须稳。谁乱动,谁出故障。
// 伪代码模拟 I2C 起始 + 地址发送
void i2c_start() {
    SDA_HIGH();
    SCL_HIGH();
    delay();   // 确保稳定
    SDA_LOW(); // 产生 START
    delay();
    SCL_LOW(); // 准备传数据
}

uint8_t 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(); // 采样
        SCL_LOW();  delay(); // 准备下一位
    }
    // 释放 SDA,等待 ACK
    SDA_HIGH();
    SCL_HIGH(); delay();
    uint8_t ack = (SDA_READ() == 0) ? 1 : 0;
    SCL_LOW();
    return ack;
}

1.4 地址与读写方向

每个 I2C 从设备都有一个唯一的地址(7 位,共 128 个,但保留 16 个用于特殊用途)。你发完地址,第 8 位决定后续方向:0 = 主写从读,1 = 主读从写。注意!方向只能在一次传输中切换,如果想改方向,得发一个重复起始条件(Repeated START),再发一次地址。

  • 7位地址示例:0x48(常用温度传感器)
  • 加上写位:0x90(0x48 << 1 | 0)
  • 加上读位:0x91(0x48 << 1 | 1)

实际编程中,大部分驱动库的地址都是左移后的 8 位值,写代码时千万别搞混。我最初就是因为这个左移没对齐,调试了一整个下午。

1.5 时钟同步与总线仲裁

多主设备同时发起传输时怎么办?I2C 用了线与(Wire-AND)逻辑来解决。

  • 时钟同步:每个主设备都会产生自己的 SCL。因为开漏,谁拉的 SCL 周期最短,谁就胜出——实际上所有主设备被同步到最慢的时钟。
  • 仲裁(Arbitration):数据线上,多个主设备同时拉低时,谁能保持低电平谁输(失去总线控制权)。赢的那个继续传输。

时钟拉伸跟这个有关系吗?当然有。如果一个从设备需要更多时间处理数据,它会主动拉低 SCL(把这个时钟周期伸长)。主设备检测到 SCL 被拉低,就会等待直到 SCL 变高再继续。这是正常功能,但一旦从设备拉低时间过长或主设备没有超时处理,总线就“卡死”了——这是我们这门课的重点。

1.6 速度模式与标准

I2C 目前有几种主流速度模式:

模式时钟频率主要特点
标准模式 (Sm)100 kHz兼容性最广,几乎所有设备都支持
快速模式 (Fm)400 kHz主流,速度快 4 倍
快速模式+ (Fm+)1 MHz需要更小的上拉电阻
高速模式 (Hs)3.4 MHz专用电流源驱动,需特殊握手
超快模式 (UFm)5 MHz单向传输,常用于显示

实际项目中,大多数场景用 Fm 就够。如果超过 400 kHz,波形整形、上拉电阻、走线长度都得仔细算,否则时钟拉伸问题会成倍放大。

核心思路:不管是起步条件、应答机制还是时钟同步,I2C 的每一项设计都围绕“最少引脚、最简连线、最可靠的多主通信”。但“可靠”是有前提的——每个参与者都要遵守开漏时期和应答窗口。一旦有设备拖了一个时钟周期不放手,整个总线的节奏就乱了。这就是后面我们专门聊“时钟拉伸故障”的起点。

下面这张图展示 I2C 协议核心元素之间的关系,方便你快速建立全局印象。

I2C 协议核心要素关系图 主设备 从设备 1 从设备 2 SDA (数据线) SCL (时钟线) 上拉电阻 Rp 起始 / 停止 / 数据 地址 + 读写 + 应答 时钟拉伸 从设备拉低 SCL 所有设备共享 SDA/SCL,通过开漏 + 上拉实现线与逻辑 时钟拉伸是 SCL 被从设备主动拉低的正常行为,但异常拉伸会导致总线卡死

好,I2C 基础就到这儿。别看今天讲的都是老生常谈,后面的时钟拉伸故障全靠这几点知识去拆。掌握了这些,你才能精准定位:到底是哪个家伙把时钟拽住了,拽了多久,为什么拽。