3. I2C地址与读写操作:7位地址与10位地址、读/写位、从设备寻址流程

地址,是I2C总线上主设备找到从设备的唯一凭证。说白了,就像你家门牌号。主设备要跟哪个芯片通信,就得在总线上喊出它的地址。这个地址怎么编码、怎么发送、怎么区分读写,是每个I2C开发者必须啃透的基础。

我刚开始做I2C驱动时,就因为在地址上栽过跟头——明明从设备手册上写着0x50,我写进代码却死活不通。后来才发现,手册给的是7位地址,而我直接当8位用了。嗯,这种坑,咱们今天一次性填平。

3.1 7位地址结构

I2C标准地址长度是7位,范围从0x00到0x7F。但并不是所有地址都能用。I2C规范保留了一些特殊地址,比如0x00用于通用呼叫,0x01用于START字节等。实际可用的从设备地址,通常在0x08到0x77之间。

7位地址的发送格式是这样的:

  • 高7位:从设备地址(bit7~bit1)
  • 最低位(bit0):读写控制位(R/W)

所以,当你从手册上看到某个传感器的地址是0x48时,它指的是7位地址。但在总线上实际发送的第一个字节,是 (0x48 << 1) | R/W。左移一位,再拼上读写位。

关键点:7位地址左移1位后,占bit7~bit1;bit0是R/W位。这是初学者最容易搞混的地方。

3.2 10位地址格式

7位地址最多支持112个设备。对于复杂系统来说,这显然不够用。所以I2C规范在扩展模式下支持10位地址。10位地址的发送分两个阶段:

  1. 第一个字节:高5位固定为11110,接着是10位地址的高2位(bit9~bit8),最后是R/W位。格式:11110 A9 A8 R/W
  2. 第二个字节:10位地址的低8位(bit7~bit0)

举个例子,如果从设备10位地址是0x2A4(二进制10 1010 0100):

  • 高2位:10
  • 低8位:1010 0100
  • 第一个字节:11110 10 R/W → 0xF4(写)或0xF5(读)
  • 第二个字节:1010 0100 → 0xA4

我的经验:10位地址在实际项目中用得不多。大多数传感器、EEPROM、ADC都只用7位地址。但如果你在做多路复用器或复杂背板系统,10位地址就很有用了。我曾经在一个服务器主板上同时挂了16个温度传感器,7位地址根本不够分,最后不得不启用10位模式。

3.3 读写位(R/W)

R/W位决定这次传输的方向:

R/W值 方向 说明
0 写 主设备发送数据给从设备
1 读 从设备发送数据给主设备

R/W位是地址字节的最低位。所以,同一个从设备,写地址和读地址相差1。比如7位地址0x48:

  • 写地址字节:0x48 << 1 | 0 = 0x90
  • 读地址字节:0x48 << 1 | 1 = 0x91

很多芯片手册会直接给出8位地址,比如写地址0x90、读地址0x91。这时候你就不用再左移了,直接用就行。但要注意区分——有些手册给的是7位,有些给的是8位。我建议你拿到新芯片时,先确认手册上写的是哪种格式。

避坑指南:我曾经在调试一个加速度传感器时,手册上写着"Slave Address = 0x1D"。我以为是7位地址,左移后变成0x3A,结果从设备死活没应答。折腾了两小时才发现,手册给的0x1D已经是左移后的8位地址。直接写0x1D就对了。所以,拿到手册先看地址说明,确认是7位还是8位。

3.4 从设备寻址流程

主设备怎么在总线上找到从设备?流程其实很清晰:

  1. 主设备产生START条件:SCL高电平时,SDA从高变低。
  2. 主设备发送地址字节:包含7位或10位地址 + R/W位。
  3. 主设备释放SDA:在第9个SCL时钟周期,从设备拉低SDA作为应答(ACK)。
  4. 如果收到ACK:地址匹配成功,开始数据传输。
  5. 如果收到NACK:没有设备响应这个地址,主设备可以发STOP终止,或者重试。

对于10位地址,流程稍微复杂一点:

  • 先发第一个字节(11110 + 高2位 + 写位)
  • 收到ACK后,再发第二个字节(低8位)
  • 再收ACK
  • 然后才能开始数据传输

