4. I2C 总线协议基础
I2C(Inter-Integrated Circuit)总线由 Philips 公司(现 NXP)于 1982 年开发,是一种同步、多主机、串行通信总线。它仅需两根线(SCL 时钟线、SDA 数据线)即可连接多个外设,是嵌入式系统中连接传感器、EEPROM、ADC/DAC、实时时钟等低速外设的事实标准。在 Zephyr OS 中,I2C 驱动是 i2c.h API 的核心实现对象,理解其物理层与时序是编写正确设备驱动的第一步。
4.1 物理层与电气特性
I2C 总线使用开漏(Open-Drain)输出结构,通过外部上拉电阻将 SCL 和 SDA 线拉至高电平。任何设备都可以将线拉低,实现“线与”逻辑。
| 参数 | 标准模式 (Standard) | 快速模式 (Fast) | 高速模式 (Fast+ / HS) |
|---|---|---|---|
| 最大时钟频率 | 100 kHz | 400 kHz | 1 MHz / 3.4 MHz |
| 总线电容限制 | 400 pF | 400 pF | 100 pF / 400 pF |
| 上拉电阻典型值 | 4.7 kΩ | 2.2 kΩ | 1.0 kΩ |
| 逻辑低电平 (VIL) | ≤ 0.3 × VDD | ≤ 0.3 × VDD | ≤ 0.3 × VDD |
| 逻辑高电平 (VIH) | ≥ 0.7 × VDD | ≥ 0.7 × VDD | ≥ 0.7 × VDD |
pinctrl 的上拉属性,总线可能无法正常产生高电平,导致 SCL 或 SDA 线卡在低电平。
4.2 时序与信号
I2C 通信以 SCL 时钟为同步基准,SDA 数据在 SCL 低电平时变化,在 SCL 高电平时被采样。以下为基本信号单元:
- 起始条件 (START): SCL 高电平时,SDA 由高变低。
- 停止条件 (STOP): SCL 高电平时,SDA 由低变高。
- 数据位传输: 每 8 位数据后跟一个应答位(ACK/NACK)。
- 应答 (ACK): 接收方在第 9 个 SCL 周期将 SDA 拉低。
- 非应答 (NACK): 接收方在第 9 个 SCL 周期保持 SDA 高电平。
4.3 设备地址与寻址
I2C 使用 7 位或 10 位地址。7 位地址是最常见的形式,每个从设备在总线上拥有唯一地址。地址由器件类型标识和引脚配置共同决定。
| 地址类型 | 地址范围 | 保留地址 | 典型应用 |
|---|---|---|---|
| 7 位地址 | 0x00 – 0x7F | 0x00 (通用呼叫), 0x01 (起始字节), 0x04-0x07 (HS 模式) | EEPROM, 传感器, ADC |
| 10 位地址 | 0x00 – 0x3FF | 0x00 – 0x07 (保留) | 复杂外设, 多路复用器 |
reg = <0x48> 表示 7 位地址。若总线上两个设备使用相同地址,必须通过硬件引脚(如 ADDR0、ADDR1)或 I2C 多路复用器(如 TCA9548A)解决冲突。
4.4 读写操作流程
I2C 通信以主从模式进行。主机产生时钟、发起 START 条件、发送从机地址和读写位,从机应答后传输数据。
4.4.1 写操作
- 主机发送 START 条件。
- 主机发送 7 位从机地址 + 写位(0)。
- 从机发送 ACK。
- 主机发送寄存器地址(内部偏移)。
- 从机发送 ACK。
- 主机发送数据字节,每字节后等待 ACK。
- 主机发送 STOP 条件。
4.4.2 读操作
- 主机发送 START 条件。
- 主机发送 7 位从机地址 + 写位(0)。
- 从机发送 ACK。
- 主机发送寄存器地址。
- 从机发送 ACK。
- 主机发送重复起始条件(RESTART)。
- 主机发送 7 位从机地址 + 读位(1)。
- 从机发送 ACK。
- 从机发送数据字节,主机发送 ACK(继续读)或 NACK(最后一字节)。
- 主机发送 STOP 条件。
4.5 多主机与仲裁
I2C 支持多个主机共享同一总线。多主机仲裁机制基于开漏结构的“线与”特性:当多个主机同时尝试驱动总线时,检测到 SDA 电平与自己输出不一致的主机将主动退出。
4.5.1 仲裁过程
- 每个主机在发送地址或数据时,持续监控 SDA 电平。
- 若某主机输出高电平但检测到低电平(另一主机拉低),则该主机失去仲裁,立即释放 SCL 和 SDA。
- 赢得仲裁的主机继续通信,不丢失任何数据位。
4.5.2 时钟同步
多主机模式下,所有主机共同参与 SCL 时钟同步。每个主机在 SCL 低电平时开始计时,最先结束低电平周期的主机将 SCL 拉高。其他主机若未完成低电平周期,会强制保持 SCL 低电平,实现时钟拉伸(Clock Stretching)。
| 场景 | 仲裁结果 | Zephyr 处理建议 |
|---|---|---|
| 两个主机发送相同地址 | 数据内容决定胜负 | 确保不同主机访问不同从机地址 |
| 主机与从机冲突 | 从机不会主动发起传输,不会参与仲裁 | 标准 I2C 设计,无需额外处理 |
| 时钟拉伸超时 | 从机拉低 SCL 超过 35 ms 视为超时 | 在 Zephyr 驱动中配置 I2C_TIMEOUT 或使用 KConfig 调整 |
4.6 Zephyr 中的 I2C 协议抽象
在 Zephyr OS 中,I2C 协议细节被封装在 struct i2c_driver_api 中。驱动开发者无需手动处理 START/STOP/ACK 时序,而是通过以下 API 完成操作:
// Zephyr I2C 核心 API 示例
#include <zephyr/drivers/i2c.h>
const struct device *i2c_dev = DEVICE_DT_GET(DT_NODELABEL(i2c1));
uint8_t buf[4];
uint16_t reg_addr = 0x10;
// 写操作:向从机地址 0x48 的寄存器 0x10 写入 4 字节
i2c_burst_write(i2c_dev, 0x48, reg_addr, buf, 4);
// 读操作:从从机地址 0x48 的寄存器 0x10 读取 4 字节
i2c_burst_read(i2c_dev, 0x48, reg_addr, buf, 4);
// 自定义事务:使用 i2c_transfer 实现重复起始
struct i2c_msg msgs[2];
msgs[0].buf = ®_addr;
msgs[0].len = 1;
msgs[0].flags = I2C_MSG_WRITE;
msgs[1].buf = buf;
msgs[1].len = 4;
msgs[1].flags = I2C_MSG_READ | I2C_MSG_RESTART;
i2c_transfer(i2c_dev, &msgs[0], 2, 0x48);
i2c_burst_read/write 时,驱动内部自动处理寄存器地址发送和重复起始条件。若需更精细控制(如 10 位地址、无寄存器地址的纯数据流),应使用 i2c_transfer 并手动构造 struct i2c_msg。
4.7 本章小结
- 物理层: 开漏 + 上拉电阻,SCL/SDA 两线制,支持多设备。
- 时序: START → 地址 → ACK → 数据 → ACK → STOP,数据在 SCL 高电平时采样。
- 地址: 7 位为主,10 位用于扩展;设备树中
reg属性定义地址。 - 读写: 写操作直接发送数据;读操作需先写寄存器地址,再通过 RESTART 切换方向。
- 多主机仲裁: 基于线与逻辑,低电平优先;Zephyr 需启用
CONFIG_I2C_MULTI_HOST。