IgH EtherCAT主站:PDO、WKC、DC与ESM调试
简介:围绕 EtherCAT 实时运动控制与 Linux 开源主站 IgH 的流程讲解,面向具备一定嵌入式或工控基础、希望打通主从通信并驱动汇川 SV630N 伺服电机的开发者。内容以 IgH 主站实现为主线,结合 Linux 平台上的实时化改造(Xenomai 内核)、伺服调试与六轴机械臂运动控制,并附英文论文《Motion Control of 6-DOF Manipulator Based on EtherCAT》,可对照梳理三度 B 样条轨迹规划、改进 D-H 参数逆运动学求解等关键环节,理解从协议配置、主从通信到轨迹执行的完整链路。压缩包共 1 个 PDF,约 1.63MB,篇幅集中,便于按主站流程、实时内核和机械臂控制主题查阅;尤其适合正在搭建实验平台、调试主站或开展课程设计与科研复现的读者,用于快速建立概念框架、核对实现流程并快速定位关键问题。已有 5228 人学习,可作为偏重流程梳理的 EtherCAT 入门与工程落地整体参考。
1. 一帧报文穿过所有从站:EtherCAT 快在哪,IgH 又难在哪
EtherCAT 能做到 100μs 级周期,并不是因为物理层比别人更快,它的线速就是普通的百兆或千兆以太网。真正的秘密在拓扑里:主站发出的一帧报文,会依次穿过链路上每一个从站,每个从站在转发的同时,把自己那一段数据就地改掉,帧不停、不拆包、不排队,绕一圈回来时,所有从站的数据都已经装在里面了。这个动作叫 processing on the fly,它是整个协议性能的基础,也是后面所有复杂度的根源。
而 IgH 要做的,是把这套原本由专用芯片承担的时序行为,用普通网卡加普通 Linux 在软件侧复现出来:认网卡、扫拓扑、把几十个从站的 PDO 拼进同一块逻辑地址空间、按状态机一步步把从站推到 OP、再保证每毫秒在同一时刻把帧发出去。这套流程里任何一步错位,表现出来的都不是一句明确的报错,而是“数据全是 0”或者“周期抖到没法用”。搞懂它为什么这么设计,比记住调用顺序有用得多。
2. On the Fly:从站不“收包”,那一帧数据是怎么被改出来的
2.1 报文经过从站时发生了什么
常规以太网设备收到一帧,流程是:存进缓冲区、中断 CPU、协议栈解析、拷贝到应用、应用处理完再组一帧发回去。每个节点都要付一次“收-解-拷”的代价,节点一多,延迟就叠上去了。EtherCAT 把这条路砍掉了:从站的 ESC 芯片在帧的比特流经过它的那一刻就开始判断,这个报文是不是我该处理的、逻辑地址落不落在我这段范围里,如果是,就直接在帧里原地读或者原地写,然后把帧从另一个口转发出去。
关键在于,这个动作发生在帧还没收完的时候,所以叫“边转发边处理”。从站不需要完整的接收缓冲区,不需要 CPU 参与,也不需要协议栈。报文到达链尾后按原路返回(物理上是全双工的两条独立通道),回到主站时携带着全部从站的输入数据。一帧报文搞定所有节点,这是 EtherCAT 和“每个节点一问一答”的轮询式总线最本质的差别。
报文结构上,一帧 EtherCAT 帧的以太网类型是 0x88A4,紧跟一个 2 字节的 EtherCAT 头(低 11 位是后续 datagram 的总长度),然后是一个或多个 datagram 首尾相接。每个 datagram 长这样:1 字节命令码、1 字节索引、4 字节地址、2 字节长度与标志、2 字节中断、若干字节数据、2 字节 working counter。命令码决定了寻址方式,比如 LRD 是逻辑读、LWR 是逻辑写、LRW 是逻辑读写、APRD 是自增物理读、BRD 是广播读。
# 拆一帧 EtherCAT,看清 datagram 是怎么串起来的 ETHERCAT_ETHERTYPE = 0x88A4 def walk_datagrams(frame: bytes): ecat = frame[14:] # 跳过 14 字节以太网头 hdr = int.from_bytes(ecat[0:2], "little") total = hdr & 0x07FF # 低 11 位:后续 datagram 总长度 pkt_type = (hdr >> 12) & 0x0F # 1 表示 EtherCAT 命令帧 off = 2 # 一帧里可以塞多个 datagram,主站按顺序把它们串成链 while off < 2 + total: cmd = ecat[off] # LRD/LWR/LRW/APRD... idx = ecat[off + 1] # 索引,用于匹配请求与响应 addr = int.from_bytes(ecat[off + 2:off + 6], "little") len_flags = int.from_bytes(ecat[off + 6:off + 8], "little") dlen = len_flags & 0x07FF # 该 datagram 的数据长度 more = (len_flags >> 13) & 0x01 # 后面还有 datagram 吗 payload = ecat[off + 8: off + 8 + dlen] wkc = int.from_bytes(ecat[off + 8 + dlen: off + 10 + dlen], "little") yield cmd, idx, addr, payload, wkc off += 10 + dlen if not more: break # 读一帧时最该盯的是 wkc:它才是“数据到底有没有被写进去”的唯一证据上面这段解析代码不是让你真去手写协议栈,而是说明一件事:一帧里有几个 datagram、每个 datagram 落在哪个逻辑地址、WKC 涨没涨,这三件事是后面所有调试的抓手。主站软件做的绝大部分工作,就是构造这样一帧、发出去、收回来、检查 WKC。
2.2 FMMU:逻辑地址怎么落到某个从站的 DPRAM
主站视角看,所有从站的过程数据是一整块连续的地址空间,叫 domain。主站只认逻辑地址,比如 0x00000000 到 0x0000000B 这 12 个字节,前 4 个字节是 1 号从站的输出,中间 4 个是 2 号从站的输出,后 4 个是全部从站的输入。主站不需要知道数据具体存在哪个从站的哪个物理地址。
但报文是广播式地在链上跑的,从站怎么知道“这一段是给我的”?答案在 FMMU(Fieldbus Memory Management Unit)里。每个 ESC 内部有若干个 FMMU 单元,配置项包括:逻辑起始地址、逻辑长度(按位算)、物理起始地址、物理起始位、操作类型(读/写)、使能位。从站收到一个逻辑寻址的 datagram 时,硬件拿 datagram 的地址去比对自身 FMMU 的逻辑区间,命中就把这段数据映射到本地物理地址上,读也好写也好,直接落到 DPRAM 里对应的位置。
这个设计带来一个很实际的后果:从站的 PDO 顺序、长度、在 domain 里的偏移,全部是在初始化阶段算好并写进 FMMU 的,运行期不能变。想改映射就得重新走一遍配置流程,把从站退回 PREOP 再重来。很多人第一次做 IgH 时,想当然地在周期任务里改偏移量,结果就是数据错位,还查不出原因。
举个具体例子。假设三个从站,每个输出 4 字节、输入 4 字节,domain 一共 24 字节:
| 逻辑地址范围 | 归属 | 方向 | 从站物理地址 |
|---|---|---|---|
| 0x0000 - 0x0003 | 从站 1 | 输出 | 0x1100 |
| 0x0004 - 0x0007 | 从站 2 | 输出 | 0x1100 |
| 0x0008 - 0x000B | 从站 3 | 输出 | 0x1100 |
| 0x000C - 0x000F | 从站 1 | 输入 | 0x1400 |
| 0x0010 - 0x0013 | 从站 2 | 输入 | 0x1400 |
| 0x0014 - 0x0017 | 从站 3 | 输入 | 0x1400 |
注意输出和输入的物理地址在各从站内部可以是相同的,因为逻辑地址把它们隔开了。这正是这套机制最巧妙的地方:物理内存不用改,逻辑视图随便编排,主站像访问一块平坦内存一样访问整条总线。
2.3 SyncManager:为什么过程数据必须是缓冲的
FMMU 解决的是“地址对应关系”,SyncManager 解决的是“时序对齐”。这两个东西经常被混在一起讲,但职责完全不同。
问题出在:报文到达从站是一瞬间的事,而从站的应用(伺服固件的控制环)是按自己的节奏跑的。如果报文直接写到应用正在读的内存上,就会出现读到一半被改掉的撕裂。ESC 的做法是在中间加一层缓冲,也就是 SyncManager。每个 ESC 有 16 个 SM,约定俗成的分工是:SM0 和 SM1 给邮箱用(SDO 这类非周期性、大数据量的通信),SM2 用于输出过程数据(主站写、从站读),SM3 用于输入过程数据(从站写、主站读)。
SM2 和 SM3 通常配置成三缓冲模式。报文到达时写入当前空闲的缓冲,然后切换指针;从站应用从另一个缓冲读,两者互不阻塞。这样即使某一次周期报文来晚了一点或者早了一点,数据也不会错乱,最坏情况只是多等一个周期。代价是引入了最多一个周期的额外延迟,这是所有现场总线的通病,不是 EtherCAT 特有的。
SM 还承担一致性校验:如果主站在从站还没读完上一帧时又写了新数据,ESC 会标记出溢出,主站能在状态寄存器里读到一个错误标志。这个信息在调试周期抖动时非常有用,它直接告诉你“是你的周期不稳,不是从站的问题”。
2.4 Working Counter:唯一可信的验收标准
每个 datagram 尾部有 2 个字节的 Working Counter。规则很朴素:从站每成功完成一次读操作,WKC 加 1;每完成一次写操作,加 1;LRW 这种读写合一的,加 2。主站在发帧前把 WKC 清成期望值,收回来时对比,就能知道“这一帧里期望被处理的从站,是不是都处理了”。
这个机制看着简陋,实际是整套调试体系里最有用的东西。WKC 少了,说明有从站没收到或者没处理,可能的原因有一大串:从站掉线、状态没到 OP、FMMU 没使能、逻辑地址算错、SM 长度配错。但它给了你一个确定的判断依据:只要 WKC 对不上,就别去怀疑应用层的代码,问题一定在通信配置这一层。
很多人第一次跑 IgH 时,看到ecrt_domain_state()里wc_state是EC_WC_INCOMPLETE,第一反应是去改周期任务的逻辑。正确的顺序恰好相反:先看ethercat slaves里所有从站是不是都在 OP,再看ethercat pdos的输出和你的映射表对不对得上,最后才怀疑时序。WKC 是黑匣子,它只告诉你结果不对,但结合从站状态和 PDO 列表,就能把范围缩到很小。
3. ESM 状态机:为什么不能跳过 PREOP 直接写 PDO
3.1 四个状态各自解锁了什么能力
EtherCAT 从站有一个强制实现的状态机,叫 ESM。四个基础状态:Init(I)、Pre-Operational(P)、Safe-Operational(S)、Operational(O),外加一个可选的 Bootstrap(B)。之所以要分这么细,是因为从站从“刚上电什么都不知道”到“能跟着主站每毫秒跑控制环”,中间需要好几个不可逆的配置动作,而这些动作必须在特定窗口里做。
Init 状态是上电默认态。这个时候只有 ESC 的寄存器可以访问,SM 和 FMMU 都还没配置,邮箱通信也不通。主站要在这个阶段做的事是:读从站的类型和基本信息、配置 SM0/SM1 的邮箱地址和长度、配置 FMMU。
Pre-Operational 是第一个能干活的状态。邮箱通了,意味着可以用 CoE 协议读写 SDO:读 ESI 描述、读从站支持的 PDO 列表、把 PDO 映射写进 0x1C12(RxPDO 分配)和 0x1C13(TxPDO 分配)、配置 DC 参数。这个阶段过程数据还不可用,因为 SM2/SM3 的长度和方向还没最终确定。
Safe-Operational 开始有输入过程数据了。从站把实际采集到的值往 SM3 里写,主站能读到,但输出保持在上一次的值或者安全值,不会真正驱动执行机构。这个状态的意义是让你先验证“读”这条链路通不通,而不会因为写错一个数据让机械臂动起来。做伺服调试时这个状态特别值钱,可以先看看编码器反馈对不对,确认没问题再往下走。
Operational 才是输入输出全开。从站应用真正开始执行控制环,主站写进 SM2 的值会被当作指令执行。从 Safe-Op 到 Op 这一步,是唯一一个“一旦跨过去,机器就可能动”的门槛。
Bootstrap 是个特殊态,只保留邮箱通信,用来做固件升级。一般应用碰不到。
3.2 主站是怎么“推”状态的:AL Control 与 AL Status
状态切换不是从站自己升级,是主站请求、从站确认的握手过程。主站往从站的 AL Control 寄存器(0x0120)写目标状态码,从站在自己准备好的时候把当前状态写进 AL Status(0x0130),如果失败,错误码放在 0x0134。
这个握手的语义有几个细节值得注意。第一,从站可以拒绝。比如 PDO 映射的总长度和 SM2 的实际长度对不上,从站会停在 Pre-Op 并回一个错误码,主站得去读 0x0134 才知道具体原因。第二,状态可以逐级升也可以逐级降,但不能跳。想从 Init 直接到 Op 是不行的,硬件层面就不接受。第三,从 Op 退回 Safe-Op 是运行期允许的,主站发现某个从站异常时,可以通过退回状态来快速切断输出。
在 IgH 里,这套推进大部分是自动的。应用调用ecrt_master_activate()之后,内核模块接管,按 Init → Pre-Op → Safe-Op → Op 的顺序推进所有配置好的从站,应用只需要在周期任务里检查状态。
/* 周期任务里检查主站和从站状态,这一段比读 PDO 更该先写 */ ec_master_state_t ms; ecrt_master_state(master, &ms); /* wc_state 反映上一周期域数据的完整性,links 反映端口链路 */ if (ms.slaves_responding < expected_slaves || ms.al_states != 0x08) { /* al_states 是位图:bit3 对应 Operational。 只要有一个从站没到 Op,这一位就不会置起来 */ printf("master not ready: slaves=%u al_states=0x%02x\n", ms.slaves_responding, ms.al_states); /* 这里不要去写 PDO,先退回去检查拓扑和配置 */ return; } /* 单个从站的状态也能单独查,定位是哪一个卡住了 */ ec_slave_config_state_t scs; ecrt_slave_config_state(sc, &scs); if (!scs.online || scs.al_state != 0x08) { printf("slave offline or not OP, al_state=0x%02x\n", scs.al_state); }al_states这个位图字段容易被忽略。它把所有从站的 AL Status 按位或在一起,任何一个从站没到 Op,对应位就不会置起来。用它做粗筛,再用ecrt_slave_config_state()逐个定位,比一上来就抓包高效得多。
3.3 卡在 SAFEOP 到 OP 之间的三种典型原因
这个卡点在实际项目里出现频率最高,而且症状很像:所有从站都在线,Pre-Op 能到,Safe-Op 也能到,就是最后一步上不去。
第一种,PDO 长度和 SM 长度不一致。从站在 Pre-Op 阶段接受了你的 0x1C12/0x1C13 映射,但 SM2 的实际长度是硬件固定的或者是 ESI 里写死的,如果你的映射总字节数和它对不上,从站就拒绝进 Op。典型场景是你按 PDO 列表算了 8 字节输出,实际伺服只接受 12 字节,中间差了 4 字节的填充。查这个要看从站的 ESI 文件里 SM2 那一段,而不是只看 PDO 列表。
第二种,DC 启动时间设得太早或者太晚。如果启用了分布式时钟,从站需要在收到 Sync0 信号之后才允许进 Op,而 Sync0 的起始时间如果在配置完成之前就已经过去了,从站会一直等下一个周期,表现出来就是“卡住”。调试时可以先关掉 DC 把流程跑通,再打开 DC 定位问题,这个二分法屡试不爽。
第三种,从站固件要求先写某个 SDO 才能进 Op。有些伺服驱动器要求先通过 SDO 设置运行模式、清除故障字、或者使能某个功能位,才允许进入 Operational。这类要求不一定写在手册显眼位置,属于血泪经验。排查方法是把ethercat sdos完整的对象字典打出来,对照厂商文档看有没有“进 Op 前置条件”的说明。
注意:从 Safe-Op 退回 Pre-Op 时,PDO 映射会被清掉,重新进 Op 需要重新配置一遍。如果你的程序里有“出错就重启”的逻辑,要确认它会走完整的配置流程,而不是只重发一次状态请求。
4. 周期任务:IgH 的 send/receive/queue 到底在什么时候发生
4.1 初始化时序:从 request_master 到 activate
IgH 的应用侧 API 看着函数挺多,其实可以按“配置期”和“运行期”切成两段。配置期做的事是把从站信息翻译成内核里的数据结构,一次性的;运行期只做四件事,每周期重复。把这两段分清,大部分“为什么调了没反应”的问题都能自己解释。
顺序大致是:申请主站、创建 domain、给每个从站建 slave_config、配 PDO 映射、注册 PDO 入口拿到偏移、配 DC、最后 activate。activate 之前,主站不会真正在总线上跑周期报文;activate 之后,内核模块开始按你配的参数推进状态机并开始收发。
#include <ecrt.h> static ec_master_t *master = NULL; static ec_domain_t *domain = NULL; static ec_slave_config_t *sc = NULL; static uint8_t *domain_data = NULL; /* PDO 入口注册表:每一项告诉主站“把哪个对象挂到 domain 的哪个偏移” */ static unsigned int off_ctrl_word, off_target_pos, off_status_word, off_actual_pos; static const ec_pdo_entry_reg_t pdo_regs[] = { { 0, 1, 0x00000000, 0x00000000, 0x6040, 0x00, &off_ctrl_word }, { 0, 1, 0x00000000, 0x00000000, 0x607A, 0x00, &off_target_pos }, { 0, 1, 0x00000000, 0x00000000, 0x6041, 0x00, &off_status_word }, { 0, 1, 0x00000000, 0x00000000, 0x6064, 0x00, &off_actual_pos }, {} }; /* PDO 内容:CSP 模式下最常用的四个对象 */ static ec_pdo_entry_info_t pdo_entries[] = { { 0x6040, 0x00, 16 }, /* controlword */ { 0x607A, 0x00, 32 }, /* target position */ { 0x6041, 0x00, 16 }, /* statusword */ { 0x6064, 0x00, 32 }, /* position actual value */ }; /* SyncManager 分配:2 号出、3 号入,这是 CiA402 的固定约定 */ static ec_pdo_info_t pdos[] = { { 0x1600, 2, pdo_entries + 0 }, /* RxPDO:主站 -> 从站 */ { 0x1A00, 2, pdo_entries + 2 }, /* TxPDO:从站 -> 主站 */ }; static ec_sync_info_t syncs[] = { { 0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE }, { 1, EC_DIR_INPUT, 0, NULL, EC_WD_DISABLE }, { 2, EC_DIR_OUTPUT, 1, pdos + 0, EC_WD_ENABLE }, { 3, EC_DIR_INPUT, 1, pdos + 1, EC_WD_DISABLE }, { 0xFF } }; int init_master(void) { master = ecrt_request_master(0); /* 对应 /dev/EtherCAT0 */ if (!master) return -1; domain = ecrt_master_create_domain(master); if (!domain) return -1; /* 三个 0 分别代表 alias、position、以及厂商/产品号, 实际项目里厂商号和产品号必须和 ESI 完全一致,否则找不到从站 */ sc = ecrt_master_slave_config(master, 0, 1, 0x00000000, 0x00000000); if (!sc) return -1; /* EC_END 表示 syncs 数组到此结束,映射一次性下发 */ if (ecrt_slave_config_pdos(sc, EC_END, syncs)) return -1; /* 注册 PDO 入口,函数返回后 off_xxx 里就是 domain 内的字节偏移 */ if (ecrt_domain_reg_pdo_entry_list(domain, pdo_regs)) return -1; /* DC:0x0300 表示同时使能 Sync0 和 Sync1,周期 1ms,无相位偏移 */ ecrt_slave_config_dc(sc, 0x0300, 1000000, 0, 0, 0); /* activate 之后才开始真正上总线跑周期 */ if (ecrt_master_activate(master)) return -1; domain_data = ecrt_domain_data(domain); return domain_data ? 0 : -1; }几个参数值得单独说。ecrt_master_slave_config的 alias 和 position 组合用于在环上定位从站,如果 ESI 里给从站配了 alias,用 alias 定位比用 position 稳,因为换线序时 position 会变。厂商号和产品号必须和 ESI 严格一致,填错的话函数会返回非空指针但后面扫不到从站,这种“成功返回但没用”的返回值和玄学差不多。
EC_WD_ENABLE/EC_WD_DISABLE是看门狗开关,只对输出 SM 有意义。使能之后,如果主站超过设定时间没发帧,从站会自动把输出切到安全值。调试时建议先关掉,免得刚跑两步就触发。
ecrt_domain_reg_pdo_entry_list的入参里那个&off_xxx是函数写回的位置,不是你自己算的偏移。很多人会自己按 PDO 顺序累加字节数,一旦中间有个位域对象(比如 1 位的状态位)就全错了。让库来算。
4.2 周期循环里的四步与域数据指针
ecrt_master_activate()返回之后,主站进入自主运行模式:内核模块按你配的周期自动收发。应用侧要做的,是在自己的实时任务里按固定顺序调四个函数。
void cycle_task(void) { /* 1. 收:把上一周期发出去又绕回来的帧取进来,数据落到 domain 缓冲区 */ ecrt_master_receive(master); /* 2. 处理:让主站检查 WKC、更新域状态、把输入数据放到正确偏移 */ ecrt_domain_process(domain); /* 3. 读写:此时 domain_data 里的输入区是新鲜的,输出区等待填充 */ uint16_t status = EC_READ_U16(domain_data + off_status_word); if (status & 0x0008) { /* 伺服已使能 */ EC_WRITE_U16(domain_data + off_ctrl_word, 0x000F); EC_WRITE_S32(domain_data + off_target_pos, next_target); } /* 4. 交回:把改好的输出区标记为待发送 */ ecrt_domain_queue(domain); /* 5. 发:组帧并推上网卡 */ ecrt_master_send(master); }这五步的顺序不能乱。receive在前是因为要先拿到上周期结果;process必须在读数据之前,否则读到的是上一周期的旧值;queue必须在send之前,否则改的数据不会被装进帧。反过来做,现象是“控制有延迟”,但代码看着一切正常,这一类错最容易翻车。
domain_data是一块连续内存,输入区和输出区按 FMMU 的编排混在同一块里,用偏移访问。EC_READ_U16/EC_WRITE_S32这些宏不是简单的强转,它们处理了字节序问题。EtherCAT 线上是小端,如果主机是大端架构,直接强转会读反,这类问题在 x86 上永远看不到,换到某些 ARM 平台上才暴露。
还有一个细节:ecrt_domain_process之后,域的状态(WKC 是否完整、是否有从站掉线)才可读。上节那段状态检查代码应该放在 process 之后、读写之前,位置放错就等于拿上一周期的结论做本周期决策。
4.3 DC 同步:偏移、漂移和 Sync0 的三个寄存器
分布式时钟解决的是“所有从站的本地时钟对齐到同一个时间基准”这件事。没有 DC 的时候,各从站按自己晶振跑,几百微秒之后相位就散了,多轴联动会看到明显的位置不同步。开了 DC,典型同步精度能到 100ns 量级。
机制分三层。第一层是传播延时补偿:帧从主站到各从站的路径长度不同,到达时刻天然有差。主站用广播读命令测量每段链路的延时,把累计值写进各从站的 System Time Offset 寄存器(0x0920),让每个从站的本地时间加上这个偏移后对齐到参考时钟。
第二层是漂移补偿。晶振频率有微小差异,即使初始对齐了,跑久了也会慢慢偏。从站硬件会周期性测量自己的时钟和参考时钟的差值,写进 System Time Difference(0x0918),主站或者从站根据这个差值微调本地时钟的步进。这个过程是硬件自动做的,软件只需要保证参考时钟选对了。
第三层是 Sync0/Sync1 信号。Cycle Time(0x09A0)定义周期,Shift(0x09A4)定义相对周期的相位偏移,Start Time(0x0990)定义第一个脉冲的绝对时刻。从站收到 Sync0 上升沿时锁存输入、应用输出,这就是“所有轴在同一时刻动作”的物理基础。
在 IgH 里,参考时钟通过ecrt_master_select_reference_clock()指定,一般选链路上第一个支持 DC 的从站。如果这个从站掉线,整个 DC 体系会重新选主,期间所有从站会短暂失去同步,多轴设备上这往往表现为一次明显的抖动。所以在选参考时钟时,优先选物理位置稳定、不容易被拔掉的那个节点。
/* 指定 0 号从站为参考时钟,必须在 activate 之前调用 */ ecrt_master_select_reference_clock(master, ecrt_master_slave_config( master, 0, 0, VENDOR_ID, PRODUCT_CODE)); /* 每个从站的 DC 参数要单独配:Sync0 周期 1ms,相位偏移 500us */ ecrt_slave_config_dc(sc, 0x0300, 1000000, /* sync0_cycle:单位 ns */ 500000, /* sync0_shift:相对周期起点的偏移 */ 0, 0); /* sync1 不启用 */相位偏移这个参数在实际设备上要调。伺服的控制环在自己的中断里跑,如果 Sync0 正好落在它中断处理的中途,采样时刻会有抖动。把 shift 调开一点,让 Sync0 落在控制环空闲窗口里,效果立竿见影。这个值没有通用答案,得对着具体驱动器的时序图试,属于典型的“手册不写、只能实测”的参数。
5. 排查:IgH 跑不通时,先看这几个地方
5.1 主站设备起不来,/dev/EtherCAT0不存在
现象是ecrt_request_master(0)返回 NULL,dmesg里能看到模块加载失败或者“no device”之类的信息。
原因基本集中在网卡驱动这一层。IgH 的主站要直接操作网卡的收发描述符,绕开 Linux 网络协议栈,所以它需要两种驱动之一:一种是专门改过的原生驱动(针对特定型号的网卡),另一种是通用的ec_generic驱动。原生驱动性能好但支持的网卡型号有限;通用驱动什么网卡都能用,但每个周期要多一次内存拷贝,抖动会大一些。装完模块之后如果没指定用哪张网卡,主站不会自己猜。
解决办法是先看lspci确认网卡型号,再决定用原生驱动还是通用驱动。用通用驱动时要在加载模块时传参指定网卡名,比如modprobe ec_generic配合主站配置里的 interface 字段。另外这张网卡一旦被 IgH 接管,就不能再当普通网卡用了,ip addr里看不到它,这是正常现象,不是驱动没装上。做实验时最好用独立网卡,别拿管理口去接。
5.2 从站扫不到,或者状态推不动
现象是ethercat slaves输出为空,或者列出了从站但状态一直停在 PREOP。
扫不到的第一嫌疑是接线和拓扑。EtherCAT 支持线型、环型、星型等多种拓扑,但物理上是从站的 IN 口进、OUT 口出,接反了就不通。第二个嫌疑是厂商号和产品号填错。第三个是 ESI 文件缺失或者版本不对,主站认不出从站的身份,就不会给它分配配置。
状态推不动的位置不同,原因也不一样。停在 PREOP 通常是 PDO 映射被拒,去看从站 0x1C12/0x1C13 的实际值和你的映射是否一致。停在 SAFEOP 大多是上一节说的三种情况:SM 长度不匹配、DC 启动时间已过、缺一个前置 SDO。
排查时有个屡试不爽的顺序:先用ethercat slaves -v确认所有从站都能被识别,再用ethercat pdos对比实际映射和预期,然后用ethercat upload单独读几个关键对象确认邮箱通,最后才去看应用代码。跳过前两步直接改代码,基本是在浪费时间。
5.3 WKC 不涨,或者数据全是零
现象是周期任务跑起来了,但ecrt_domain_state()里wc_state一直是EC_WC_INCOMPLETE,读出来的输入数据全是 0。
原因通常是逻辑地址没对上。可能是 domain 大小算错了(少算了某个从站的填充字节),可能是 FMMU 没使能(配置流程走了一半就 activate 了),也可能是从站状态没到 OP 但主站还在发逻辑读写命令,从站根本不响应。
还有一种隐蔽情况:从站在 OP,WKC 也是对的,但数据就是不对。这时候要怀疑字节序和位域。比如某个状态字里第 3 位表示“使能”,你按大端读了,位就跑到别的地方去了。用EC_READ_U16这类宏而不是自己写指针强转,能规避掉大部分这类问题。
判断方法很直接:把期望的 WKC 值算出来(每个 LRW 操作每命中一个从站加 2,读加 1),和实际值对比。差多少就说明有几个从站没处理。差值是偶数还是奇数,还能进一步提示是读没成还是写没成。
5.4 周期抖动大,偶尔丢帧
现象是控制环跑起来感觉“肉”,示波器上看 Sync0 信号周期不稳,或者日志里偶尔出现 WKC 不完整。
先分清是主站侧还是调度侧的问题。主站侧看ecrt_master_state()里的tx_count/rx_count,如果收发不匹配,说明帧在物理层就丢了,检查线缆和接头。调度侧看应用任务的实际周期分布,如果抖动在几十微秒到几百微秒量级,那基本就是 Linux 调度器把任务切走了。
普通 Linux 内核不是硬实时系统,即使把任务设成 SCHED_FIFO 最高优先级,中断处理和内核临界区还是会让它晚一点。要压到几十微秒以内的抖动,得上实时补丁:Xenomai 采用双内核方案,实时任务跑在一个独立的微内核上,抖动能压到个位数微秒;RT-Preempt 是单内核方案,把大部分内核临界区改造成可抢占的,抖动在几十微秒量级,但部署和维护简单得多。
选哪个看你的周期。1ms 周期配 RT-Preempt 够用,250μs 以下建议上 Xenomai。另外不管用哪个,实时任务里都不要有内存分配、文件 IO、printf,这些都会引入不可预测的延迟。任务开始前先把内存锁页(mlockall),也是个几乎零成本的好习惯。
6. 取舍:为什么这套栈值得自己编译一遍
走到这里,几个设计取舍其实已经摆在台面上了。
主站选型上,IgH 和另一套常见的开源主站最大的差别不在功能列表,而在职责划分。IgH 把主站做进了内核,状态机推进、DC 维护、WKC 校验都在内核态完成,应用只负责读写一块内存;另一套方案是纯用户态实现,轻量、好移植、编译一把就能跑,但状态推进和时序保障得自己做。做单轴验证用后者更快,做六轴联动的产线设备,前者的确定性更重要。这个取舍没有对错,取决于你能不能接受“多花两天配环境,换后面少调三天时序”。
实时方案上,Xenomai 和 RT-Preempt 的选择同理。前者抖动更低但生态更窄,内核版本一换就得重新适配;后者抖动够用、主线支持好,但对极端周期无能为力。我一般会先按目标周期倒推:如果控制周期在 1ms 这个量级,先用 RT-Preempt 把功能跑通,等性能瓶颈真的出现了再换,而不是一开始就上最重的方案。
轨迹规划这块是另一个维度的取舍。三次 B 样条的价值不在于“曲线更光滑”这种笼统说法,而在于它把轨迹的连续性从 C1 提到了 C2——位置、速度、加速度都连续,jerk 有界。对六自由度机械臂来说,这意味着每个关节的加速度不会突变,伺服不会因为指令跳变而出现电流尖峰,机械结构上的冲击也小。代价是需要求解控制点,逆解的计算量比关节空间直线插值大,但在毫秒级周期里,这点计算量换来的是运动平稳性,很划算。
用改进 D-H 参数法建坐标系、拿解析解算逆运动学,也是同样的思路:数值解通用但慢,解析解快但推导麻烦。六自由度且满足 Pieper 准则的构型,解析解能把单次求解压到微秒级,省下来的时间预算可以留给轨迹规划和更复杂的控制算法。这个“先算清楚时间花在哪”的习惯,比具体用哪种算法重要得多。
那次调试印象很深:所有从站都在 OP,WKC 也对,但六轴联动时总有一个关节滞后半拍。查了两天才发现是那个从站的 Sync0 相位偏移用了默认值,正好撞在它控制环的中断窗口里。从那以后,我每次配 DC 参数都强制走一遍“示波器看 Sync0、对照驱动器中断时序调 shift”的流程,再也不敢照抄示例代码里的默认值。希望这些内容能帮到你,少走一点弯路。
本文还有配套的精品资源,点击获取