I2C
Author:余生
在深入理解了 USART(通用同步 / 异步收发器)的底层原理后,我们现在转向另一种广泛使用的串行通信协议:I²C(Inter-Integrated Circuit,读作 “I-squared-C” 或 “I-two-C”)。我们将从最底层的物理电平开始,一步步揭示 I²C 是如何从两个引脚上的高低电平演化为一套完整、可靠、多设备共存的通信协议的。同时,我们会剖析其底层寄存器操作和库函数实现,并与 USART 进行全面对比,涵盖物理层、电气特性、协议结构、主从机制、时钟同步、错误处理、软件实现等多个维度。
一:从物理层开始 —— I²C 的电气基础
1.1 I²C 的物理连接
I²C 只需要两根线即可实现多设备通信:
- SDA(Serial Data Line):串行数据线,双向传输。
- SCL(Serial Clock Line):串行时钟线,由主设备控制。
所有设备都通过这两根线并联到总线上,形成一个总线型拓扑结构。
注意:I²C 是开漏(Open-Drain)或开集电极(Open-Collector) 输出,必须外接上拉电阻(Pull-up Resistor)到 VDD(如 3.3V 或 5V)。
1.2 为什么是开漏 + 上拉?
- 开漏输出:MOSFET 只能将引脚拉低(接地),不能主动输出高电平。
- 上拉电阻:在没有设备拉低时,将引脚 “拉” 到高电平。
这样做的好处是:
- 线与(Wired-AND)逻辑:任何设备拉低,总线即为低电平。
- 避免总线冲突:多个设备可以安全地共享总线,不会因同时输出高低电平而烧毁。
电平状态:
- 低电平(0):设备内部 MOSFET 导通,将 SDA/SCL 拉到 GND。
- 高电平(1):所有设备释放总线,上拉电阻将电压拉高。
二:从电平到通信 —— I²C 协议的诞生
2.1 同步通信的本质
与 USART 的异步通信(无共享时钟)不同,I²C 是同步通信:SCL 提供时钟信号,SDA 在 SCL 的控制下传输数据。
这意味着:
- 发送方和接收方共享同一个时钟源(SCL),无需事先约定波特率。
- 数据在 SCL 的边沿被采样,在中间被稳定。
2.2 通信的起点:起始条件(START Condition)
在空闲状态下,SDA 和 SCL 都为高电平。
如何告诉所有设备 “我要开始通信了”?
答案:起始条件(START):
当 SCL 为高电平时,SDA 从高电平变为低电平。
SCL: ──────┬─────────────
│
SDA: ──────┼──┐──────────
│ ▼
└─── START
2
3
4
5
这个电平跳变被所有连接在总线上的设备检测到,表示一次通信的开始。
2.3 通信的终点:停止条件(STOP Condition)
通信结束时,发送主设备发出:
当 SCL 为高电平时,SDA 从低电平变为高电平。
SCL: ──────┬─────────────
│
SDA: ───┐ └────┬────────
▲ │
└────────┘ STOP
2
3
4
5
STOP 条件后,总线回到空闲状态。
2.4 数据传输:位传输规则
I²C 的每一位传输都遵循以下规则:
- 数据稳定:SDA 上的数据必须在 SCL 为低电平期间改变。
- 数据采样:SDA 上的数据在 SCL 为高电平期间被采样(保持稳定)。
SCL: ────┬───┬───┬───┬───
│ │ │ │
SDA: ──┬─▼─┬─▼─┬─▼─┬─▼─┬─
▲ ▲ ▲ ▲ ▲
└─────┴───┴───┴───┘
数据变化区 数据采样区
2
3
4
5
6
数据位:SDA 高电平 = 1,低电平 = 0。
2.5 字节传输与应答机制(ACK/NACK)
I²C 以字节为单位传输,但每个字节后都有一个应答位(ACK Bit)。
传输一个字节 + ACK
- 主设备发送 8 位地址或数据。
- 第 9 个时钟周期:
- 如果接收方成功接收,释放 SDA(让上拉电阻拉高),主设备检测到高电平 → ACK。
- 如果接收方未准备好或拒绝接收,拉低 SDA → NACK。
特点:
- 发送方释放总线,等待接收方控制 SDA。
- 主设备始终控制 SCL,即使在 ACK 阶段。
SCL: ─┬─┬─┬─┬─┬─┬─┬─┬─┬─
│ │ │ │ │ │ │ │ │
SDA: D0 D1 D2 D3 D4 D5 D6 D7 ACK
↑ ↑
数据位(8位) 应答位
2
3
4
5
三:I²C 协议的核心 —— 寻址与多设备共存
3.1 设备寻址(7 位地址模式)
I²C 支持多从设备共存。主设备通过地址选择与哪个从设备通信。
地址帧格式
[ 7位从设备地址 ] [ R/W 位 ]
- 7 位地址:从设备的唯一标识(0x00 ~ 0x7F)。
- R/W 位:第 8 位,0 = 写(主→从),1 = 读(主←从)。
示例:主设备要向地址为 0x50 的 EEPROM 写数据:
发送:
1 0 1 0 0 0 0 0
→ 二进制1010000
+0
= 0xA0
3.2 完整的 I²C 通信流程
场景 1:主设备写数据到从设备
主设备 从设备
│ │
├── START ──────────────────────►│
│ │
├── 地址 + W (0xA0) ────────────►│ → ACK
│ │
├── 寄存器地址 (Reg) ───────────►│ → ACK
│ │
├── 数据1 ──────────────────────►│ → ACK
│ │
├── 数据2 ──────────────────────►│ → ACK
│ │
└── STOP ◄───────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
场景 2:主设备读取从设备数据(需先写地址)
主设备 从设备
│ │
├── START ──────────────────────►│
│ │
├── 地址 + W (0xA0) ────────────►│ → ACK
│ │
├── 寄存器地址 (Reg) ───────────►│ → ACK
│ │
├── REPEATED START ─────────────►│
│ │
├── 地址 + R (0xA1) ────────────►│ → ACK
│ │
├── 数据1 ◄─────────────────────┤ ← ACK
│ │
├── 数据2 ◄─────────────────────┤ ← NACK
│ │
└── STOP ◄───────────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
REPEATED START:在不发出 STOP 的情况下,再次发出 START,用于切换读写方向。
四:STM32 的 I²C 外设与底层实现
4.1 STM32 I²C 外设架构
STM32 内部有专用的 I²C 控制器,支持标准模式(100 kbps)、快速模式(400 kbps)甚至高速模式(3.4 Mbps)。它自动处理:
- SCL 时钟生成
- 起始 / 停止条件生成
- 地址发送与检测
- ACK/NACK 处理
- 数据移位
4.2 主要寄存器(以 STM32F103 I²C1 为例)
寄存器 | 作用 |
---|---|
I2C_CR1 | 控制寄存器 1(使能、中断等) |
I2C_CR2 | 控制寄存器 2(时钟频率、缓冲区中断) |
I2C_OAR1/OAR2 | 从设备地址寄存器 |
I2C_DR | 数据寄存器(8 位) |
I2C_SR1/SR2 | 状态寄存器 1 和 2 |
I2C_CCR | 时钟控制寄存器(设置 SCL 频率) |
I2C_TRISE | 上升时间寄存器(用于补偿上拉电阻延迟) |
4.3 波特率(SCL 频率)设置
SCL 频率由 I2C_CCR
和 I2C_TRISE
决定。
标准模式(100 kHz)
// 假设 PCLK1 = 36 MHz
// Mode: Standard (100 kHz)
// CCR = PCLK / (2 * SCL_Frequency) = 36e6 / (2 * 100e3) = 180
void I2C_SetBaudRate(I2C_TypeDef* I2Cx, uint32_t sclFreq) {
uint32_t pclk1 = 36000000;
uint16_t ccr = pclk1 / (2 * sclFreq);
I2Cx->CCR = ccr;
I2Cx->TRISE = 36 + 1; // Rise time <= 1000ns, TRISE = PCLK/1e6 + 1
}
2
3
4
5
6
7
8
9
10
4.4 发送起始条件(底层实现)
void I2C_GenerateSTART(I2C_TypeDef* I2Cx) {
I2Cx->CR1 |= I2C_CR1_START; // 置位 START 位
}
2
3
硬件自动检测 SCL 高电平时拉低 SDA。
4.5 等待事件(状态轮询)
uint8_t I2C_WaitEvent(I2C_TypeDef* I2Cx, uint32_t event) {
uint32_t timeout = 10000;
while (!(I2Cx->SR1 & event)) {
if (--timeout == 0) return ERROR;
}
return SUCCESS;
}
2
3
4
5
6
7
常用事件:
I2C_EVENT_MASTER_MODE_SELECT
:START 发出后,检测到 SB=1I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED
:地址 + 写 发出后,检测到 ADDR=1 且 TxE=1
4.6 主设备写一个字节到从设备(轮询方式)
uint8_t I2C_Master_Write(I2C_TypeDef* I2Cx, uint8_t devAddr, uint8_t reg, uint8_t data) {
// 1. 生成 START
I2C_GenerateSTART(I2Cx);
if (I2C_WaitEvent(I2Cx, I2C_SR1_SB) != SUCCESS) return ERROR;
// 2. 发送设备地址 + 写 (0)
I2Cx->DR = (devAddr << 1) & 0xFE; // 7位地址左移,最低位=0(写)
if (I2C_WaitEvent(I2Cx, I2C_SR1_ADDR) != SUCCESS) return ERROR;
(void)I2Cx->SR2; // 清除 ADDR 标志
// 3. 发送寄存器地址
I2Cx->DR = reg;
if (I2C_WaitEvent(I2Cx, I2C_SR1_TXE) != SUCCESS) return ERROR;
// 4. 发送数据
I2Cx->DR = data;
if (I2C_WaitEvent(I2Cx, I2C_SR1_BTF) != SUCCESS) return ERROR; // Byte Transfer Finished
// 5. 生成 STOP
I2Cx->CR1 |= I2C_CR1_STOP;
return SUCCESS;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
4.7 中断与 DMA 支持
- 中断:可配置在 TXE(发送寄存器空)、RXNE(接收寄存器非空)、SB(起始位)等事件触发中断。
- DMA:可与 I²C 外设联动,实现大数据块的高效传输,无需 CPU 干预。
五:I²C vs USART —— 全面对比
特性 | I²C | USART |
---|---|---|
通信线数 | 2 根(SDA, SCL) | 2 根(TX, RX),可加 RTS/CTS |
通信模式 | 同步(SCL 时钟) | 异步(无时钟)或同步 |
拓扑结构 | 总线型,支持多主多从 | 点对点,或 RS-485 多点 |
设备寻址 | 7/10 位地址,支持多设备 | 无地址,靠物理连接区分 |
速度 | 标准 100kbps,快速 400kbps,高速 3.4Mbps | 通常 9600 ~ 115200 bps,高速可达 Mbps 级 |
硬件复杂度 | 较高(需上拉电阻,协议复杂) | 简单(TTL 电平直连) |
抗干扰能力 | 一般(开漏,长线易受干扰) | 一般,长距离需 RS-232/485 |
数据完整性 | 有 ACK/NACK 机制 | 无,靠上层协议校验 |
主从关系 | 明确,主设备控制 SCL | 通常点对点,无主从 |
时钟源 | 主设备提供 SCL | 双方独立时钟,靠波特率匹配 |
应用场景 | 板内设备通信(EEPROM、传感器、RTC) | 设备间通信(PC、GPS、蓝牙) |
六:I²C 的高级特性与挑战
6.1 多主竞争与仲裁(Arbitration)
I²C 支持多主设备。当两个主设备同时发送 START 时,通过仲裁机制决定谁获得总线控制权。
- 仲裁规则:每个主设备在发送数据的同时也读取 SDA。
- 如果发送高电平但读到低电平,说明其他设备正在拉低 → 仲裁失败,自动退出。
仲裁基于地址或数据内容,优先级由地址值决定。
6.2 时钟拉伸(Clock Stretching)
从设备如果处理不过来,可以主动拉低 SCL,迫使主设备等待。这称为时钟拉伸。
- 主设备必须等待 SCL 被释放(变高)才能继续发送时钟。
- 这是 I²C 实现流量控制的核心机制。
6.3 错误处理
I²C 外设可检测:
- 总线忙(BUSY):总线未释放
- 应答失败(AF):从设备未 ACK
- 总线错误(BERR):非法起始 / 停止条件
- 超时(TIMEOUT):长时间无响应
七:总结 —— 两种协议的本质差异
维度 | USART | I²C |
---|---|---|
哲学 | “我发你收,你发我收” | “我问你答,你答我听” |
同步方式 | 时间约定(波特率) | 共享时钟(SCL) |
扩展性 | 差(点对点) | 强(多设备总线) |
可靠性 | 依赖上层协议 | 内建 ACK/NACK |
效率 | 高(全双工) | 低(半双工,ACK 开销) |
实现复杂度 | 简单 | 复杂(仲裁、时钟拉伸) |
结语
从两个引脚的高低电平,到一套支持多设备、带寻址、有应答、可仲裁的通信协议,I²C 展现了嵌入式系统中 “用简单硬件实现复杂逻辑” 的工程智慧。它不像 USART 那样 “直来直去”,而是通过共享时钟、总线仲裁、应答机制、地址寻址,构建了一个协作式、可扩展的通信生态。
理解 I²C 不仅是掌握一种协议,更是理解总线思想、同步机制、错误恢复、多设备协同的绝佳范例。它与 USART 各有优劣,互为补充,共同构成了嵌入式系统通信的基石。