ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

I2C多主机仲裁与时钟延展:从开漏输出到软件实现

I2C多主机仲裁与时钟延展:从开漏输出到软件实现 1. 为什么多主机仲裁是 I2C 最精妙的设计I2C 总线只用两根线就能挂载几十个设备这个特性让它在板级低速通信里几乎无处不在。但真正让 I2C 从“能用”变成“好用”的是它天生支持多主机的能力。你想想看SPI 要选片、UART 只能点对点而 I2C 允许多个主机同时挂在同一组 SDA/SCL 上谁想说话就说话冲突了还能自动分出胜负输的那个默默退场赢的那个继续把数据发完整个过程不需要软件干预也不需要额外的仲裁线。这套机制就是多主机仲裁配合时钟延展构成了 I2C 协议里最值得反复琢磨的部分。我第一次认真研究这块是因为一个实际项目里两个 MCU 都要读写同一片 EEPROM。当时想当然地以为“两个主机分时复用就行”结果现场跑起来偶尔出现数据错乱用逻辑分析仪抓波形才发现两边同时发起传输时总线电平被拉出了一些奇怪的毛刺。后来把仲裁和时钟延展的时序彻底捋了一遍才明白问题出在哪。这篇文章就把我踩过的坑和总结出来的经验完整讲一遍从开漏输出的物理层讲起到仲裁的逐位比较过程再到时钟延展的实际影响最后给出一套可以直接复现的验证方法。不管你是刚接触 I2C 的新手还是已经用过但没深究过时序的老手应该都能从中拿到一些能直接用的东西。2. 开漏输出仲裁和时钟延展的物理基础2.1 为什么 I2C 必须用开漏输出要理解仲裁先得理解 I2C 的引脚为什么必须是开漏输出Open-Drain。开漏输出的结构很简单输出级只有一个 NMOS 管漏极接到引脚源极接地栅极由内部逻辑控制。引脚外面必须接一个上拉电阻到 VDD。当 NMOS 导通时引脚被拉到地输出低电平当 NMOS 截止时引脚处于高阻态靠外部上拉电阻把电平拉到 VDD。这个结构带来一个关键特性总线上的任何一个设备只能主动拉低不能主动拉高。高电平是靠上拉电阻“被动”产生的。这意味着如果两个设备同时输出一个想拉低、一个想释放总线结果一定是低电平。这个“线与”逻辑就是仲裁能够工作的物理前提。对比一下推挽输出推挽输出的高电平是主动驱动上去的如果一个设备驱动高、另一个驱动低两个 MOS 管同时导通会形成从 VDD 到 GND 的低阻通路瞬间大电流烧毁引脚。所以 I2C 规范明确禁止在 SDA/SCL 上使用推挽输出。你在选型或者写 GPIO 初始化代码时一定要把这两个引脚配成开漏模式否则轻则通信失败重则损坏芯片。2.2 上拉电阻的选型计算上拉电阻的阻值不是随便选的它直接影响上升沿的时间和总线负载能力。I2C 标准模式100 kHz和快速模式400 kHz对上升时间有明确要求标准模式上升时间不超过 1000 ns快速模式不超过 300 ns。上升时间由 RC 时间常数决定R 是上拉电阻C 是总线电容包括引脚电容、走线电容和器件电容。假设你的总线电容实测约 200 pF快速模式下要求上升时间小于 300 ns。RC 充电到 0.7 VDD 左右大约需要 1.2 个时间常数所以 1.2RC ≤ 300 ns算出来 R ≤ 300 ns / (1.2 × 200 pF) ≈ 1.25 kΩ。但电阻也不能太小因为设备拉低时灌电流不能超过规范允许值通常 3 mA 左右。如果 VDD 是 3.3 VR 1.25 kΩ 时灌电流约 2.64 mA还在安全范围内。实际选型时我一般先用 2.2 kΩ 或 4.7 kΩ 试然后用示波器看上升沿如果太慢就减小电阻如果灌电流太大就增大电阻或者降低总线电容。注意总线电容的估算很容易偏小。每增加一个设备通常要加 10 pF 左右的引脚电容走线每厘米大约 1 pF。如果挂了 10 个设备光引脚电容就 100 pF 了再加上走线200 pF 是很常见的。所以长总线、多设备场景下上拉电阻往往要选得比理论值更小或者用有源上拉电路。2.3 开漏输出与推挽输出的实际对比特性开漏输出推挽输出高电平驱动靠外部上拉被动内部 PMOS 主动驱动低电平驱动NMOS 主动拉低NMOS 主动拉低多设备共享支持线与逻辑可仲裁禁止直接并联会短路上升沿速度受 RC 限制较慢快由驱动能力决定典型应用I2C、SMBus、PMBusSPI、UART、GPIO 输出这张表里最值得记住的一点是开漏输出的“慢”不是缺点而是仲裁能够实现的前提。正因为高电平是“弱”的任何设备都能把它拉低仲裁才有意义。如果高电平是强驱动的两个设备一个拉高一个拉低直接就是电源短路。3. 多主机仲裁的逐位比较过程3.1 仲裁发生在什么时候多主机仲裁只在两个或多个主机同时发起传输时才会触发。如果总线上只有一个主机在活动从机只是被动响应不会产生仲裁。仲裁发生在地址阶段和数据阶段每一个 bit 的传输都是一次“竞争”的机会。具体来说主机在发送每一个 bit 时会先把 SDA 设置成自己想要的电平然后释放 SCL或者拉低 SCL 再释放取决于时钟同步阶段。在 SCL 高电平期间主机读取 SDA 的实际电平。如果读回来的电平和自己发送的一致说明目前没有冲突继续发下一个 bit。如果读回来的是低电平而自己发的是高电平说明有另一个主机在拉低 SDA自己仲裁失败必须立即退出停止驱动 SDA 和 SCL转为从机接收模式或者等待总线空闲。3.2 一个完整的仲裁实例假设总线上有两个主机 A 和 B同时想发起传输。A 要访问地址 0x50B 要访问地址 0x48。地址字节是 7 位地址加 1 位读写位我们只看前 7 位。地址 0x50 的二进制是 1010000地址 0x48 的二进制是 1001000。两个主机从最高位开始逐位发送第 1 位A 发 1B 发 1SDA 实际为 1双方都读到 1继续。第 2 位A 发 0B 发 0SDA 实际为 0双方都读到 0继续。第 3 位A 发 1B 发 0。A 释放 SDA想发 1B 拉低 SDA发 0。由于线与逻辑SDA 实际为 0。A 读回 0发现自己发的是 1 但读回 0仲裁失败立即退出。B 读回 0和自己发的一致继续传输。整个过程没有任何数据丢失B 的传输完全不受影响A 只是“安静地”退出了。这就是仲裁的精妙之处失败方不会破坏总线上的数据成功方甚至感知不到有人曾经竞争过。3.3 仲裁失败的几种典型场景仲裁失败不一定只发生在地址阶段数据阶段同样可能发生。我整理了几种常见情况地址不同两个主机访问不同从机地址在地址字节的某一位分出胜负。读写位不同两个主机访问同一从机但一个读一个写在地址字节的最后一位R/W 位分出胜负。数据不同两个主机访问同一从机且都是写操作但写入的数据不同在数据字节的某一位分出胜负。重复起始条件冲突一个主机想发重复起始条件另一个主机还在发数据可能在时序上产生冲突。实操心得仲裁失败的主机必须能够检测到自己失败了并且立即释放总线。如果你用软件模拟 I2C在发送每一位之后都要读一次 SDA 引脚确认实际电平和自己发送的一致。很多新手写的软件 I2C 代码只负责发不负责读回校验在多主机场景下就会出问题。3.4 仲裁与时钟同步的关系仲裁和时钟同步是两件事但经常同时发生。时钟同步解决的是“多个主机同时拉低 SCL 时SCL 的低电平周期怎么算”的问题。由于 SCL 也是开漏的任何一个主机拉低 SCLSCL 就是低。只有当所有主机都释放 SCL 时SCL 才会被上拉电阻拉高。所以 SCL 的低电平周期由拉低时间最长的那个主机决定高电平周期由释放最早的那个主机决定。这天然实现了“慢的主机拖住快的主机”让所有主机都能跟上节奏。仲裁则是在 SDA 上进行的决定谁的数据有效。两者配合保证了多主机环境下总线不会混乱。4. 时钟延展从机也能控制节奏4.1 时钟延展是什么时钟延展Clock Stretching是 I2C 里另一个容易被忽略但非常重要的机制。正常情况下SCL 由主机驱动从机只是被动采样。但 I2C 允许从机在需要更多时间处理数据时主动把 SCL 拉低强制主机等待。这就是时钟延展。典型场景主机发送一个读命令从机需要时间去采集传感器数据或者从内部 Flash 读取数据这段时间可能几毫秒甚至几十毫秒。如果从机不能拉低 SCL主机就会按照自己的节奏继续发时钟从机来不及响应数据就错了。有了时钟延展从机可以在准备好之前一直拉低 SCL主机检测到 SCL 没有按时释放就会等待直到从机释放 SCL 后才继续。4.2 时钟延展的时序细节时钟延展发生在 SCL 低电平期间。主机释放 SCL 后SCL 应该被上拉电阻拉高。但如果从机此时拉低 SCLSCL 就保持低电平。主机在释放 SCL 后会检测 SCL 的实际电平如果发现 SCL 还是低就知道从机在延展时钟于是等待。直到 SCL 变高主机才继续下一个时钟周期。这里有一个容易混淆的点时钟延展和仲裁失败都会导致 SCL 被拉低但它们的含义完全不同。仲裁失败是主机之间的竞争失败方退出时钟延展是从机主动要求等待主机必须配合。主机在检测到 SCL 被拉低时需要根据上下文判断是哪种情况。4.3 时钟延展对软件 I2C 的影响如果你用 GPIO 模拟 I2C时钟延展处理不好就会出大问题。很多软件 I2C 实现里SCL 拉高之后直接延时固定时间然后就读 SDA根本不检查 SCL 是否真的变高了。如果从机在延展时钟SCL 还是低这时候读 SDA 读到的是无效数据。正确的做法是SCL 拉高之后进入一个循环等待 SCL 引脚实际读到高电平再继续后面的操作。这个循环要有超时保护防止从机一直拉低 SCL 导致程序卡死。下面是一段典型的软件 I2C 时钟处理代码// 释放 SCL等待其实际变高处理时钟延展 void i2c_scl_release_and_wait(void) { SCL_PIN 1; // 开漏输出写 1 表示释放 uint32_t timeout 0; while (SCL_READ() 0) { timeout; if (timeout MAX_TIMEOUT) { // 超时处理可能是从机故障或总线被拉死 i2c_bus_recovery(); return; } } }这段代码里SCL_PIN 1在开漏模式下表示释放 SCL而不是主动驱动高。然后循环读取 SCL 引脚的实际电平直到变高或者超时。超时阈值根据你的从机最坏响应时间来定一般设几毫秒到几十毫秒。4.4 哪些从机常用时钟延展不是所有从机都会用时钟延展但以下几类设备很常见EEPROM写入操作需要内部擦写时间通常 5 ms 左右期间会拉低 SCL 或者让主机轮询应答。传感器比如温度、湿度、压力传感器转换时间可能几十毫秒期间会延展时钟。IO 扩展芯片某些型号在内部状态更新时会延展时钟。微控制器作为从机如果从机 MCU 的中断响应慢可能会延展时钟。注意有些主机特别是硬件 I2C 外设对时钟延展的支持不完整。比如某些老型号的 MCU硬件 I2C 在从机延展时钟超过一定时间后会报超时错误。选型时要查数据手册确认硬件 I2C 是否支持时钟延展以及最大支持多长时间。5. 用逻辑分析仪实测仲裁与时钟延展5.1 测试环境搭建光看理论不够我习惯用逻辑分析仪把波形抓出来看。下面是我常用的一套测试环境两个 MCU 开发板都配置为 I2C 主机挂在同一组 SDA/SCL 上。一个 I2C 从机设备比如 24C02 EEPROM 或者 SSD1306 OLED。逻辑分析仪至少支持 I2C 协议解码采样率建议 24 MHz 以上。上拉电阻 4.7 kΩVDD 3.3 V。接线很简单两个 MCU 的 SDA 接在一起SCL 接在一起分别接上拉电阻到 3.3 V从机的 SDA/SCL 也接上去。逻辑分析仪的通道 0 接 SCL通道 1 接 SDA共地。5.2 抓取仲裁波形让两个 MCU 同时发起传输一个访问地址 0x50一个访问地址 0x48。逻辑分析仪设置 I2C 解码触发条件设为 SDA 下降沿起始条件。抓到的波形里你可以看到起始条件之后两个主机同时发送地址字节。在前几位SDA 波形正常两个主机发送的位相同。到某一位时SDA 保持低电平但其中一个主机的传输突然停止SCL 继续由另一个主机驱动。解码器会显示其中一个地址字节完整另一个地址字节不完整。如果你用的是带协议解码的逻辑分析仪它通常会直接标出“仲裁丢失”或者显示不完整的地址。我用的那款会在波形上标注“Arbitration Lost”非常直观。5.3 抓取时钟延展波形时钟延展的波形更隐蔽一些。让主机读取一个传感器传感器转换时间设为 10 ms。抓到的波形里主机发送读地址从机应答。主机继续发时钟准备读取数据。在某一个时钟周期SCL 被拉低后保持低电平很长时间比如 10 ms远超正常的时钟低电平时间。这段时间里SDA 可能保持高电平或者低电平取决于从机状态。10 ms 后SCL 被释放时钟继续数据正常读出。逻辑分析仪的 I2C 解码器通常会把这段长时间的低电平标注为“Clock Stretching”或者直接显示一个很长的低电平周期。如果你看到 SCL 低电平时间远大于正常值基本就是时钟延展了。5.4 实测数据记录我整理了一组实测数据供参考测试项条件实测结果仲裁失败点地址 0x50 vs 0x48第 3 位分出胜负0x48 获胜仲裁失败后总线状态失败方立即释放SCL/SDA 由获胜方继续驱动无毛刺时钟延展时长EEPROM 写周期约 5 msSCL 低电平保持时钟延展时长传感器转换约 12 msSCL 低电平保持软件 I2C 未处理延展固定延时读 SDA读到错误数据概率约 30%软件 I2C 处理延展等待 SCL 实际变高数据正确率 100%这张表里最值得关注的是最后两行。同样的硬件软件 I2C 如果不处理时钟延展错误率能到 30%这在批量生产里是不可接受的。加上等待 SCL 变高的逻辑之后问题彻底消失。6. 常见问题与排查技巧实录6.1 仲裁失败后总线锁死怎么办仲裁失败本身不会锁死总线但如果失败方没有正确释放 SDA/SCL或者释放时序不对可能导致总线状态异常。我遇到过一种情况失败方在仲裁失败后没有立即把 SDA 和 SCL 都释放而是只释放了 SDASCL 还拉低着结果获胜方发完当前字节后发现 SCL 被拉低以为有时钟延展一直等总线就卡住了。解决办法仲裁失败的处理逻辑里必须同时释放 SDA 和 SCL并且把引脚重新配置为输入模式或者开漏输出高阻态。下面是一段参考代码// 仲裁失败处理 void i2c_arbitration_lost_handler(void) { SDA_PIN 1; // 释放 SDA SCL_PIN 1; // 释放 SCL // 切换为从机模式或者等待总线空闲 i2c_state I2C_STATE_IDLE; // 等待总线空闲SDA 和 SCL 都为高 while (SDA_READ() 0 || SCL_READ() 0) { // 超时保护 } }6.2 时钟延展导致主机超时有些硬件 I2C 外设有超时机制如果 SCL 被拉低超过设定时间会报超时错误并复位总线。如果你的从机时钟延展时间较长比如 20 ms而主机超时设的是 10 ms就会误报。解决办法是查从机数据手册确认最大时钟延展时间然后把主机超时设得比它大。如果主机不支持调整超时可能需要在软件层面做重试。6.3 多主机环境下地址冲突多主机仲裁能解决同时发起传输的问题但解决不了地址冲突。如果两个从机用了同一个地址主机访问这个地址时两个从机都会应答SDA 上的应答电平可能被两个从机同时拉低看起来正常但数据阶段就会混乱。所以多主机系统里地址分配一定要提前规划好避免重复。6.4 常见问题速查表现象可能原因排查方法解决措施总线偶尔锁死仲裁失败后未释放 SCL逻辑分析仪看 SCL 是否被持续拉低失败处理里同时释放 SDA/SCL读数据错误未处理时钟延展看 SCL 低电平是否异常长软件 I2C 等待 SCL 实际变高主机报超时从机延展时间超过主机超时对比从机手册和主机配置调整主机超时或降低从机延展多主机数据错乱地址冲突或仲裁逻辑错误抓波形看仲裁点重新分配地址检查仲裁代码上升沿太慢上拉电阻太大或总线电容太大示波器看上升时间减小上拉电阻或减少设备从机不应答地址错误或从机未上电逻辑分析仪看地址字节核对地址检查供电6.5 几个容易忽略的细节第一个细节是重复起始条件。在多主机环境里一个主机发重复起始条件时另一个主机可能还在传输中。如果时序不对可能导致仲裁异常。建议在发重复起始条件之前确认总线空闲。第二个细节是SDA 建立时间和保持时间。仲裁过程中SDA 的电平变化必须在 SCL 低电平期间完成SCL 高电平期间 SDA 必须稳定。如果软件 I2C 的延时不够SDA 变化发生在 SCL 高电平期间会被误判为起始或停止条件。第三个细节是总线恢复。如果总线真的锁死了可以通过发送 9 个时钟脉冲来尝试恢复。具体做法是把 SCL 配置为开漏输出手动发 9 个时钟然后发一个停止条件。很多 MCU 的硬件 I2C 外设有自动总线恢复功能但软件 I2C 需要自己实现。7. 软件 I2C 完整实现中的仲裁与延展处理7.1 发送一个字节的完整逻辑软件 I2C 发送一个字节时需要逐位发送并在每一位之后检查仲裁是否丢失。下面是一段参考实现// 发送一个字节返回 0 成功-1 仲裁失败 int i2c_send_byte(uint8_t data) { for (int i 7; i 0; i--) { // 拉低 SCL SCL_PIN 0; i2c_delay(); // 设置 SDA if (data (1 i)) { SDA_PIN 1; // 释放 SDA输出高 } else { SDA_PIN 0; // 拉低 SDA } i2c_delay(); // 释放 SCL等待其实际变高 SCL_PIN 1; uint32_t timeout 0; while (SCL_READ() 0) { timeout; if (timeout MAX_TIMEOUT) { return -2; // 时钟延展超时 } } // 检查仲裁如果发送的是 1但 SDA 读回 0说明仲裁失败 if ((data (1 i)) (SDA_READ() 0)) { return -1; // 仲裁失败 } i2c_delay(); } return 0; }这段代码里关键点是发送 1 时释放 SDA然后读回 SDA 实际电平。如果读回 0说明有其他主机在拉低自己仲裁失败。发送 0 时不需要检查因为拉低是“强”操作不会有冲突。7.2 接收一个字节的逻辑接收字节时主机释放 SDA由从机驱动。主机只负责发时钟并在 SCL 高电平期间读取 SDA。同样需要处理时钟延展// 接收一个字节返回读到的数据 uint8_t i2c_receive_byte(void) { uint8_t data 0; for (int i 7; i 0; i--) { SCL_PIN 0; i2c_delay(); SDA_PIN 1; // 释放 SDA由从机驱动 i2c_delay(); SCL_PIN 1; // 等待 SCL 实际变高 uint32_t timeout 0; while (SCL_READ() 0) { timeout; if (timeout MAX_TIMEOUT) { return 0xFF; // 超时 } } // 读取 SDA if (SDA_READ()) { data | (1 i); } i2c_delay(); } return data; }接收时不需要检查仲裁因为主机已经释放了 SDA不会和从机竞争。但如果总线上有另一个主机也在接收理论上可能发生冲突不过这种情况很少见一般多主机系统里读操作由单个主机发起。7.3 起始和停止条件的处理起始条件是在 SCL 高电平期间SDA 从高变低。停止条件是在 SCL 高电平期间SDA 从低变高。这两个条件都需要在 SCL 高电平期间操作 SDA所以必须确保 SCL 已经实际变高否则时序不对。// 发送起始条件 void i2c_start(void) { SDA_PIN 1; SCL_PIN 1; i2c_delay(); SDA_PIN 0; // SCL 高时 SDA 下降沿 i2c_delay(); SCL_PIN 0; i2c_delay(); } // 发送停止条件 void i2c_stop(void) { SDA_PIN 0; SCL_PIN 1; i2c_delay(); SDA_PIN 1; // SCL 高时 SDA 上升沿 i2c_delay(); }实操心得起始和停止条件的延时很重要。如果 SDA 变化太快逻辑分析仪可能抓不到从机也可能误判。我一般用 5 μs 左右的延时对应 100 kHz 的标准模式。快速模式可以缩短到 1 μs 左右但要确保满足建立时间和保持时间要求。8. 多主机仲裁与时钟延展的扩展思考8.1 与 PMBus 和 SMBus 的关系PMBus 和 SMBus 都是基于 I2C 的扩展协议。SMBus 在 I2C 基础上增加了超时机制和更严格的电气规范PMBus 又在 SMBus 上增加了电源管理相关的命令集。它们都继承了 I2C 的多主机仲裁和时钟延展机制但在超时时间上有更明确的要求。比如 SMBus 规定从机时钟延展不能超过 25 ms主机超时通常设 35 ms。如果你用 I2C 的硬件外设去兼容 SMBus 设备要注意超时配置是否匹配。8.2 多主机仲裁在冗余系统中的应用在一些高可靠性系统里会故意设计两个主机互为备份同时挂在同一组 I2C 总线上。正常工作时只有一个主机活动另一个处于监听状态。如果主主机故障备主机立即接管。仲裁机制保证了切换过程中不会出现两个主机同时驱动总线导致数据冲突。这种设计在工业控制和汽车电子里比较常见。8.3 时钟延展对实时性的影响时钟延展虽然解决了从机响应慢的问题但也引入了不确定性。主机无法预知从机会延展多长时间最坏情况下可能导致实时任务超时。所以在硬实时系统里要么选择不支持时钟延展的从机要么把 I2C 操作放在低优先级任务里避免阻塞关键路径。我个人的经验是如果系统对实时性要求高I2C 总线上尽量不要挂会长时间延展时钟的设备或者用硬件 I2C 配合 DMA减少 CPU 占用。8.4 逻辑分析仪选型建议抓 I2C 波形对逻辑分析仪的要求不算高但有几个点要注意。采样率至少是总线频率的 10 倍以上100 kHz 总线用 1 MHz 采样率勉强够但要看毛刺的话建议 24 MHz 以上。协议解码功能很重要能直接标出地址、数据、ACK/NACK 和仲裁丢失。通道数至少 2 路如果要同时看多个总线4 路或 8 路更灵活。我用过几款便宜的和贵的在解码准确性上差别不大但贵的在毛刺捕捉和长时间录制上更稳。9. 我个人在实际操作中的几点体会多主机仲裁和时钟延展这两个机制看文档的时候觉得很简单真正动手调的时候才会发现细节很多。我最大的体会是软件 I2C 一定要处理时钟延展硬件 I2C 一定要确认超时配置。这两点做到了大部分诡异问题都能避免。另外逻辑分析仪是必备工具。很多问题光靠看代码想不出来抓一次波形就一目了然。我现在的习惯是任何 I2C 相关的功能第一次调试必须抓波形确认起始条件、地址、数据、ACK、停止条件都正常再往下做业务逻辑。最后分享一个小技巧如果你怀疑总线有仲裁问题可以故意让两个主机同时发起传输然后用逻辑分析仪抓波形看仲裁点是否正常。这个测试可以在实验室里反复做比在现场碰运气强得多。
返回列表