3、I2C地址与寻址:7位地址与10位地址格式、广播地址、保留地址、通用呼叫地址,地址冲突的排查方法

说到I2C通信,地址寻址这块儿,我估计不少朋友都踩过坑。我自己刚入行那会儿,就因为在地址上犯了个低级错误,折腾了整整两天。说白了,I2C的地址机制是整个协议的核心骨架,搞不懂它,你连设备都叫不醒。

3.1 7位地址格式:最常用的寻址方式

I2C总线上的每个从设备,都有一个唯一的地址。最常见的格式就是7位地址。你想想看,7位能表示多少个地址?2的7次方,128个。但实际能用的没那么多,后面会讲。

7位地址的传输过程是这样的:主机发送起始条件后,紧跟着发送一个字节。这个字节的高7位就是地址,最低位是读写位(0表示写,1表示读)。

关键点:7位地址在总线上是以8位形式传输的。比如设备地址是0x50,实际发送的是0xA0(写操作)或0xA1(读操作)。

我习惯把7位地址叫做「原生地址」。大部分传感器、EEPROM、ADC芯片用的都是这种格式。比如常见的AT24C02 EEPROM,它的7位地址是0x50(1010000),加上读写位后变成0xA0/0xA1。

// 7位地址示例:向地址0x48(温度传感器)写配置
// 实际发送的字节 = (0x48 << 1) | 0 = 0x90
uint8_t addr = 0x48;
uint8_t write_byte = (addr << 1) | 0;  // 0x90
i2c_start();
i2c_send_byte(write_byte);
// 等待ACK...

我的习惯:在代码里,我通常直接用7位地址定义宏,然后在发送时左移一位。这样代码可读性更好,也方便调试。

3.2 10位地址格式:扩展寻址空间

7位地址只有128个,随着I2C设备越来越多,明显不够用了。于是就有了10位地址格式。说实话,我在实际项目中用到10位地址的情况不多,但一旦遇到,处理起来确实要小心。

10位地址的传输分两个阶段:

  1. 第一个字节:高5位固定为11110,接着是10位地址的高2位,最后是读写位。
  2. 第二个字节:剩下的8位地址。
位序 7 6 5 4 3 2 1 0
字节1 1 1 1 1 0 A9 A8 R/W
字节2 A7 A6 A5 A4 A3 A2 A1 A0

举个例子,10位地址0x2B4(二进制10 1011 0100):

  • 高2位是10,所以第一个字节 = 1111 0 10 + R/W
  • 低8位是1011 0100,即第二个字节 = 0xB4

注意:10位地址和7位地址不能混用在同一总线上?其实可以,但主机必须明确知道哪个设备是10位地址。我曾经在一个项目里混用过,结果调试时把自己绕晕了。建议能不用10位地址就别用,除非迫不得已。

3.3 广播地址与通用呼叫地址

广播地址,也叫通用呼叫地址,是0x00。当主机发送地址0x00时,理论上所有支持广播的从设备都应该响应。这个功能在实际中用的不多,但确实存在。

通用呼叫地址的用途:

  • 复位从设备:发送特定数据让所有设备复位
  • 写入配置:同时配置多个相同型号的设备
  • 查询状态:让所有设备报告自己的状态

我记得有一次,需要同时配置16个相同的传感器。一个一个写配置太慢了,我就用了广播地址。嗯,这里要注意,不是所有设备都支持广播,用之前一定要看数据手册。

// 通用呼叫示例:复位所有支持广播的从设备
i2c_start();
i2c_send_byte(0x00);  // 广播地址
i2c_send_byte(0x06);  // 复位命令(由具体设备定义)
i2c_stop();

3.4 保留地址:哪些地址不能用?

I2C协议规定了一些保留地址,这些地址不能分配给普通设备。说白了,就是系统预留的「专用通道」。

地址(7位) 用途 说明
0x00 通用呼叫地址 广播给所有设备
0x01 起始字节 用于慢速设备
0x02 CBUS地址 保留给CBUS协议
0x03-0x07 保留 未来扩展使用
0x78-0x7B 10位地址前缀 用于10位地址格式
0x7C-0x7F 保留 测试和特殊用途

实际可用的7位地址范围:0x08 ~ 0x77,共112个地址。但还要考虑设备默认地址的冲突问题。

3.5 地址冲突的排查方法

地址冲突,是我在项目中遇到最多的问题之一。两个设备用了同一个地址,总线就乱套了。怎么排查?我总结了几个实用方法。

方法一:逻辑分析仪抓波形

这是最直接的方法。用逻辑分析仪抓取起始条件后的第一个字节,看看主机到底发了什么地址。如果发现两个设备都ACK了,那肯定冲突了。

方法二:逐个设备测试

先把所有从设备断开,只接一个。用主机扫描地址,记录下能响应的地址。然后逐个添加设备,每次添加后重新扫描。这样就能找出哪个设备占用了哪个地址。

// 地址扫描函数示例
void i2c_scan_addresses(void) {
    for (uint8_t addr = 0x08; addr <= 0x77; addr++) {
        i2c_start();
        i2c_send_byte((addr << 1) | 0);  // 写操作
        if (i2c_get_ack()) {
            printf("发现设备: 0x%02X\n", addr);
        }
        i2c_stop();
    }
}

我的经验:很多I2C设备都有地址选择引脚(比如A0、A1、A2)。通过改变这些引脚的电平,可以改变设备地址。比如AT24C02,三个地址引脚可以组合出8个不同的地址。

方法三:检查数据手册

听起来简单,但很多人就是不看。每个I2C设备的数据手册里,都会明确写出它的默认地址和地址配置方式。我曾经遇到一个项目,两个不同厂家的传感器默认地址都是0x48,结果一上电就冲突。后来查手册才发现,其中一个可以通过引脚改变地址。

避坑指南:我曾经在一个项目里,用了两个相同型号的传感器,但忘了改地址引脚。结果两个设备都响应同一个地址,数据全乱了。从那以后,我每次画PCB都会把地址选择引脚引出来,哪怕用跳线帽也要留个修改的余地。

方法四:使用I2C多路复用器

如果实在无法避免地址冲突,可以考虑用I2C多路复用器,比如PCA9548A。它可以把一条I2C总线分成8条子总线,每个子总线上可以挂相同地址的设备。

说白了,地址冲突的排查,核心就是「确认每个设备的唯一性」。你想想看,如果总线上有两个设备都叫「张三」,你喊一声「张三」,两个人都答应,那不乱套了吗?

嗯,关于I2C地址与寻址,今天就聊到这儿。下一章我们聊聊I2C的时序细节,包括起始条件、停止条件、数据有效性这些。到时候我会分享一些我在调试高速I2C设备时遇到的坑,敬请期待。