4. I2C数据传输模式:主设备发送/接收、从设备发送/接收、复合传输格式、重复起始条件

I2C总线上的数据传输严格遵循主-从架构,所有传输均由主设备发起并控制时钟信号(SCL)。理解四种基本传输模式以及复合传输与重复起始条件(Repeated START)是编写正确I2C设备驱动的核心基础。本章将逐一拆解这些模式的时序、数据流向与Linux内核中的对应实现。

4.1 基本传输模式概述

根据主设备与从设备的数据流向,I2C传输可分为四种基本模式。下表总结了各模式下的角色与数据方向:

模式 主设备角色 从设备角色 数据方向 典型应用场景
主设备发送 发送数据 接收数据 主 → 从 写入寄存器配置、发送命令
主设备接收 接收数据 发送数据 从 → 主 读取传感器数据、状态寄存器
从设备发送 接收数据 发送数据 从 → 主 从设备主动上报(极少见,需主设备配合)
从设备接收 发送数据 接收数据 主 → 从 从设备接收配置参数
驱动视角: 在Linux I2C驱动中,主设备发送与接收是最常用的两种模式。从设备模式通常用于实现I2C从设备控制器驱动,在嵌入式系统中较少直接由普通外设驱动处理。

4.2 主设备发送模式

主设备发送模式是I2C通信中最基本的写操作流程。主设备主动产生时钟并输出数据,从设备在每个字节后回复ACK信号。

4.2.1 时序流程

  1. START条件: SCL高电平时,SDA由高变低。
  2. 发送从设备地址(7位)+ 写位(0): 主设备发送地址字节,等待从设备ACK。
  3. 发送数据字节: 主设备逐字节发送数据,每字节后等待从设备ACK。
  4. STOP条件: SCL高电平时,SDA由低变高。
关键点: 从设备必须在每个字节的第9个SCL周期将SDA拉低以应答(ACK)。若从设备未应答(NACK),主设备应发送STOP终止传输。
主设备发送模式时序图 S 地址(7bit) + W 数据字节 P ACK ACK 主设备控制SCL,从设备在ACK周期拉低SDA

4.2.2 Linux内核代码示例

// 主设备发送模式:向从设备写入2字节数据
#include <linux/i2c.h>

int i2c_master_send_example(struct i2c_client *client)
{
    u8 buf[2] = {0x10, 0xFF}; // 寄存器地址 + 数据
    int ret;

    ret = i2c_master_send(client, buf, 2);
    if (ret < 0) {
        dev_err(&client->dev, "I2C send failed: %d\n", ret);
        return ret;
    }
    return 0;
}

4.3 主设备接收模式

主设备接收模式用于从从设备读取数据。主设备先发送地址和读位,然后切换为接收者角色,从设备开始发送数据。

4.3.1 时序流程

  1. START条件
  2. 发送从设备地址(7位)+ 读位(1): 等待从设备ACK。
  3. 接收数据字节: 主设备读取SDA上的数据,每字节后主设备发送ACK(继续接收)或NACK(最后一个字节)。
  4. STOP条件
常见错误: 在读取最后一个字节时,主设备必须发送NACK(将SDA保持高电平)以通知从设备停止发送,否则从设备会继续输出数据导致总线冲突。
主设备接收模式时序图 S 地址(7bit) + R 数据字节(从→主) P ACK NACK 主设备在最后一个字节发送NACK,通知从设备停止发送

4.3.2 Linux内核代码示例

// 主设备接收模式:从从设备读取2字节数据
int i2c_master_recv_example(struct i2c_client *client)
{
    u8 buf[2];
    int ret;

    ret = i2c_master_recv(client, buf, 2);
    if (ret < 0) {
        dev_err(&client->dev, "I2C recv failed: %d\n", ret);
        return ret;
    }
    dev_info(&client->dev, "Received: 0x%02X 0x%02X\n", buf[0], buf[1]);
    return 0;
}

4.4 复合传输格式与重复起始条件

复合传输(Combined Transfer)是I2C协议中最强大的特性之一,它允许在一次总线事务中切换数据方向,而无需释放总线。其核心机制是重复起始条件(Repeated START,Sr)。

4.4.1 重复起始条件定义

重复起始条件是指在一次传输过程中,在发送STOP条件之前,再次产生一个START条件。这允许主设备在不失去总线控制权的情况下改变通信方向或切换从设备。

4.4.2 典型复合传输:先写后读

