最近在整理人形机器人上的 EtherCAT 实时控制相关内容,把之前自己零零散散记下来的东西重新串了一遍。

这篇主要是从工程实现角度,把下面几件事放到一条线上讲清楚:

  • EtherCAT 三环怎么理解
  • 基于 SOEM 的主站启动流程是什么样
  • DC(Distributed Clocks)到底是在什么时候配置、什么时候锁定的
  • Sync0 和 Sync1 的区别是什么
  • PRE-OP、SAFE-OP、OP 各阶段分别能做什么
  • 运行期为什么还要继续修正 drift
  • 主从通信除了常规实时线程方式,也可以怎么通过 UDP共享内存 去做上层交互

关于 EtherCAT 协议和底层原理,其实已经有不少文章讲得很细了,所以这篇不打算再按教材那种方式从头解释协议细节,而是更偏向按我自己实际看代码、搭流程、排问题时的思路来梳理。这里主要讨论的是 PDO(Process Data Object) 这种实时过程数据通信方式,而不是 SDO(Service Data Object) 这种更偏参数配置和对象读写的方式。


一、EtherCAT 三环运行

在伺服控制里,最常接触的就是三环:位置环、速度环、电流环。从主站角度看,核心就是:

主站通过 PDO 把目标量发给驱动器,从站驱动器内部再去完成对应的控制闭环。

位置环

位置环可以理解成一种位置跟踪模式。按我当前使用的这一套定义:

  • 取值范围:-65535 ~ 65535
  • 0 代表 65535 代表 180°-65535 代表 -180°

换算公式:

1
TargetPosition = pulse × (180 / 65535)

给到 65535,对应就是 180° 位置。

速度环

速度环设置 TargetVelocity,单位按脉冲/秒理解,换算关系:

1
TargetVelocity = X × 60 / 131072   RPM

这里的 131072 不是固件固定值,要根据电机输出轴编码器位数来定。如果你的电机是 X2-7 或 X4-P36,输出轴编码器为 18 位,公式替换为:

1
TargetVelocity = X × 60 / 262144   RPM

举例:输入脉冲值 10000,则输出端转速 10000 × 60 / 131072 ≈ 4.57 RPM,换算输入端为 4.57 × 20 = 91.5 RPM。主站侧给的值,最后还要结合编码器分辨率和传动比,才能对应到实际轴端速度。

电流环

电流环根据电机额定电流和目标输入电流,反推需要写入的目标 IQ 值:

1
额定电流 / 1000 = 输入电流 / 目标IQ值

举例:额定电流 1A,想输入 0.1A,则目标 IQ 值 = 100。

额定 IQ 电流值可以从上位机高级参数中读取,对应 SDO 对象:

1
MAX Torque = MaxTorque(0x6072) × 0.001f × MOTOR RATE IQCURRENT

二、DC 时钟与 Sync0

DC 是什么

DC,全称 Distributed Clocks,也就是 EtherCAT 的分布式时钟。可以把它理解成:每个从站内部都有一个高精度硬件时钟,主站通过计算偏移量,把这些从站时钟校准成全网统一的时间基准。

DC 的意义不只是”大家时间一样”,更在于:

  • 周期控制更稳定
  • 多轴同步更准(人形机器人上多个关节需要严格同步)
  • 主站发送时刻可以持续贴合从站硬件时钟
  • 抖动更容易控制

Sync0 和 Sync1 的区别

在 EtherCAT 的 DC 体系里,Sync0Sync1 都是由从站 ESC(EtherCAT Slave Controller)根据 DC 时间产生出来的同步脉冲:

  • Sync0:主同步脉冲,最常用于周期控制
  • Sync1:辅助同步脉冲,通常用于更复杂的同步场景

在大多数伺服周期同步控制里,Sync0 是最核心、最常用的主节拍。


三、主站实时性保障

在讲启动流程之前,有必要先把”主站为什么要做这些实时性保障”和”jitter 是什么”讲清楚,因为后面所有的配置和等待逻辑,本质上都是在对抗 jitter。

Jitter 是什么

Jitter(抖动) 就是实际执行时刻和理想执行时刻之间的偏差。

举个例子,假设实时线程设定的周期是 1ms,理想情况下每次循环应该严格间隔 1000μs 触发。但实际上由于操作系统调度、中断处理、缓存未命中等各种因素,真实间隔可能是 998μs、1003μs、1010μs……这个偏差就是 jitter。

为什么 jitter 很重要?

  • jitter 大了,PDO 的发送时刻就不稳定,从站收到数据的节拍会飘
  • jitter 大了,DC 相位修正的效果也会打折扣,因为修正的前提是主站自身节拍可信
  • jitter 大了,多轴同步精度就会下降,在人形机器人上可能导致关节动作不协调

