Skip to content

I2C 通信原理与实践

I2C(Inter-Integrated Circuit,集成电路间总线)是一种两线制的多主多从同步串行总线。相比于 SPI 的四线制,I2C 用更少的引脚连接更多的设备——在机器人主控板上,当引脚资源紧张时,I2C 是连接传感器的首选。

上一篇:SPI 通信原理与实践


1. 概念——两线制的多设备总线

1.1 与其他协议的对比

协议 线数 最大设备数 速度 应用场景
I2C 2 (SCL + SDA) 127 (7-bit 地址) 100k~3.4Mbps 传感器、EEPROM、OLED
SPI 4+ (含 CS) 理论无限(实际受 CS 引脚数限制) 几十 Mbps IMU、Flash、高速 ADC
UART 2 (TX + RX) 1 对 1 几 Mbps 调试、遥控器、GPS

1.2 I2C 的特点

  • 两线制:SCL(时钟)+ SDA(数据)
  • 多主多从:理论上多个主设备可以共存(实践中通常只有一个 MCU 做主设备)
  • 7-bit 地址:每个从设备有唯一的地址,通信时先发地址选择目标
  • 半双工:同一时刻只能一个方向传输(因为只有一根数据线)
  • 开漏输出:需要外部上拉电阻(通常 4.7kΩ)

为什么 I2C 只用 2 根线(对比 SPI 的 4 根)? 在 PCB 空间紧张的主控板上,引脚数量是稀缺资源。I2C 只需 2 个引脚就能连接最多 127 个设备;而 SPI 连接 N 个设备需要 N+3 个引脚(SCLK + MOSI + MISO + N×CS)。当机器人主控板需要同时连接磁力计、OLED、温度传感器等多个外设时,I2C 的引脚节省优势非常明显。代价是速度——I2C 的最快模式(3.4Mbps)仍远低于 SPI 的数十 Mbps,但对于传感器这种低速设备完全够用。

为什么选用 7-bit 地址? 1982 年 Philips 发明 I2C 时,总线上的设备数量极少,127 个地址看起来无穷无尽。近四十年后的今天,7-bit 地址确实在某些场景下捉襟见肘——因此 I2C 后来扩展了 10-bit 地址模式。但在实际工程中,10-bit 地址极少使用,原因是:绝大多数 I2C 总线上只有不到 10 个设备,127 个地址绰绰有余;且 10-bit 地址增加了协议复杂度,并非所有从设备都支持。


2. 物理层——开漏 + 上拉

2.1 为什么用开漏

I2C 总线上的所有设备共享 SDA 和 SCL 线。如果任何一个设备输出高电平,另一个输出低电平,就会发生短路

开漏输出解决了这个问题:设备只能把总线拉到低电平(主动),不能拉到高电平。高电平由外部上拉电阻提供(被动)。

I2C 总线结构:

    VDD ──┬────────────┬──────────────────
          │            │
       [4.7kΩ]      [4.7kΩ]         ← 上拉电阻
          │            │
          ├────────────┼──── SDA (数据线)
          │            │
          ├────────────┼──── SCL (时钟线)
          │            │
    ┌─────┴─────┐ ┌───┴───────┐
    │   MCU     │ │  传感器    │  ← 所有设备都是开漏输出
    │   (主)    │ │  (从)      │
    └───────────┘ └───────────┘

开漏的逻辑:
- 任一设备拉低 → 总线 = 低电平(线与特性,Wired-AND)
- 所有设备释放 → 上拉电阻将总线拉到 VDD = 高电平

2.2 线与(Wired-AND)特性

多个开漏输出接在同一根线上,只要有一个设备输出低电平,整条线就被拉低。这个特性被 I2C 用于:

  • 仲裁:多个主设备同时发送时,自动检测冲突
  • 时钟拉伸:从设备可以把 SCL 拉低,让主设备等待("我还没准备好")

为什么必须用开漏而不是推挽输出? 开漏输出除了实现线与(Wired-AND)之外,还支撑了 I2C 的两个关键特性:(1)时钟拉伸(Clock Stretching)——当从设备处理数据速度跟不上主设备时,它可以主动将 SCL 拉低,主设备检测到 SCL 为低就会等待。推挽输出做不到这一点,因为从设备无法强制改变主设备驱动的信号。(2)多主仲裁——两个主设备同时发送时,它们各自监控 SDA 的电平。如果主设备 A 发出了高电平却读到低电平,就知道另一个主设备 B 同时拉低了总线,A 会主动退出。这种"非破坏性仲裁"只有在开漏结构下才能实现。


3. 通信流程

3.1 起始条件与停止条件

I2C 用 SDA 在 SCL 高电平期间的边沿变化来定义开始和结束:

