ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Python+pysoem实现EtherCAT伺服参数化控制实战

用Python+pysoem实现EtherCAT伺服参数化控制实战 做工业运动控制的人迟早会撞上这么一堵墙上位机要用 Python 做逻辑和界面底下却拖着一堆 EtherCAT 伺服总不能每个项目都请人写一坨 C 主站吧。前阵子我正好接了个活儿要在一台工控机上同时管理好几台汇川和松下的伺服要求快速读写 CiA402 对象字典里的参数还要把使能、点动、回零这些动作理得明明白白。我把 pysoem、EtherCAT、CiA402、SDO 这几样东西串起来用纯 Python3 做了一套伺服参数化控制程序跑得很稳。这篇文章就是把我这次从环境搭建到故障排查的完整过程记录下来给那些想入坑 EtherCAT 又不想被 C 语言劝退的朋友一份可以直接抄作业的参考。先说清楚这套东西能干什么、适合谁。它适合用在伺服参数批量配置、产线上位机初始化、小型运动控制验证平台以及复杂工艺里需要动态改写伺服模式的场景。不适合用它做几百微秒周期的插补运动那是硬实时的活儿Python 扛不住。我的定位很明确把 SDO 邮箱通道用好解决参数化、状态切换和诊断问题这是 Python 最舒服的位置。1. 项目全貌与技术选型思路1.1 我到底要解决什么问题看起来只是用 Python 控制伺服实际上拆开以后是四个独立问题。第一是 EtherCAT 主站怎么和从站建立通信这涉及网卡底层帧收发是最容易卡住新人的地方。第二是 CoE 邮箱协议怎么用EtherCAT 从站里的 SDO 请求不是直接发个 UDP 包就完事它有完整的请求响应帧结构。第三是 CiA402 驱动器状态机怎么切上电以后怎么从无使能一路走到运行使能中间任何一步顺序错了电机就是不动。第四是对象字典里那么多索引、子索引、数据类型哪些是伺服参数哪些是运动控制参数怎么区分。pysoem 这个库帮我挡掉了前两个问题的大部分细节。它是 SOEMSimple Open EtherCAT Master的 Python 绑定把 C 主站里那些 frame、ioctl、mapping 的脏活全包了暴露给我的是一个个从站对象每个对象上有 sdo_read、sdo_write 方法。我要做的就是把对象字典搞清楚然后按照 CiA402 的状态机规则走流程。1.2 为什么选 pysoem 而不是其他方案我对比过四条路线直接用 SOEM 的 C 库、用 IGH 开源主站、用 CODESYS 这类商业软 PLC、以及 pysoem。SOEM C 库功能完整但要自己维护编译环境特别是跑在 Windows 上还要装 WinPcap 或者 Npcap光环境就能折腾一整天。IGH 主站在 Linux 上很强支持标准内核打实时补丁但它的配置过程牵扯到网卡绑定、内核模块编译如果只是参数控制有点杀鸡用牛刀。CODESYS 是专业 PLC 方案现场也稳定问题在于它不是免费的而且把 Python 开发者挡在门外没法自由地在主站里跑自己写的业务逻辑。pysoem 的优势在于它直接支持 Windows 和 Linux 双平台安装非常简单pip install pysoemAPI 设计也比较贴近 Python 风格。实测下来我在 RK3568 这种 ARM 板子上也能跑对嵌入式场景很友好。它最大的局限是丢掉了 SOEM 的一部分单线程实时保障但对于 SDO 参数化控制和周期 1ms 以上的低速同步完全够用。1.3 这套方案的应用场景边界我建议用 Python pysoem 做这几种活产线设备开机时的伺服参数自检和自动下发、配方切换时批量改写增益和速度参数、研发阶段的运动控制原型验证、或者作为主 PLC 之外的诊断运维通道。不要用它做这些事多轴高速插补周期要求低于 500us、安全相关的运动控制、以及需要确定性强实时保证的场合。归根结底 Python 的 GIL 和解释执行决定了它做不到硬实时这是设计使然不是库的锅。但反过来说在工艺允许 1ms 到 10ms 响应的场景里Python 的灵活性和生态优势实在太香了改个参数不用重新编译挂个 FastAPI 还能直接做远程调试。2. 环境搭建与主站初始化2.1 硬件准备清单这部分看起来简单其实坑很多。我这次用的硬件是一台普通 Intel i5 工控机装 Ubuntu 20.04、一个板载 Intel I210 网卡后来发现这个选择非常重要、四台带 EtherCAT 的伺服驱动器汇川 IS620N 和松下 A5B 各两台、以及一个 24V 开关电源给驱动器的控制回路供电。网卡是第一个大坑。EtherCAT 主站对网卡有要求最好选 Intel 芯片的千兆网卡尤其是 I210、I211 这些被工业届验证过无数次的型号。Realtek 网卡也能跑但你会发现包延迟抖动大得离谱从站扫描偶尔失败而且连续跑几小时以后偶发超时很难查。所以我的建议是别在网卡上省钱一块 Intel 网卡能帮你排除掉 50% 的疑难杂症。伺服驱动器这边的接线也有讲究EtherCAT IN 接主站EtherCAT OUT 接下一台从站形成菊花链。问题是很多人第一次会把 IN 和 OUT 接反结果从站扫描死活找不到设备。还有个细节是终端电阻EtherCAT 不像 CAN 那样要求严格终端匹配大部分从站内部已经处理了但驱动器上往往有专门的终端开关链尾设备要按说明书拨对。2.2 安装 pysoem 以及我踩过的编译坑安装本身很简单pip3 install pysoem但如果你用的是精简版 Linux 系统会缺一些基础依赖导致编译失败。我的建议是提前装好sudo apt-get install build-essential python3-dev linux-headers-$(uname -r)在 Windows 上安装 pysoem 要特别注意它默认依赖 Npcap 或者 WinPcap 来抓原始以太网帧。第一次装完以后我发现导入 pysoem 不报错但 open 网卡的时候一直失败查了半天才发现是 Npcap 没装或者安装时没勾选WinPcap API 兼容模式。这个坑网上稍微搜一下就能看到一堆人踩我在这里先帮你排掉。顺便说下我踩过的另一个坑pysoem 的版本迭代挺快早期的 0.4.x 和现在的 1.x API 有一点差异。如果你看到网上的老教程写的master.sdo_read不存在不用慌很可能是你装到了新版本改了 API 路径。我强烈建议先跑一个简单的版本检查import pysoem print(pysoem.__version__)锁定版本以后再查对应文档能省下半天时间。2.3 第一个 EtherCAT 主站程序我把最基础的主站初始化写出来这是所有后续操作的起点import pysoem import time master pysoem.Master() master.open(eth0) # 注意换成你的网卡名 # 扫描总线上所有从站 if master.config_init() 0: print(f发现 {master.config_init()} 个从站)注意我调用了config_init()两次第一次用于判断数量之后还要再调一次用于实际初始化。严格来说config_init()每调用一次会做一次从站信息扫描返回的是从站数量。更常见的写法是if master.config_init() 0: master.config_map() master.state pysoem.SAFE_OP master.workers_read() print(主站切换至 SAFE_OP 状态)这里有个容易误导新手的点config_init()之后从站默认处在 PRE_OP你要把master.state设为pysoem.SAFE_OP然后调用workers_read()让主站把状态请求发送出去。这一步不执行后面你访问从站对象字典会一直报错。如果你在这一步发现从站数量是 0别急着怀疑代码先查三件事网卡名是否写对ip addr看下、网线是不是接到了 IN 口、驱动器控制电源有没有上电。我遇到过一次很诡异的情况驱动器面板亮了但 EtherCAT 接口没反应结果是因为伺服的控制电源和主回路电源分开供电我只上了主回路电EtherCAT 收发器根本没工作。3. CiA402 协议基础与 SDO 参数化控制原理3.1 认识对象字典伺服世界的寄存器CiA402 是 CANopen 应用层在驱动器上的配置规范EtherCAT 完全继承了这套对象字典机制。每个对象有两个关键标识索引16 位和子索引8 位主索引区分功能大类子索引区分同一类下面的具体参数。我在项目里最常用的几个对象列成表给你们索引子索引名称数据类型作用0x60400x00ControlwordU16控制字驱动状态机切换0x60410x00StatuswordU16状态字读取当前状态0x60600x00Modes of operationI8写运行模式位置/速度/力矩0x60610x00Modes of operation displayI8读当前模式0x607A0x00Target positionI32目标位置0x60810x00Profile velocityU32轮廓速度0x60830x00Profile accelerationU32轮廓加速度0x60840x00Profile decelerationU32轮廓减速度0x60910x01/0x02Gear ratioU32电子齿轮比分子/分母0x608F0x01/0x02Position encoder resolutionU32电机编码器分辨率分子/分母很多人第一次看对象字典会头晕我的理解方式是把 0x6000 段想象成驱动器的内存映射索引就是寄存器地址子索引就是寄存器里的子字段。你要写电机速度就找到 0x6081往里面塞一个无符号 32 位数协议层会自动把它放到周期同步帧里送给伺服。需要注意不同品牌伺服的对象字典在基础 CiA402 对象之外会有差异比如汇川的 IS620N 有一些私有对象用于调整刚性对象索引是 0x2910 开头的厂商区段。这些都是正常的。3.2 SDO 读写实操从让电机听懂说起SDO 在 EtherCAT 里走的是邮箱通道属于慢通道。它不保证实时性但能传输任意长度的参数数据。pysoem 封装得比较干净读一个对象slave master.slaves[0] # 读状态字 data slave.sdo_read(0x6041, 0x00, 2, timeout1000) status_word int.from_bytes(data, byteorderlittle) print(f当前状态字: 0x{status_word:04X})这里sdo_read的第二个参数是子索引第三个参数是期望读取的字节数。注意 EtherCAT 是小端字节序所以拿到 bytes 后要用int.from_bytes(data, byteorderlittle)解析。我见过有人用大端解析读出来的控制字完全不对还以为是驱动器坏了。写一个对象# 写入控制字 0x0006命令状态机进入 Ready to switch on controlword 0x0006.to_bytes(2, byteorderlittle) slave.sdo_write(0x6040, 0x00, controlword, timeout1000)pysoem的sdo_write第一参数是索引第二是子索引第三是待写入的 bytes 数据第四是超时时间毫秒。你可能会疑惑为什么控制字要自己转成 bytes而不是直接传整数。这是 pysoem 的一个设计特点它尽可能不替用户做类型推断因为 CiA402 里同一个索引在不同厂商下的数据类型可能不同你自己指定字节序列反而最稳妥。3.3 CiA402 状态机伺服使能的核心逻辑伺服驱动器内部跑着一个有限状态机所有操作都要按照状态机规则来否则驱动直接拒绝执行。简化以后是这样的Switch on disabled上电初始状态Ready to switch onSwitched onOperation enabled允许电机转动Fault故障态每次从上到下切换要靠往 0x6040 控制字写特定数值目标状态控制字数值十六进制从任何状态进入 Ready to switch on0x06进入 Switched on0x07进入 Operation enabled0x0F回到 Switch on disabled0x00清除故障并重新使能0x8F我一开始天真地以为直接把控制字写成 0x0F 就能一步使能结果伺服完全不理我。后来查协议文档才明白CiA402 要求状态切换必须逐级走过去跳步操作会被直接忽略或者触发错误。所以标准的使能序列是写 0x06 等状态字变成Ready to switch on写 0x07 等状态字变成Switched on写 0x0F 等状态字变成Operation enabled。判断当前处于哪个状态要看 0x6041 状态字的各个 bitbit0Ready to switch onbit1Switched onbit2Operation enabledbit3Faultbit5Quick stopbit6Switch on disabled比如读到的状态字是 0x0231二进制是0010 0011 0001bit0、bit1、bit2、bit5、bit9 为 1这表示驱动器处于 Operation enabled 且 Quick stop 未触发。说实话第一次看这堆 bit 很容易懵我的建议是写个小的解析函数把状态字拆成有意义的枚举打印出来调试效率高很多。4. 完整实操SDO 参数化控制一台伺服4.1 扫描从站并读取电机铭牌参数正式操作前我先写了一个脚本把所有从站的基础信息列出来。这一步相当于确认总线上的设备跟我心里想的是否一致。import pysoem master pysoem.Master() master.open(eth0) # 扫描从站 slave_count master.config_init() print(f总线从站数量: {slave_count}) for i, slave in enumerate(master.slaves[:slave_count]): print(f从站{i}: 厂商ID0x{slave.man_id:08X}, 产品码0x{slave.product_id:08X}) # 读取类型对象设备名称 name slave.sdo_read(0x1008, 0x00, 16, timeout1000) print(f设备名称: {name.decode(ascii, errorsignore).strip(chr(0))})这里有个小细节0x1008 是设备名称对象很多驱动器存储在里面的字符串后面会补 0读出来以后要 strip 掉结尾的空字符。我这次扫描得到的结果是从站 0 是汇川 IS620N从站 1 是松下 A5B。如果你发现扫描到的厂商 ID 和产品码跟铭牌对不上先检查固件版本是否支持 EtherCAT有些老版本伺服需要刷固件才能启用 CoE 功能。4.2 批量下发运行参数配方切换的核心伺服参数化控制最常见的场景是配方切换。比如这一次要跑高速轻载加速度拉满、速度拉满下一次要跑重载低速加速度保守、速度限制。每一次切配方我都不想人工去触摸屏点几十个菜单那太容易出错了。用 SDO 批量下发参数的核心代码如下def write_profile_parameters(slave, velocity, accel, decel): # 必须先确保驱动器不在使能状态否则部分参数写入会被拒绝 # 写入轮廓速度 0x6081 slave.sdo_write(0x6081, 0x00, velocity.to_bytes(4, little), timeout1000) # 写入轮廓加速度 0x6083 slave.sdo_write(0x6083, 0x00, accel.to_bytes(4, little), timeout1000) # 写入轮廓减速度 0x6084 slave.sdo_write(0x6084, 0x00, decel.to_bytes(4, little), timeout1000) print(运动参数下发完成) # 例子设置目标速度 3000 rpm加速时间 200ms减速时间 200ms write_profile_parameters(master.slaves[0], 3000, 200, 200)我踩过一个大坑在伺服处于 Operation enabled 状态下很多驱动器会拒绝写 0x6081 这类运动参数返回 SDO abort因为运行中改参数可能导致意外。解决办法很简单先把控制字写成 0x00 让驱动器回到 Switch on disabled参数写完以后再重新走使能流程。另外要注意单位换算问题不同品牌厂商对加速度的定义可能不一样。汇川 IS620N 的 0x6083 单位是什么我实测下来跟 CiA402 标准一致是内部单位每秒平方通常是编码器计数每秒平方但你要是换算错了电机加速过程就会非常吓人。所以我每次写这类参数之前都会先读一遍当前值确认量纲符合我的预期。4.3 使能、点动和回零操作这是整套程序里最常用的功能我把每一步都拆开说明。首先是使能流程def enable_servo(slave): # 发送 shutdown 命令确保之前状态被清掉 slave.sdo_write(0x6040, 0x00, (0x0006).to_bytes(2, little), timeout1000) time.sleep(0.1) # 读取状态字确认进入 Ready to switch on status parse_statusword(slave.sdo_read(0x6041, 0x00, 2, timeout1000)) if not status.ready_to_switch_on: raise RuntimeError(无法进入 Ready to switch on) # 切换到 Switched on slave.sdo_write(0x6040, 0x00, (0x0007).to_bytes(2, little), timeout1000) time.sleep(0.1) # 使能运行 slave.sdo_write(0x6040, 0x00, (0x000F).to_bytes(2, little), timeout1000) time.sleep(0.1) status parse_statusword(slave.sdo_read(0x6041, 0x00, 2, timeout1000)) if not status.operation_enabled: raise RuntimeError(伺服使能失败状态字: 0x{:04X}.format(status.raw)) print(伺服已使能)注意每次写入控制字之后最好 sleep 一个短暂时间。很多新手的错误是控制字写完立刻读状态字发现状态没变就以为驱动器坏了。实际上驱动器的状态机处理需要一点毫秒级的时间加上 EtherCAT 邮箱通信本身有延迟多等 50ms 到 100ms 是最稳妥的。然后是点动操作。我采用位置模式模式码 1Profile Position每次给一个小的增量位置速度用轮廓速度限制def jog(slave, direction, distance10000): # 切换到位置模式 slave.sdo_write(0x6060, 0x00, (1).to_bytes(1, little), timeout1000) # 读取当前位置 current_pos int.from_bytes( slave.sdo_read(0x6064, 0x00, 4, timeout1000), byteorderlittle, signedTrue ) target current_pos direction * distance slave.sdo_write(0x607A, 0x00, target.to_bytes(4, little, signedTrue), timeout1000) # 触发位置启动 # bit4 New set point, bit5 Change set immediately slave.sdo_write(0x6040, 0x00, (0x003F).to_bytes(2, little), timeout1000)这个点动流程的精髓在于控制字的 bit4New set point和 bit5Change set immediately你写完目标位置以后不能只发使能字 0x000F必须把 bit4 拉高为 1驱动才会把 0x607A 里的数值当成新目标来跑。如果不拉驱动会一直守着旧目标给你一种指令没发出去的错觉。回零操作各家厂商差异很大有的支持标准 CiA402 的 Homing 模式模式码 6有的要靠私有对象。汇川支持标准回零代码我简单给个思路def home(slave): # 切换到 Homing 模式 slave.sdo_write(0x6060, 0x00, (6).to_bytes(1, little), timeout1000) # 设置回零方法比如 35 表示当前点作为原点 slave.sdo_write(0x6098, 0x00, (35).to_bytes(1, little), timeout1000) # 触发回零 slave.sdo_write(0x6040, 0x00, (0x003F).to_bytes(2, little), timeout1000)回零方法 0x6098 从 1 到 35 各有各的含义涉及正负限位开关、编码器 Z 相、当前位置等等。我给的建议是先在驱动器厂家调试软件里试一遍回零确定编号再把它固化到代码里。不然你从协议文档里挑了一个回零方式现场硬件上根本没有对应的传感器伺服就一直来回找限位看着很吓人。4.4 动态修改运行模式CSP 与 CSV 切换除了最基础的位置点动很多设备需要在运行中把模式从位置模式切到速度模式或者力矩模式。CiA402 允许你随时写 0x6060 来切换工作模式但切换前最好先让电机停稳。def switch_to_csp(slave): # CSP 周期同步位置模式 slave.sdo_write(0x6060, 0x00, (8).to_bytes(1, little), timeout1000) def switch_to_csv(slave): # CSV 周期同步速度模式 slave.sdo_write(0x6060, 0x00, (9).to_bytes(1, little), timeout1000) def switch_to_cst(slave): # CST 周期同步力矩模式 slave.sdo_write(0x6060, 0x00, (10).to_bytes(1, little), timeout1000)我这次项目里就用到了一次动态切换先让伺服在位置模式下把物料送到相机拍照位置然后切到速度模式做匀速追剪动作追完再切回位置模式。整个过程在 Python 里写就是几次 SDO 调用的事但要注意一点CSP、CSV、CST 这些周期同步模式严格来说需要 PDO 映射配合光切模式不映射 PDO 的话驱动里读到目标位置一直是 0。我用 SDO 控制的时候其实用的是位置轮廓模式PPM代码 1和速度轮廓模式PVM代码 3这两个模式对 PDO 映射不敏感走 SDO 也能驱动更加省事。如果你非要用 CSP 做周期同步控制那就要配置 0x1A00 系列的 RxPDO 映射把 0x6040 控制字和 0x607A 目标位置映射进去然后再用master.send_processdata()周期发送。这属于另一套玩法跟 SDO 参数化控制是两个层面这里就不展开但你需要知道有这个边界。5. 常见问题与排查技巧实录5.1 主站状态切换失败从站一直卡在 PRE_OP我调试的第一天就遇到这个问题config_init()能发现从站但把主站切到 SAFE_OP 以后从站状态怎么都上不去。后来我在循环里持续打印从站状态发现它在 PRE_OP 和 SAFE_OP 之间反复横跳。排查步骤是这样的第一步检查网线是不是用了屏蔽双绞线EtherCAT 对线缆质量要求比普通以太网高我用一根 10 米的普通网线在长距离传输时就不稳定第二步去掉总线上多余的从站只保留一台伺服做最小系统排除级联问题第三步检查伺服驱动器的 EtherCAT 配置里有没有启用自动重启或者看门狗功能有些驱动默认会开看门狗主站没有及时喂狗就从站会退回 PRE_OP。我那次最终查到是两个因素叠加网线质量差加上看门狗超时太短。解决办法是换了六类网线同时把驱动器看门狗时间从 100ms 改到 500ms。5.2 SDO 读写超时为什么驱动拒绝我的请求SDO 超时是最常见的报错代码长这样SDO abort 0x06090030: 未知的对象排查思路其实很清晰。先区分是对象不存在还是数据格式不对。如果是0x06090030说明索引、子索引组合在驱动器的对象字典里不存在。这时候要打开厂商的通讯对象手册确认你写的索引号没抄错。我犯过的错是把子索引从 1 开始数但有些对象子索引就是 0比如 0x6060 模式对象只有 0 号子索引没有 1。如果是0x06040041说明数据长度不匹配。比如 0x6040 控制字是 16 位你只传了一个字节。这种错误 Python 端一眼就能看出来你只需要把to_bytes的字节数改成对应长度。还有一种坑是只读对象写入比如 0x6061模式显示只能读不能写你要是往它里面 SDO 写数据驱动直接拒绝。这类问题我觉得不算 bug属于对协议不够熟悉多翻几遍对象字典就能避免。5.3 使能后电机不转但有抱闸声音这个问题我印象太深了。当时控制字已经切到 0x0F状态字也确认是 Operation enabled驱动器面板也没有报警但电机就是不动。后来发现驱动器默认使能以后需要给一个脉冲使能信号因为我把伺服的使能端子设置在外部输入外部信号没拉高内部使能和外部使能是与的关系。解决方法是把驱动器参数里使能控制方式从外部端子使能改成通讯控制使能或者在硬件上给使能端子接一个高电平。具体参数索引厂家各不相同汇川的是 0x2910 附近的厂商区松下的是 Pr4.20 这类面板参数。我在代码里做了一层判断使能前先把伺服的门使能相关对象读出来确认是通讯控制模式再往下走。5.4 Python 脚本跑久了偶发卡死如何优雅处理异常长时间运行的 SDO 参数化脚本偶发卡死是家常便饭。原因很多网卡驱动偶发丢帧、主站和从站邮箱通道占满、系统调度抖动导致超时。我最终的做法是给所有 SDO 调用包一层重试逻辑并且每隔一段时间调用一次master.workers_read()来维持健康状态。异常处理的基本原型def safe_sdo_write(slave, index, subindex, data, timeout1000, retries3): for attempt in range(retries): try: slave.sdo_write(index, subindex, data, timeouttimeout) return True except Exception as e: print(f写入 0x{index:04X}:{subindex:02X} 失败第 {attempt1} 次重试) time.sleep(0.2) raise RuntimeError(SDO 写入多次重试仍然失败)底层逻辑是EtherCAT 邮箱通道有重传机制异常通常能在一两次重试后恢复。如果重试三次仍然失败那就不要继续重试了大概率是硬件断了或者从站进了 Fault 状态这个时候应该先读故障码再决定要不要复位。另外一个实用技巧是在脚本末尾关闭主站时调用master.close()否则网卡文件描述符不释放下次打开会报占用。Windows 下长时间跑 Python 脚本还要记得定期重启解释器或者做对象清理避免内存碎片导致性能下降。写在最后的经验之谈这次项目跑通以后我最大的感受是Python 做 EtherCAT 从站参数化控制完全可行关键是要把职责边界想清楚——SDO 负责慢速、大块、非实时的参数交互PDO 才负责快速、周期、实时的小数据同步。pysoem 把主站通信的复杂性藏了起来让我可以把精力集中在 CiA402 状态机编排和对象字典管理上。给后面上手的朋友三个小建议。第一任何时候开始写代码之前先把对象字典和状态机时序图打印出来贴在工位上这比任何调试工具都管用。第二多利用厂商调试软件汇川的 InoProShop、松下的 PANATERM观察驱动器的实时状态当你用 Python 写了半天没反应时去调试软件里看一眼状态字往往能一秒钟发现问题。第三保留一份操作记录日志每次 SDO 写什么参数、改写成了什么值、当时状态字是多少全打出来。等现场出问题时这份日志就是最有力的排查依据。最后再分享一个细节如果你要交付给现场使用建议把最关键的状态机切换代码封装成一个类把使能、点动、回零、切配方作为公开方法内部自动处理状态检查、超时重试和日志记录。这样即使现场维护的人不懂 Python也能照着接口说明上手操作。工业自动化项目里稳定性和可维护性比炫技重要得多。
返回列表