从 EtherCAT 主站拓扑判定说起:工业级"稳定"不是简单延时
做工控协议栈的人大概都懂,真正难的不是"能通",而是"通了之后怎么证明它是真的通了"。在自研 EtherCAT 主站时,遇到一个很典型的工程问题:**如何判断从站拓扑已经稳定?**1
最初的想法很朴素:检测到 wkc_nu(工作计数器)变化后,延时若干周期再确认。但工业现场远比实验室复杂,接触不良、线缆松动、电磁干扰都可能导致 wkc_nu 频繁跳变。如果简单地用"延时确认",系统可能陷入反复的"稳定→不稳定→稳定"的 flapping 状态,这对实时控制系统是致命的。
于是逐步演进出了三级判定机制,这里分享一下设计思路。
第一级:毛刺滤波
最基础的思路是去抖动 。检测到拓扑变化后,不立即响应,而是等待 N 个 PDO 周期。如果在这 N 个周期内 wkc_nu 保持不变,才认为拓扑确实变了。
这里有一个很容易踩的坑:计数器必须以 PDO 周期为单位递增,而不是以系统时钟为单位。40MHz 时钟下,1ms 的 PDO 周期对应 40000 个时钟周期。如果计数器按时钟递增,200 个计数只需要 5µs,完全失去了去抖的意义。
正确做法是用 beat_tick(每个 PDO 周期产生一个时钟宽度的脉冲)来门控计数器:
verilog
// 示意代码,非核心实现
if (beat_tick) begin
if (wkc_nu_diff) begin
wkc_stable_beats <= 8'd0; // 检测到变化,重置计数
end else begin
if (wkc_stable_beats < TOPO_STABLE_CYCLES)
wkc_stable_beats <= wkc_stable_beats + 8'd1;
end
end
第二级:稳定确认
去抖之后,还需要确认拓扑确实稳定下来了。这里引入了一个"稳定窗口"的概念:连续 M 个 PDO 周期内 wkc_nu 无变化,才认为拓扑稳定,允许进入 OP 模式或执行 SDO 操作。
第三级:振荡检测
这是最容易被忽视的一级。工业现场有一种情况:拓扑不是持续抖动,而是低频振荡------比如每隔几百毫秒跳变一次。这种低频振荡如果只靠前两级,可能会被误判为"稳定",因为每个稳定窗口内恰好没有变化。
于是增加了第三级:在一个更长的时间窗口内,统计 wkc_nu 变化的总次数。如果超过阈值,就标记 topo_unstable,阻塞 SDO 操作,向上层报告拓扑异常。
这里有一个重要的设计选择:一旦标记为不稳定,恢复条件必须是整个窗口内零变化。有人可能会觉得这太严格了------难道不能允许偶尔一两次抖动吗?
但在工业场景下,任何 **wkc_nu** 变化都意味着物理层存在隐患。允许"容忍少量抖动"看似灵活,实际上会让系统长期处于"半稳定"状态,反复在稳定/不稳定之间切换,这对实时控制系统是不可接受的。正确的做法是:标记不稳定 → 阻塞危险操作 → 等待上层介入(如回退到 Safe-Op) → 彻底修复后重新进入 OP。
几个容易忽略的工程细节
1. 变化检测不需要 beat_tick 门控
wkc_nu_diff 本身是一个边沿检测信号(wkc_nu != wkc_nu_prev),而 wkc_nu_prev 每个时钟周期都会更新为当前值,所以 wkc_nu_diff 天然就是单拍脉冲。振荡计数器对它计数时,每次变化恰好 +1,不需要额外的 beat_tick 门控。这和稳定计数器是两回事------稳定计数器需要"持续无变化"的语义,而振荡计数器需要"累计变化次数"的语义。
2. 跨 always 块的信号传递要注意 NBA 时序
Verilog 中非阻塞赋值(<=)在同一时间步内不会立即更新。如果模块 A 在 always 块中计算 topo_changed,模块 B 在另一个 always 块中读取它,B 读到的是上一拍的值。这在大多数情况下是正确的(延迟一拍响应),但设计者必须清楚这一点,避免误以为信号是"实时"传递的。
3. 状态机中的标志位清除要互斥
类似 idx_seen 这样的标志位,置位条件和清零条件必须在功能上互斥,否则可能出现同拍置 1 又清 0 的情况。虽然 Verilog 的 NBA 语义保证了最终值确定(后写的覆盖先写的),但从设计意图上,这种情况本就不应该发生。仔细梳理状态机的时序关系,确保各分支互斥,是避免隐性 Bug 的关键。
写在最后
做工控协议栈,最大的挑战往往不是协议本身,而是如何在不确定性的物理世界上构建确定性的软件行为。一个看似简单的"拓扑稳定判定",背后是去抖、确认、振荡检测三层防护,每一层都有其存在的理由。
工业级代码和实验室代码的区别,大概就在于这些"多想一步"的防御性设计吧。