4. 地址冲突问题:同型号器件地址重叠与总线竞争
地址冲突,说白了就是两个设备抢同一个“门牌号”。I2C 总线上每个从机都得有唯一地址,否则主机叫一声,好几个设备同时应答——总线直接就乱套了。我刚开始调 I2C 时觉得这问题很简单,直到有一次把四片同样的传感器挂到一条总线上,才发现事情没那么简单。
4.1 同型号器件地址重叠
很多 I2C 器件,尤其是传感器、EEPROM、IO 扩展芯片,出厂时只给了一两个可选的地址引脚。比如常见的 AT24C02(EEPROM),它的 7 位设备地址是 1010xxx,其中低三位由 A0/A1/A2 引脚的电平决定。理论上最多挂 8 片,但实际中经常遇到:
- 地址引脚被固定:有些模块把 A0/A1/A2 直接焊死在 GND,你没法改。
- 地址范围不够用:比如某温度传感器只有 1 个地址引脚,只能选 2 个地址,挂 3 片就冲突。
- 地址与其它器件撞车:0x48 这个地址,很多温度传感器、ADC 都用,混用时就打架。
4.2 地址总线竞争现象
地址总线竞争发生时,你会看到这些典型症状:
- ACK 异常:主机发送地址后,SDA 在第九个时钟周期没有被拉低,或者出现半高电平。
- 数据错乱:读回来的数据全是 0xFF 或 0x00,偶尔夹杂正确值。
- 总线锁死:SDA 被某个从机一直拉低,SCL 还能跑,但主机无法再发起通信。
- 间歇性故障:上电时正常,运行几分钟后出错——可能是某个从机内部状态机跑飞,错误地响应了不属于自己的地址。
我曾经遇到一个案例:四片同样的气压传感器挂一条总线,地址分别设为 0x76、0x77、0x76、0x77(两两重复)。结果主机读 0x76 时,两片传感器同时应答,数据在 SDA 上叠加,读出来的数值忽高忽低,完全没法用。
4.3 解决方案
解决地址冲突,我一般按优先级从高到低试这几个方案:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 硬件地址引脚重配置 | 器件有多个地址引脚 | 不改软件,直接改硬件 | 需要 PCB 飞线或改板 |
| I2C 多路复用器 | 同型号器件数量超过地址范围 | 彻底隔离,无竞争 | 增加成本,占用额外 I2C 地址 |
| 软件地址重映射 | 器件支持软件配置地址 | 灵活,不改硬件 | 需要器件支持,上电初始化复杂 |
| 分时使能(片选) | 器件有使能引脚 | 简单可靠 | 需要额外 GPIO |
| 更换不同型号器件 | 实在无法解决冲突 | 一劳永逸 | 可能需要重新选型、改驱动 |
4.3.1 硬件地址引脚重配置
这是最直接的办法。比如 AT24C02,把 A0、A1、A2 分别接 VCC 或 GND,就能得到 8 个不同地址。我习惯在 PCB 设计时给每个地址引脚预留 0Ω 电阻或跳线,方便调试时切换。
// AT24C02 地址计算示例
// 设备地址 = 0b1010 + A2 A1 A0
// 假设 A2=1, A1=0, A0=0 → 0b1010100 = 0x54
#define EEPROM_ADDR_0 0x50 // A2=0, A1=0, A0=0
#define EEPROM_ADDR_1 0x52 // A2=0, A1=1, A0=0
#define EEPROM_ADDR_2 0x54 // A2=1, A1=0, A0=0
4.3.2 I2C 多路复用器
当同型号器件数量超过地址范围时,多路复用器是标准解法。常用的芯片有 TCA9548A(8 通道)和 PCA9546A(4 通道)。
工作原理很简单:多路复用器本身占用一个 I2C 地址(比如 0x70),主机先向它写一个控制字节,选择要开启的通道。只有被选中的通道上的从机才能与主机通信,其他通道的从机电气隔离。
// TCA9548A 使用示例
// 多路复用器地址:0x70
// 选择通道 0:写 0x01
// 选择通道 1:写 0x02
void tca_select_channel(uint8_t ch) {
uint8_t data = 1 << ch; // ch: 0~7
i2c_write(0x70, &data, 1);
}
// 使用流程
tca_select_channel(0); // 先切到通道0
i2c_read(sensor_addr, buf, len); // 读通道0上的传感器
tca_select_channel(1); // 再切到通道1
i2c_read(sensor_addr, buf, len); // 读通道1上的传感器
4.3.3 软件地址重映射
部分现代 I2C 器件支持通过软件命令修改自身地址。比如某些温度传感器,上电后默认地址是 0x48,你可以通过写配置寄存器把它改成 0x4A。这样即使硬件引脚相同,也能区分。
但要注意:
- 软件改地址的操作必须在器件唯一可访问时进行(比如刚上电,总线上只有它一个)。
- 掉电后地址会恢复默认,每次上电都要重新配置。
- 如果器件不支持软件改地址,这个方法无效。
4.3.4 分时使能(片选)
有些器件有 Enable 或 Shutdown 引脚。你可以用 MCU 的 GPIO 分别控制每个器件的使能脚,同一时间只让一个器件工作。这样即使地址相同,也不会冲突。
我曾在项目里用这个方法挂了 6 片同样的 ADC:每片 ADC 的 SDA/SCL 都并联,但每个的 EN 脚接不同的 GPIO。读哪片就拉高哪片的 EN,其他片保持禁用。效果很好,就是多占用了 6 个 GPIO。
4.4 地址冲突排查流程
如果你怀疑总线上有地址冲突,按这个步骤排查:
- 断开所有从机,只留主机,用示波器或逻辑分析仪确认主机发出的地址帧正确。
- 逐个挂载从机,每挂一个就扫描一次总线,记录每个从机的实际地址。
- 对比地址表,看是否有重复。如果有,按上面的方案解决。
- 全挂载后跑通信,抓取 ACK 窗口,确认每个地址只有唯一应答。
- 长时间压力测试(至少 24 小时),看是否有间歇性冲突。
4.5 地址冲突解决决策流程图
下面这张图总结了遇到地址冲突时的决策路径,你可以把它贴在调试台旁边:
这张图把决策路径画得很清楚。你从“检测到地址冲突”开始,顺着箭头走,根据器件特性选择方案,最后一定要做压力测试。我见过太多人改完地址就跑一次通信,觉得没问题就收工,结果产线上批量出问题——就是因为没做长时间验证。
4.6 小结
地址冲突是 I2C 多设备通信中最常见也最容易被忽视的问题。解决思路其实就三条:改硬件地址、加复用器、软件重映射。具体用哪个,取决于你的器件特性和项目约束。
我个人习惯是在原理图阶段就把地址规划好,给每个器件预留地址配置电阻或跳线。这样调试时万一撞地址,飞根线就能解决,不用改代码。嗯,这算是花小钱省大麻烦的做法。