4. 主站代码框架解析
4.1 为什么先读框架,再读代码
EtherCAT主站代码动辄数万行,直接逐行阅读极易迷失。正确路径是:先建立“目录—模块—流程”三层地图,再带着问题深入具体函数。本章以两个最主流的开源主站——SOEM(Simple Open EtherCAT Master,侧重于轻量嵌入式)和IgH(EtherLab IgH Master,侧重于Linux内核/实时性)——为对象,剖析它们的目录结构、核心模块划分以及一次典型周期通信的工作流程。
核心结论先行:SOEM采用“用户态库+网卡驱动”的扁平架构,适合MCU和RTOS;IgH采用“内核模块+应用接口”的分层架构,适合Linux强实时场景。理解二者差异,是选型和二次开发的基础。
4.2 SOEM 目录结构与核心模块
SOEM 的源码结构非常紧凑,所有 EtherCAT 协议栈代码集中在 soem/ 目录下,上层应用只需包含 ethercat.h 并链接 libsoem 即可。其核心文件与职责如下表:
| 文件/目录 | 职责 | 关键函数 |
|---|---|---|
nicdrv.c | 底层以太网帧收发,封装了 raw socket 或 pcap | ec_setupnic(), ec_send_processdata() |
ethercatmain.c | 主站初始化、从站扫描、状态机切换 | ec_init(), ec_config(), ec_statechange() |
ethercatcoe.c | CoE 协议: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 的典型工作流程可归纳为四个阶段:
- 初始化与网卡绑定:调用
ec_init("eth0")打开指定网卡,并绑定原始套接字。 - 从站扫描与配置:调用
ec_config(FALSE, 0),SOEM 自动发送广播帧,枚举总线上所有从站,读取其 EEPROM 中的厂商 ID、产品码,并建立从站信息列表。 - 状态机切换:通过
ec_statechange()将每个从站从 INIT 依次切换到 PRE-OP、SAFE-OP,最终进入 OP 状态。此过程会配置 SM(同步管理器)和 FMMU(现场总线内存管理单元)。 - 周期数据交换:在 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 有本质区别,它依赖内核的实时调度:
- 模块加载:通过
insmod ec_master.ko加载主站模块,模块初始化时注册网络设备通知链,等待网卡驱动绑定。 - 应用注册:用户态应用调用
ecrt_request_master(0)请求主站,然后通过ecrt_master_create_domain()创建数据域(Domain),用于管理周期数据。 - 从站配置:应用通过
ecrt_master_slave_config()指定从站地址、厂商 ID、产品码,并配置 PDO 映射。此阶段不直接操作硬件,而是构建配置数据结构。 - 激活与周期运行:调用
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 在嵌入式领域更流行。