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位地址的传输分两个阶段:
- 第一个字节:高5位固定为11110,接着是10位地址的高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设备时遇到的坑,敬请期待。