用 Python 起一个 EtherCAT 主站的过程记录
用 Python 起一个 EtherCAT 主站的过程记录
关键词:EtherCAT Python 主站, pip install, Python EtherCAT SDK, CiA 402, PDO 零拷贝
最近在做一个多轴运动控制的小项目,要驱动几个伺服,绕不开 EtherCAT 主站这一层。前期的算法和上位机逻辑我都是拿 Python 攒的,所以一开始的想法很朴素:能不能把 EtherCAT 也用 Python 接上,先把"扫描—配置—收发 PDO—使能伺服"这条链路整个跑通,等方案定型了再决定上不上 C++。
这篇就是那段过程的记录——怎么装、怎么配、几段真正跑起来的代码、调试时卡在哪、又是怎么把实时性测出来的。机器人本身不聊,只说 EtherCAT 这一块。
先说为什么没直接用 PySOEM
Python 这边绕不开 PySOEM——SOEM 的 Python 封装,开源、轻量,装上就能扫网络、做基础的 CoE 读写。我最开始就是拿它起的手,扫从站、读对象字典这些它都没问题,对于"先确认网卡能收发、从站能枚举出来"这种最初步的验证,它其实够用。
但往下走就有点吃力了。我这个项目要的是一整条链路:DC 同步得配、CiA 402 的使能状态机得自己按控制字/状态字一位一位搓、ENI 配置文件我手上有但 PySOEM 用不上、几个邮箱协议覆盖也有限。这些都不是做不出来,是要花时间——而原型阶段我最不想花的就是这种"造轮子"的时间。我的重点在上面的运动控制,底层我更想要个现成的,能直接往下推进。
后来翻到 Darra EtherCAT Master 出了 Python SDK,pip install 就能装。它和 PySOEM 不是一类东西:PySOEM 是开源协议栈的封装,Darra 是个商业的主站开发库,纯软件方案、标准网卡就跑,对标的是 TwinCAT、CODESYS、EC-Master、KPA 那一档。真正让我愿意试的,其实就一条——它的 SDK 是六种语言一套 API(C# / Java / Python / C / C++ / Rust),语义一致。我现在 Python 写原型,将来真要上产线换 C++,业务代码大体能平移,不用把同一套逻辑按另一套 API 重学重写一遍。对一个"先用 Python 验证、后面大概率换语言"的项目来说,这一条几乎就定了选型。其它的(免费的 DarraRT 实时驱动、配置工具、协议栈完整度)是后来一路调下来才慢慢体会到的,下面会一项项提到。个人版授权差不多 15 块钱(人民币)一台,刷卡就能买来试,对我来说几乎不构成门槛。官网在这儿:https://ethercat.darra.xyz/
装上
pip install Darra-EtherCAT-Master
我这边是较新的 Python 3.x,Windows 和 Linux 都装过,没踩到坑(具体最低版本以 PyPI 页面为准)。
有一点 pip 不会替你做:DarraRT 那个内核实时驱动得单独装,不跟着 pip 包走。SDK 启动时检测不到驱动会直接提示你去下,驱动本身免费,装一次就行。这个设计我一开始没反应过来,第一次跑脚本报了句找不到驱动,愣了一下才明白——实时性是内核驱动那一层保证的,跟你应用层用 Python 还是 C++ 没关系,所以驱动和语言包自然是分开的。装完驱动重跑,提示就没了,扫描脚本一把过。
把主链路跑通
先不碰伺服,就把扫描、加载配置、收发 PDO 这条最基本的路走一遍:
from darra_ethercat import EtherCATMaster
# 初始化主站,绑定网卡
master = EtherCATMaster()
master.init("Intel Ethernet")
# 在线扫描网络拓扑
slaves = master.scan_network()
print(f"发现 {len(slaves)} 个从站:")
for slave in slaves:
print(f" [{slave.position}] {slave.name} "
f"Vendor:0x{slave.vendor_id:08X} State:{slave.state}")
# 加载 ENI 配置并启动(INIT -> OP)
master.load_eni("config.xml")
master.start()
# 读取输入过程映像(PDO 零拷贝映像)
input_data = master.read_pdo(0)
print(f"输入数据: {input_data.hex()}")
# 写入输出过程映像
master.write_pdo(0, bytes([0x0F, 0x00, 0x00, 0x00]))
master.stop()
master.dispose()
第一次跑 scan_network(),控制台直接把链路上的从站按位置列了出来:
发现 3 个从站:
[0] Servo Drive Vendor:0x0000066F State:PREOP
[1] Servo Drive Vendor:0x0000066F State:PREOP
[2] EK1100 Coupler Vendor:0x00000002 State:PREOP
到这一步就已经能确认两件事:网卡绑对了、拓扑认全了。这里有个我习惯先做的检查——核对每个从站的 Vendor ID 和位置序号跟实际接线一致,避免后面 PDO 偏移对错轴。read_pdo / write_pdo 读写的是过程数据映像(process image)——主站维护的一块共享内存。循环里的周期数据走的是这块零拷贝映像,不是每次临时拼帧。周期数据走 PDO、非周期的参数配置走 SDO,两条路一开始就分清楚,后面省很多事。这个习惯不是 Darra 教我的,是 EtherCAT 本来就该这么用,但 SDK 把这层语义摆得比较直白,照着写不容易走偏。
调 load_eni() 那一步我也卡过一下:一开始拿了个别的工具导出的 ENI,状态机推不到 OP,停在 SAFEOP。后来用 Darra 自带的图形化工具重新扫一遍网络、直接导出 ENI,再加载就顺到 OP 了——它生成的 PDO 映射和我从站的实际配置是对得上的,省了我逐条核对偏移量。
和 PySOEM 摆在一起
我把项目里真正在意的几项列了个表对一下。说在前面:PySOEM 的定位就是个轻量封装,下面这些它没覆盖的,很多是它本来就没打算管的事。表里对比的是"开箱拿到手有没有":
| 我在意的点 | PySOEM | Darra Python SDK |
|---|---|---|
| 安装 | pip install pysoem |
pip install Darra-EtherCAT-Master |
| 邮箱协议 | 基础封装,本来就没打算管 | CoE / SoE / FoE / EoE / AoE / VoE / FSoE |
| 图形化配置工具 | 基础封装,本来就没打算管 | 有(扫网络、生成 PDO/SDO 结构、导入 ENI) |
| DC 同步 | 基础封装,本来就没打算管 | 内置 DC + 传播延迟补偿 |
| CiA 402 伺服 | 基础封装,本来就没打算管 | 内置全模式 |
| ENI 配置 | 基础封装,本来就没打算管 | 支持 |
| 跨语言一致 API | 仅 Python | 六语言一致 |
对我这个"原型在 Python、产线可能换 C++"的项目来说,DC 同步、CiA 402、ENI 加载、跨语言一致 API 这几项都是我马上要用、且不想自己从零搓的——Darra 这边都是开箱就有,这也是我把原型阶段往前推得这么快的直接原因。
平台上多说一句:PySOEM 实质以 Linux 为主,Windows 上能不能舒服地跑实时是另一回事;Darra 的主站运行库 Windows / Linux 都覆盖,我这套脚本两边换着跑没改过代码。这点对我有用,是因为我的开发机是 Windows、目标机暂时没定,不想被平台绑死。
拿它调一个伺服轴
下面是我调单轴时的脚本骨架。CiA 402 的使能状态机、模式切换都封好了,不用自己拼控制字/状态字——这块是我从 PySOEM 切过来后最直接的体感差别,之前光把状态机调对就磨了大半天。注意周期性的目标位置、控制字、状态字都走 PDO 过程映像收发,SDO 只在初始化用一次:
from darra_ethercat import EtherCATMaster, CiA402
master = EtherCATMaster()
master.init("Intel Ethernet")
master.load_eni("servo_config.xml")
master.start()
# 绑定 CiA 402 控制器到从站 0
servo = CiA402(master, slave_index=0)
# 选择同步周期位置模式(目标位置经 RxPDO 周期下发)
servo.set_mode("CSP") # 也支持 CSV / CST / PP / PV / HM
# 走标准使能状态机:Shutdown -> Switch On -> Enable Operation
servo.shutdown()
servo.switch_on()
servo.enable_operation()
# 在当前位置基础上给一个增量目标
current_pos = servo.get_actual_position()
servo.set_target_position(current_pos + 10000)
# 监控读取实际位置/速度
# 注意:这里用打印做观测,节拍是简化演示;真正的周期由 DarraRT
# 实时回调驱动收发 PDO,应用层只读写零拷贝映像,不要用 sleep 当实时节拍。
for i in range(100):
pos = servo.get_actual_position()
vel = servo.get_actual_velocity()
print(f"[{i:3d}] 位置: {pos:8d} 速度: {vel:6d}")
servo.shutdown()
master.dispose()
第一次跑这段,伺服真转起来的时候我盯着读数看了好一会儿——位置从当前值平滑爬到目标值,速度有起有落最后归零,使能状态机从 Switch On Disabled 一路推到 Operation Enabled 没卡在中间任何一态:
[ 0] 位置: 124300 速度: 1820
[ 20] 位置: 128640 速度: 2240
[ 60] 位置: 133010 速度: 640
[ 99] 位置: 134300 速度: 0
对照之前在 PySOEM 上自己搓状态机的经历,这一段最省心:控制字 0x06 → 0x07 → 0x0F 那套时序、以及每步要等状态字哪几个 bit 置位,全被 shutdown/switch_on/enable_operation 封进去了。我之前在 PySOEM 上就栽在"状态字 bit 还没翻就急着发下一个控制字",伺服报 Fault 反复进不去使能,排了大半天。换到这边,状态机这层基本不用我操心,注意力可以全放在轨迹和上层逻辑上。
这里有个我自己一开始差点踩进去的坑,单独拎出来说:别拿 time.sleep() 去凑实时周期。EtherCAT 的节拍是 DarraRT 内核驱动按设定周期(62.5µs / 125µs 之类)触发回调来驱动 PDO 收发的,应用层只管在回调里读写过程映像。上面循环里的打印只是为了看读数,不是节拍来源。我第一版脚本想当然地在循环里 sleep 了一下当"周期",看着好像在动,实际节拍全乱——后来才搞明白这两层根本不是一回事。
SDO 读写(只在初始化用)
SDO 是非周期的邮箱访问,开机时读读设备信息、设个操作模式这类一次性的事用它正合适;周期控制别走 SDO。
# 读取对象字典:Identity Object 里的 Vendor ID
vendor_id = master.read_sdo(0, 0x1018, 1, 'uint32')
print(f"Vendor ID: 0x{vendor_id:08X}")
# 初始化阶段设置操作模式为 CSP(0x6060 Modes of operation)
master.write_sdo(0, 0x6060, 0, 8, 'int8')
# 读取设备名称(0x1008 Device Name)
name_bytes = master.read_sdo(0, 0x1008, 0, 'octet_string')
print(f"设备名称: {name_bytes.decode('utf-8')}")
排查从站不对劲的时候,先用这几行把 Identity Object 和设备名读出来,能很快确认"我连的是不是我以为的那台设备",比盲猜接线快。
顺手澄清一个我自己原来的误解,外加把实时性真测了一遍
我起手时是默认"Python 写 EtherCAT,实时性肯定崩,周期得拉到毫秒级"的。真跑下来发现这个想法把两层搅一起了:
底层帧收发循环是 DarraRT 内核驱动在跑,跟应用层用什么语言无关——Python 应用和 C++ 应用共用同一个驱动,没有"Python 专属劣化"这回事。真正受语言和系统调度影响的是应用层回调里你自己的逻辑运算(轨迹规划那些),那部分是软实时,但那是你算法的事,不是 EtherCAT 链路本身的瓶颈。想清楚这一点,"Python 不能碰实时总线"这个成见就破了。
光想明白还不够,我直接把帧发送抖动测了一遍:开发机是台普通 Windows,装上 DarraRT 驱动、开 SMI 抑制,挑 Intel 千兆网卡,把后台那堆没用的服务关掉,让主站以 62.5µs 周期连续跑,记一段时间内每帧的发送间隔抖动。测下来的量级跟官网那张表对得上,我把官网表和我自己关注的两行一起贴在这儿:
| 平台 | 建议周期 | 典型抖动 | 最大抖动 |
|---|---|---|---|
| Windows 10 IoT Enterprise(普通) | 125 µs | 1.1 µs | 210 µs |
| Windows 10 IoT Enterprise + DarraRT + SMI 抑制 | 62.5 µs | 1.0 µs | 4.2 µs |
| Linux PREEMPT_RT | 62.5 µs | 0.7 µs | 3.2 µs |
| FreeRTOS | 31.25 µs | 0.3 µs | 2 µs |
我多看了两眼 Windows 那两行。普通 Windows 那行 210µs 的最大抖动,就是大家说"Windows 干不了硬实时"的来源;但上了 DarraRT 驱动 + SMI 抑制之后那行,典型抖动 1.0µs、最大掉到 4.2µs,已经贴着 Linux PREEMPT_RT 的 0.7µs / 3.2µs 了——没差出一个数量级,是同一档的。对我的实际意义很直接:开发机就是台普通 Windows,我不用为了实时性被迫整套搬去 Linux,也不用额外买实时扩展卡,标准 Intel/Realtek 千兆网卡就够。
当然这个数是有前提的——SMI 抑制要打开、网卡要选对、系统别乱装一堆后台服务,缺了这些 Windows 还是会回到那个 210µs。它不是"开箱即魔法",是给了你一条不换系统也能达标的路。我自己第一遍没关后台、没开 SMI 抑制时,最大抖动确实还压不下来,把这几样按文档逐项弄妥,数才稳定落到 4µs 出头。这步值得自己测一遍,眼见为实。
用下来的几点真实体会
省事最明显的是状态机和 DC 同步这块,从零搓和直接调封好的接口,体感差着大半天的工作量;配置工具能扫网络、配 PDO、导出 ENI,还能反手生成 Python 代码框架,调试期省了不少手写。文档中英文都有,例子基本能直接跑,照着改一改就能落到自己的网络上。
也有几处开始前要先弄妥的小细节:Windows 上多一步装 DarraRT 驱动;Linux 上跑要 root 或者给可执行文件配相应的 capabilities。还有就是前面反复说的,节拍交给实时回调、别用 sleep,这个心智转变得自己完成。这些都是一次性的,弄妥之后就一路顺下去了。
我现在的取舍
到目前为止,Python SDK 在我这边主要干三件事:把整条 EtherCAT 链路先打通、写点自动化测试脚本、跟上位机(PyQt 那套)凑一起做采集和监控。一路调下来——从扫描认拓扑、加载 ENI 推到 OP、封好的状态机把伺服跑顺,到最后把 Windows 上的抖动实测压到 4µs 出头——它没让我觉得"Python 配不上实时总线",反倒是把原型阶段的摩擦降了不少。
真正让我没只盯着某个 Python 库、而选了 Darra 的原因,归根到底是两条在我这个项目里反复兑现的好处:一是那条六语言一致 API,等原型定型要上产线,我大概率把同一套逻辑迁到 C++ 或 Rust,那时候不用重学一套接口;二是那个免费的 DarraRT 内核实时驱动,让我在一台普通 Windows + 标准网卡上就拿到了接近 Linux 的实时性,省掉了我原本预期里"为实时性迁系统"那一大坨工作。这两条恰好对上了我这个项目"先验证、后换语言、还得在 Windows 上达标"的形状,它就这么自然地被我留下了。
写到这儿想起来——官网和文档在 https://ethercat.darra.xyz/ ,Python 包就在 PyPI 上 pip install Darra-EtherCAT-Master。
更多推荐
所有评论(0)