3. I2C 地址与仲裁:7 位地址与 10 位地址格式、多主机仲裁机制、时钟同步与拉伸
好,咱们进入第三章。这一章讲的是 I2C 协议里最容易让人迷糊,也最容易出 Bug 的几个点:地址格式、多主机抢总线、还有时钟拉伸。说实话,我早年做项目时,在这上面栽过不止一个跟头。尤其是时钟拉伸,很多芯片手册里写得不清楚,实际调起来才发现坑很深。
咱们一个一个来拆。
3.1 7 位地址与 10 位地址格式
I2C 的地址,说白了就是给每个从机发一个“门牌号”。主机发数据前,先喊一声这个门牌号,对应的从机应答,然后才开始通信。
标准模式下,地址是 7 位的。但 7 位能表示 128 个地址,其中 0x00 是通用呼叫地址,0x01~0x07 是保留地址,真正能用的也就 120 个左右。这在复杂系统里不够用,所以后来扩展出了 10 位地址。
7 位地址格式
7 位地址的传输格式很简单:主机发送起始条件后,先发一个字节,高 7 位是地址,最低位是读写位(0 表示写,1 表示读)。
起始条件 | 地址[7:1] + R/W | 应答 | 数据... | 停止条件
举个例子:如果从机地址是 0x32(二进制 011 0010),主机要写数据,那么发送的第一个字节就是:
0x32 << 1 | 0 = 0x64
嗯,这里要注意:很多新手会把 0x32 直接当成发送字节,结果从机死活不应答。我刚开始也犯过这个错,后来养成了习惯——写驱动时一定把地址左移一位,再拼上读写位。
10 位地址格式
10 位地址的格式稍微复杂一点。主机需要发送两个字节来标识地址:
- 第一个字节:高 5 位固定为 11110,接着是 10 位地址的高 2 位,最后是读写位。
- 第二个字节:10 位地址的低 8 位。
格式如下:
起始条件 | 11110 + Addr[9:8] + R/W | 应答 | Addr[7:0] | 应答 | 数据... | 停止条件
举个例子:如果从机 10 位地址是 0x2B4(二进制 10 1011 0100),那么:
- 高 2 位是 10
- 低 8 位是 1011 0100(0xB4)
- 第一个字节:1111 0 10 + 0(写)= 0xF4
- 第二个字节:0xB4
你想想看,10 位地址的好处是能挂更多设备,但代价是多传一个字节,通信效率略低。我个人习惯是:如果系统里从机不超过 10 个,用 7 位地址就够了;超过 10 个或者需要兼容未来扩展,才用 10 位地址。
| 特性 | 7 位地址 | 10 位地址 |
|---|---|---|
| 地址范围 | 0x00 ~ 0x7F(保留 8 个) | 0x000 ~ 0x3FF |
| 有效设备数 | 最多 112 个 | 最多 1024 个 |
| 地址传输字节数 | 1 字节 | 2 字节 |
| 兼容性 | 所有设备都支持 | 部分旧设备不支持 |
3.2 多主机仲裁机制
I2C 支持多个主机挂在同一条总线上。但问题来了——如果两个主机同时发起通信,谁说了算?
答案是:仲裁。
仲裁的规则很简单:谁先拉低 SDA 线,谁就赢。因为 I2C 是开漏输出,多个设备同时拉低时,总线电平就是低电平。如果一个主机想发高电平,但检测到总线是低电平,说明有其他主机在抢,它就自动退出。
具体流程是这样的:
- 两个主机同时发送起始条件。
- 然后开始逐位发送地址和数据。
- 每个主机在发送每一位后,都会读取 SDA 线的实际电平。
- 如果发送的电平与读取的电平不一致,说明仲裁失败,该主机立即释放总线。
- 仲裁获胜的主机继续通信,失败的主机等待总线空闲后重试。
这里有个关键点:仲裁只发生在 SDA 线上,SCL 线始终由主机共同驱动。也就是说,时钟是同步的,数据是仲裁的。
我在项目中遇到过一个问题:两个主机同时访问同一个从机,结果从机收到了混乱的数据。后来排查发现,其中一个主机的 SDA 引脚配置成了推挽输出,而不是开漏输出。推挽输出在拉高时强行输出高电平,导致仲裁机制失效。嗯,这个坑我印象很深——I2C 的 SDA 和 SCL 必须配置为开漏输出,这是协议的基础。
3.3 时钟同步与时钟拉伸
时钟同步和时钟拉伸,这两个概念经常被混在一起说。其实它们干的活不一样。
时钟同步
时钟同步发生在多主机场景下。多个主机各自产生 SCL 时钟,但它们的时钟频率和相位可能不同。I2C 的同步机制是:所有主机共同驱动 SCL 线,谁先拉低 SCL,总线就进入低电平;谁最后释放 SCL,总线才进入高电平。
说白了,就是“低电平取最长,高电平取最短”。这样所有主机的时钟就被强制同步了。
举个例子:
- 主机 A 的 SCL 低电平时间是 4μs,高电平时间是 4μs。
- 主机 B 的 SCL 低电平时间是 5μs,高电平时间是 3μs。
- 同步后,SCL 低电平时间是 5μs(取最长),高电平时间是 3μs(取最短)。
这样总线的时钟频率就被拉到了较慢的那个主机的水平。所以,如果你在系统里混用不同速度的 I2C 主机,整体速度会被拖慢。
时钟拉伸
时钟拉伸是从机的一种“反制”手段。从机如果处理不过来,可以主动拉低 SCL 线,告诉主机:“慢点,我还没准备好。”
主机在 SCL 高电平期间采样数据,但在低电平期间会检测 SCL 线的状态。如果从机拉低了 SCL,主机就会等待,直到从机释放 SCL 为止。
时钟拉伸的典型场景:
- 从机需要时间处理接收到的数据(比如写入 EEPROM)。
- 从机需要时间准备要发送的数据。
- 从机正在处理中断,暂时无法响应 I2C 通信。
在 RTOS 环境下,时钟拉伸对任务调度有直接影响。如果主机任务因为等待时钟拉伸而阻塞,其他任务可能趁机抢占 CPU,导致 I2C 通信超时。我建议的做法是:在 I2C 驱动中为时钟拉伸设置一个最大等待时间,超时后主动复位总线。
3.4 知识结构图
下面这张图把本章的核心知识点串起来了。你可以把它当成一个快速索引。
3.5 避坑指南
最后,我把自己踩过的坑整理成几条,你写驱动时多留个心眼:
- 地址左移:芯片手册里写的地址通常是 7 位原始地址,发送时要左移一位。别直接拿手册里的值去发。
- 开漏输出:SDA 和 SCL 必须配成开漏,不能是推挽。否则仲裁和时钟拉伸都会失效。
- 时钟拉伸超时:从机可能无限期拉低 SCL,主机必须设超时。我一般设 10ms,超时后复位 I2C 外设。
- 多主机冲突:如果系统里有多主机,确保所有主机都支持仲裁。有些简化版 I2C 控制器不支持仲裁,只能当单主机用。
- 10 位地址兼容性:部分旧从机芯片不支持 10 位地址,选型时一定要确认。