
EtherCAT 从站通信延迟优化从 2 个周期降低到 1 个周期的时序分析与实践在 EtherCAT 系统里我们经常会关注总线周期是多少PDO 有多大主站实时性怎么样DC 同步精度能做到多少。但在实际做运动控制时还有一个非常关键、却很容易被忽略的问题主站这一周期下发的数据到底要过多久才能经过从站应用层处理再反馈回主站这次项目中遇到的问题就是EtherCAT 周期 T 主站第 N 周期下发命令 ↓ 从站处理 ↓ 主站直到 N2 周期 才能看到对应反馈也就是说端到端存在2T的周期级延迟。进一步测试发现SM 模式2 个周期 DC 模式2 个周期说明问题并不在 EtherCAT DC 本身而在从站内部的 PDO 数据处理链路。最终通过重新拆分从站应用层的数据处理流程将通信延迟从2T降低到了1T这篇文章记录整个测试、分析和优化过程。1. 先定义一下这里说的“通信延迟”这里说的通信延迟并不是EtherCAT Frame 从主站网口发出去 ↓ 到达从站 PHY这种纯粹的物理层或链路层传播延迟。真正关心的是主站某一周期发送一个新的控制量从站接收到并经过应用逻辑处理以后再把对应结果反馈回来主站需要等待几个控制周期。也就是Master │ │ RxPDO ▼ ESC │ ▼ Application │ ▼ Control / FOC │ ▼ Application │ ▼ ESC │ │ TxPDO ▼ Master这是一个真正的端到端控制链路延迟。对于 1 kHz 控制系统1 Cycle 1 ms那么2 Cycle 2 ms和1 Cycle 1 ms的差别其实已经非常明显。对于机器人运动控制来说这种周期级延迟甚至比几十微秒的总线传输时间更加值得关注。2. 如何测量这种延迟为了避免凭感觉判断我在 PDO 中专门增加了一个frameNum用于做周期延迟测试。主站每发送一帧frameNum;然后通过 RxPDO 下发给从站。从站收到后在 FOC 相关处理路径中将它保存下来RxPDO frameNum ↓ tempFrameNum之后再把tempFrameNum写入 TxPDO 返回给主站。整个测试链路Master │ │ frameNum N ▼ RxPDO │ ▼ Slave │ │ tempFrameNum N ▼ TxPDO │ ▼ Master如果在同一个 EtherCAT 报文周期中看到Master SendN Slave ReturnN - 1说明延迟 1 Cycle如果看到Master SendN Slave ReturnN - 2则说明延迟 2 Cycle这种办法非常简单但很实用。它的好处是不依赖 CPU 时间戳也不依赖主从两边时钟同步直接通过数据本身判断跨了多少个控制周期。3. 优化前SM 模式存在 2 个周期延迟首先测试 SM 同步模式。Wireshark 抓包看到主站发送 frameNum 0x2609而同一时刻从站反馈frameNum 0x2607两者相差0x2609 - 0x2607 2因此SM 模式延迟 2 Cycle如果控制周期为 1 ms2 Cycle ≈ 2 ms当然这里的“2 ms”是周期意义上的延迟不等同于每一次都恰好固定为物理时间 2.000 ms。更准确地说当前反馈数据落后于当前控制命令两个 EtherCAT 周期。4. DC 模式同样存在 2 个周期延迟接下来切换到 EtherCAT DC 模式。抓包结果Master Send frameNum 0x069D从站返回frameNum 0x069B仍然相差2因此DC 模式延迟 2 Cycle到这里实际上可以排除一个很容易出现的误区开启 DC 并不会自动降低 PDO 端到端延迟。DC 主要解决的是时钟同步 SYNC0 / SYNC1 相位同步 多轴执行时刻同步而这里的问题是PDO 数据什么时候被应用层读取 控制算法什么时候使用它 新的反馈什么时候重新写进 ESC这是另一条链路。所以SM → 2T DC → 2T反而说明应该把排查重点放到Slave Application Data Path上。5. 优化目标目标很明确优化前 Master Send N ↓ Slave ↓ Master Receive N-2希望变成优化后 Master Send N ↓ Slave ↓ Master Receive N-1即2 Cycle ↓ 1 Cycle同时要求SM 模式有效 DC 模式同样有效这意味着不能只针对某一种同步模式“打补丁”而要真正梳理从站内部 PDO 的数据路径。6. 从站内部真正参与通信的三个关键函数继续分析 SSC 应用层代码以后发现这个延迟主要和三个函数有关。6.1APPL_OutputMapping()voidAPPL_OutputMapping(UINT16*pData)它负责ESC Process RAM ↓ RxPDO ↓ Outputt也就是说把主站写入 ESC 的 Output Process Data搬到 MCU 应用层变量中。可以理解成EtherCAT 世界 ↓ Application 世界之间的入口。6.2MotorDriver()原来的MotorDriver();其实同时做了两类完全不同的事情。第一类Outputt ↓ FOC 控制参数也就是消费主站命令。第二类FOC 实时状态 ↓ Inputt也就是生成即将返回给主站的反馈数据。因此原函数实际上把EtherCAT RxPDO → Control和Control → EtherCAT TxPDO两个方向的数据处理混在了一起。6.3APPL_InputMapping()voidAPPL_InputMapping(UINT16*pData)负责Inputt ↓ TxPDO ↓ ESC Process RAM最终主站下一次进行 EtherCAT 数据交换时就能拿到这份数据。所以完整链路其实是Master RxPDO ↓ ESC RAM ↓ APPL_OutputMapping() ↓ Outputt ↓ MotorDriver() ↓ FOC ↓ Inputt ↓ APPL_InputMapping() ↓ ESC RAM ↓ Master TxPDO一旦把这条链路画出来问题就开始变得清晰了。7. SM 模式下的数据处理时序先看 SM 模式。原来的执行顺序可以简化为PDI IRQ │ ▼ APPL_OutputMapping() │ ▼ MotorDriver() │ ▼ APPL_InputMapping()也就是ESC RxPDO ↓ OutputMapping ↓ Application ↓ InputMapping ↓ ESC TxPDO乍一看这个顺序似乎完全合理。甚至很容易产生一种直觉既然三个函数都在一次 PDI 中断里执行那么主站刚写进来的数据应该可以立刻处理再马上反馈回去。但真正的问题在于MotorDriver()中的反馈数据并不只是由本次Outputt简单计算得到。其中很多反馈量来自FOC 周期 实时状态 传感器结果 控制器输出也就是说收到 RxPDO和对应控制结果真正产生在时间上并不是完全同一个事件。8. DC 模式下的数据路径更容易暴露这个问题DC 模式的执行逻辑和 SM 不完全一样。原工程中PDI IRQ ↓ APPL_OutputMapping()而SYNC0 IRQ ↓ MotorDriver() ↓ APPL_InputMapping()也就是说EtherCAT 数据到达 ↓ PDI IRQ ↓ OutputMapping │ │ 等待 SYNC0 ▼ SYNC0 IRQ ↓ Application ↓ InputMapping这里已经能够非常明显地看到数据搬运和控制任务之间存在明确的时间边界。而且 TxPDO 什么时候写回 ESC很大程度上取决于APPL_InputMapping()到底是在什么时刻执行。9. 问题的本质不是 EtherCAT 慢而是“数据更新晚了一拍”这是整个问题里最重要的一点。抓包看到N ↓ N - 2第一反应很容易是是不是 EtherCAT 带宽不够 是不是主站 1 kHz 太快 是不是 DC 有额外延迟 是不是 ESC 三缓冲导致的但沿着数据路径分析以后发现真正导致额外一个周期的并不是帧在网线上多跑了一圈而是 TxPDO 使用的数据在从站应用层晚更新了一个周期。也就是说Physical Communication Latency和Application Data Age必须区分开来。主站收到一帧 EtherCAT 报文只能说明报文到了但不代表里面的数据一定是当前周期刚刚计算出来的最新值它可能已经是N - 1 N - 2甚至更早的状态。这就是很多实时通信系统里真正需要关注的Data Freshness数据新鲜度。10. 原来的函数设计存在什么问题原来MotorDriver();同时负责方向 A EtherCAT → FOC和方向 B FOC → EtherCAT从软件结构上看RASA_motordriver_ETG │ ┌────────┴────────┐ ▼ ▼ Update Control Update Feedback Parameters Data这两个动作其实具有完全不同的时间语义。比如EtherCAT → FOC希望尽可能靠近APPL_OutputMapping()因为刚收到主站的新命令应该尽快让控制算法看到。而FOC → EtherCAT希望尽可能靠近APPL_InputMapping()因为在真正把 TxPDO 提交给 ESC 之前希望采集最新的控制状态。这两个方向被放进同一个函数以后就失去了分别调整执行位置的能力。11. 优化方案把双向数据更新彻底拆开因此最终做的关键修改其实非常简单。原来的MotorDriver();拆成两个函数updateEcatOutputData();updateEcatInputData();两者职责完全不同。11.1updateEcatOutputData()负责Outputt ↓ FOC / Control Parameters也就是把主站最新下发的 EtherCAT 命令同步到控制算法使用的数据区。可以理解成EtherCAT → Control11.2updateEcatInputData()负责FOC / System State ↓ Inputt即在准备上传 TxPDO 之前把控制系统最新状态刷新到 EtherCAT Input 数据区。可以理解成Control → EtherCAT12. 拆函数的真正意义不是“代码更漂亮”如果只是从软件工程角度看这次修改好像只是一个大函数 ↓ 两个小函数似乎只是一次普通重构。实际上并不是。真正的价值是拆开以后可以分别决定 Rx 数据什么时候进入控制算法以及最新控制状态什么时候进入 TxPDO。也就是从Function Coupling变成了Timing Control这类实时系统中的函数拆分很多时候并不是为了代码复用而是为了精确控制代码运行时刻这一点在 EtherCAT、FOC、实时中断系统里非常重要。13. SM 模式优化后的处理链路优化以后SM 模式的数据路径变得更加明确。核心思想收到主站命令 ↓ 立即更新控制参数 准备返回 TxPDO ↓ 尽可能更新最新反馈可以理解为PDI IRQ │ ▼ APPL_OutputMapping() │ ▼ updateEcatOutputData() │ │ │ 控制系统运行 │ ▼ updateEcatInputData() │ ▼ APPL_InputMapping()这样Output Path和Input Path在代码结构上已经完全解耦。主站新命令一旦通过APPL_OutputMapping()进入Outputt马上就可以updateEcatOutputData()让控制算法看到。而在 TxPDO 真正写入 ESC 之前updateEcatInputData()会尽量刷新最新的控制反馈。14. DC 模式优化后的处理链路DC 模式下同样按照这一思路处理。PDI 中断PDI IRQ │ ▼ APPL_OutputMapping() │ ▼ updateEcatOutputData()也就是EtherCAT RxPDO 到达 ↓ 立即更新控制参数SYNC0 到来以后SYNC0 │ ▼ Control / FOC │ ▼ updateEcatInputData() │ ▼ APPL_InputMapping()这样做以后RxPDO和TxPDO各自都尽可能靠近真正需要它的时间点。整个数据链路变成Master │ │ RxPDO ▼ ESC RAM │ ▼ APPL_OutputMapping │ ▼ updateEcatOutputData │ ▼ FOC │ ▼ updateEcatInputData │ ▼ APPL_InputMapping │ ▼ ESC RAM │ │ TxPDO ▼ Master相比原来Output Input 全部绑在 RASA_motordriver_ETG现在数据流更加清晰。15. 从实时系统角度看这实际上是在缩短“数据年龄”可以引入一个很有用的概念Age of Data假设主站在t0下发命令Command[N]如果从站到t0 1T才真正让控制算法使用它那么这份数据在进入控制算法时已经1T old同理。如果控制系统最新状态已经产生但Inputt直到下一周期才更新那么主站最终拿到的状态同样是stale data所以这次优化的本质可以概括为减少额外 Buffer / Stage ↓ 缩短 Data Age ↓ 降低 End-to-End Latency这和单纯提高EtherCAT Bus Rate不是同一类优化。16. 为什么没有办法做到真正的“0 周期延迟”优化完成后Master Send N Master Receive N - 1也就是1 Cycle Latency有人可能会继续问为什么不能这一帧发送 N这一帧同时收到 N对于周期性交换的主从闭环系统来说这通常并不现实。因为必须经过Master Send N ↓ Slave Receive N ↓ Slave Application Process ↓ Generate Feedback N ↓ Master Next Exchange ↓ Receive Feedback N至少要经历一个 causality boundary也就是说结果必须在原因发生之后产生。所以在这种软件架构下1 Cycle已经非常接近合理的最小周期级端到端延迟。当然EtherCAT 的 ESC 可以实现非常低的 on-the-fly forwarding latency但那是Frame Forwarding层面的能力。和Master Command → Slave Application → FOC → Feedback → Master这一整套闭环应用延迟完全不是一回事。17. SM 模式优化后测试结果优化完成后重新抓包。主站当前下发frameNum 0x15F2主站收到的从站反馈frameNum 0x15F1两者相差1因此SM Mode 2 Cycle ↓ 1 Cycle达到了优化目标。18. DC 模式优化后测试结果再测试 DC 模式。主站发送frameNum 0x0649从站返回frameNum 0x0648同样相差1因此DC Mode 2 Cycle ↓ 1 Cycle同样符合预期。最终优化前 优化后 SM Mode 2T 1T DC Mode 2T 1T证明这次优化解决的是Slave Application Data Path而不是某一种同步模式下的特殊问题。19. 为什么这个结果比单纯“减少 1 ms”更重要假设控制周期T 1 ms表面看2 ms ↓ 1 ms好像只是减少了1 ms但在闭环控制系统中这个 1 ms 会进入整个控制链。例如Robot Controller ↓ EtherCAT ↓ Joint Controller ↓ Motor ↓ Sensor ↓ EtherCAT ↓ Robot Controller通信延迟本质上相当于控制环路中的Transport Delay而纯延迟会直接带来Phase Lag对于频率为f的信号时间延迟Td对应的相位滞后近似为φ -2πfTd换成角度φ -360° × f × Td例如f 50 Hz如果Td 2 ms相位滞后约-36°而如果Td 1 ms则约为-18°所以在高动态控制系统里一个 EtherCAT 周期的延迟并不是一个可以随便忽略的小量。尤其随着整机控制带宽提高这种影响会越来越明显。20. 通信周期高不等于系统延迟低这次优化也让我重新认识了一个问题。很多人评价一套实时通信方案会首先看1 kHz 2 kHz 4 kHz好像通信频率越高Latency就一定越低。其实并不完全成立。比如两个系统System A 2 kHz EtherCAT 2 Cycle Application Delay那么周期T 0.5 ms应用延迟1 ms另一个System B 1 kHz EtherCAT 1 Cycle Application Delay周期T 1 ms应用延迟同样1 ms所以真正应该关注的是Control Frequency Cycle Jitter End-to-End Latency Data Age而不是只看Cycle Rate21. 从这次问题可以抽象出一个通用排查方法以后再遇到实时通信延迟过大我会优先画一张Data Flow Diagram把数据真正经过的每个阶段全部列出来。例如Master App ↓ Master PDO ↓ EtherCAT Frame ↓ Slave ESC ↓ Output Mapping ↓ Application Buffer ↓ Control Task ↓ Feedback Buffer ↓ Input Mapping ↓ Slave ESC ↓ EtherCAT Frame ↓ Master PDO ↓ Master App然后对每一个边界问这里有没有跨周期 这里有没有旧缓存 这里的数据什么时候刷新 这里消费的是当前值还是上一周期值 这里会不会等待另一个中断 这里是不是存在 double / triple buffer 这里生产者和消费者是否处于不同执行上下文这样通常比直接抓 Wireshark ↓ 看到慢 ↓ 怀疑 EtherCAT有效得多。22. 一个非常重要的原则不要只测“通信”要测“数据链路”Wireshark 能够告诉我们Frame 什么时候出去 什么时候回来但是它看不到从站 MCU 内部OutputMapping Application FOC InputMapping到底经历了什么。所以真正完整的实时通信延迟测试最好分层。Level 1Network Latency测EtherCAT Frame关注报文周期 WKC Frame Lost 传播时间Level 2PDO Latency加入frameNum关注Master Send N Slave Return N-k从而测出k Cycle LatencyLevel 3Application Latency用 GPIO 或硬件时间戳测RxPDO 到达 ↓ 控制任务开始以及控制任务完成 ↓ TxPDO 生效Level 4Control End-to-End Latency最终测Master Command ↓ Motor Torque / Position Response ↓ Master Feedback这才是整个机器人系统真正关心的延迟。23. 如果继续优化还可以做什么这次已经从2T降低到1T继续往下优化就不应该只盯着 PDO Mapping。还可以进一步测量整个周期内部的时间预算。例如1 ms Cycle ┌──────────────────────────────────────────────┐ │ │ │ Linux Wakeup │ │ ↓ │ │ EtherCAT Receive │ │ ↓ │ │ Master Control Algorithm │ │ ↓ │ │ EtherCAT Send │ │ ↓ │ │ Slave PDI IRQ │ │ ↓ │ │ Output Mapping │ │ ↓ │ │ Control / FOC │ │ ↓ │ │ Input Mapping │ │ │ └──────────────────────────────────────────────┘可以分别记录T_master_jitter T_frame T_pdi T_mapping T_control T_feedback最终建立Latency Budget这样后续再优化时就可以知道时间到底花在哪里而不是盲目把所有代码都继续“加速”。24. 这次优化给我的几个启发24.1 延迟问题首先看数据路径不要首先看总线EtherCAT 本身可能很快。真正慢的可能是Buffer Task ISR Mapping Control Loop因此Communication Problem并不一定发生在Communication Bus上。24.2 “函数在哪运行”往往比“函数运行多久”更重要这次MotorDriver()本身并不一定耗时很多。真正的问题是它什么时候运行以及它里面的不同数据处理为什么必须绑在同一时刻运行在实时系统中经常存在Execution Time20 us并不危险。但Execution Phase晚 1 个周期却可能直接产生1 ms latency所以性能优化不能只看CPU Cost还要看Scheduling Phase24.3 实时系统中的函数拆分本质上经常是时序拆分普通应用开发里把函数拆开可能主要为了可读性 单一职责 代码复用而实时控制系统中还有一个更重要的原因让不同逻辑能够在不同时间点执行这次updateEcatOutputData()和updateEcatInputData()拆开以后真正得到的是Timing Freedom而不只是Cleaner Code24.4 frame counter 是非常实用的实时通信诊断手段这次只加了一个frameNum就很直观地发现了N → N-2而优化以后N → N-1除了通信延迟之外这种序号机制还可以继续用于检测丢帧 重复帧 乱序 周期跳变 数据停滞 从站处理卡顿例如if(frame_num!last_frame_num1){communication_error;}所以在设计实时通信协议时我现在比较倾向于专门保留Sequence Counter哪怕只占16 bit通常都非常值得。25. 总结这次问题最开始看到的现象很简单Master Send N ↓ Slave Return N-2无论SM Mode还是DC Mode都存在两个周期的端到端延迟。最终沿着数据路径分析ESC RxPDO ↓ APPL_OutputMapping ↓ MotorDriver ↓ FOC ↓ MotorDriver ↓ APPL_InputMapping ↓ ESC TxPDO发现问题并不在 EtherCAT 总线本身而在从站应用层的数据更新时序。最终将MotorDriver();拆分为updateEcatOutputData();updateEcatInputData();分别负责EtherCAT → Control和Control → EtherCAT再根据 SM / DC 的实际执行时序重新安排调用位置。优化完成以后Before After SM 2T 1T DC 2T 1T如果控制周期为T 1 ms就相当于端到端数据链路减少了约1 个控制周期这次优化让我印象比较深的一点是实时通信系统里的“延迟”很多时候并不是数据在线缆上传输了多久而是数据在软件系统中等了多久。真正想降低端到端延迟不能只研究 EtherCAT 带宽、周期和 PHY还要继续往从站内部看什么时候收到数据 什么时候把数据交给控制算法 什么时候产生新的反馈 什么时候写回 Process RAM 主站下一次拿到的到底是哪一周期的数据把这些问题回答清楚以后很多看起来像“通信性能”的问题最后其实都是一个数据流与执行时序设计问题。