3. I2C协议基础:I2C总线物理层、时序、地址、读写操作流程
I2C(Inter-Integrated Circuit)总线是一种由Philips公司(现NXP)开发的同步、多主从、串行通信总线。它仅需两根线(SDA数据线、SCL时钟线)即可完成设备间的数据交换,是嵌入式系统中连接传感器、EEPROM、RTC、ADC等外设最常用的总线之一。在QEMU模拟I2C设备时,精确理解其物理层与时序是驱动验证的前提。
3.1 物理层与电气特性
I2C总线由两根双向开漏线构成,外部需接上拉电阻至VDD(通常为3.3V或5V)。
- SDA(Serial Data Line):数据线,用于传输地址和数据。
- SCL(Serial Clock Line):时钟线,由主设备产生,同步数据传输。
- 开漏输出:设备只能将总线拉低(输出0),释放总线由上拉电阻拉高(输出1)。
- 线与逻辑:多个设备可同时连接,任一设备拉低则总线为低电平。
QEMU模拟要点:在QEMU的I2C模型中,物理层的开漏特性被抽象为电平状态机。驱动开发者无需关心实际电阻值,但必须理解“SCL高电平时采样SDA”这一核心时序约束。
3.2 总线时序与信号定义
I2C通信的每个bit由SCL时钟同步。数据在SCL高电平时被采样,在SCL低电平时允许变化。关键信号包括起始条件、停止条件和数据位传输。
| 信号 | SCL状态 | SDA行为 | 说明 |
|---|---|---|---|
| 起始条件 (S) | 高电平 | 下降沿(高→低) | SCL高电平时SDA由高变低,表示传输开始 |
| 停止条件 (P) | 高电平 | 上升沿(低→高) | SCL高电平时SDA由低变高,表示传输结束 |
| 数据位 | 低电平 | 允许变化 | 数据在SCL低电平时准备就绪 |
| 数据采样 | 高电平 | 稳定不变 | 接收方在SCL高电平时读取SDA电平 |
| 应答位 (ACK) | 第9个时钟脉冲 | 接收方拉低 | 接收方在应答周期拉低SDA表示确认 |
常见驱动错误:在SCL高电平时改变SDA数据(除起始/停止条件外)会导致通信错误。驱动中必须确保数据在SCL低电平时更新,高电平时保持稳定。
3.3 设备地址与寻址机制
I2C总线上的每个从设备都有一个唯一的地址。标准模式使用7位地址,支持最多112个节点(保留16个地址)。扩展模式支持10位地址。
- 7位地址:地址范围 0x00 ~ 0x7F,其中 0x00 为通用呼叫地址,0x01~0x07 保留。
- 10位地址:由首字节 11110XX 标识,后跟第二个字节的低8位。
- 读写位 (R/W):地址字节的最低位。0 表示写操作,1 表示读操作。
| 地址类型 | 首字节格式 | 地址范围 | 典型设备 |
|---|---|---|---|
| 7位标准 | A6 A5 A4 A3 A2 A1 A0 R/W | 0x00 ~ 0x7F | AT24C02 (0x50), MPU6050 (0x68) |
| 10位扩展 | 1111 0 A9 A8 R/W + A7~A0 | 0x00 ~ 0x3FF | 部分高精度ADC、数字电位器 |
QEMU地址配置:在QEMU设备模型中,从设备地址通过
i2c_slave_init() 的 address 参数设置。驱动验证时需确保QEMU命令行或设备树中指定的地址与驱动代码中的地址一致。
3.4 读写操作流程
I2C通信以主设备发起起始条件开始,以主设备发起停止条件结束。每次传输包含地址字节、数据字节和应答位。以下是标准读写流程。
3.4.1 写操作流程
- 起始条件 (S):主设备拉低SDA(SCL高电平时)。
- 发送从设备地址 + 写位 (0):主设备发送8位(7位地址 + R/W=0)。
- 从设备应答 (ACK):从设备拉低SDA表示确认。
- 发送寄存器地址:主设备发送1字节,指定要写入的内部寄存器或存储地址。
- 从设备应答 (ACK):从设备再次确认。
- 发送数据字节:主设备发送1字节数据。
- 从设备应答 (ACK):从设备确认接收。
- 重复步骤6~7:可连续发送多个数据字节。
- 停止条件 (P):主设备释放SDA(SCL高电平时SDA由低→高)。
3.4.2 读操作流程
- 起始条件 (S):主设备发起。
- 发送从设备地址 + 写位 (0):先以写模式访问从设备。
- 从设备应答 (ACK)。
- 发送寄存器地址:指定要读取的内部寄存器地址。
- 从设备应答 (ACK)。
- 重复起始条件 (Sr):主设备再次发起起始条件(不发送停止条件)。
- 发送从设备地址 + 读位 (1):切换为读模式。
- 从设备应答 (ACK)。
- 接收数据字节:主设备读取1字节数据,并在每个字节后发送ACK。
- 主设备发送NACK:读取最后一个字节后,主设备不拉低SDA(发送NACK),表示不再读取。
- 停止条件 (P):主设备发起停止条件。
3.5 数据传输格式与代码示例
在Linux内核中,I2C驱动通过 struct i2c_msg 描述一次传输,使用 i2c_transfer() 或 i2c_smbus_read/write 系列函数完成通信。以下是典型的读写操作代码片段。
/* I2C写操作:向从设备0x50的寄存器0x10写入数据0xAB */
#include <linux/i2c.h>
int i2c_write_example(struct i2c_client *client)
{
struct i2c_msg msg;
uint8_t buf[2];
buf[0] = 0x10; /* 寄存器地址 */
buf[1] = 0xAB; /* 要写入的数据 */
msg.addr = client->addr; /* 从设备地址,如0x50 */
msg.flags = 0; /* 写操作 */
msg.len = 2;
msg.buf = buf;
return i2c_transfer(client->adapter, &msg, 1);
}
/* I2C读操作:从从设备0x50的寄存器0x10读取1字节 */
int i2c_read_example(struct i2c_client *client, uint8_t *value)
{
struct i2c_msg msg[2];
uint8_t reg_addr = 0x10;
/* 第一条消息:写寄存器地址 */
msg[0].addr = client->addr;
msg[0].flags = 0; /* 写 */
msg[0].len = 1;
msg[0].buf = ®_addr;
/* 第二条消息:读取数据 */
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; /* 读 */
msg[1].len = 1;
msg[1].buf = value;
return i2c_transfer(client->adapter, msg, 2);
}
QEMU验证提示:在QEMU中模拟I2C设备时,驱动通过
i2c_transfer() 发送的消息会被QEMU的I2C总线模型捕获,并转发给对应的从设备模拟器。开发者可在QEMU的从设备模型中添加 printf 或日志输出,实时观察地址、数据和ACK的交互过程。
3.6 常见问题与避坑指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 从设备无应答 (NACK) | 地址错误、设备未上电、总线冲突 | 核对设备地址(含读写位);检查QEMU设备模型是否注册 |
| 数据错位或丢失 | 时序不匹配、时钟延展未处理 | 确保SCL低电平时更新SDA;检查从设备是否支持时钟延展 |
| 总线死锁 (SDA一直为低) | 从设备未释放SDA、未发送停止条件 | 发送9个时钟脉冲复位从设备;检查驱动是否遗漏停止条件 |
| QEMU中通信超时 | 模拟器时钟频率与驱动不匹配 | 调整QEMU的I2C总线频率参数;使用 i2c_smbus 接口简化时序 |
关键提醒:在QEMU平台下验证I2C驱动时,建议先使用
i2cget、i2cset 等用户空间工具测试基本通信,确认QEMU设备模型响应正确后,再加载内核驱动。这可以快速区分是硬件模拟问题还是驱动代码问题。