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 引脚,这样一条总线上就能挂多个同型号器件。

关键点: 主机发送地址时,先发7位地址,再发1位读写位。读写位为 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(右移一位后)。

注意: 数据手册里给出的地址,通常都是7位地址。你在代码里使用时,必须左移一位,再拼上读写位。千万别直接拿手册地址当字节发!

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();
经验之谈: 在写寄存器地址后要读数据时,务必使用重复起始条件。不要发Stop再发Start,否则很多从机不认。这个坑,我至少见过三个同事踩过。

3.4 多主机仲裁:谁抢到总线谁说话

I2C总线允许多个主机同时发起通信。但总线只有一条,数据线SDA和时钟线SCL都是开漏输出,靠上拉电阻拉高。这就意味着:谁先拉低SDA,谁就赢得了仲裁。

仲裁的规则其实很简单:

  1. 多个主机同时发送 Start 信号,同时开始输出时钟和数据。
  2. 每个主机在发送数据位时,都会 监测SDA电平。如果自己发送的是高电平,但SDA上却是低电平,说明有其他主机在拉低总线——自己就输了。
  3. 输掉仲裁的主机,必须立即释放SDA和SCL,转为从机模式,等待下一次机会。

我画了一张图,帮你理解仲裁过程:

I2C多主机仲裁流程 主机A 主机B SDA总线(开漏) 仲裁判定:谁先拉低SDA? 主机A赢得仲裁 主机B释放总线 仲裁规则 发送高电平但检测到低电平 → 输掉仲裁 输掉的主机立即释放总线,转为从机模式

仲裁只发生在 数据位 和 应答位 上。地址字节的仲裁决定了哪个主机能继续通信。一旦某个主机赢得了仲裁,其他主机就不能再干扰它了。

重要提醒: 仲裁过程中,不会丢失任何数据。赢得仲裁的主机继续正常通信,输掉的主机自动退出。这也是I2C总线设计精妙的地方——无冲突、无损仲裁。

在实际项目中,多主机仲裁并不常见。大多数嵌入式系统只有一个MCU作为主机。但如果你在做 双MCU通信 或 热插拔板卡 的系统,就必须考虑仲裁逻辑。我记得有一次调试一个双主机的电源管理板,两个MCU同时去读同一个电压传感器,结果总线一直卡死。后来发现是其中一个MCU的仲裁检测代码有bug,没有正确释放总线。修好之后,系统就稳定了。

3.5 本章小结

地址和读写位,是I2C通信的起点。搞懂了这些,你就能看懂波形图上第一个字节的含义了。总结几个要点:

  • 7位地址 最常用,记得左移一位再拼读写位。
  • 10位地址 用两个字节,高5位固定为 11110。
  • 读写位 控制数据方向,改变方向时用重复起始条件。
  • 多主机仲裁 靠监测SDA电平实现,谁先拉低谁赢。