空闲状态:   SCL = 高, SDA = 高

起始条件 (START):     停止条件 (STOP):
SDA  ────┐            SDA  ────────┐
          │                         │
          └──────                   └──────
SCL  ────────────     SCL  ────────────────

SDA 在 SCL 高时从高→低    SDA 在 SCL 高时从低→高

3.2 数据位传输

数据在 SCL 低电平时变化,在 SCL 高电平时采样:

SCL:  ──┐   ┌───┐   ┌───┐   ┌──
        │   │   │   │   │   │
        └───┘   └───┘   └───┘
SDA:  ────┬───────┬───────────
          │       │
    SDA 稳定    SDA 变化
    (采样点)    (允许改变)

3.3 ACK/NACK 应答机制

每传输 8 bits(1 字节)后,接收方必须在第 9 个时钟周期拉低 SDA 作为应答(ACK):

SCL:  ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐ ┌─┐   ┌─┐
      └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘   └─┘
      D7  D6  D5  D4  D3  D2  D1  D0  ACK
                                        │
SDA:  ──────────────────────────────────┘
                        接收方拉低 = ACK
                        接收方不拉低 = NACK
  • ACK (0):收到数据,继续
  • NACK (1):不想继续接收(或地址不匹配)

为什么每字节都要应答? I2C 的硬件层没有 CRC 校验——不像 CAN 那样有 15-bit CRC 自动检错。ACK 是 I2C 唯一的逐字节确认机制:发送方知道接收方确实收到了这一字节,才能继续发送下一字节。如果从设备收到不认识的命令,它可以回复 NACK 来告诉主设备"我拒绝"。在读操作中,主设备在最后一个字节回复 NACK 来告知从设备"不用再发了",然后发送 STOP 条件结束传输。没有 ACK/NACK,I2C 通信就像对着黑暗喊话——你不知道对方是否在听。

3.4 完整通信帧

主设备写数据到从设备

┌──┐ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬───┐ ┌──────┬───┐ ┌──┐
│ST│ │A6 A5 │A4 A3 │A2 A1 │A0 R/W│  ACK │ D7.. │  ACK │ D7.. │ACK│ │ D7.. │ACK│ │SP│
│RT│ │      │      │      │  =0  │      │  D0  │      │  D0  │   │ │  D0  │   │ │  │
└──┘ └──────┴──────┴──────┴──────┴──────┘ └──────┴──────┘ └──────┴───┘ └──────┴───┘ └──┘
      ←── 地址帧 (7-bit 地址 + R/W) ──→  ←─ 数据帧 ─→  ←─ 数据帧 ─→ ... ← 数据帧 →
      ↑                                ↑                             ↑
      Master 发送,Slave 应答          Master 发送,Slave 应答         STOP

主设备从从设备读数据

┌──┐ ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬───┐ ┌──────┬───┐ ┌──────┬───┐ ┌──┐
│ST│ │A6 A5 │A4 A3 │A2 A1 │A0 R/W│  ACK │ D7.. │  ACK │ D7.. │ACK│ │ D7.. │NAK│ │SP│
│RT│ │      │      │      │  =1  │      │  D0  │      │  D0  │   │ │  D0  │   │ │  │
└──┘ └──────┴──────┴──────┴──────┴──────┘ └──────┴──────┘ └──────┴───┘ └──────┴───┘ └──┘
      ←── 地址帧 (7-bit 地址 + R/W) ──→  ←  Slave→Master ─→  ← 数据帧 → ← 最后一帧 →
      ↑                                  ↑                  ↑          ↑
      Master 发送                       Slave 发送          Master ACK  NACK+STOP

4. HAL 库的 I2C 操作

4.1 基本 API

// 写数据到指定地址的从设备
HAL_I2C_Master_Transmit(&hi2c1, slaveAddr << 1, txData, len, timeout);

// 从指定地址的从设备读数据
HAL_I2C_Master_Receive(&hi2c1, slaveAddr << 1, rxData, len, timeout);

// 先写后读(典型传感器模式:写入寄存器地址,再读数据)
HAL_I2C_Mem_Write(&hi2c1, slaveAddr << 1, regAddr, I2C_MEMADD_SIZE_8BIT, txData, len, timeout);
HAL_I2C_Mem_Read(&hi2c1, slaveAddr << 1, regAddr, I2C_MEMADD_SIZE_8BIT, rxData, len, timeout);

// DMA 模式(GSRL 使用)
HAL_I2C_Master_Transmit_DMA(&hi2c1, slaveAddr << 1, txData, len);
HAL_I2C_Master_Receive_DMA(&hi2c1, slaveAddr << 1, rxData, len);

