3、多从机地址分配策略:7位地址与10位地址、地址冲突避免
地址分配这事儿,看着简单,坑却不少。我刚开始做I2C项目时,觉得不就是给每个设备配个地址嘛,结果一上电,两个从机都不响应——地址冲突了。后来查了半天,才发现是某款传感器默认地址和另一片EEPROM撞车了。
说白了,I2C总线上的每个从机都得有唯一地址。STM32作为主机,发地址帧时,所有从机都会收到,但只有地址匹配的那个才会应答。所以地址分配是通信的第一步,也是故障的高发区。
3.1 7位地址 vs 10位地址
I2C规范定义了两种地址长度:7位和10位。STM32的I2C外设都支持这两种模式,但用法和场景差别很大。
| 特性 | 7位地址 | 10位地址 |
|---|---|---|
| 地址范围 | 0x00 ~ 0x7F(128个) | 0x000 ~ 0x3FF(1024个) |
| 保留地址 | 0x00~0x07、0x78~0x7F | 0x000~0x00F(部分保留) |
| 可用地址数 | 约112个 | 约1008个 |
| 帧格式 | 1字节地址+1位R/W | 2字节地址(首字节11110xx+后8位) |
| STM32支持 | 全系列原生支持 | 需配置OAR1寄存器 |
| 典型应用 | 传感器、EEPROM、RTC | 复杂系统、多路复用器 |
7位地址是主流。市面上90%以上的I2C器件都只用7位地址。比如常见的AT24C02 EEPROM,地址是0x50~0x57;MPU6050陀螺仪,地址是0x68或0x69。这些地址在数据手册里都会明确给出。
10位地址呢?说实话,我在实际项目中用得不多。它主要解决的是总线挂载设备太多的问题。比如一个系统里接了20多个传感器,7位地址就不够用了。但10位地址的帧格式更复杂,传输效率也低一些。
关键区别:7位地址的地址字节是 AAAAAAA + R/W,而10位地址的首字节是 11110AA + R/W,其中AA是10位地址的高两位。这个格式决定了10位地址的兼容性——有些老旧的从机芯片根本不认。
3.2 STM32的地址配置实战
STM32的I2C外设通过OAR1和OAR2寄存器来配置自身地址。注意,这里说的是STM32作为从机时的地址。但作为主机,我们只需要知道目标从机的地址,然后发出去就行。
配置7位地址很简单。以STM32F1的I2C1为例:
// 配置I2C1的自身地址(从机模式)
I2C1->OAR1 = (0x50 << 1) | I2C_OAR1_ADD0; // 7位地址0x50,左移1位
// 或者用HAL库
hi2c1.Init.OwnAddress1 = 0x50;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
HAL_I2C_Init(&hi2c1);
10位地址的配置稍微麻烦点:
// 10位地址0x02A3
hi2c1.Init.OwnAddress1 = 0x02A3;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_10BIT;
HAL_I2C_Init(&hi2c1);
// 寄存器层面
I2C1->OAR1 = (0x02A3 << 1) | I2C_OAR1_ADDMODE; // 置位ADDMODE表示10位
个人习惯:我一般用7位地址,除非系统里从机数量超过20个。10位地址的调试成本更高,很多逻辑分析仪对10位地址的解析也不够友好。
3.3 地址冲突的常见场景
地址冲突,说白了就是两个从机用了同一个地址。STM32发地址帧时,两个从机都会应答,总线就乱套了。
我遇到过几种典型情况:
- 同型号器件:两片AT24C02,默认地址都是0x50。如果不改地址引脚,直接冲突。
- 不同厂商的器件:某款温湿度传感器和某款气压计,默认地址都是0x76。这最坑,因为数据手册里不会告诉你别的器件用啥地址。
- 地址引脚悬空:有些芯片的地址引脚内部有上拉或下拉,悬空时可能读到不确定电平,导致地址漂移。
我曾经踩过的坑:有一款国产加速度传感器,数据手册说默认地址是0x18。结果我焊了三片上去,全部不响应。后来用逻辑分析仪抓波形,发现它实际应答的地址是0x19。查了勘误表才知道,芯片内部有个保留位,默认是1。所以,拿到新器件,第一件事就是用I2C扫描程序扫一遍它的真实地址。
3.4 地址冲突的避免策略
避免地址冲突,有几种成熟的做法:
- 硬件地址引脚:大多数I2C器件都有A0/A1/A2引脚,通过接VCC或GND来改变地址。比如AT24C02,地址是0x50 + (A2<<2) + (A1<<1) + A0。这样最多可以挂8片。
- 地址扫描:上电后,主机先发一个广播扫描,看看哪些地址有应答。如果发现两个从机应答同一个地址,就报错。
- I2C多路复用器:用PCA9548这类芯片,把总线分成多路。每路挂不同的从机,地址可以重复。
- 地址映射表:在项目设计阶段,就把所有器件的地址列出来,人工检查是否有冲突。
我个人的做法是:项目初期就建一个地址表,把所有从机的型号、默认地址、可配置地址、实际使用的地址都写清楚。这样后期调试时,一眼就能看出问题。
3.5 地址分配的知识体系
下面这张图,把地址分配的核心逻辑串起来了:
3.6 地址扫描的实用代码
调试时,我经常用一段地址扫描代码,快速找出总线上有哪些设备。这段代码在STM32上跑一遍,就能看到所有应答的地址:
// I2C地址扫描函数
void I2C_ScanBus(I2C_HandleTypeDef *hi2c)
{
uint8_t device_count = 0;
printf("I2C Bus Scanning...\r\n");
for (uint8_t addr = 0x01; addr < 0x7F; addr++)
{
// 发送起始条件+地址+写位,等待应答
if (HAL_I2C_IsDeviceReady(hi2c, (addr << 1), 1, 10) == HAL_OK)
{
printf("Found device at 0x%02X\r\n", addr);
device_count++;
}
}
printf("Scan complete. %d device(s) found.\r\n", device_count);
}
这段代码会遍历0x01到0x7E的所有地址。如果某个地址有应答,就打印出来。注意,0x00是通用呼叫地址,0x7F是保留地址,一般不用。
实用技巧:扫描时,如果发现某个地址有应答,但你的原理图上没接这个器件,那就要警惕了——可能是地址冲突,也可能是某个器件的地址引脚焊错了。
3.7 地址映射表模板
我建议每个项目都建一个地址映射表。下面是我常用的模板:
| 器件型号 | 功能 | 默认地址 | 配置引脚 | 实际地址 | 备注 |
|---|---|---|---|---|---|
| AT24C02 | EEPROM | 0x50 | A2=GND, A1=GND, A0=GND | 0x50 | 存储配置参数 |
| MPU6050 | 6轴IMU | 0x68 | AD0=GND | 0x68 | 姿态数据 |
| BMP280 | 气压计 | 0x76 | SDO=GND | 0x76 | 高度测量 |
| PCF8574 | IO扩展 | 0x20 | A2=VCC, A1=VCC, A0=VCC | 0x27 | 控制LED |
这个表在原理图设计阶段就要填好。如果发现两个器件实际地址相同,赶紧改引脚配置或者换型号。别等到PCB打样回来再改,那成本就高了。