2. I2C协议基础:起始条件、停止条件、数据有效性、字节格式与应答机制
I2C(Inter-Integrated Circuit)总线协议由Philips(现NXP)设计,是一种同步、多主从、串行通信总线。理解其底层信号时序是驱动开发与故障排查的基石。本章将拆解I2C协议的核心原子操作:起始/停止条件、数据采样窗口、字节传输格式以及应答机制。
2.1 总线空闲状态与电气特性
在讨论具体时序前,需明确总线的默认状态:
- SCL(时钟线):空闲时为高电平,由主机驱动。
- SDA(数据线):空闲时为高电平,通过上拉电阻保持。
- 线-与逻辑:任何设备拉低SDA或SCL都会使总线变为低电平。这是时钟拉伸和仲裁的物理基础。
驱动提示: 在初始化I2C控制器时,务必确认GPIO配置为开漏输出,并检查外部上拉电阻(通常4.7kΩ)是否焊接正常。缺少上拉电阻会导致总线卡死在低电平,无法产生起始条件。
2.2 起始条件(START Condition)与停止条件(STOP Condition)
起始和停止条件是I2C通信的“帧定界符”,由主机独占产生。
| 条件 | 信号序列 | 驱动代码关键点 |
|---|---|---|
| 起始条件 (S) | SCL保持高电平时,SDA从高电平切换到低电平 | 必须先拉高SCL,再拉低SDA;需满足tHD;STA保持时间 |
| 停止条件 (P) | SCL保持高电平时,SDA从低电平切换到高电平 | 必须先拉低SDA,再释放SCL;需满足tSU;STO建立时间 |
| 重复起始条件 (Sr) | 在数据传输中,不产生停止条件,直接产生起始条件 | 用于改变数据方向(如读寄存器前先写地址) |
总线故障场景: 如果从机时钟拉伸导致SCL被拉低,主机无法产生停止条件。此时总线会永久阻塞。驱动中必须实现超时机制,在SCL低电平超过阈值(如25ms)时强制复位总线状态机。
2.3 数据有效性(Data Validity)
I2C协议对数据采样有严格规定,这是驱动中SCL时序控制的依据:
- 数据稳定期:SCL为高电平时,SDA上的数据必须保持稳定。
- 数据变化期:SCL为低电平时,SDA允许变化(准备下一位数据)。
- 例外:起始和停止条件利用SCL高电平时的SDA跳变,不属于数据传输。
驱动实现要点: 在SCL低电平时更新SDA输出寄存器,在SCL高电平中间点采样SDA输入。大多数硬件I2C控制器自动处理此逻辑;若使用GPIO模拟I2C(Bit-Bang),必须严格按此顺序编写延时循环。
2.4 字节格式与数据传输
I2C总线以字节(8位)为基本传输单元,高位(MSB)在前。每传输一个字节后,必须跟一个应答位(ACK/NACK)。
| 字段 | 位数 | 说明 |
|---|---|---|
| 从机地址 | 7位 或 10位 | 标准模式为7位地址,后跟1位读写方向位 |
| 读写位 (R/W) | 1位 | 0 = 主机写(发送),1 = 主机读(接收) |
| 数据字节 | 8位 × N | 每个字节后跟一个应答位,无字节数上限 |
| 应答位 (ACK) | 1位 | 接收方在第9个SCL周期拉低SDA表示应答 |
2.5 应答机制(ACK/NACK)
应答是I2C协议可靠性的核心。每个字节传输完成后,接收方必须反馈应答信号:
- ACK (Acknowledge):接收方在第9个SCL周期拉低SDA。表示“已收到,请继续发送下一字节”。
- NACK (Not Acknowledge):接收方在第9个SCL周期保持SDA高电平。表示“无法接收更多数据”或“未找到设备”。
2.5.1 应答在驱动中的典型场景
| 场景 | 发送方 | 接收方 | 驱动处理 |
|---|---|---|---|
| 主机发送地址 | 主机 | 从机 | 若收到NACK,说明从机不存在或忙,驱动应重试或报错 |
| 主机发送数据 | 主机 | 从机 | 若收到NACK,从机无法处理数据,主机应停止传输 |
| 主机接收数据 | 从机 | 主机 | 主机在最后一个字节发送NACK,通知从机停止发送 |
驱动最佳实践: 在Linux I2C适配器驱动中,应答处理通常由硬件控制器自动完成。但在GPIO模拟模式下,必须在第9个SCL周期释放SDA并采样其电平。采样超时(如SCL被从机拉低)应触发错误恢复流程。
2.6 完整数据帧示例:写寄存器操作
以下是一个典型的I2C写操作帧结构,用于向从机设备写入一个寄存器地址和数据:
| S | 从机地址 (7位) | W (0) | ACK | 寄存器地址 (8位) | ACK | 数据字节 (8位) | ACK | P |
对应的Linux用户态操作(通过i2c-dev接口):
#include <linux/i2c-dev.h>
#include <sys/ioctl.h>
struct i2c_msg msgs[1];
struct i2c_rdwr_ioctl_data msgset;
unsigned char buf[2] = {0x10, 0xA5}; // 寄存器地址0x10, 数据0xA5
msgs[0].addr = 0x50; // 从机地址
msgs[0].flags = 0; // 写操作
msgs[0].len = 2;
msgs[0].buf = buf;
msgset.msgs = msgs;
msgset.nmsgs = 1;
if (ioctl(fd, I2C_RDWR, &msgset) < 0) {
perror("I2C write failed");
// 处理NACK或总线错误
}
故障排查提示: 如果ioctl返回-EIO或-EREMOTEIO,通常表示从机返回了NACK。常见原因包括:地址错误、从机处于睡眠状态、寄存器地址越界、或总线被其他设备占用。使用逻辑分析仪抓取SDA/SCL波形是定位问题的最直接手段。