4. I2C读写操作:单字节读写、多字节读写、复合操作

读写操作,是I2C协议里最核心的实战内容。说白了,你写驱动就是在跟这几个操作打交道。我刚开始做嵌入式时,总觉得I2C就是发个地址、读个数据,结果第一次调一个温湿度传感器就翻车了——读回来的数据全是0xFF。后来才发现,是复合操作的Stop条件没处理好。

这一节,咱们把I2C的读写操作拆开揉碎。从最简单的单字节,到常用的多字节,再到容易踩坑的复合操作,一个一个过。

4.1 单字节读写

单字节读写,是I2C通信的原子操作。你想想看,所有复杂操作都是基于它拼出来的。

4.1.1 单字节写

流程很简单:

  1. 主机发Start信号
  2. 发从机地址 + 写位(0)
  3. 等从机ACK
  4. 发寄存器地址
  5. 等从机ACK
  6. 发数据字节
  7. 等从机ACK
  8. 发Stop信号

代码实现(基于Linux内核):

static int i2c_write_byte(struct i2c_client *client, u8 reg, u8 data)
{
    struct i2c_msg msg;
    u8 buf[2];
    int ret;

    buf[0] = reg;
    buf[1] = data;

    msg.addr = client->addr;
    msg.flags = 0;          // 写操作
    msg.len = 2;
    msg.buf = buf;

    ret = i2c_transfer(client->adapter, &msg, 1);
    if (ret != 1) {
        dev_err(&client->dev, "单字节写失败\n");
        return -EIO;
    }
    return 0;
}
我的习惯:写操作时,buf[0]放寄存器地址,buf[1]放数据。这样一次transfer搞定,不用拆成两次。

4.1.2 单字节读

读操作稍微绕一点。你得先写寄存器地址,再重新发Start信号读数据。这就是所谓的「复合操作」的雏形。

static int i2c_read_byte(struct i2c_client *client, u8 reg, u8 *data)
{
    struct i2c_msg msg[2];
    int ret;

    // 第一条消息:写寄存器地址
    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = ®

    // 第二条消息:读数据
    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = 1;
    msg[1].buf = data;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret != 2) {
        dev_err(&client->dev, "单字节读失败\n");
        return -EIO;
    }
    return 0;
}
注意:读操作必须用两条消息。第一条写寄存器地址,第二条读数据。内核的i2c_transfer会自动在两条消息之间插入Repeated Start,而不是Stop+Start。这一点非常关键。

4.2 多字节读写

实际项目中,单字节读写效率太低。比如读一个16位的ADC值,或者配置一个传感器的多个寄存器,你肯定希望一次搞定。

4.2.1 多字节写

多字节写,就是在单字节写的基础上,连续发多个数据字节。从机收到每个字节后都会回ACK,直到主机发Stop。

static int i2c_write_block(struct i2c_client *client, u8 reg, 
                           u8 *data, u16 len)
{
    struct i2c_msg msg;
    u8 *buf;
    int ret;

    buf = kmalloc(len + 1, GFP_KERNEL);
    if (!buf)
        return -ENOMEM;

    buf[0] = reg;
    memcpy(buf + 1, data, len);

    msg.addr = client->addr;
    msg.flags = 0;
    msg.len = len + 1;
    msg.buf = buf;

    ret = i2c_transfer(client->adapter, &msg, 1);
    kfree(buf);

    if (ret != 1)
        return -EIO;
    return 0;
}

4.2.2 多字节读

多字节读也一样,先写寄存器地址,然后连续读多个字节。从机每发一个字节,主机要回ACK(除了最后一个字节回NACK)。

static int i2c_read_block(struct i2c_client *client, u8 reg, 
                          u8 *data, u16 len)
{
    struct i2c_msg msg[2];
    int ret;

    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 1;
    msg[0].buf = ®

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].len = len;
    msg[1].buf = data;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret != 2)
        return -EIO;
    return 0;
}
关键点:多字节读时,len参数决定了读多少个字节。内核的i2c-core会在最后一个字节自动发NACK,你不用手动处理。

4.3 复合操作(Combined Transaction)

复合操作,是I2C协议里最灵活、也最容易出问题的地方。它允许你在一次传输中,组合多个写和读操作,中间用Repeated Start连接,不释放总线。

4.3.1 什么是复合操作?

标准I2C协议允许这样的序列:

  • Start + 从机地址W + 数据... + Repeated Start + 从机地址R + 数据... + Stop

这就是复合操作。它和「Stop + Start」的区别在于:复合操作不释放总线,其他主机不能插入。这在多主系统中特别重要。

4.3.2 什么时候用复合操作?

我总结了几种典型场景:

场景 说明 示例
先写后读 先发寄存器地址,再读数据 传感器数据读取
写多个寄存器 连续写不同地址 配置DAC输出
读多个寄存器 连续读不同地址 读取系统状态
原子操作 需要保证操作不被中断 EEPROM页写入

4.3.3 复合操作的实现

在Linux内核中,复合操作就是i2c_msg数组。每个msg代表一个段,内核自动处理Repeated Start。

static int i2c_combined_transfer(struct i2c_client *client)
{
    struct i2c_msg msg[3];
    u8 write_buf1[] = {0x10, 0xAA};  // 写寄存器0x10,数据0xAA
    u8 write_buf2[] = {0x20};         // 写寄存器地址0x20
    u8 read_buf[2];                   // 读2字节

    // 段1:写操作
    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].len = 2;
    msg[0].buf = write_buf1;

    // 段2:写寄存器地址(准备读)
    msg[1].addr = client->addr;
    msg[1].flags = 0;
    msg[1].len = 1;
    msg[1].buf = write_buf2;

    // 段3:读数据
    msg[2].addr = client->addr;
    msg[2].flags = I2C_M_RD;
    msg[2].len = 2;
    msg[2].buf = read_buf;

    // 一次transfer,三个段
    return i2c_transfer(client->adapter, msg, 3);
}
我曾经踩过的坑:有一次我写一个音频Codec驱动,用了复合操作。结果发现每次读回来的数据都错位。查了两天才发现——从机不支持复合操作中的某些段组合。有些老器件,你必须在读之前先Stop再Start,不能用Repeated Start。所以,用复合操作前,一定先看数据手册!

4.4 知识结构图

下面这张图,把I2C读写操作的体系梳理清楚了:

I2C读写操作知识结构图 I2C读写操作知识体系 I2C读写操作 单字节读写 多字节读写 复合操作 写操作 读操作 连续写 连续读 先写后读 多段组合 核心要点 复合操作使用Repeated Start,不释放总线;多字节读写效率远高于单字节

4.5 实战避坑指南

做I2C驱动这么多年,我总结了几条血泪教训:

  • 不要假设所有器件都支持复合操作——有些老器件必须在读操作前先Stop。我吃过这个亏,调了三天。
  • 注意NACK的处理——读最后一个字节时,主机必须发NACK。内核帮你做了,但如果你自己用GPIO模拟I2C,千万别忘。
  • 多字节读的len参数——别搞错。读2字节就传2,读4字节就传4。传错了,数据全乱。
  • buf要保证连续——i2c_msg的buf必须是连续内存。用kmalloc分配,别用栈上的局部数组(除非你确定不会出问题)。
我的建议:写驱动时,先封装好单字节读写函数。然后基于它们封装多字节和复合操作。这样调试时,可以逐层排查问题。别一上来就搞复合操作,容易翻车。

嗯,这一节的内容就到这儿。I2C读写操作,说白了就是Start、地址、数据、Stop这几个元素的排列组合。你只要把单字节搞透了,多字节和复合操作就是水到渠成的事。