注意slaveAddr << 1 是因为 HAL 期望的是 8-bit 地址(7-bit 地址左移一位,最低位由 HAL 自动填充 R/W)。

4.2 典型传感器读取流程

以常见的 I2C 传感器为例,读取寄存器的通用模式:

// 读取 WHO_AM_I 寄存器(设备 ID)验证传感器是否正确连接
uint8_t regAddr = 0x0F;    // WHO_AM_I 寄存器地址
uint8_t whoAmI = 0;

HAL_I2C_Mem_Read(&hi2c1, (0x68 << 1), regAddr,
                 I2C_MEMADD_SIZE_8BIT, &whoAmI, 1, 100);

if (whoAmI == 0x68) {
    // 传感器连接正常
} else {
    // 通信异常
}

5. I2C 常见问题

5.1 总线死锁

现象:SDA 一直被拉低,所有通信停止。

原因:主设备正在读数据时被复位,从设备还在等主设备的第 9 个时钟(ACK),一直拉着 SDA 不放。

解决

// 软件解锁:手动产生 9 个 SCL 脉冲,让从设备释放 SDA
void I2C_Bus_Unlock(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    // 将 SCL/SDA 配置为开漏输出
    // 手动翻转 SCL 9 次
    for (int i = 0; i < 9; i++) {
        HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_RESET);
        delay_us(5);
        HAL_GPIO_WritePin(SCL_GPIO_Port, SCL_Pin, GPIO_PIN_SET);
        delay_us(5);
    }
    // 发送 STOP 条件
    // 恢复为 I2C 复用功能
}

5.2 上拉电阻选择

总线速率 推荐上拉电阻 总线电容限制
100kHz (标准模式) 4.7kΩ < 400pF
400kHz (快速模式) 2.2kΩ < 200pF
1MHz (快速+) 1kΩ < 100pF

上拉电阻太小 → 功耗大(下拉时需要更大电流才能拉到低电平) 上拉电阻太大 → 上升沿缓慢 → 高速通信不可靠

5.3 地址冲突

同一条 I2C 总线上不能有两个相同地址的设备。如果传感器的地址固定,可能需要用 I2C 多路复用器(Multiplexer)或使用不同的 I2C 外设(如 I2C1 + I2C2)分别连接。


6. I2C 在 GSRL 中的应用

GSRL 中使用 I2C 的场景:

  • IST8310 磁力计(通过 I2C 读取 3 轴磁场数据)
  • OLED 显示屏(调试用)
  • 板上温度传感器

在 CubeMX 配置中,CubeMX_BSP/Inc/i2c.hCubeMX_BSP/Src/i2c.c 包含了 I2C1、I2C2、I2C3 的初始化代码:

// CubeMX 生成的 I2C 初始化
void MX_I2C1_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.ClockSpeed = 400000;    // 400kHz 快速模式
    hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
    HAL_I2C_Init(&hi2c1);
}

GSRL 的 I2C 外设在 stm32f4xx_it.c 中通过 DMA 中断管理:

void DMA1_Stream7_IRQHandler(void)
{
    HAL_DMA_IRQHandler(&hdma_i2c2_tx);  // I2C2 TX DMA 中断
}

7. 协议对比总结

特性 I2C SPI UART CAN
线数 2 4+ 2 2 (差分)
拓扑 多主多从 一主多从 点对点 多主广播
速度 100k~3.4Mbps ~50Mbps ~6Mbps 1Mbps
双工 半双工 全双工 全双工 半双工
设备寻址 7-bit 地址 CS 引脚 无(点对点) 11/29-bit ID
错误检测 无(仅有 ACK) 可选校验位 CRC15 + 多种帧错误检测
抗干扰 好(差分信号)

8. 总结

要点 说明
两线制 SCL (时钟) + SDA (数据)
开漏 + 上拉 允许线与,实现多主仲裁和时钟拉伸
7-bit 地址 最多 127 个从设备(含保留地址)
ACK/NACK 每字节后接收方应答,实现流控
起始/停止 SDA 在 SCL 高时的边沿变化
总线死锁 从设备未释放 SDA,需手动发送时钟脉冲解锁
GSRL 应用 磁力计 (IST8310)、OLED 等低速传感器

与 GSRL 的关系

GSRL 的 I2C 外设主要用于连接磁力计等外设。虽然在当前工程中 I2C 的使用不如 CAN 和 SPI 频繁,但理解 I2C 的物理层(开漏、上拉、线与)有助于理解其他开漏总线(如 I2C 衍生协议 SMBus、PMBus)的工作方式。

下一篇:CAN 通信原理与实践

作者: Qing | 修改日期: 2026-07-18