📚 I2C 技术系列 · 持续更新中
本系列从 OD 开漏物理层一路打到 Linux i2c子系统,把"两根线"背后那套电气/协议/仲裁/恢复/驱动全拧成一根线。
👉 想系统追更,戳这里查看完整目录:
本篇定位 :I2C 能多主多从,靠的是 OD 开漏的线与特性派生出的两套机制------时钟同步 (SCL 线与)和仲裁(SDA 线与)。本篇把这两个机制讲透,再加时钟拉伸(Clock Stretching)。这是 I2C 协议最精妙的部分,也是你做多主系统、调总线卡死问题的核心知识。
多主是 I2C 区别于 SPI 的杀手锏SPI 严格一主多从,从机不能主动发言。I2C 多主,任何主都能发起通信,靠线与 + 仲裁无冲突共存。这套机制的物理根基就是 【I2C 技术系列 01】I2C 电气特性与物理层的 OD 开漏。理解本篇,你才真正懂"I2C 为什么能多主"。
目录
- 一、多主总线概述
-
- [1.1 多主拓扑](#1.1 多主拓扑)
- [1.2 多主的核心问题](#1.2 多主的核心问题)
- [二、SCL 线与与时钟同步](#二、SCL 线与与时钟同步)
-
- [2.1 SCL 线与](#2.1 SCL 线与)
- [2.2 时钟同步过程](#2.2 时钟同步过程)
- [2.3 同步的本质](#2.3 同步的本质)
- [三、SDA 线与与仲裁](#三、SDA 线与与仲裁)
-
- [3.1 仲裁原理](#3.1 仲裁原理)
- [3.2 仲裁过程示例](#3.2 仲裁过程示例)
- [3.3 仲裁的优雅之处](#3.3 仲裁的优雅之处)
- [3.4 仲裁丢失的场景](#3.4 仲裁丢失的场景)
- 四、仲裁丢失处理
-
- [4.1 控制器硬件行为](#4.1 控制器硬件行为)
- [4.2 软件处理](#4.2 软件处理)
- [4.3 仲裁 + 时钟拉伸的交互](#4.3 仲裁 + 时钟拉伸的交互)
- [五、时钟拉伸(Clock Stretching)](#五、时钟拉伸(Clock Stretching))
-
- [5.1 什么是时钟拉伸](#5.1 什么是时钟拉伸)
- [5.2 拉伸的合法用途](#5.2 拉伸的合法用途)
- [5.3 拉伸的实现](#5.3 拉伸的实现)
- [5.4 拉伸的滥用与超时 ⚠️](#5.4 拉伸的滥用与超时 ⚠️)
- [5.5 不同模式对拉伸的支持](#5.5 不同模式对拉伸的支持)
- 六、多主系统设计要点
-
- [6.1 多主适用场景](#6.1 多主适用场景)
- [6.2 设计注意事项](#6.2 设计注意事项)
- [6.3 多主的局限](#6.3 多主的局限)
- [七、关键问题 Q&A](#七、关键问题 Q&A)
-
- Q1:多主同时发,会不会损坏器件?
- Q2:仲裁谁赢?能预测吗?
- Q3:时钟拉伸和仲裁有什么关系?
- [Q4:从机一直拉伸 SCL,主机怎么办?](#Q4:从机一直拉伸 SCL,主机怎么办?)
- Q5:单主系统需要管时钟同步和仲裁吗?
- Q6:仲裁丢失后立即重试,会怎样?
- 八、实战案例
-
- [案例 1:双主冗余系统,偶发数据错乱](#案例 1:双主冗余系统,偶发数据错乱)
- [案例 2:从机拉伸导致主机超时](#案例 2:从机拉伸导致主机超时)
- [案例 3:多主系统,一个主死机拖垮总线](#案例 3:多主系统,一个主死机拖垮总线)
- [案例 4:仲裁丢失未处理,数据错乱](#案例 4:仲裁丢失未处理,数据错乱)
- 九、本篇小结与速查
- 延伸阅读
- 参考资料
一、多主总线概述
1.1 多主拓扑
VDD
│
Rp
│
┌────────┼────────┐
│ │ │
Master1 Master2 Slaves
│ │ │
└────────┼────────┘
│
SDA / SCL(共线)
- 多个 Master + 多个 Slave 挂同一 SDA/SCL
- 任一 Master 可发起通信
- 同一时刻只能一个 Master 成功占用总线
1.2 多主的核心问题
| 问题 | 解法 |
|---|---|
| 多主同时发 SCL,频率不同怎么办? | 时钟同步(SCL 线与) |
| 多主同时发 SDA,数据冲突怎么办? | 仲裁(SDA 线与) |
| 从机来不及处理怎么办? | 时钟拉伸(从机拉低 SCL) |
三套机制全靠 OD 开漏的线与。
二、SCL 线与与时钟同步
2.1 SCL 线与
SCL 是 OD 开漏 + 上拉,多个主同时驱动:
任一主拉低 SCL → SCL = 0(线与)
所有主都释放(不拉低)→ SCL = 1(上拉拉高)
- SCL 是线与,任何一个拉低就是低
- 这天然实现了时钟同步
2.2 时钟同步过程
两个主频率不同(M1 快、M2 慢),SCL 同步到"低电平最长者":
M1 想发的 SCL: ‾‾‾‾╲‾‾╲‾‾╲‾‾╲‾‾(快,短周期)
╲ ╲ ╲ ╲
M2 想发的 SCL: ‾‾‾‾‾‾‾╲‾‾‾‾‾‾╲‾‾(慢,长周期)
╲ ╲
实际总线 SCL: ‾‾‾‾‾‾‾╲‾‾‾‾‾‾╲‾‾(同步:低取最长,高取最短)
╲ ╲
↑ ↑
M2拉低(长) M2拉低(长)
M1 想要(快): ‾‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___/‾‾‾\___
M2 想要(慢): ‾‾‾‾‾‾‾‾\_______/‾‾‾‾‾‾‾\_______/‾‾‾‾‾‾‾\_______/‾‾‾‾‾‾‾\_______
实际总线 : ‾‾‾‾\___________/‾‾‾\___________/‾‾‾\___________/‾‾‾\___________
谁在拉低 : 11112222BBBB 11112222BBBB 11112222BBBB 11112222BBBB
↑ ↑ ↑ ↑
│ │ │ └─ 只有 M1、M2 都释放,线才变高(高取最短)
│ │ └───── M2 释放后 M1 又拉低,线再次变低
│ └─────────────── M1 先拉低(4 格),随后 M2 也拉低 → 被拉长到 12 格
└──────────────────── 起始:两线均高,总线空闲
图例:1 = 仅 M1 拉低 2 = 仅 M2 拉低 B = Both 同时拉低
规则:
- SCL 低电平:任一主拉低,SCL 就是低。SCL 低电平时间 = 最长那个主的低电平时间
- SCL 高电平:所有主都释放(都到高电平计时),SCL 才高。SCL 高电平时间 = 最短那个主的高电平时间
2.3 同步的本质
-
每个主内部有"期望 SCL"和"实际 SCL"两个状态
-
主拉低 SCL 后,等待 SCL 实际变高(读 SCL 输入),才进入高电平计时
-
这样慢的主拉低时间长,快的主被"拖住",最终同步
主内部逻辑:
拉低 SCL(输出0)
延时 tLOW
释放 SCL(输出1,OD 模式变高阻)
等待 SCL 实际读为 1(可能被其他主拉着,要等)
延时 tHIGH
拉低 SCL(下一周期)
时钟同步是仲裁的前提
没有时钟同步,两个主频率不同,SDA 采样时刻错位,仲裁无法进行。时钟同步保证所有主在同一 SCL 边沿采样 SDA,仲裁才能逐位比较。这两套机制是配套的。
三、SDA 线与与仲裁
3.1 仲裁原理
多主同时发,SDA 也是线与。仲裁规则:输出 1 但读到 0 的主,丢失仲裁。
线与:任一主拉低 SDA → SDA=0
仲裁:主输出 1(释放)却读到 0(被别人拉低),说明别人在发 0,自己"输"了
- 输出 0 的主不会丢(它自己也拉低,读到 0 正常)
- 输出 1 的主若读到 0,说明有别的主在发 0 → 自己丢了
3.2 仲裁过程示例
两个主同时发,地址前几位相同,第 4 位不同(M1 发 0,M2 发 1):
SCL: 1 2 3 4 5 6 ...
M1发: 1 0 1 0 ... (M1 第4位发0)
M2发: 1 0 1 1 ... (M2 第4位发1)
SDA线:1 0 1 0 ... (线与:M2发1被M1拉低)
↑
第4拍 SCL 高时:
M2 输出1,读 SDA=0 → 丢失仲裁!
M1 输出0,读 SDA=0 → 正常,继续
- 前 3 位相同(1,0,1),SDA 都一致,无冲突
- 第 4 位:M1 发 0(拉低),M2 发 1(释放)。线与结果 SDA=0
- M2 输出 1 却读到 0 → 检测到冲突,丢失仲裁,立即停止输出,转监听模式
- M1 继续完成传输
3.3 仲裁的优雅之处
- 无损仲裁:赢的主不知道发生过仲裁,数据完整;输的主主动退出,不破坏总线
- 逐位比较:类似"地址优先级",地址数值小的(0 多的)优先赢
- 不会数据损坏:线与保证不会两个推挽打架
3.4 仲裁丢失的场景
主 A 发 0x50(0101 0000)
主 B 发 0x52(0101 0010)
↑↑↑↑ ↑↑
位: 0101 00 → 前6位相同
第7位: A发0, B发1 → B输
所以 A(0x50)赢,B(0x52)输
- 谁地址的二进制先出现"0 而别人是 1",谁赢
- 等价:地址数值小的赢(0x50 < 0x52)
嵌入式视角:地址数值小者赢
仲裁结果是"地址数值小的优先"。设计多主系统时,关键主(高优先级)给小地址。但这不是"优先级机制"的本意,是线与的副产物。别把仲裁当可靠的优先级调度,它只解决"同时发的冲突",不解决"谁先发"。
四、仲裁丢失处理
4.1 控制器硬件行为
当控制器检测到"输出 1 读到 0"(仲裁丢失):
- 立即停止 SDA 驱动(转高阻)
- 置
ARB_LOST状态标志 - 触发中断(若使能)
- SCL 继续跟着同步(不干扰赢方)
- 转从机模式监听(可选,看是否自己地址)
4.2 软件处理
c
// 典型中断处理
void i2c_irq_handler(void) {
if (status & ARB_LOST) {
// 仲裁丢失,清除标志
clear_arb_lost();
// 记录,稍后重试
xfer.aborted = true;
schedule_retry();
return;
}
if (status & NACK) { ... }
if (status & TX_EMPTY) { ... }
}
- 仲裁丢失后,软件应退避 + 重试(类似 CSMA/CD)
- 不能立即重试(会再次冲突),加随机延时
- 多次失败上报错误
4.3 仲裁 + 时钟拉伸的交互
- 从机也可用时钟拉伸暂停,主等
- 仲裁期间若从机拉伸,所有主都等
- 仲裁丢失的主不参与后续,直到赢方完成
五、时钟拉伸(Clock Stretching)
5.1 什么是时钟拉伸
从机拉低 SCL,强制主机等待。这是从机"我还没准备好,等等"的机制:
主机: SCL ‾‾‾‾╲(释放,期望高)‾‾‾‾╲
╲ ╲
从机: ╲________________╲(拉低,拉伸)
↑ ↑
主释放SCL 从机准备好,释放
但从机拉着 SCL变高,主机继续
- 主机释放 SCL(期望变高),但从机拉低 → SCL 保持低
- 主机读 SCL,发现还是低 → 等
- 从机准备好后释放 SCL → SCL 变高 → 主机继续
5.2 拉伸的合法用途
| 场景 | 从机为什么拉伸 |
|---|---|
| 处理慢 | 从机 CPU 忙,来不及响应 |
| 准备数据 | 传感器 ADC 转换未完成 |
| EEPROM 写入 | 写入周期(5ms)未结束 |
| 协议要求 | SMBus 的 PEC 计算 |
5.3 拉伸的实现
从机在 ACK 拍之前或之后拉低 SCL:
... 主发地址 → ACK 拍前,从机拉低 SCL(拉伸) → 准备好后 ACK + 释放 SCL
- 从机拉低 SCL 期间,主机干等
- 拉伸时间不限(规范没规定上限,实际器件几 μs 到几 ms)
5.4 拉伸的滥用与超时 ⚠️
问题 :从机异常(死机/上电未完成)一直拉低 SCL,主机永远等 → 总线挂死
对策:
- 主机加超时检测:主机 SCL 释放后等 N ms 还没变高,判超时,报错
- 控制器配超时:部分控制器有 stretch timeout 寄存器
- 9 时钟脉冲恢复:总线挂死后,主机手动发 9 个 SCL 脉冲,把从机从卡死状态顶出来(详见 \[10-I2C错误处理与总线恢复])
- 禁用拉伸:部分主控可配"不允许拉伸",但失去兼容性
5.5 不同模式对拉伸的支持
| 模式 | 拉伸支持 |
|---|---|
| 标准模式 | 允许 |
| 快速模式 | 允许 |
| 快速+ | 允许,但部分主控可禁 |
| 高速模式 | 主机不要求支持拉伸(从机只能有限拉伸) |
| 超快模式 | 不支持(推挽) |
六、多主系统设计要点
6.1 多主适用场景
- 冗余系统(双主控互备)
- 多 CPU 协作(应用 CPU + 通信 CPU 共享传感器)
- 调试主 + 运行主(调试器临时接入)
6.2 设计注意事项
| 要点 | 说明 |
|---|---|
| 地址分配 | 各主给不同地址(仲裁有先后) |
| 互斥层 | 关键操作软件加锁(仲裁只防数据冲突,不防逻辑竞态) |
| 拉伸超时 | 必配,防从机挂死拖垮所有主 |
| 上拉强度 | 多主多从,电容大,Rp 要重算 |
| 仲裁丢失重试 | 退避 + 随机延时,防活锁 |
| 监控 | 统计仲裁丢失次数,异常告警 |
6.3 多主的局限
- 仲裁只到地址位不同处,若两主发同地址(同从机),会一路仲裁到数据位,可能长时间冲突
- 实际工程多主用得少,多数系统单主 + 多从更简单可靠
- 多主调试复杂,推荐除非必要不引入
七、关键问题 Q&A
Q1:多主同时发,会不会损坏器件?
A :不会。OD 开漏 + 线与,任一拉低都是低,无 VDD→GND 直通。这是 OD 物理特性保证的。多主仲裁无损,数据不冲突,器件不损坏。
Q2:仲裁谁赢?能预测吗?
A:赢的是"地址二进制先出现 0(别人是 1)的主",等价地址数值小的赢。可预测,但别当优先级机制用------它只解决"同时发的冲突",不保证关键主先发。
Q3:时钟拉伸和仲裁有什么关系?
A:
- 仲裁:多主之间解决 SDA 冲突(谁发)
- 时钟拉伸:主从之间解决"从机来不及"(节奏)
- 两者都靠 SCL/SDA 线与,但方向不同:仲裁是主主,拉伸是从主(从机拉 SCL)
- 仲裁期间从机也可拉伸,叠加效果
Q4:从机一直拉伸 SCL,主机怎么办?
A:
- 主机配拉伸超时(几 ms),超时报错
- 触发总线恢复:9 个 SCL 脉冲 + STOP,顶出从机
- 软件复位 I2C 控制器
- 极端:硬件复位从机(若有复位引脚)
详见 \[10-I2C错误处理与总线恢复]。
Q5:单主系统需要管时钟同步和仲裁吗?
A:不需要主动管,但:
- 时钟同步:单主无其他主同步,但从机拉伸仍生效(从机拉低 SCL,主等)------这本质也是 SCL 线与
- 仲裁:单主无仲裁,但控制器硬件仍可能检测 ARB_LOST(误触发,如噪声)
- 知识上要懂,单主系统重点是拉伸超时 + 总线恢复
Q6:仲裁丢失后立即重试,会怎样?
A :可能活锁------两主同时重试又同时冲突。正确做法:退避 + 随机延时重试(类似以太网 CSMA/CD)。多次失败(如 10 次)上报错误,别无限重试。
八、实战案例
案例 1:双主冗余系统,偶发数据错乱
现象:双主控共享 I2C 传感器,偶发读到对方请求的数据。
分析:两主同时发,仲裁期间地址相同(都读同传感器),一路仲裁到数据位,从机响应给当前赢方,但两主都以为是自己。
解决:
- 软件层加互斥锁(共享信号量),关键读操作前抢锁
- 或物理隔离:每个主独立 I2C 接独立传感器(功能安全推荐)
案例 2:从机拉伸导致主机超时
现象:读 EEPROM,主机偶发超时报错。
分析:EEPROM 写入后 5ms 内会拉伸 SCL(写周期未结束),主机若不配拉伸超时,等到 5ms 后才继续;若配了超时(< 5ms),误判为挂死。
解决:拉伸超时设 > EEPROM 最大写周期(10ms),或写后 sleep 5ms 再读,避免拉伸。
案例 3:多主系统,一个主死机拖垮总线
现象:主 B 死机且拉低 SDA(或 SCL),主 A 无法通信。
分析:OD 线与,任一设备拉低,总线就低。死机主拉低不放,整总线瘫痪。
解决:
- 主 A 检测总线 BUSY 超时,触发恢复
- 9 时钟脉冲 + STOP,尝试顶出死机主
- 若死机主硬件拉死,只能硬件复位或断电
- 设计上加看门狗,死机主自动复位释放总线
案例 4:仲裁丢失未处理,数据错乱
现象:多主系统,主控代码没处理 ARB_LOST,丢失后继续发,数据错乱。
分析:ARB_LOST 后控制器已停止 SDA 驱动,但软件没查标志,继续写数据寄存器,实际没发出。
解决:中断里查 ARB_LOST 标志,丢失则 abort 本次传输,退避重试。
九、本篇小结与速查
核心要点
- 多主靠 OD 线与,派生两套机制:时钟同步(SCL)+ 仲裁(SDA)
- 时钟同步:SCL 线与,低取最长、高取最短,所有主同频
- 仲裁:SDA 线与,输出 1 读到 0 者丢失,逐位比较,地址数值小者赢
- 仲裁无损:赢方不知情,输方主动退出
- 仲裁丢失:清标志 + 退避 + 随机延时重试,防活锁
- 时钟拉伸:从机拉低 SCL 强制主机等,合法用途(慢处理/写周期)
- 拉伸超时必配:防从机异常挂死总线
- 多主设计:地址分配 + 互斥 + 拉伸超时 + 上拉重算 + 监控
- 单主系统也要懂:拉伸仍生效,ARB_LOST 可能误触发
机制对照表
| 机制 | 谁发起 | 作用 | 线 |
|---|---|---|---|
| 时钟同步 | 多主 | 同频 SCL | SCL 线与 |
| 仲裁 | 多主 | 解 SDA 冲突 | SDA 线与 |
| 时钟拉伸 | 从机 | 强制主机等 | SCL 线与 |
速查表
| 现象 | 机制 | 对策 |
|---|---|---|
| 多主同发 | 仲裁 | 输方退避重试 |
| 从机来不及 | 拉伸 | 主机等(配超时) |
| 总线 BUSY 卡死 | 拉伸超时/死机 | 9 时钟恢复 |
| 仲裁频繁 | 多主竞争 | 软件互斥/减并发 |
| 数据错乱 | 仲裁未处理 | 查 ARB_LOST 标志 |
延伸阅读
- \[01-I2C电气特性与物理层] ------ 线与的物理根基
- \[04-I2C协议与时序详解] ------ 仲裁在地址帧进行
- \[08-I2C控制器寄存器与IP架构] ------ ARB_LOST/拉伸检测寄存器
- \[10-I2C错误处理与总线恢复] ------ 拉伸超时 + 9 时钟恢复(你专利主场)
- \[13-I2C调试与排查] ------ 仲裁丢失/拉伸的波形识别
参考资料
- NXP UM10204 §3.1.7(仲裁)、§3.1.9(时钟同步)、§3.1.10(拉伸)
- AN282(I2C 多主系统设计)
- SMBus Spec(拉伸超时规范)
\[00-I2C技术系列总目录] · 上一篇:\[04-I2C协议与时序详解] · 下一篇:\[06-I2C速度模式与时序参数]
✨ 感谢你的阅读 ✨
技术之路漫漫,分享是为了更好地交流。
如果本文的内容对你有启发,希望能得到你的支持:
👍 点赞 | ⭐ 收藏 | 👀 关注
如果你在调试过程中遇到了其他问题,
欢迎在 评论区 💬 留言,我们一起探讨。
关注我,一起交流底层开发的那些事儿!🚀