工程里通常会在每个周期记录 jitter 值,用来判断系统实时性是否达标:

  • jitter 在几十微秒以内,对于 1ms 周期的 EtherCAT 控制来说是比较健康的
  • 如果 jitter 经常超过 100μs 甚至更高,就需要排查内核、内存、CPU 隔离等问题

下面这些手段,本质上都是在想办法把 jitter 压下来。

Linux 实时内核(RT-PREEMPT)

普通 Linux 内核不是硬实时的,调度器为通用场景设计,高负载或中断密集时实时线程可能被延迟调度,导致周期抖动变大。

RT-PREEMPT 补丁打上之后,内核行为有几个关键变化:

  • 几乎所有内核路径都变成可抢占的:普通内核中很多地方持有自旋锁时不可抢占,RT 补丁将这些锁替换成可抢占的互斥锁,实时线程可以更快地抢到 CPU
  • 中断线程化:硬件中断处理被包装成内核线程,可以被实时线程抢占
  • 调度延迟更可控:对于设置了 SCHED_FIFOSCHED_RR 的高优先级线程,能更稳定地保证在期望的时间点被调度到

简单说,RT-PREEMPT 让 Linux 从”尽力而为”的通用系统,变成”调度延迟有上界保证”的实时系统。不用实时内核,1ms 周期在高负载时很容易出现几百微秒甚至毫秒级调度延迟,对运动控制来说是不可接受的。