这是传感器驱动中最常见的模式:先向从设备写入寄存器地址,然后立即读取该寄存器的值。

  1. START条件
  2. 发送从设备地址 + 写位(0) → 从设备ACK
  3. 发送寄存器地址字节 → 从设备ACK
  4. 重复起始条件(Sr) → 不发送STOP
  5. 发送相同从设备地址 + 读位(1) → 从设备ACK
  6. 接收数据字节 → 主设备NACK(最后字节)
  7. STOP条件
工程价值: 使用重复起始条件可以保证读操作的原子性,避免在写地址和读数据之间其他主设备篡改从设备的内部指针。在Linux内核中,这通过i2c_transfer()函数配合struct i2c_msg数组实现。
复合传输:先写寄存器地址后读数据(重复起始) S 地址+W 寄存器地址 Sr 地址+R 数据(从→主) P ACK ACK ACK NACK 重复起始条件(Sr)在寄存器地址之后、读地址之前产生,不释放总线 保证从设备内部指针在写地址和读数据之间不被其他主设备修改 Linux驱动中通过 i2c_transfer() 发送 msg[0](写)和 msg[1](读)实现

4.4.3 Linux内核复合传输实现

// 复合传输:先写寄存器地址,再读数据(使用重复起始条件)
int i2c_combined_transfer(struct i2c_client *client, u8 reg_addr)
{
    struct i2c_msg msgs[2];
    u8 wbuf[1] = {reg_addr};
    u8 rbuf[1] = {0};
    int ret;

    // msg[0]: 写寄存器地址
    msgs[0].addr = client->addr;
    msgs[0].flags = 0;          // 写标志
    msgs[0].len = 1;
    msgs[0].buf = wbuf;

    // msg[1]: 读寄存器值
    msgs[1].addr = client->addr;
    msgs[1].flags = I2C_M_RD;   // 读标志
    msgs[1].len = 1;
    msgs[1].buf = rbuf;

    ret = i2c_transfer(client->adapter, msgs, 2);
    if (ret < 0) {
        dev_err(&client->dev, "Combined transfer failed: %d\n", ret);
        return ret;
    }
    dev_info(&client->dev, "Register 0x%02X = 0x%02X\n", reg_addr, rbuf[0]);
    return 0;
}

4.5 从设备发送与接收模式

从设备模式通常由I2C控制器IP核的从机模式实现,在Linux中对应i2c-slave子系统。普通外设驱动极少直接实现从设备逻辑,但理解其机制有助于调试总线问题。

4.5.1 从设备接收(Slave Receiver)

  • 从设备检测到自身地址+写位后,开始接收主设备发送的数据。
  • 每接收一个字节,从设备需在ACK周期拉低SDA。
  • 若从设备无法及时处理数据,可发送NACK请求主设备停止。

4.5.2 从设备发送(Slave Transmitter)

  • 从设备检测到自身地址+读位后,开始向主设备发送数据。
  • 主设备每接收一个字节后发送ACK(继续)或NACK(停止)。
  • 从设备需在SCL低电平期间准备下一个字节的数据。
调试技巧: 使用逻辑分析仪抓取I2C总线时,注意观察从设备是否在地址阶段正确应答ACK。若从设备未应答(SDA保持高),通常表明地址错误、从设备未上电或总线被拉死。

4.6 四种模式对比总结

特性 主设备发送 主设备接收 从设备发送 从设备接收
发起者 主设备 主设备 从设备(被动) 从设备(被动)
时钟控制 主设备 主设备 主设备 主设备
数据流向 主 → 从 从 → 主 从 → 主 主 → 从
地址位R/W 0(写) 1(读) 1(读) 0(写)
ACK控制者 从设备 主设备 主设备 从设备
Linux API i2c_master_send() i2c_master_recv() i2c_slave_event() i2c_slave_event()
驱动使用频率 极高 极高 极低 极低

4.7 工程实践要点

  • 原子操作: 对于多字节读写,优先使用复合传输(重复起始)而非分开的写/读操作,避免总线被其他主设备抢占。
  • 错误处理: 检查i2c_transfer()返回值是否等于消息数量(num),小于0表示总线错误,等于0表示NACK。
  • 超时机制: 在驱动中为I2C操作设置合理的超时时间(通常20ms~100ms),防止从设备无响应时系统挂死。
  • 时钟延展: 部分从设备支持时钟延展(Clock Stretching),主设备驱动需正确处理SCL被从设备拉低的情况。
避坑指南: 不要在I2C中断上下文中调用i2c_transfer(),因为I2C控制器驱动可能依赖调度器或等待队列。应在进程上下文(如工作队列、线程)中执行I2C传输。