如果是10位地址读操作,还需要一个额外的步骤:先发写方向来发送完整地址,然后发重复START条件,再发读方向的第一个字节。

核心要点:寻址的关键是ACK。没有ACK,说明从设备不在总线上、地址不对、或者从设备没准备好。调试时,先确认能不能收到ACK,这是I2C通信的"握手"。

3.5 寻址流程图

下面这张图把7位地址和10位地址的寻址流程画清楚了。我建议你把它存下来,写驱动时对照着看。

I2C从设备寻址流程图 I2C从设备寻址流程 主设备产生START 地址类型?7位 / 10位 7位 发送7位地址+R/W 10位 发送11110+A9:A8+W 收到ACK? 是 开始数据传输 否 发送STOP或重试 收到ACK? 是 发送低8位地址 收到ACK? 是 开始数据传输 说明 10位地址读操作需先发写方向 再发重复START+读方向

3.6 地址冲突与避免

实际项目中,多个从设备可能共用同一个地址。比如两个不同型号的传感器,地址都是0x48。这时候怎么办?

常见解决方案:

  • 硬件地址引脚:很多芯片有ADDR引脚,通过拉高或拉低来改变地址。比如AT24C02 EEPROM,三个地址引脚可以组合出8个不同地址。
  • 多路复用器:用I2C多路复用器(如TCA9548A)分时选通不同的总线分支。
  • 软件轮询:如果设备支持,可以通过软件命令切换地址。

我的习惯:设计电路时,我会提前规划好所有I2C设备的地址,确保不冲突。如果实在避不开,优先用硬件地址引脚。多路复用器是最后的选择——它会增加延迟和复杂度。我曾经在一个项目里用了三个TCA9548A,调试时总线拓扑搞得我头大。

3.7 代码示例:Linux内核中地址处理

在Linux内核I2C驱动中,地址处理是自动的。你只需要告诉内核从设备的7位地址,它会帮你处理好左移和R/W位。看个例子:

#include <linux/i2c.h>

static struct i2c_board_info my_i2c_devices[] = {
    {
        I2C_BOARD_INFO("my_sensor", 0x48),  // 7位地址0x48
        .platform_data = &sensor_platform_data,
    },
};

// 或者动态创建客户端
struct i2c_client *client;
client = i2c_new_client_device(adapter, &info);
if (IS_ERR(client)) {
    dev_err(dev, "Failed to create i2c client\n");
    return PTR_ERR(client);
}

// 读写操作时,内核自动处理地址
int ret;
u8 reg = 0x10;
u8 value;

// 写:先发寄存器地址,再发数据
struct i2c_msg msg_w = {
    .addr = client->addr,  // 内核已经存了7位地址
    .flags = 0,            // 写操作
    .len = 1,
    .buf = &reg,
};

// 读:发寄存器地址后,重新开始读
struct i2c_msg msg_r = {
    .addr = client->addr,
    .flags = I2C_M_RD,     // 读操作
    .len = 1,
    .buf = &value,
};

ret = i2c_transfer(adapter, &msg_w, 1);
if (ret != 1) {
    dev_err(dev, "Write failed\n");
    return -EIO;
}

ret = i2c_transfer(adapter, &msg_r, 1);
if (ret != 1) {
    dev_err(dev, "Read failed\n");
    return -EIO;
}

注意看,client->addr 存的是7位地址。内核在发送时自动左移并加上R/W位。你不需要手动处理。但如果你在裸机或MCU上写驱动,就得自己拼地址字节了。

避坑指南:我曾经在移植一个传感器驱动时,发现读回来的数据全是0xFF。用逻辑分析仪抓波形,发现地址字节发的是0x90(写地址),但读操作时应该发0x91。检查代码才发现,我在读操作前手动左移了地址,但内核已经帮我左移过一次了。双重左移导致地址错误。所以,用内核API时,千万别自己处理地址移位。

3.8 小结

地址是I2C通信的起点。7位地址够用就用7位,不够用再考虑10位。读写位决定数据流向,ACK决定通信成败。写驱动时,搞清楚手册给的是7位还是8位地址,别在这个细节上翻车。