锁内存(mlockall

1
mlockall(MCL_CURRENT | MCL_FUTURE);

这个操作把当前进程的所有内存页都锁定在物理内存中,不允许被操作系统换出到磁盘(swap)。

原因很简单:普通情况下 Linux 虚拟内存管理会把长时间未访问的页换出到 swap,程序再次访问时会触发缺页中断(page fault),可能耗费几百微秒到几毫秒。对实时线程来说,这个延迟是灾难性的。锁内存就是提前把所有页钉死在物理 RAM 里,彻底杜绝这种不确定延迟。

绑定 CPU(CPU Affinity)

把实时线程固定到某个特定 CPU 核心上跑,好处是:

  • 避免线程在不同核心之间迁移时带来的缓存失效(cache miss)
  • 减少和其他非实时任务争抢 CPU 的机会
  • 配合 isolcpus 内核参数,可以把该核心完全隔离出来专门给实时线程用

在人形机器人上,通常把 EtherCAT 实时线程绑在一个隔离核心上,其他业务逻辑、网络、UI 等全部跑在别的核心,实时线程的调度抖动可以压到很低。


四、基于 SOEM 的启动流程

整体线程结构

从 SOEM 主站程序结构看,整个系统一般拆成三部分:主线程负责初始化和状态切换,实时线程负责周期性 PDO 收发与控制,监控线程负责监测 WKC、掉线和错误恢复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
main()
└── 创建线程
└── bringup_network() 初始化 EtherCAT
├── ecx_init()
├── ecx_config_init() --> PRE-OP
├── ecx_configdc() --> 配置分布式时钟
├── ecx_dcsync0() --> 配置 Sync0
├── ecx_config_map_group() --> PDO 映射
├── wait_for_sync_lock() --> PRE-OP 内等待 DC 锁定
├── 切 SAFE-OP
├── wait_safe_op_stability()
├── 切 OP
└── dorun = 1 --> 实时循环开始

ecatthread() ecatcheck()
├── 循环: └── 监测 WKC 与从站掉线,自动恢复
│ ├── 记录 jitter
│ ├── 接收 PDO
│ ├── csp_application_step()
│ ├── DC 相位误差修正 ec_sync()
│ └── 发送 PDO
└── (直到 Ctrl+C)

shutdown_network()
└── OP → SAFE-OP → INIT → ecx_close()

逐步走一遍

物理上电:从站刚上电时,内部 DC 硬件时钟就已经开始计数,但还没有同步。Sync0 没有被配置,不会产生同步脉冲,0x6040 = 0。时钟在走,但只是”自己走自己的”。

ecx_init():打开网卡,建立 EtherCAT 主站接口。主站具备了开始工作的通道,但还没进入完整配置流程。

ecx_config_init() → 进入 PRE-OP:扫描从站并让从站进入 PRE-OP。这时 PDO 还没有完成映射,DC 还没有同步,不能进行业务 PDO 循环。PRE-OP 是”配置准备阶段”,大部分重要的准备动作都在这里完成。

ecx_configdc() → 配置 DC:告诉从站要启用 DC 体系了。它确认从站是否具备 DC 能力、激活 DC 相关寄存器访问、建立主站参考时间与从站 DC 时间之间的同步基础。需要注意:这一步并不依赖 PDO,DC 的初始配置是通过 ESC 寄存器和报文时间戳完成的。先配置 DC,再配置 Sync0,这个顺序不能搞反。

ecx_dcsync0() → 配置 Sync0:给每个从站配置 Sync0 参数:

1
2
ecx_dcsync0(context, slave, TRUE, 1000000, 0);
// 开启 Sync0,周期 1ms,相位偏移 0

在 PRE-OP 配置了 Sync0,不代表它已经进入业务控制意义上的正式输出状态。真正稳定周期运行,要等到后续 OP 状态、PDO 通道生效并且 DC 已锁定之后。

ecx_config_map_group() → PDO 映射:分配 I/O Map 和 PDO 区域,根据扫描结果确定 PDO 的内存布局,把原始 PDO 区域映射到程序定义的结构体指针上。从这一刻起,主站程序才真正知道”输入区和输出区在哪、每个字段怎么读写”,后面实时线程才能按结构体方式操作 PDO 数据。

在 PRE-OP 内等待 DC 锁定:很多问题不是出在”有没有配置 DC”,而是出在”有没有等它真正锁住”。工程里通常写一个等待函数:

1
wait_for_sync_lock()

典型的判定思路:偏差阈值 DC_LOCK_TOLERANCE_NS,连续满足 DC_LOCK_REQUIRED_CYCLES = 3000 个周期,超时 DC_LOCK_TIMEOUT_MS = 5000(5 秒)。这一步完成,才能认为从站 DC 时钟和主站参考时间真正对齐了。

切 SAFE-OP:PDO 输入有效,PDO 输出无效。主站能读从站数据,但还不能真正下发控制输出。这个阶段用来确认 PDO 映射、输入链路、DC 状态是否都正常,再观察一段时间(检查 WKC 稳定、DC 锁定持续成立),才切 OP。

切 OP:PDO 数据通道完全生效,Sync0 正式由 DC 时钟驱动,可以开始执行 CSP 等模式配置,可以做实际运动控制。OP 是一个分界点:前面所有动作属于”准备系统能跑起来”,到了 OP 才是”系统真正开始跑”。

用状态机角度看整个流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────┐
│ INIT │
│ - 清 DC / 身份验证 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ PRE-OP(最关键) │
│ - 配置 SM / FMMU │
│ - 配置 DC 时基、Sync0 │
│ - DC 初次对表(offset) │
│ - PDO 映射 / 等待 DC 锁定 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ SAFE-OP │
│ - 检查 PDO 映射 / 确认 DC 锁定 │
└─────────────────────────────────────────┘

┌─────────────────────────────────────────┐
│ OP │
│ - 开启 PDO 循环 │
│ - 持续 DC drift 调整(运行期) │
└─────────────────────────────────────────┘

五、运行期控制循环

实时线程在干什么

进入 OP 以后,实时线程按固定周期(比如 1ms)运行:

1
接收 PDO → 读取 DCtime → 做相位修正 → 执行控制算法 / 轨迹规划 → 发送 PDO

ecx_receive_processdata() 这一步不只是接收输入 PDO,它还会让从站 ESC 通过 EtherCAT 报文提供 DC 时间信息,SOEM 解析后写到 ctx.DCtime

有了 ctx.DCtime,主站就可以通过 ec_sync() 修正自己下一次发送的节拍,让发送节奏持续贴着 EtherCAT 网络的 DC 时间走。这一步对周期抖动控制非常关键。

应用层控制逻辑(csp_application_step())里可能包括:目标位置更新、插补、轨迹规划、模式切换、CiA402 状态控制、限幅和安全检查。在人形机器人场景下,还可能有多关节协调运动规划、重心平衡控制、步态生成、动力学补偿等。

Offset 和 Drift 的区别

这是最容易混淆的一块。

Offset 是主站和从站之间的固有时间差。在 PRE-OP 阶段,主站通过 EtherCAT 报文拿到从站当前的 DC 时间后,计算出一个 offset 并写给从站,让从站的 DC 时钟对齐主站参考时间——从站不会自己猜着对齐,而是主站算出来再写进去。这是第一次”对表”。

Drift 是运行过程中时钟继续慢慢漂移的部分。原因包括温度变化、晶振特性、硬件误差、长时间运行积累的微小偏移。即便已经做过一次 offset 对齐,后面也还是会偏。ec_sync() 不是一次性的,它每个周期都在微调主站的发送时间点,抵消 drift,保持长期同步。

CiA402 状态机

进入 OP、PDO 输出真正生效以后,还要按 CiA402 顺序驱动电机进入可运行状态,向 0x6040 依次写入:

1
6 → 7 → 15

顺序不对或切换太快,都会导致明明 EtherCAT 网络正常但电机就是不使能。

监控线程

监控线程主要看三类东西:

WKC(工作计数器):由从站硬件自动累加,近似等于 所有从站输出数 + 所有从站输入数。WKC 明显低于预期,一般说明链路或从站状态出了问题。

从站掉线:在人形机器人上,某个关节驱动器掉线可能导致严重运动不协调或失衡,需要尽快识别并尝试恢复。

SAFE-OP + ERROR:如果某个从站进入这个状态,主站要及时发现,否则表面上主循环还在跑,实际上控制已经不可信了。


六、主从通信:UDP 与共享内存

前面说的都是 EtherCAT 主站怎么跟从站走实时控制流程。但实际工程里还有一个问题:上层业务程序、算法程序,怎么和 EtherCAT 主站进程通信?

这里说的”主从通信”,不是 EtherCAT 物理总线上的协议主从,而是上层应用和 EtherCAT 主站程序之间的数据交换方式。在人形机器人场景下,这个分层很常见:上层算法层(运动规划、步态生成、视觉反馈等)→ EtherCAT 主站层(实时周期控制、PDO 收发)→ 从站驱动器层。

UDP

把 EtherCAT 主站程序单独做成一个服务进程,上层通过 UDP 和它通信:

1
2
3
4
5
上层应用 / UI / 算法进程
↓↑ UDP
EtherCAT 主站进程(SOEM)
↓↑ EtherCAT
EtherCAT 从站

优点:结构清晰,模块边界明确;天然跨进程、跨机器;发送线程 + 接收线程就能搭起来。

缺点:不保证可靠送达,应用层必须自己做带序号、时间戳、超时保护的机制;实时性受网络栈影响,不等于硬实时。UDP 适合做参数下发、目标值下发、状态上报等非强实时指令,而不是直接作为 1ms 周期闭环控制的通道本身。

共享内存

共享内存适用于同一台机器上的多个进程之间交换数据:

1
2
3
4
5
上层应用进程
↓↑ 共享内存
EtherCAT 主站进程(SOEM)
↓↑ EtherCAT
EtherCAT 从站

上层把目标位置、使能命令写到共享内存,主站实时线程每周期读取,再把当前位置、状态字、错误码写回,上层随时读取。

优点:本质上是同一块内存的读写,没有网络协议栈开销,速度快、延迟低,适合同机高频数据交换。

缺点:只能用于同机进程间;同步问题要自己处理,通常需要设计锁、无锁环形缓冲区、双缓冲、版本号或时间戳机制,否则容易出现半包数据或状态不一致;调试门槛稍高,出问题时难以从外部直接抓包定位。

怎么选

UDP 共享内存
适用范围 跨机器、跨进程 同机进程间
延迟 受网络栈影响 极低
可靠性 需应用层保护 取决于同步设计
调试 可抓包 靠日志和状态标志

很实际的工程思路是两种都用:共享内存给本机高频控制通道,UDP给上位机、远端监控、参数配置和日志回传。在我的人形机器人项目里就是这么做的:本地算法层和 EtherCAT 主站用共享内存高速交换控制指令,上位机监控和调试工具通过 UDP 远程连接。


关键结论速查

# 结论
1 DC 时钟在从站上电后就运行,但未同步
2 DC 初次同步主要发生在 PRE-OP
3 Sync0 必须在 PRE-OP 配置,OP 后才真正进入业务节拍
4 PDO 映射必须在 PRE-OP 完成
5 SAFE-OP 只允许 PDO 输入;OP 才允许 PDO 输出和控制命令
6 OP 才是 CSP 真正可控的状态
7 主站必须在 PRE-OP 内把 DC 锁住,否则进 OP 后容易抖
8 运行期仍要通过 ec_sync() 持续修正 drift
9 实时线程可以早启动,但 OP 前不能当成真正控制线程使用
10 UDP 适合服务化接口和跨机器通信;共享内存适合同机高频低延迟

写在最后

这篇文章到这里就结束了,也算是把 EtherCAT 实时控制流程时的思路和踩坑经验系统地串了一遍。从三环控制到 DC 时钟同步,从状态机切换到运行期 drift 修正,再到上层通信方式的选择,这些东西单独看可能都很好理解,但真正串起来做工程时,很多细节就容易搞混。

特别是在人形机器人这种多自由度、强实时、多层级控制的场景下,任何一个环节出问题,都可能导致整机抖动、失步甚至失控。

希望这篇整理,能给同样在做 EtherCAT 实时控制,或者在人形机器人底层开发方向上的朋友一些参考;对我自己来说,也算是把这些零散的理解沉淀下来,方便以后再做相关开发时能更快复现、更快排查问题。