I2C 寻址机制:7 位地址与 10 位地址、广播地址、保留地址、地址冲突检测

I2C 的寻址,说白了就是主设备怎么叫醒它想通信的那个从设备。总线上挂着一堆器件,主设备喊一声,只有地址匹配的那个才会应答。这个机制看起来简单,但坑不少。我早年调一块板子,I2C 总线上挂了 4 个传感器,结果死活只认出来 3 个。查了半天,发现是地址冲突——两个器件用了同一个地址。嗯,从那以后我每次画原理图都会先拉一张地址分配表。

7 位地址结构

标准 I2C 用 7 位地址。主设备发送起始条件后,先发 7 位地址 + 1 位读写标志。从设备收到后,如果地址匹配,就在第 9 个 SCL 周期拉低 SDA 作为应答。

7 位地址的范围是 0x00 ~ 0x7F。但别高兴太早,不是所有地址都能用。实际可用的地址大概在 0x08 ~ 0x77 之间,因为两头都被保留地址占了。

地址格式速记:

  • 7 位地址 + R/W 位 = 第一个字节
  • R/W = 0 表示写,R/W = 1 表示读
  • 例如:地址 0x50(写)→ 发送 0xA0

10 位地址扩展

7 位地址最多挂 112 个器件,有些场景不够用。所以 I2C 规范定义了 10 位地址。怎么区分 7 位和 10 位?看第一个字节的高 5 位。如果高 5 位是 11110,那就是 10 位地址模式。

10 位地址的发送分两步:

  1. 第一个字节:11110 + 地址高 2 位 + R/W
  2. 第二个字节:地址低 8 位

我个人习惯,能用 7 位地址就别用 10 位。10 位地址的兼容性问题比较多,有些老器件或者廉价 MCU 的 I2C 外设根本不支持。我在一个项目里用过一次 10 位地址,结果发现某款国产 MCU 的 I2C 模块在硬件上就不支持,最后只能软件模拟。

特性 7 位地址 10 位地址
地址字节数 1 字节 2 字节
最大设备数 112 1024
兼容性 所有器件支持 部分器件不支持
传输效率 高 略低

广播地址与通用呼叫

地址 0x00 是广播地址,也叫通用呼叫地址。主设备发送 0x00 时,所有从设备都应该应答。这个功能一般用来让主设备广播一些全局信息,比如复位所有从设备、更新固件之类的。

但说实话,我在实际项目中很少用广播地址。为什么?因为不是所有从设备都实现了通用呼叫处理。有些器件收到 0x00 后直接忽略,有些会应答但什么都不做。你想想看,如果总线上挂了 5 个器件,只有 3 个应答了广播,那剩下的 2 个怎么办?

注意:使用广播地址前,务必确认所有从设备的数据手册都明确支持通用呼叫功能。否则可能出现部分设备响应、部分设备不响应的混乱局面。

保留地址一览

I2C 规范保留了一些特殊地址。这些地址不能分配给普通器件。我列个表,你调试时对照着看:

地址(7 位) 用途 说明
0x00 通用呼叫 广播地址
0x01 起始字节 用于慢速 MCU 检测总线
0x02 ~ 0x07 保留 未来扩展用
0x78 ~ 0x7B 10 位地址前缀 11110xx 模式
0x7C ~ 0x7F 保留 测试/特殊用途

我曾经遇到过一个坑:某款温度传感器默认地址是 0x48,但数据手册没写清楚它其实占用了 0x48 和 0x49 两个地址(因为内部有两个通道)。结果我在总线上又挂了一个地址为 0x49 的 EEPROM,两个器件直接打架。所以看数据手册时,一定要确认器件到底占用了几个地址。

地址冲突检测

地址冲突是 I2C 调试中最常见的问题之一。怎么检测?我总结了几种方法:

  • 逻辑分析仪抓波形:这是最直接的方法。抓取起始条件后的第一个字节,看看主设备发了什么地址,从设备有没有应答。
  • 逐个断开测试:先把所有从设备断开,然后逐个接上,看哪个地址出现重复。
  • 读器件 ID:很多现代 I2C 器件都有器件 ID 寄存器,可以读出来确认身份。

我的调试习惯:画原理图时,我会在每路 I2C 器件的 SDA/SCL 上预留测试点。调试时用示波器或逻辑分析仪直接勾上去看波形,比靠代码打印日志快得多。

地址冲突的典型表现:主设备发送地址后,SDA 没有被拉低(没有应答),或者应答位出现毛刺。为什么会这样?因为两个从设备都试图应答,一个拉低 SDA,另一个也拉低,结果 SDA 电平不确定。

SVG:I2C 寻址流程与地址空间结构

I2C 寻址机制总览 主设备 发送地址字节 7 位地址模式 高 5 位 ≠ 11110 10 位地址模式 高 5 位 = 11110 从设备应答 地址空间分布 0x00 广播 | 0x01~0x07 保留 | 0x08~0x77 可用地址 | 0x78~0x7F 保留/10位前缀

实际调试中的地址问题

我遇到过几种典型的地址问题,列出来给你参考:

  • 地址写错了:数据手册给的地址可能是 7 位值,但代码里需要左移一位。比如器件地址是 0x50,发送时要发 0xA0。我见过有人直接发 0x50,结果总线一直没应答。
  • 地址引脚配置不对:很多 I2C 器件有地址选择引脚(A0/A1/A2)。如果这些引脚悬空或者电平不对,地址就会偏移。我调一个音频编解码器时,发现 A0 引脚虚焊,地址从 0x1A 变成了 0x1B,折腾了半天。
  • 多个器件地址相同:有些器件没有地址选择引脚,或者地址选择范围不够。比如某款加速度计只有 2 个可选地址,但板子上要放 3 个。这时候只能用 I2C 多路复用器(如 PCA9548)来扩展。

避坑指南:我曾经在一个项目里用了 4 片同样的温度传感器,每片通过 A0/A1 引脚设置不同地址。结果生产时贴片厂把其中两片的 A0 焊错了,导致地址冲突。从那以后,我要求产线在贴片后必须做 I2C 地址扫描测试,确保每个地址唯一。

地址扫描代码示例

调试时我经常写一段地址扫描代码,看看总线上到底有哪些器件。下面是一个简单的 I2C 扫描函数:

void i2c_scan(void)
{
    uint8_t addr;
    uint8_t data = 0;

    printf("I2C Bus Scanning...\n");

    for (addr = 0x01; addr < 0x7F; addr++)
    {
        // 跳过保留地址
        if (addr < 0x08 || addr > 0x77)
            continue;

        i2c_start();
        // 发送地址 + 写标志
        if (i2c_write(addr << 1) == I2C_ACK)
        {
            printf("Found device at 0x%02X\n", addr);
            i2c_stop();
        }
        else
        {
            i2c_stop();
        }
    }

    printf("Scan complete.\n");
}

这段代码会遍历所有可用地址,发送地址字节后检查是否有应答。有应答就打印地址,没应答就跳过。我在调试新板子时,第一件事就是跑这个扫描函数,看看实际挂载的器件和原理图是否一致。

小技巧:扫描时注意保留地址区域(0x00~0x07 和 0x78~0x7F)。有些从设备对保留地址的访问会做出异常响应,甚至导致总线锁死。我一般只扫描 0x08~0x77 这个安全区间。

好了,I2C 寻址这块就聊这么多。记住一句话:地址对了,通信就成功了一半。下次你调 I2C 时,先拿逻辑分析仪看看地址字节对不对,能省下大把时间。