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.h 和 CubeMX_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