EtherCAT 从站通信延迟优化:从 2 个周期降低到 1 个周期的时序分析与实践
在 EtherCAT 系统里,我们经常会关注:
- 总线周期是多少;
- PDO 有多大;
- 主站实时性怎么样;
- DC 同步精度能做到多少。
但在实际做运动控制时,还有一个非常关键、却很容易被忽略的问题:
主站这一周期下发的数据,到底要过多久才能经过从站应用层处理,再反馈回主站?
这次项目中遇到的问题就是:
EtherCAT 周期 = T 主站第 N 周期下发命令 ↓ 从站处理 ↓ 主站直到 N+2 周期 才能看到对应反馈也就是说,端到端存在:
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 Send:N Slave Return:N - 1说明:
延迟 = 1 Cycle如果看到:
Master Send:N Slave Return:N - 2则说明:
延迟 = 2 Cycle这种办法非常简单,但很实用。
它的好处是:
不依赖 CPU 时间戳,也不依赖主从两边时钟同步,直接通过数据本身判断跨了多少个控制周期。
3. 优化前:SM 模式存在 2 个周期延迟
首先测试 SM 同步模式。
Wireshark 抓包看到:
主站发送: frameNum = 0x2609而同一时刻从站反馈:
frameNum = 0x2607两者相差:
0x2609 - 0x2607 = 2因此:
SM 模式延迟 = 2 Cycle如果控制周期为 1 ms:
2 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 1:Network Latency
测:
EtherCAT Frame关注:
报文周期 WKC Frame Lost 传播时间Level 2:PDO Latency
加入:
frameNum关注:
Master Send N Slave Return N-k从而测出:
k Cycle LatencyLevel 3:Application Latency
用 GPIO 或硬件时间戳测:
RxPDO 到达 ↓ 控制任务开始以及:
控制任务完成 ↓ TxPDO 生效Level 4:Control 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 Time:20 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_num+1){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? 主站下一次拿到的到底是哪一周期的数据?把这些问题回答清楚以后,很多看起来像“通信性能”的问题,最后其实都是一个:
数据流与执行时序设计问题。