4. 主站代码框架解析:开源主站(SOEM、IgH)的目录结构、核心模块与工作流程

4.1 为什么先读框架,再读代码

EtherCAT主站代码动辄数万行,直接逐行阅读极易迷失。正确路径是:先建立“目录—模块—流程”三层地图,再带着问题深入具体函数。本章以两个最主流的开源主站——SOEM(Simple Open EtherCAT Master,侧重于轻量嵌入式)和IgH(EtherLab IgH Master,侧重于Linux内核/实时性)——为对象,剖析它们的目录结构、核心模块划分以及一次典型周期通信的工作流程。

核心结论先行:SOEM采用“用户态库+网卡驱动”的扁平架构,适合MCU和RTOS;IgH采用“内核模块+应用接口”的分层架构,适合Linux强实时场景。理解二者差异,是选型和二次开发的基础。

SOEM 与 IgH 主站框架对比 SOEM(用户态库) nicdrv.c 原始以太网收发 ethercattype.h 数据类型/帧结构定义 ethercatmain.c 状态机/初始化/周期 ethercatcoe.c CoE(SDO/PDO) ethercatfoe.c FoE 固件升级 ethercatsoe.c SoE 伺服驱动 ethercatdc.c 分布式时钟 DC ethercatconfig.c 从站配置/映射 应用调用 ec_init → ec_config → ec_send/ec_receive 循环 IgH(Linux 内核主站) master.c 主站核心/状态机 ethercat_devices 从站设备驱动 cyclictask.c 实时周期任务 cdev.c 字符设备接口 ec_master.h 主站内部头文件 ec_slave.h 从站抽象接口 用户态库 libethercat 应用通过 rt_dev_open / rt_dev_ioctl 访问 应用 → rt_dev_ioctl → 内核 master → 周期任务 → 网卡驱动 SOEM:单线程轮询,适合裸机/RTOS,代码量约 2 万行 IgH:内核态实时调度,适合 Linux + PREEMPT_RT,代码量约 10 万行

4.2 SOEM 目录结构与核心模块

SOEM 的源码结构非常紧凑,所有 EtherCAT 协议栈代码集中在 soem/ 目录下,上层应用只需包含 ethercat.h 并链接 libsoem 即可。其核心文件与职责如下表:

文件/目录职责关键函数
nicdrv.c底层以太网帧收发,封装了 raw socket 或 pcapec_setupnic(), ec_send_processdata()
ethercatmain.c主站初始化、从站扫描、状态机切换ec_init(), ec_config(), ec_statechange()
ethercatcoe.cCoE 协议:SDO 上传/下载、PDO 映射ec_SDOread(), ec_SDOwrite()
ethercatdc.c分布式时钟同步,计算时钟偏移与传播延迟ec_configdc(), ec_dcsync0()
ethercatconfig.c从站配置、FMMU/SM 通道分配ec_config_map(), ec_mapping()
osal/操作系统抽象层(定时器、线程、互斥锁)osal_timer(), osal_thread()

SOEM 的典型工作流程可归纳为四个阶段:

  1. 初始化与网卡绑定:调用 ec_init("eth0") 打开指定网卡,并绑定原始套接字。
  2. 从站扫描与配置:调用 ec_config(FALSE, 0),SOEM 自动发送广播帧,枚举总线上所有从站,读取其 EEPROM 中的厂商 ID、产品码,并建立从站信息列表。
  3. 状态机切换:通过 ec_statechange() 将每个从站从 INIT 依次切换到 PRE-OP、SAFE-OP,最终进入 OP 状态。此过程会配置 SM(同步管理器)和 FMMU(现场总线内存管理单元)。
  4. 周期数据交换:在 OP 状态下,应用循环调用 ec_send_processdata() 发送输出帧,随后调用 ec_receive_processdata() 接收输入帧。整个过程是阻塞式轮询,周期由应用层定时器控制。

实操提示:在 SOEM 的 test/ 目录下,simple_test.c 是最精简的示例。它演示了从 ec_init 到 ec_config 再到周期收发的最小代码路径,建议作为阅读起点。

4.3 IgH 主站的目录结构与分层架构

IgH 主站采用 Linux 内核模块 + 用户态库的双层结构。源码根目录下主要子目录包括:

  • master/:内核主站模块源码,包含 master.c(主站核心)、slave.c(从站对象管理)、cyclictask.c(实时周期任务)、cdev.c(字符设备接口)。
  • lib/:用户态库 libethercat,提供 ecrt_* 系列 API,封装了对内核的 ioctl 调用。
  • devices/:各类从站设备驱动,例如 generic.c(通用数字量 IO)、beckhoff_el*(Beckhoff 端子模块)。
  • tool/:命令行工具,如 ethercat 命令,用于查看总线状态、从站信息、SDO 读写。
  • script/:启动脚本,负责加载内核模块并创建设备节点。

