EtherCAT 从站通信延迟优化:从 2 个周期降低到 1 个周期的时序分析与实践
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.1 APPL_OutputMapping()
void APPL_OutputMapping(UINT16 *pData)
它负责:
ESC Process RAM
↓
RxPDO
↓
Outputt
也就是说:
把主站写入 ESC 的 Output Process Data,搬到 MCU 应用层变量中。
可以理解成:
EtherCAT 世界
↓
Application 世界
之间的入口。
6.2 MotorDriver()
原来的:
MotorDriver();
其实同时做了两类完全不同的事情。
第一类:
Outputt
↓
FOC 控制参数
也就是消费主站命令。
第二类:
FOC 实时状态
↓
Inputt
也就是生成即将返回给主站的反馈数据。
因此原函数实际上把:
EtherCAT RxPDO → Control
和:
Control → EtherCAT TxPDO
两个方向的数据处理混在了一起。
6.3 APPL_InputMapping()
void APPL_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.1 updateEcatOutputData()
负责:
Outputt
↓
FOC / Control Parameters
也就是:
把主站最新下发的 EtherCAT 命令同步到控制算法使用的数据区。
可以理解成:
EtherCAT → Control
11.2 updateEcatInputData()
负责:
FOC / System State
↓
Inputt
即:
在准备上传 TxPDO 之前,把控制系统最新状态刷新到 EtherCAT Input 数据区。
可以理解成:
Control → EtherCAT
12. 拆函数的真正意义不是“代码更漂亮”
如果只是从软件工程角度看,这次修改好像只是:
一个大函数
↓
两个小函数
似乎只是一次普通重构。
实际上并不是。
真正的价值是:
拆开以后,可以分别决定 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 Rate
21. 从这次问题可以抽象出一个通用排查方法
以后再遇到:
实时通信延迟过大
我会优先画一张:
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 Latency
Level 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 Phase
24.3 实时系统中的函数拆分,本质上经常是时序拆分
普通应用开发里,把函数拆开可能主要为了:
可读性
单一职责
代码复用
而实时控制系统中,还有一个更重要的原因:
让不同逻辑能够在不同时间点执行
这次:
updateEcatOutputData()
和:
updateEcatInputData()
拆开以后,真正得到的是:
Timing Freedom
而不只是:
Cleaner Code
24.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?
主站下一次拿到的到底是哪一周期的数据?
把这些问题回答清楚以后,很多看起来像“通信性能”的问题,最后其实都是一个:
数据流与执行时序设计问题。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)