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”这一核心时序约束。
I2C总线物理连接拓扑 R R VDD SDA SCL 主设备 (Master) 从设备1 (Slave 0x50) 从设备2 (Slave 0x68) ● 所有设备SDA/SCL引脚均为开漏输出 ● 总线空闲时SDA和SCL均为高电平(由上拉电阻维持) ● 每个从设备有唯一7位或10位地址

3.2 总线时序与信号定义

I2C通信的每个bit由SCL时钟同步。数据在SCL高电平时被采样,在SCL低电平时允许变化。关键信号包括起始条件、停止条件和数据位传输。

信号 SCL状态 SDA行为 说明
起始条件 (S) 高电平 下降沿(高→低) SCL高电平时SDA由高变低,表示传输开始
停止条件 (P) 高电平 上升沿(低→高) SCL高电平时SDA由低变高,表示传输结束
数据位 低电平 允许变化 数据在SCL低电平时准备就绪
数据采样 高电平 稳定不变 接收方在SCL高电平时读取SDA电平
应答位 (ACK) 第9个时钟脉冲 接收方拉低 接收方在应答周期拉低SDA表示确认
常见驱动错误:在SCL高电平时改变SDA数据(除起始/停止条件外)会导致通信错误。驱动中必须确保数据在SCL低电平时更新,高电平时保持稳定。
I2C总线时序图(起始条件 + 数据位 + 应答) SCL SDA S 起始 D7 D6 D5 D4 D3 D2 D1 D0 ACK 应答 P 停止 ● 起始条件:SCL高电平时SDA由高→低;停止条件:SCL高电平时SDA由低→高 ● 数据在SCL高电平时被采样,在SCL低电平时允许变化;每字节后跟一个ACK位

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 写操作流程

  1. 起始条件 (S):主设备拉低SDA(SCL高电平时)。
  2. 发送从设备地址 + 写位 (0):主设备发送8位(7位地址 + R/W=0)。
  3. 从设备应答 (ACK):从设备拉低SDA表示确认。
  4. 发送寄存器地址:主设备发送1字节,指定要写入的内部寄存器或存储地址。
  5. 从设备应答 (ACK):从设备再次确认。
  6. 发送数据字节:主设备发送1字节数据。
  7. 从设备应答 (ACK):从设备确认接收。
  8. 重复步骤6~7:可连续发送多个数据字节。
  9. 停止条件 (P):主设备释放SDA(SCL高电平时SDA由低→高)。

3.4.2 读操作流程

  1. 起始条件 (S):主设备发起。
  2. 发送从设备地址 + 写位 (0):先以写模式访问从设备。
  3. 从设备应答 (ACK)。
  4. 发送寄存器地址:指定要读取的内部寄存器地址。
  5. 从设备应答 (ACK)。
  6. 重复起始条件 (Sr):主设备再次发起起始条件(不发送停止条件)。
  7. 发送从设备地址 + 读位 (1):切换为读模式。
  8. 从设备应答 (ACK)。
  9. 接收数据字节:主设备读取1字节数据,并在每个字节后发送ACK。
  10. 主设备发送NACK:读取最后一个字节后,主设备不拉低SDA(发送NACK),表示不再读取。
  11. 停止条件 (P):主设备发起停止条件。
I2C读写操作流程对比 写操作流程 ① 起始条件 (S) ② 发送地址 + W (0) ③ 从设备 ACK ④ 发送寄存器地址 ⑤ 从设备 ACK ⑥ 停止条件 (P) 读操作流程 ① 起始条件 (S) ② 发送地址 + W (0) ③ 从设备 ACK ④ 发送寄存器地址 ⑤ 重复起始 (Sr) ⑥ 停止条件 (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   = &reg_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设备模型响应正确后,再加载内核驱动。这可以快速区分是硬件模拟问题还是驱动代码问题。