ARTICLE DETAIL

资讯详情

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

I2C时钟延展与死锁恢复:RTL状态机从定时到握手的工程实践

I2C时钟延展与死锁恢复:RTL状态机从定时到握手的工程实践 1. 时钟延展不是慢而是总线上的谈判机制很多人第一次在逻辑分析仪上看到 SCL 被从机拉低不放第一反应是总线挂了或者主机驱动有问题。实际上时钟延展Clock Stretching是 I2C 协议里一个完全合法、且被大量从机器件使用的流控手段。它的本质是从机在还没准备好数据的时候把 SCL 线钳在低电平告诉主机你先别急着打节拍等我一下。这件事在协议层面非常简单但在 RTL 落地的时候坑就多了。因为大多数工程师写 I2C Master 的思路是状态机按节拍走SCL 的高低由状态机定时翻转根本没有考虑 SCL 会被外部拉低这种情况。一旦从机开始延展主机如果还傻乎乎地按自己的节奏翻转 SCL就会出现两种情况要么主机以为自己发出了高电平实际上线上还是低因为从机在拉导致采样点错位要么主机在 SCL 还没真正释放的时候就去采样 SDA读到错误数据。所以这一讲的核心不是教你怎么支持时钟延展而是教你怎么把主机设计成尊重时钟延展。这两个词差别很大。支持是被动兼容尊重是主动把 SCL 的真实电平当作状态机推进的条件之一。1.1 为什么从机要延展三个真实场景第一个场景是EEPROM 写周期。像 AT24C 系列这类器件收到一帧写数据后内部要花几毫秒把数据烧进存储单元。这段时间它不会响应任何新的传输如果主机紧接着发下一帧从机会直接不 ACK。有些器件会选择在这段时间拉低 SCL让主机等着这就是最典型的延展。第二个场景是传感器转换。比如某些温湿度、压力传感器收到测量命令后需要几十毫秒完成一次 ADC 转换。转换期间它可能通过延展 SCL 来拖住主机避免主机过早来读结果读到旧数据。第三个场景是MCU 作为从机时的软件响应延迟。当一颗 MCU 用软件模拟 I2C 从机时它响应中断、准备数据都需要时间。如果主机速度太快从机来不及就会用延展来争取时间。这也是为什么很多软件 I2C 从机在高速主机下容易出问题的原因。理解了这三个场景你就明白时钟延展不是异常而是从机的一种礼貌请求。主机要做的是在每个 SCL 高电平阶段真正去读 SCL 的输入值而不是假设自己写出去的就是线上的值。1.2 主机 RTL 里最容易被忽略的一行代码大部分 I2C Master 的 RTL 长这样状态机在某个状态把scl_out置 1然后等几个时钟周期再置 0。问题就出在等几个时钟周期这个动作上。正确的做法是置 1 之后进入一个等待状态条件是scl_in 1注意是输入不是输出只有真正读到高电平才继续往下走。// 错误写法按固定周期翻转 SCL_HIGH: begin scl_out 1b1; cnt cnt 1; if (cnt CYCLE) state SCL_LOW; end // 正确写法等待 SCL 真正变高 SCL_HIGH: begin scl_out 1b1; // 释放 SCL注意是开漏 if (scl_in 1b1) // 真正读到高电平才推进 state SCL_HIGH_WAIT; end这里有个关键细节scl_out在开漏结构下置 1 其实是释放置 0 才是拉低。所以主机永远无法强制 SCL 变高只能释放让它被上拉电阻拉高。如果从机在拉低scl_in就一直是 0主机的状态机就会老老实实停在这里等。这就是尊重时钟延展的物理基础。提示如果你的 I2C 用的是推挽输出而不是开漏那时钟延展根本无从谈起而且多主机会直接烧管子。开漏加外部上拉是 I2C 的硬性要求不是可选项。2. 把时钟延展做进状态机从定时到握手的思维转变上一节讲了原理这一节讲怎么落地。核心思路是把 I2C Master 的状态机从时间驱动改成事件驱动。时间驱动是我数到 100 个周期就翻 SCL事件驱动是我等到 SCL 真的变高/变低再进入下一步。这个转变听起来简单但会牵动整个状态机的结构。2.1 状态机重构加入 SCL 采样等待态一个能正确处理时钟延展的 Master 状态机至少要有这几个和 SCL 相关的状态状态动作退出条件SCL_RELEASE释放 SCLscl_out1无条件进入 WAIT_HIGHWAIT_HIGH保持释放采样 scl_inscl_in1 且满足建立时间SCL_PULL拉低 SCLscl_out0无条件进入 WAIT_LOWWAIT_LOW保持拉低采样 scl_inscl_in0确认拉低生效注意 WAIT_HIGH 的退出条件里有个建立时间。这是因为 SCL 变高之后SDA 需要保持稳定一段时间从机才能正确采样。这个时间由 I2C 标准规定标准模式 4.7us快速模式 0.6us 等。所以 WAIT_HIGH 不能一读到高就立刻走还要再等一个建立时间计数器。WAIT_LOW 看起来多余其实不然。在多主机或者总线电容较大的情况下SCL 从高变低也需要时间。确认scl_in0再往下走可以避免在 SCL 还没完全拉低时就去改 SDA造成误采样。2.2 超时保护延展和死锁只有一线之隔时钟延展是合法的但如果从机因为某种原因比如上电异常、内部状态机卡死一直拉着 SCL 不放主机就会永远停在 WAIT_HIGH。这就是死锁。所以一个健壮的 Master 必须给 WAIT_HIGH 加一个超时计数器。超时值怎么定我的经验是取正常传输最长单比特时间的 10 到 50 倍。比如快速模式 400kHz单比特 2.5us那超时可以设 100us 到 250us。太小会误判正常的延展为死锁太大则失去保护意义。WAIT_HIGH: begin if (scl_in 1b1) begin // 正常进入建立时间等待 state SETUP_TIME; end else if (timeout_cnt TIMEOUT_MAX) begin // 超时判定为死锁进入恢复流程 state BUS_RECOVERY; end else begin timeout_cnt timeout_cnt 1; end end这里有个细节超时计数器应该在进入 WAIT_HIGH 时清零而不是全局一直数。否则前面传输累积的时间会误触发超时。2.3 延展期间的 SDA 保持时钟延展期间SCL 是低的SDA 应该保持什么状态答案是保持当前比特的值不变。因为从机在延展的时候它可能正在采样 SDA如果主机这时候动了 SDA从机会读到错误数据。所以在 WAIT_HIGH 和 WAIT_LOW 这两个状态里SDA 的输出必须锁死不能有任何变化。这一点在写 RTL 时容易被忽略因为很多人习惯在状态切换时顺手更新 SDA。正确的做法是把 SDA 的更新放在 SCL 拉低之后、拉高之前的那段时间里也就是 SCL 低电平期间改 SDA这是 I2C 的基本规则。3. 死锁恢复当总线真的卡住了怎么办超时只是发现了死锁真正的难点是恢复。总线死锁的典型表现是 SCL 或 SDA 被某个器件持续拉低主机无法发起任何新的传输。这时候你需要一套恢复流程把总线从卡死状态里拽出来。3.1 死锁的两种形态SCL 卡低和 SDA 卡低SCL 卡低通常是某个从机在延展时内部状态机挂了一直不放 SCL。SDA 卡低则更麻烦可能是某个从机在发送数据时中途复位把 SDA 拉低后没释放也可能是主机自己在某个错误状态下把 SDA 拉死了。两种情况的恢复策略不同。SCL 卡低时主机能做的不多因为 SCL 被从机控制主机只能等或者尝试发复位序列。SDA 卡低时主机可以主动发时钟脉冲让从机把剩余的数据位吐完从而释放 SDA。3.2 九脉冲恢复法让从机把话说完九脉冲恢复法是业界最常用的死锁恢复手段。原理是如果从机在发送数据时卡住它可能还有几个比特没发完。主机手动发送最多 9 个 SCL 脉冲一个字节 8 位加 1 个 ACK 位每发一个脉冲就检查 SDA 是否释放。一旦 SDA 变高说明从机已经把数据吐完总线恢复空闲。具体流程确认 SDA 被拉低sda_in 0。主机接管 SCL发送一个 SCL 脉冲拉低、等待、释放、等待变高。检查sda_in如果变高跳到第 5 步。重复第 2-3 步最多 9 次。发送一个 STOP 条件SCL 高时 SDA 从低变高把总线置回空闲态。如果 9 个脉冲后 SDA 还是低说明问题严重可能需要硬件复位。// 九脉冲恢复的简化状态机 RECOVERY_PULSE: begin scl_out 1b0; // 拉低 SCL state RECOVERY_LOW_WAIT; end RECOVERY_LOW_WAIT: begin if (scl_in 1b0) state RECOVERY_RELEASE; end RECOVERY_RELEASE: begin scl_out 1b1; // 释放 SCL state RECOVERY_HIGH_WAIT; end RECOVERY_HIGH_WAIT: begin if (scl_in 1b1) begin pulse_cnt pulse_cnt 1; if (sda_in 1b1 || pulse_cnt 9) state RECOVERY_STOP; else state RECOVERY_PULSE; end end这段代码的关键点是每个脉冲都要等 SCL 真正变高再计数这和前面讲的尊重时钟延展是同一个道理。如果恢复期间从机还在延展主机也要等。3.3 恢复之后的收尾STOP 条件不能省很多人做完九脉冲就直接开始新的传输结果发现还是不通。原因是总线没有回到明确的空闲态。I2C 的空闲态定义是 SCL 高、SDA 高。九脉冲结束后SDA 可能已经是高但 SCL 的状态不确定而且从机的内部状态机可能还停在等待 ACK的状态。所以恢复流程的最后一步必须是发一个标准的 STOP 条件SCL 保持高SDA 从低拉到高。这个动作会通知总线上所有从机本次传输结束大家回到空闲态。发完 STOP 之后再等几个周期确认 SCL 和 SDA 都是高才算真正恢复。注意STOP 条件要求 SDA 在 SCL 高电平期间变化。如果你的恢复逻辑里 SCL 和 SDA 的时序没对齐可能发出一个假的 START 而不是 STOP反而让从机进入新的传输状态。这个坑我在早期项目里踩过逻辑分析仪上看得清清楚楚。4. 总线鲁棒性不只是延展和死锁时钟延展和死锁恢复是总线鲁棒性的两个具体问题但鲁棒性本身是个更大的话题。一个真正健壮的 I2C 控制器还要考虑毛刺过滤、多主机仲裁、上电初始化等问题。这一节把这些相关点串起来讲。4.1 输入毛刺过滤为什么 scl_in 不能直接用I2C 总线通常走板级走线长度可能几十厘米加上上拉电阻和总线电容信号边沿不会很陡。如果环境里有干扰scl_in和sda_in上可能出现窄毛刺。如果状态机直接采样这些毛刺可能误判为时钟脉冲或数据变化。标准做法是对输入做数字滤波连续采样 N 次比如 3 次或 5 次只有连续相同才认为是有效电平。这个滤波器要放在输入端口和状态机之间。// 简单的 3 采样多数表决滤波器 always (posedge clk) begin scl_sync {scl_sync[1:0], scl_in_raw}; if (scl_sync 3b111) scl_filtered 1b1; else if (scl_sync 3b000) scl_filtered 1b0; // 其他情况保持 end滤波器的采样时钟要足够快至少是 SCL 频率的 10 倍以上否则会引入额外延迟影响时序余量。4.2 多主机仲裁延展之外的另一种等待多主机系统里两个主机可能同时发起传输。仲裁的规则是谁先发出高电平而线上是低谁就退出。这个过程和时钟延展有个相似点主机都要读回线上的真实电平而不是假设自己写的就是对的。仲裁失败的主机要立刻切换到从机模式或者退出并且释放 SDA。如果仲裁逻辑写得不好可能出现两个主机都以为自己赢了同时驱动 SDA造成总线冲突。这也是为什么 I2C 必须是开漏结构——开漏下线与任何一方拉低都是低不会出现推挽的短路。4.3 上电初始化别让从机在主机之前说话有些从机上电后会主动拉低 SDA 或 SCL表示自己还在初始化。如果主机这时候就开始发传输会直接冲突。健壮的做法是主机上电后先等待一段时间比如 10ms然后检查 SCL 和 SDA 是否都是高。如果不是先执行九脉冲恢复再开始正常传输。这个上电自检步骤在很多参考设计里被省略但在实际产品里非常必要。我见过一个项目因为某个传感器上电慢主机启动太快导致每次冷启动都有一定概率总线卡死加了上电自检之后问题消失。5. 实测验证用逻辑分析仪看延展和恢复RTL 写完了怎么确认它真的能处理时钟延展和死锁恢复靠仿真只能覆盖你想到的情况真实器件的行为往往更野。我的做法是搭一个测试平台用逻辑分析仪抓波形人为制造延展和死锁。5.1 制造时钟延展用一颗慢速从机最简单的办法是找一颗会延展的从机比如某些 EEPROM 在写周期会拉低 SCL。如果手头没有可以用另一颗 MCU 模拟从机在收到数据后故意延时再释放 SCL。逻辑分析仪上应该能看到 SCL 被拉低的时间明显长于正常比特周期而主机的状态机停在那里等没有继续翻转。验证要点有三个第一延展期间 SDA 保持不变第二延展结束后主机从正确的位置继续第三整个传输的数据内容正确。第三点最重要因为延展处理错了往往表现为数据错位而不是完全不通。5.2 制造死锁把 SDA 强行拉低死锁测试可以用一根跳线把 SDA 短接到地模拟从机卡死。然后触发主机的传输观察它是否在超时后进入恢复流程发出九脉冲最后发 STOP。逻辑分析仪上应该能看到 9 个 SCL 脉冲然后 SDA 被释放如果你在这之前把跳线拿掉最后是一个 STOP 条件。这个测试要注意跳线拿掉的时机要配合恢复流程。如果一直短接九脉冲之后 SDA 还是低主机应该报告恢复失败而不是无限重试。5.3 常见波形异常对照表波形现象可能原因排查方向SCL 被拉低后主机继续翻转状态机没等 scl_in检查 WAIT_HIGH 逻辑延展后数据错位SDA 在延展期间被改动检查 SDA 更新时机九脉冲后总线仍忙没发 STOP 或从机未复位补 STOP检查从机恢复后首次传输失败从机状态机未回空闲增加恢复后延时随机性总线卡死上电时序或毛刺加上电自检和滤波这张表是我自己调试时总结的基本上覆盖了 80% 的延展和死锁相关问题。遇到波形异常先对照这张表定位方向比盲目改代码效率高得多。6. 几个只有踩过坑才知道的细节最后分享几个在实际项目里踩出来的经验这些在标准文档里通常不会写但很影响成败。第一个是延展超时值不要设成固定值。不同从机的延展时间差别很大EEPROM 可能几毫秒传感器可能几十毫秒。如果超时设得太小正常延展会被误判为死锁触发不必要的恢复流程反而把总线搞乱。我的做法是把超时值做成寄存器可配不同从机用不同配置。第二个是恢复流程要能被打断。如果主机在九脉冲恢复期间收到更高优先级的任务应该能安全暂停而不是卡在恢复状态里。这要求恢复状态机也是可中断的把当前脉冲计数和状态保存下来。第三个是DFT 复位对 I2C 控制器的影响。做 DFT 插复位的时候I2C 控制器的状态机和输出寄存器会被复位但总线上的从机不会跟着复位。如果复位发生在传输中途从机会一直等后续时钟导致总线卡死。所以 DFT 复位策略里I2C 控制器最好有独立的软复位复位后自动执行一次总线恢复而不是简单地把所有寄存器清零。第四个是SDA 的方向切换时机。I2C 是半双工SDA 在发送和接收之间要切换方向。切换必须发生在 SCL 低电平期间而且切换后要等一个稳定时间再采样。如果切换太晚可能错过从机的 ACK切换太早可能干扰当前比特。这些细节单独看都很小但组合起来决定了一个 I2C 控制器是能用还是好用。我在实际项目里的体会是I2C 的协议本身很简单难的是把各种边界情况都考虑到尤其是在从机行为不完全符合预期的时候主机要有足够的鲁棒性去应对。时钟延展和死锁恢复只是其中两个最典型的场景把这两个做扎实了整个控制器的可靠性会上一个台阶。
返回列表