IgH 的工作流程与 SOEM 有本质区别,它依赖内核的实时调度:

  1. 模块加载:通过 insmod ec_master.ko 加载主站模块,模块初始化时注册网络设备通知链,等待网卡驱动绑定。
  2. 应用注册:用户态应用调用 ecrt_request_master(0) 请求主站,然后通过 ecrt_master_create_domain() 创建数据域(Domain),用于管理周期数据。
  3. 从站配置:应用通过 ecrt_master_slave_config() 指定从站地址、厂商 ID、产品码,并配置 PDO 映射。此阶段不直接操作硬件,而是构建配置数据结构。
  4. 激活与周期运行:调用 ecrt_master_activate() 后,主站进入 OP 状态。随后应用启动一个实时线程(通常使用 rt_task_start 或 POSIX 实时线程),在每次周期中断中调用 ecrt_master_send() 和 ecrt_master_receive() 完成数据交换。

关键差异:IgH 的周期任务由内核的 cyclictask 驱动,它使用高精度定时器(hrtimer)触发,确保抖动在微秒级。而 SOEM 的周期完全依赖应用层定时器,在非实时操作系统上抖动较大。

4.4 案例:用 SOEM 读取一个 EL1004 数字量输入模块

假设总线上挂接了一个 Beckhoff EL1004(4 通道数字量输入),使用 SOEM 读取其状态的代码路径如下:

// 1. 初始化
ec_init("eth0");
// 2. 扫描并配置从站
ec_config(FALSE, 0);
// 3. 查找从站索引(假设 EL1004 是第 1 个从站)
int slave_idx = 1;
// 4. 切换到 OP 状态
ec_slave[slave_idx].state = EC_STATE_OPERATIONAL;
ec_statechange(slave_idx);
// 5. 周期读取输入
while (1) {
    ec_send_processdata();
    ec_receive_processdata(EC_TIMEOUTRET);
    // EL1004 的输入数据位于 ec_slave[1].inputs 缓冲区
    uint8_t *input_data = ec_slave[1].inputs;
    // 第 0 通道状态
    int ch0 = (*input_data) & 0x01;
    usleep(1000); // 1ms 周期
}

这段代码展示了 SOEM 的“直接内存映射”风格:从站输入输出数据直接映射到 ec_slave[] 结构体的 inputs 和 outputs 指针,应用层读写这些缓冲区即可完成过程数据交换,无需额外 API。

4.5 常见误区与复盘

误区一:忽略状态机切换的时序要求。初学者常直接调用 ec_statechange() 试图一步切换到 OP,但 EtherCAT 规定必须逐级切换(INIT→PRE-OP→SAFE-OP→OP),且每步需等待从站确认。SOEM 的 ec_statechange() 内部会循环等待,但若从站未正确配置 SM/FMMU,会卡死在 SAFE-OP。正确做法是先调用 ec_config_map() 完成 PDO 映射,再切换状态。

误区二:在 IgH 中混淆 Domain 与从站的关系。Domain 是 IgH 中管理周期数据交换的容器,一个 Domain 可以包含多个从站的 PDO。但每个从站的 PDO 映射必须在 ecrt_master_activate() 之前完成配置。若在激活后修改映射,会导致主站返回 -EBUSY 错误。复盘时,应检查应用代码中是否在 activate 之后调用了任何 ecrt_slave_config_pdo_assign() 之类的函数。

误区三:SOEM 的 ec_receive_processdata() 超时参数设置不当。该函数的第二个参数是等待接收的超时时间(微秒)。若设置过短,在总线负载高或从站响应慢时,会频繁返回超时错误,导致数据丢失。建议设置为 1000~5000 微秒,并检查返回值是否为 0(成功)。

4.6 框架选择的决策清单

在实际项目中,选择 SOEM 还是 IgH,可参考以下清单:

  • 运行平台:若使用 MCU(如 STM32)或 RTOS(如 FreeRTOS),选 SOEM;若使用 Linux 且要求实时性,选 IgH。
  • 从站数量与周期:从站少于 32 个、周期大于 1ms,SOEM 足够;从站超过 64 个或周期要求 250μs 以下,IgH 更稳。
  • 调试工具:IgH 自带 ethercat 命令行工具,可在线查看从站状态、SDO 读写;SOEM 需要自行编写调试打印。
  • 代码体积:SOEM 编译后约 100KB,适合资源受限设备;IgH 内核模块加库约 1MB,且依赖 Linux 内核版本。
  • 社区活跃度:IgH 在 Linux 生态中维护更频繁,SOEM 在嵌入式领域更流行。