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?

主站下一次拿到的到底是哪一周期的数据?

把这些问题回答清楚以后,很多看起来像“通信性能”的问题,最后其实都是一个:

数据流与执行时序设计问题。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