4. I2C读写操作:单字节读写、多字节读写、复合操作
读写操作,是I2C协议里最核心的实战内容。说白了,你写驱动就是在跟这几个操作打交道。我刚开始做嵌入式时,总觉得I2C就是发个地址、读个数据,结果第一次调一个温湿度传感器就翻车了——读回来的数据全是0xFF。后来才发现,是复合操作的Stop条件没处理好。
这一节,咱们把I2C的读写操作拆开揉碎。从最简单的单字节,到常用的多字节,再到容易踩坑的复合操作,一个一个过。
4.1 单字节读写
单字节读写,是I2C通信的原子操作。你想想看,所有复杂操作都是基于它拼出来的。
4.1.1 单字节写
流程很简单:
- 主机发Start信号
- 发从机地址 + 写位(0)
- 等从机ACK
- 发寄存器地址
- 等从机ACK
- 发数据字节
- 等从机ACK
- 发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;
}
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;
}
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;
}
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);
}
4.4 知识结构图
下面这张图,把I2C读写操作的体系梳理清楚了:
4.5 实战避坑指南
做I2C驱动这么多年,我总结了几条血泪教训:
- 不要假设所有器件都支持复合操作——有些老器件必须在读操作前先Stop。我吃过这个亏,调了三天。
- 注意NACK的处理——读最后一个字节时,主机必须发NACK。内核帮你做了,但如果你自己用GPIO模拟I2C,千万别忘。
- 多字节读的len参数——别搞错。读2字节就传2,读4字节就传4。传错了,数据全乱。
- buf要保证连续——i2c_msg的buf必须是连续内存。用kmalloc分配,别用栈上的局部数组(除非你确定不会出问题)。
嗯,这一节的内容就到这儿。I2C读写操作,说白了就是Start、地址、数据、Stop这几个元素的排列组合。你只要把单字节搞透了,多字节和复合操作就是水到渠成的事。