3. I2C地址与仲裁:7位与10位地址格式、多主机仲裁原理、时钟同步与总线竞争
好,咱们进入第三章。这一章聊的是I2C协议里最容易让人迷糊、也最容易出bug的两个话题——地址格式和总线仲裁。
说实话,我早年刚接触I2C时,觉得地址不就是个编号嘛,有啥好讲的?结果第一次调试多主机系统就被总线抢得死去活来,示波器上一片混乱。嗯,从那以后我再也不敢小看仲裁了。
3.1 7位地址格式
标准I2C设备最常用的就是7位地址。你想想看,一条I2C总线上最多能挂多少个设备?理论上是127个(0x00保留做广播地址),但实际受总线电容限制,一般也就十几个。
7位地址的传输格式是这样的:主机发送起始条件后,紧跟着发送的第一个字节就是地址字节。这个字节的高7位是设备地址,最低位是读写方向位——0表示写,1表示读。
地址字节结构(7位模式):
Bit 7 Bit 6 Bit 5 Bit 4 Bit 3 Bit 2 Bit 1 Bit 0
A6 A5 A4 A3 A2 A1 A0 R/W
例如:0x50(1010000)表示写操作,0x51(1010001)表示读操作。
我在项目中遇到过最坑的事:有人把设备地址写成了0xA0,结果怎么都通信不上。为什么?因为0xA0是7位地址0x50左移一位再补0的结果——很多数据手册给的是7位原始地址,但有些驱动库要求的是左移后的8位地址。这个坑,我踩过不止一次。
| 表示方式 | 值 | 说明 |
|---|---|---|
| 7位原始地址 | 0x50 | 数据手册通常给这个 |
| 8位写地址 | 0xA0 | 左移1位,R/W=0 |
| 8位读地址 | 0xA1 | 左移1位,R/W=1 |
我的习惯:在驱动代码里统一用7位原始地址,然后在发送时用宏或函数做移位。这样代码可读性高,也不容易搞混。
#define I2C_ADDR_DEVICE 0x50
#define I2C_WRITE_ADDR(addr) ((addr) << 1)
#define I2C_READ_ADDR(addr) (((addr) << 1) | 0x01)
3.2 10位地址格式
7位地址不够用怎么办?I2C规范在扩展模式里定义了10位地址。说实话,我在实际项目中用到10位地址的场景不多,但一旦遇到,处理不好就是大麻烦。
10位地址的传输分两个阶段:
- 第一个字节:高5位固定为11110,接着是地址的高2位(A9、A8),最后是R/W位。
- 第二个字节:地址的低8位(A7~A0)。
10位地址传输格式:
第一个字节: 1111 0 A9 A8 R/W
第二个字节: A7 A6 A5 A4 A3 A2 A1 A0
举个例子,10位地址0x2B4(二进制10 1011 0100):
- 高2位A9:A8 = 10
- 低8位A7:A0 = 1011 0100
- 第一个字节(写):1111 0 10 0 = 0xF0
- 第二个字节:1011 0100 = 0xB4
注意:10位地址模式下,从机在第一个字节后就要回复ACK,而不是等到第二个字节。很多初学者在这里搞错时序,导致通信失败。
我曾经调试过一个工业传感器模块,数据手册说支持10位地址,结果我按标准流程发完两个字节,设备就是不回复。折腾了两天,最后发现是第一个字节的11110前缀写成了11111——一个bit的差异,浪费了我整整两天时间。
3.3 多主机仲裁原理
I2C最牛的地方是什么?它支持多主机。多个主机可以挂在同一条总线上,谁先发起通信谁就占线,如果同时发起,就靠仲裁来决定谁赢。
仲裁的原理说白了就是:谁先输出低电平,谁就赢。
为什么?因为I2C的SCL和SDA都是开漏输出,加上拉电阻。任何一个主机拉低,总线就是低电平。所以如果两个主机同时发送数据,一个发高电平(释放总线),一个发低电平(拉低总线),总线最终是低电平——发高电平的那个主机就输了。
仲裁规则:
- 多个主机在SCL高电平时采样SDA
- 如果某个主机发送的SDA电平与总线实际电平不一致,它就失去仲裁
- 失去仲裁的主机必须立即释放SDA和SCL,转为从机模式
- 仲裁不会丢失数据,也不会破坏任何一方的数据帧
我画了一张仲裁流程图,帮你理清这个过程:
3.4 时钟同步
时钟同步和仲裁是孪生兄弟。多主机模式下,每个主机都有自己的时钟源,频率可能有微小差异。如果没有时钟同步,总线时序会乱成一锅粥。
时钟同步的原理也很简单:SCL线是线与逻辑。多个主机同时产生SCL时钟,谁先拉低SCL,总线SCL就变低;谁最后释放SCL(拉高),总线SCL才变高。
这就导致了一个有趣的现象:所有主机的SCL时钟会被同步到最慢的那个主机上。
时钟同步过程:
- 主机A的SCL低电平时间短,它想拉高SCL
- 但主机B还在保持SCL低电平,总线SCL仍然是低
- 主机A检测到SCL没变高,就知道有其他主机在拉低
- 主机A等待,直到主机B释放SCL
- 总线SCL变高后,所有主机同时开始高电平计时
我记得有一次调试一个双主机的数据采集系统,两个MCU都是主机,一个跑100kHz,一个跑400kHz。结果示波器一看,总线频率只有80kHz左右。为什么?因为时钟同步把两个主机的时钟拉到了最慢的那个水平上。
3.5 总线竞争与避坑指南
总线竞争说白了就是多个主机同时抢总线。仲裁机制能解决大部分问题,但有些坑是协议本身解决不了的。
常见总线竞争问题:
- START条件竞争:两个主机几乎同时产生START条件,仲裁从第一个地址位开始
- 重复START竞争:在数据传输过程中,多个主机同时发起重复START
- STOP条件竞争:多个主机同时释放总线,可能导致总线空闲检测出错
我曾经遇到过一个非常隐蔽的bug:两个主机都在检测总线空闲后才发起通信,但总线空闲检测需要一定时间。结果就是,主机A检测到空闲,准备发START;主机B也检测到空闲,也准备发START。两个START几乎同时出现,仲裁开始。按理说没问题,但问题出在——其中一个主机在失去仲裁后,没有正确释放总线,导致总线锁死。
我的避坑建议:
- 多主机系统中,给每个主机分配不同的优先级(通过地址高低来体现)
- 在驱动代码中加入超时机制,防止总线锁死
- 失去仲裁后,必须清零内部状态机,不能残留任何数据
- 建议在硬件上加上总线恢复电路(比如用GPIO模拟I2C时序来发9个时钟脉冲)
最后说一句:7位地址和10位地址的混用要特别小心。有些从机设备同时支持7位和10位地址模式,切换方式是通过发送特定序列。我建议你在项目初期就定好地址模式,中途切换很容易出问题。