基于 SOEM 库的 EtherCAT 主站通信与控制流程整理
最近在整理人形机器人上的 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代表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 体系里,Sync0 和 Sync1 都是由从站 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_FIFO或SCHED_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 | main() |
逐步走一遍
物理上电:从站刚上电时,内部 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 | ecx_dcsync0(context, slave, TRUE, 1000000, 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 | ┌─────────────────────────────────────────┐ |
五、运行期控制循环
实时线程在干什么
进入 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 | 上层应用 / UI / 算法进程 |
优点:结构清晰,模块边界明确;天然跨进程、跨机器;发送线程 + 接收线程就能搭起来。
缺点:不保证可靠送达,应用层必须自己做带序号、时间戳、超时保护的机制;实时性受网络栈影响,不等于硬实时。UDP 适合做参数下发、目标值下发、状态上报等非强实时指令,而不是直接作为 1ms 周期闭环控制的通道本身。
共享内存
共享内存适用于同一台机器上的多个进程之间交换数据:
1 | 上层应用进程 |
上层把目标位置、使能命令写到共享内存,主站实时线程每周期读取,再把当前位置、状态字、错误码写回,上层随时读取。
优点:本质上是同一块内存的读写,没有网络协议栈开销,速度快、延迟低,适合同机高频数据交换。
缺点:只能用于同机进程间;同步问题要自己处理,通常需要设计锁、无锁环形缓冲区、双缓冲、版本号或时间戳机制,否则容易出现半包数据或状态不一致;调试门槛稍高,出问题时难以从外部直接抓包定位。
怎么选
| 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 实时控制,或者在人形机器人底层开发方向上的朋友一些参考;对我自己来说,也算是把这些零散的理解沉淀下来,方便以后再做相关开发时能更快复现、更快排查问题。
