3. I2C地址与读写操作:7位地址与10位地址、读/写位控制、多主机仲裁
好,咱们进入第三章。地址和读写控制,这是I2C通信的“门牌号”和“方向灯”。搞不清楚这个,波形抓回来你都不知道谁在跟谁说话。
我早年刚接触I2C时,犯过一个低级错误——把7位地址左移了一位当8位发出去,结果从机死活不应答。后来查了半天手册才发现,I2C的地址字节其实是 7位地址 + 1位读写位,一共8位。这个坑,我希望你别再踩一次。
3.1 7位地址:最常用的寻址方式
7位地址,范围是 0x00 ~ 0x7F。但实际能用的大约是 0x08 ~ 0x77,因为 0x00 是通用呼叫地址,0x78 ~ 0x7F 保留给10位地址扩展用。
每个I2C从机设备,出厂时都有一个固定地址,比如常见的AT24C02 EEPROM,地址是 0x50(7位)。但有些设备允许通过引脚电平来修改部分地址位,比如 A0、A1、A2 引脚,这样一条总线上就能挂多个同型号器件。
0 表示主机要写,为 1 表示主机要读。
举个例子。你要往地址 0x50 的从机里写数据,那么主机发送的第一个字节应该是:
0x50 << 1 | 0 = 0xA0
// 二进制:1010 0000
// 高7位:1010 000(0x50)
// 最低位:0(写操作)
反过来,如果你要从 0x50 读数据,第一个字节就是:
0x50 << 1 | 1 = 0xA1
// 二进制:1010 0001
// 高7位:1010 000(0x50)
// 最低位:1(读操作)
你看,地址字节的 最低位 就是读写控制位。很多初学者在代码里直接写 I2C_SendByte(0x50),然后发现从机没反应——因为你发的是 0x50(二进制 0101 0000),读写位是 0,但地址其实被当成了 0x28(右移一位后)。
3.2 10位地址:什么时候用?怎么用?
7位地址最多挂112个设备(去掉保留地址)。对于大多数嵌入式系统,这完全够用。但有些复杂系统,比如服务器主板、基站设备,一条I2C总线上可能挂几十个传感器、电源管理芯片、温度监控器……7位地址就不够用了。
10位地址的格式,说白了就是 两个字节 来表示地址:
- 第一个字节: 高5位固定为
11110,接着是10位地址的高2位,最后是读写位。 - 第二个字节: 10位地址的低8位。
举个例子,假设从机10位地址是 0x2A4(二进制 10 1010 0100):
// 写操作
第一个字节:1111 0 [10位地址高2位: 10] [R/W=0] = 1111 0100 = 0xF4
第二个字节:10位地址低8位: 1010 0100 = 0xA4
// 读操作
第一个字节:1111 0 [10位地址高2位: 10] [R/W=1] = 1111 0101 = 0xF5
第二个字节:10位地址低8位: 1010 0100 = 0xA4
我个人的习惯是:除非项目明确要求,否则优先用7位地址。10位地址的兼容性问题比较多,有些老旧的从机芯片根本不支持。而且调试时,10位地址的波形看起来更复杂,容易看花眼。
| 特性 | 7位地址 | 10位地址 |
|---|---|---|
| 地址字节数 | 1字节(含读写位) | 2字节(含读写位) |
| 最大设备数 | 112个 | 1024个 |
| 兼容性 | 所有I2C设备都支持 | 部分老旧设备不支持 |
| 调试复杂度 | 低,波形一目了然 | 高,需要仔细分辨两个字节 |
| 典型应用 | 传感器、EEPROM、ADC/DAC | 服务器主板、多路电源管理 |
3.3 读/写位控制:方向决定一切
读写位,就是地址字节的最低位。它决定了数据流的方向:
- R/W = 0: 主机写数据到从机。数据从主机SDA输出,从机SDA输入。
- R/W = 1: 主机从从机读数据。数据从从机SDA输出,主机SDA输入。
这里有个容易忽略的细节:读写位只控制当前传输的方向。如果你要改变方向(比如先写寄存器地址,再读数据),就需要一个 重复起始条件(Repeated Start),重新发送地址字节,并改变读写位。
我曾经在一个项目中,需要先向温度传感器写入寄存器地址 0x00,然后读取温度值。我一开始的代码是这样写的:
// 错误写法
I2C_Start();
I2C_SendByte(0x90); // 地址0x48 + 写位
I2C_SendByte(0x00); // 寄存器地址
I2C_Stop();
I2C_Start();
I2C_SendByte(0x91); // 地址0x48 + 读位
data = I2C_ReceiveByte();
I2C_Stop();
结果发现读回来的数据全是 0xFF。为什么?因为我在两次传输之间发了 Stop 信号,有些从机在收到 Stop 后会复位内部状态,寄存器地址就丢了。
正确的做法是用 重复起始条件:
// 正确写法
I2C_Start();
I2C_SendByte(0x90); // 地址0x48 + 写位
I2C_SendByte(0x00); // 寄存器地址
I2C_RepeatedStart(); // 不发送Stop,直接再发一次Start
I2C_SendByte(0x91); // 地址0x48 + 读位
data = I2C_ReceiveByte();
I2C_Stop();
3.4 多主机仲裁:谁抢到总线谁说话
I2C总线允许多个主机同时发起通信。但总线只有一条,数据线SDA和时钟线SCL都是开漏输出,靠上拉电阻拉高。这就意味着:谁先拉低SDA,谁就赢得了仲裁。
仲裁的规则其实很简单:
- 多个主机同时发送
Start信号,同时开始输出时钟和数据。 - 每个主机在发送数据位时,都会 监测SDA电平。如果自己发送的是高电平,但SDA上却是低电平,说明有其他主机在拉低总线——自己就输了。
- 输掉仲裁的主机,必须立即释放SDA和SCL,转为从机模式,等待下一次机会。
我画了一张图,帮你理解仲裁过程:
仲裁只发生在 数据位 和 应答位 上。地址字节的仲裁决定了哪个主机能继续通信。一旦某个主机赢得了仲裁,其他主机就不能再干扰它了。
在实际项目中,多主机仲裁并不常见。大多数嵌入式系统只有一个MCU作为主机。但如果你在做 双MCU通信 或 热插拔板卡 的系统,就必须考虑仲裁逻辑。我记得有一次调试一个双主机的电源管理板,两个MCU同时去读同一个电压传感器,结果总线一直卡死。后来发现是其中一个MCU的仲裁检测代码有bug,没有正确释放总线。修好之后,系统就稳定了。
3.5 本章小结
地址和读写位,是I2C通信的起点。搞懂了这些,你就能看懂波形图上第一个字节的含义了。总结几个要点:
- 7位地址 最常用,记得左移一位再拼读写位。
- 10位地址 用两个字节,高5位固定为
11110。 - 读写位 控制数据方向,改变方向时用重复起始条件。
- 多主机仲裁 靠监测SDA电平实现,谁先拉低谁赢。