1. 项目动机与目标拆解
1.1 为什么要在VxWorks上跑EtherCAT
先交代一下背景。EtherCAT(Ethernet for Control Automation Technology)是一种基于以太网的高实时性工业现场总线协议,在运动控制、机器人、数控机床这类对同步精度要求极高的场景里几乎是标配。IGH(IgH EtherCAT Master)是目前应用最广的开源EtherCAT主站实现,Linux平台下生态非常成熟,代码开放、协议栈完整,很多人直接拿它做控制器原型验证。
但问题在于:工业产品一旦进入量产阶段,控制器硬件平台往往要从Linux切换到VxWorks这类商用实时操作系统。原因不复杂——VxWorks的调度确定性更强,任务切换时间可控,内核裁剪灵活,而且很多老牌运动控制厂商的控制器底软就是基于VxWorks维护的。IGH官方主线主要面向Linux,虽然有人做过非官方移植,但完整、稳定、可复用的VxWorks移植方案少之又少。
我做这个项目的目的很直接:把IGH主站完整跑在VxWorks上,让上层应用能够通过标准EtherCAT协议控制伺服驱动器,并能稳定运行在周期性同步模式下。整套工作的难点不在EtherCAT协议本身,而在于如何把Linux内核态驱动的机制映射到VxWorks的驱动框架里,同时保证实时性不打折扣。
1.2 适合谁看这份实操分享
这个项目适合两类人。一类是刚接触EtherCAT主站开发、对IGH源码结构还不熟悉,但恰好有VxWorks开发任务在手的工程师;另一类是对IGH在Linux上的运行机制已经很熟,但没做过跨平台移植,想看一套完整移植思路的人。
无论属于哪类,我建议你先对几件事有基本认知:EtherCAT的数据帧结构(至少知道寻址方式、FMMU、SyncManager这几个概念);VxWorks的设备驱动模型(至少知道如何注册驱动、中断服务程序怎么写);IGH源码目录结构(至少知道master/、devices/、lib/这些目录大致干什么用)。如果这些基础薄弱,下文在原理部分我会尽量用类比讲清楚,但涉及具体函数和数据结构时,还是建议对照IGH源码和VxWorks手册一起看。
1.3 移植前后的整体工作量评估
先给结论:在熟悉IGH源码结构的前提下,一个人完成从零移植到能跑周期性同步运动控制,实际投入大约在3到4周。其中第一阶段(环境搭建与源码梳理)约1周,第二阶段(核心机制移植)约1.5周,第三阶段(调试与稳定性验证)约1周,如果中间遇到硬件平台适配问题,时间还会延长。
这里说的“从零”,指的是拿到IGH master-release-1.5.2版本的源码,针对VxWorks 7做适配。IGH内核模块在Linux下是一个master内核模块加若干网卡驱动模块,移植到VxWorks后,整体架构需要变成VxWorks的内核驱动组件加应用层库,具体差异我会在下一节展开。
2. IGH主站内部机制详解
2.1 EtherCAT主站到底在做什么
很多人第一次看IGH源码会觉得头晕,因为主站工作涉及的东西太多。但如果把逻辑抽出来,主站无非就在做三件事。
第一件事是总线扫描与拓扑发现。上电后,主站需要向总线上发寻址帧,从站逐级响应,主站据此建立从站拓扑链表,获取每个从站的厂商ID、产品码、从站类型、PDO映射信息。IGH里对应的是ec_master结构体里的从站链表和ec_slave_config配置机制。
第二件事是周期性过程数据交换。这是EtherCAT最核心的价值。主站按照固定周期(比如1ms、500μs甚至125μs)向所有从站广播一帧数据,帧里同时携带每个从站的输入输出数据。所有从站在一个帧周期内同时锁存输入、更新输出,因此同步精度极高。IGH里对应的是ec_master_loop、ec_fsm状态机和ec_domain数据域机制。
第三件事是非周期邮箱通信。用于配置从站、读写SDO对象、上传下载固件等低速通信。IGH里对应ec_master_request和ec_fsm_coe系列状态机。
理解这三件事后,移植思路就清晰了:IGH在Linux下依赖的定时器、中断、锁、内存映射这些内核机制,本质上是为了实现上述三个功能服务的。VxWorks移植的核心工作,就是把这三件事背后的内核依赖逐一替换。
2.2 IGH的代码结构梳理
IGH源码顶层目录有master/、devices/、lib/、tool/、include/等。核心逻辑全在master/目录下,设备驱动在devices/目录下。
master/目录下有几个文件值得重点看。master.c是主站核心实现,几乎全部EtherCAT逻辑都在这;fsm_*.c系列文件是状态机实现,包括fsm_coe.c(CoE邮箱通信)、fsm_pdo.c(过程数据配置)、fsm_change.c(从站参数配置)等;mailbox.c处理邮箱通信;ethernet_base.c和ethercat_device.c是网卡接口抽象层;module.c是Linux内核模块入口。
IGH在Linux上的运行依赖几个关键内核接口:struct net_device(网卡接口、收发包回调)、struct timer_list(内核定时器,驱动周期发送)、struct semaphore和spinlock_t(并发保护)、struct sk_buff(网络数据包缓冲)、struct tasklet_struct或work_struct(下半部处理机制)。这些抽象在VxWorks上统统需要找到对应的替代物。
2.3 一个容易被忽视的设计点:主站运行模式
IGH有两种运行模式,理解了这一点,移植时才能找到正确的切入点。
第一种是内核态周期运行模式。主站逻辑以内核线程或定时器中断驱动,每个周期完成一次收发包和数据处理。应用层通过ioctl系统调用与主站交互,或者通过共享内存直接访问过程数据。这种模式实时性最好,Linux下实际项目几乎都用这种。
第二种是用户态运行模式。通过libethercat库在用户态创建套接字模拟EtherCAT帧收发,主站上下文也运行在用户态,调试方便,但实时性受限。
移植到VxWorks时,我特意保留了这两种模式的接口:内核态周期运行模式对应VxWorks的任务加信号量机制,用户态模式对应VxWorks的普通任务加BSD套接字。实际项目里两种都有用到,原型验证阶段跑用户态模式事半功倍,生产环境则必须切回内核态模式。
3. 移植方案选型与架构设计
3.1 为什么选择IGH而不是其他主站实现
严格来说,开源EtherCAT主站不止IGH一个,比如SOEM、CODESYS的底层协议栈等。我选IGH的核心原因是:它有完整的CoE(CANopen over EtherCAT)状态机实现,而很多其他开源主站只覆盖了基本过程数据交换。
在运动控制场景里,伺服驱动器的参数配置、位置模式切换、回零操作几乎都通过CoE的SDO读写完成。IGH对CoE的实现是完整的,甚至能处理复杂的分段传输、邮箱超时重传等边界情况。SOEM虽然轻量,但CoE部分需要自己补齐很多逻辑,工作量反而更大。
另外,IGH在Linux下的运行网络上有大量现成的参考例程,比如通用的ioctl封装、ethercat命令行工具源码、以及各类从站驱动的示例。这些资料搬到VxWorks上做适配时,很多逻辑可以原样保留,只是把底层接口换掉。
3.2 移植的整体架构:三层映射
我设计的目标架构分三层:底层是VxWorks的网卡驱动与内核服务,中间是IGH主站核心逻辑,上层是应用接口。
底层网卡驱动上,VxWorks 7本身带有标准BSD网络协议栈和网卡驱动,但EtherCAT的特殊性在于主站需要直接发送裸以太网帧(EtherCAT类型为0x88A4),绕过TCP/IP协议栈,并且要精确控制发送周期。IGH在Linux下用net_device的ndo_start_xmit直接把帧发出去,VxWorks下对应的做法是使用end驱动(Enhanced Network Driver)接口,或者更直接地调用底层send函数发送原始帧。
中间层是IGH主站核心逻辑,这部分代码尽量不改,只对上文提到的内核接口做适配层封装。我在源码里增加了一个arch_vxworks适配目录,把IGH内部对Linux内核接口的调用全部重定向到VxWorks的原语上。
上层应用接口层,IGH原有的一套ioctl命令结构可以保留,但VxWorks下没有标准ioctl系统调用概念,应用层的ecrt用户库需要改造为可直接调用的库函数,通过直接函数调用来访问主站,而不是通过文件描述符。
3.3 移植颗粒度:改源码还是写胶水层
这是整个项目里我踩过最多坑、也走最多弯路的一个决策点。
一开始我走的是“尽量少改IGH源码、全用宏定义做映射”的路线。想法很好:在IGH源码里用#ifdef VXWORKS包裹Linux接口,定义一套兼容宏。但实际跑起来问题很大,因为IGH对Linux内核机制的依赖不是简单的一对一,比如spinlock_t在VxWorks里对应spinLock,但IGH内部的很多语义是“锁并关中断”,单纯宏替换会丢中断上下文,导致不明原因的死锁。
最后我调整为“核心协议栈不动,内核接口层彻底重写”的方案。IGH主站与内核的交互点其实很集中,无非是几个文件:ethernet_base.c、master.c里的定时器和任务调用点、以及module.c的入口。我把这三个点抽象成一组干净的接口,然后针对VxWorks实现这一组接口。IGH内部对Linux数据结构的引用,比如struct sk_buff,则在适配层里定义一个最小化的等价结构体,保证IGH核心代码能编译通过即可。
这条路走下来验证是可行的——IGH的协议状态机完全不需要动,我们动的全是接口层。这个经验后来我也想明白了,开源代码移植的通用原则就是:协议逻辑是财富,别碰;接口依赖是负债,一定要隔离清楚。
4. 核心移植实现过程实录
4.1 环境准备与工具链
移植前需要准备好以下环境:
- VxWorks 7(我用的版本是7.0,编译器为
ccarm或ccintel取决于目标硬件架构,我用的是Intel平台,编译器为ccintel) - IGH源码,建议使用
master-release-1.5.2版本 - Wind River Workbench(用于构建VxWorks镜像和内核模块)
- 目标硬件为x86架构的工控机,主站网卡为Intel I210/I211(IGB驱动支持较好)
- 伺服驱动器若干(我用的是市面上常见的CANopen转EtherCAT或原生EtherCAT伺服),用于实测
对于IGH源码,建议直接下载release版本而不是git master分支,因为master分支经常处于开发状态,接口变动频繁,不利于移植。
另外VxWorks的内核模块构建方式和Linux的modprobe类似,但配置要复杂一些,需要把IGH编译成可加载的内核模块,或者直接静态编译进VxWorks镜像。我前期的做法是先静态编译进镜像,减少动态加载的变量,等代码稳定后再改成动态模块。
4.2 网卡驱动的移植:从ndo_start_xmit到end或raw socket
IGH发送EtherCAT帧最关键的底层依赖是网卡驱动的原始帧发送能力。Linux下igb网卡驱动配合IGH可以做到CPU几乎零拷贝发送,VxWorks下要复现这个能力不容易,因为VxWorks的end驱动接口虽然支持原始帧,但使用方式相对底层。
我最终选的路子是绕过end驱动,直接使用VxWorks的BSD套接字发送原始以太网帧。具体做法是创建一个AF_PACKET类型的原始套接字(VxWorks 7支持类BSD socket接口),绑定到目标网卡接口上,然后直接构造EtherCAT帧发送。接收侧则创建另一个原始套接字接收EtherCAT类型(0x88A4)的帧,或者通过select()多路复用接收。
这个方案有一个明显的性能短板:套接字接收路径经过了系统协议栈,延迟和抖动比Linux内核态的网卡驱动直收要大。后来我优化到比较满意的状态,实测周期任务抖动大约在微秒级,对于125μs周期确实有压力,但1ms周期的常规运动控制场景完全够用。
如果追求更高性能,还可以考虑在VxWorks包里集成IGH专用的底层网卡驱动,直接绑定到Intel网卡的DMA描述符环上。但这需要对VxWorks的end驱动框架和Intel网卡寄存器操作都非常熟,工作量会再增加一到两周,并且调试难度几何级上升。我的建议是先用原始套接字跑通整体流程,性能不达标再回头做网卡驱动专用化。
4.3 定时器与实时调度的实现
IGH在Linux下的周期运行依赖timer_list,每次定时器到期后触发主站运行一轮状态机和收发包流程。VxWorks下最自然的替代物是taskDelay配合sysClkRateSet定时器,但要注意精度问题。
VxWorks的taskDelay粒度依赖于系统时钟频率,默认系统时钟通常是60Hz或100Hz,远不够EtherCAT的周期精度要求。VxWorks可以通过sysClkRateSet()调整系统时钟频率,比如设为1000Hz甚至更高。但系统时钟频率的提高会增加系统开销,并且可能导致其他驱动(如串口)的行为变化,所以这个方案适合快速验证,不适合产品化。
更稳定可靠的方案是使用VxWorks的高精度定时器API:auxClk或者timer_create。IGH在VxWorks下最终使用的版本,我是用auxClk创建了一个高精度周期定时器,每个周期到期后通过信号量唤醒主站任务执行。这样既避免了系统时钟频率调整带来的副作用,又能把定时精度控制在亚微秒级。
调度上,主站任务应该设置为高优先级且taskLock保证不被打断,或者使用VxWorks的taskPriority配合INSTRUCTION_CACHE等配置。我实际使用的优先级是VxWorks空闲任务之上的一级(数值越小优先级越高),保证主站周期任务在任何情况下不会被低优先级任务抢占。
4.4 锁与并发控制的适配
IGH内部大量使用spinlock_t保护共享数据,比如从站配置链表、邮箱请求队列。VxWorks下的对应原语是spinLock、semTake/semGive。
关键区别在于:Linux的spinlock_t通常关中断并且不允许睡眠,而VxWorks的semTake(互斥信号量)允许任务调度,spinLock则与Linux的spinlock语义接近。IGH代码里凡是短临界区、绝对不允许睡眠的地方,比如处理从站中断返回后的数据,我用spinLock等价替代;凡是可能涉及等待、睡眠的路径,比如应用线程请求邮箱数据的等待,我用互斥信号量替代。
这个适配虽然逻辑不复杂,但必须严格对照IGH源码逐点分析。如果该用spinLock的地方错用了semTake,可能导致中断上下文非法;如果该用semTake的地方错用了spinLock,会导致系统死锁。我在这块花费的时间几乎占整个移植的三分之一,专门写了一个“IGH锁语义对照表”,把IGH里所有spinlock_t和信号量的使用场景逐一列出,再一一映射。强烈建议你也做这个表,防患于未然。
4.5 内存管理与DMA一致性映射
IGH收发EtherCAT帧时使用的是网卡DMA缓冲区,Linux下通过dma_alloc_coherent等API分配一致内存。VxWorks下对应的是dma_mem_alloc或者直接使用物理连续内存分配函数cacheDmaMalloc。
这里有个坑必须单独拿出来说:VxWorks的缓存一致性。Intel平台默认开启了CPU cache,网卡通过DMA写入数据后,如果CPU的cache中还保留着旧数据,会导致CPU读到的是陈旧值。Linux下dma_alloc_coherent会自动处理一致性问题,但VxWorks不会隐式替你处理。
我踩坑的现场是:IGH主站能正常完成总线扫描,但每过一段时间过程数据就出现随机错乱,表现为某个从站的位置反馈值偶尔跳变。排查了很久,最终定位到是DMA缓冲区没有做cache flush/invalidate。在VxWorks下解决很简单:在每次网卡接收数据前调用cacheInvalidate,在每次发送数据前调用cacheFlush,保证CPU和DMA看到的是同一份数据。
如果你用的硬件平台支持写合并(write-combining)或者IOMMU,可能避免这个问题,但工控机上通常没有这么高级的硬件特性,所以cache一致性处理不能省。
5. 实测验证与性能数据分析
5.1 测试环境与测试方法
我搭建的实测环境如下:
- 目标机:某型号x86工控机,CPU为双核2.5GHz,网卡为Intel I210
- 主站:IGH移植版,运行在VxWorks 7上
- 从站:一台EtherCAT伺服驱动器+若干数字量/模拟量IO从站模块,总线上共7个从站
- 上位机调试:VxWorks host shell + EtherCAT官方从站调试工具
测试项目主要分三类:一是总线初始化与从站识别,验证扫描和配置;二是周期性过程数据交换,在1ms和500μs两种周期下运行,观察数据刷新和同步性;三是长时间稳定性,持续运行12小时,监测丢帧率、同步误差和任务超时情况。
5.2 周期运行实测数据
以1ms周期运行压力测试,实测结果:
- 周期任务最大抖动:约为周期值的2.1%(即21μs),平均抖动在3μs左右
- 从站同步误差:各从站间反馈位置值误差在采样精度内(伺服反馈为23位编码器,误差小于1个LSB,基本无法分辨)
- 丢帧率:连续运行12小时零丢帧
- CPU占用率:主站任务及软中断等合计约占单核CPU的28%(含挡板中的其他系统任务)
500μs周期下,周期任务抖动明显增大,最大抖动达到51μs,占比超过10%,此时有两个从站的IO数据偶尔出现刷新不及时。这个结果说明,纯套接字路径的接收开销在500μs周期下已经逼近性能极限。如果你的项目要求125μs或更高频率,必须走网卡驱动直收路径,这个性能代价避不开。
5.3 稳定性问题与处理
12小时连续运行中,前4个小时出现了一次频率不高的现象:从站状态从OP(操作使能)掉到PREOP,随后IGH主站有超时重连机制,总线自动恢复。排查后发现是某个IO从站的电源上游出现了瞬间跌落,导致从站复位。
这个问题本身不是主站移植的锅,但暴露了一个问题:IGH默认的超时机制在应用层是无感知的——应用进程如果正在读过程数据,可能读到旧值而不知情。我做了一个保护:在IGH适配层增加一个“主站运行状态通知”,一旦总线掉线或主站重连,可以向上层应用回调事件。这个机制在Linux版的IGH里没有,属于移植时的增强项。建议你也加上,对运动控制项目来说,安全上多一层保障。
5.4 与Linux原生IGH的对比
同样的IGH代码,在Linux下用原生igb网卡驱动跑1ms周期,任务抖动大约是几个微秒量级,同步误差同样在编码器分辨率以内。我的VxWorks移植版抖动在21μs附近,差距主要来自网卡收发路径走了套接字而非内核驱动直收。
如果采用4.2节提到的专用网卡驱动方案,这个差距是可以缩小的。实测中我临时用了一个半直收的版本(发直收、收走套接字),任务抖动降到了8μs以内,但代码复杂度上升了不少。产品化阶段如果要追求极致性能,建议投入资源把收发都做成DMA直收直发。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 现象 | 可能原因 | 排查手段 |
|---|---|---|
| 总线扫描时从站识别不全 | 网卡发送帧被丢弃、从站供电不足 | 用抓包工具确认主站发出的帧是否到达从站 |
| 从站能扫描到但状态切换不成功 | PDO映射配置错误、邮箱响应超时 | 检查IGH日志输出,确认从站在哪个状态切换失败 |
| 过程数据周期更新但偶尔跳变 | DMA缓存一致性问题 | 检查是否调用cacheFlush/cacheInvalidate |
| 周期任务抖动突然变大 | 系统时钟频率设置冲突、高优先级任务抢占异常 | 统计主站任务实际耗时,找出是否被其他任务抢占 |
| 应用层读取过程数据读到旧值 | 数据域更新标志未正确设置 | 检查IGH的ec_domain更新逻辑是否与VxWorks的内存栅栏一致 |
6.2 定时不精准的排查实例
一次实测中,我遇到的非常典型的问题是:500μs周期的抖动在运行几分钟后突然从十几微秒飙升到几十微秒。起初以为是高优先级任务干扰,但排查后发现是高精度定时器回调里打印日志导致的——调试串口打印一次就几十微秒,直接把周期拖垮。
这个问题看起来低级,但在嵌入式调试中极其常见。IGH主站任务里千万不要有任何阻塞性打印,调试阶段可以把日志输出到内存环形缓冲区,退出调试后关掉。很多看起来“性能玄学”的实时系统问题,最后都栽在这种不起眼的阻塞调用上。
6.3 网卡帧发送失败的指导线
IGH在VxWorks下通过原始套接字发送EtherCAT帧,如果从站完全没有响应,第一件事不是看IGH代码,而是确认网卡的协议过滤规则。很多网卡默认会过滤掉非本机MAC地址的帧,或者过滤掉非TCP/IP协议类型的帧。
以Intel I210为例,需要设置网卡的接收帧过滤,允许接收EtherCAT类型0x88A4的广播帧和指定MAC地址帧。VxWorks的raw socket绑定网卡后,可能还需要额外配置网卡接口的接收全部帧模式。这个排查点如果你不熟悉,很容易在IGH代码里绕半天,实际上只是网卡一个标志位的问题。
6.4 一些实操心得
讲几个没有写在代码注释里的经验。
第一,VxWorks的FPU问题一定要提前确认。IGH主站任务如果涉及浮点运算(比如从站反馈数据的浮点转换),必须在VxWorks任务创建时或在任务的taskSpawn选项里加VX_FP_TASK标志,否则任务里使用浮点会损坏内核的浮点上下文。我在早期调试时没注意这个问题,导致反馈值偶尔变成NaN,排查了整整两天才发现是任务没有浮点支持。
第二,编译选项里要留意对齐方式。IGH内部大量使用__attribute__((packed))的结构体来映射EtherCAT帧结构,VxWorks的编译器默认对齐方式、以及packed语义跟GCC有所差异。建议在编译时统一加-fpack-struct参数,或者逐一检查IGH用到packed的关键结构体,确保帧结构映射不出偏差。如果有条件,构造一个已知数据的帧,在VxWorks下打印各字段验证一下。
第三,应用层接口尽量绕开ioctl。VxWorks虽然有ioctl系统调用,但它的实现跟Linux的file_operations逻辑差异很大,IGH在Linux下用ioctl主要是为了把应用层的ecrt库调用转到内核主站,VxWorks下我建议直接把ioctl命令封装成一组普通的函数调用,由适配层内部完成参数解析,这样更简洁、调试也更直接。
7. 后续扩展方向与个人体会
7.1 项目还可以往哪些方向扩展
IGB网卡驱动的专用化改造是最值得投入的方向。目前通过原始套接字收发EtherCAT帧,整体链路多了一道协议栈的开销,如果直接操作Intel网卡的DMA描述符,把发送和接收都做成零拷贝,周期抖动可以做到个位数微秒甚至更低。
另外,IGH的分布式时钟(DC)功能在VxWorks下的适配目前还不够精细。DC是EtherCAT实现从站间高精度同步的关键,IGH在Linux下对DC的实现依赖主站时钟的高精度读取和从站时钟补偿算法,VxWorks下需要接入目标平台的时钟源,保证时钟同步精度。我目前的版本DC是可用的,但精度与Linux版还有差距,如果项目对同步要求极高(比如多轴插补),这块值得下功夫深入调优。
7.2 移植过程中个人收获最大的一点
我是从Linux内核编程背景转到VxWorks上做开发的。两者同属类Unix体系,但底层机制差异其实相当大。这次移植最大的收获不是“把IGH跑起来了”,而是彻底理解了IGH源码里那些Linux接口到底在干什么、为什么它要用这些接口。
比如tasklet——以太网接收中断的下半部机制,在IGH里是收帧后处理数据的主要路径。我在VxWorks下想找对等物,发现没有直接对应的东西,最终用VxWorks的taskSpawn加信号量模拟了一个等价的行为:中断上半部触发一个高优先级任务,任务里做数据处理。这个过程中我对“为什么IGH的设计要分上下半部”有了真正的理解,而不只是会用API。
7.3 给后来者的一句话
如果你的项目需求和我的类似,最值得参考的移植顺序是:先跑通用户态模式,用raw socket验证EtherCAT帧收发和从站扫描;再切入内核态模式,实现定时器驱动和中断数据处理;最后才碰DMA直收和DC同步优化。另外,无论多着急,一定先把IGH源码里你打算改的部分通读一遍再动手,边改边读会让你在排查时多浪费几天时间。
技术这条路没有捷径,但移植类工作最大的规律是:接口磨平之后,剩下的就是纯粹的逻辑调试。希望这份记录能帮你少走几步弯路。