【原创】IgH EtherCAT主站详解(二十一)--从零搭建 EtherCAT 主站

从零搭建 EtherCAT 主站

8.1 — 端到端实战教程

概览

本教程将带你从零开始,完整搭建一个基于 IgH EtherCAT Master 1.5.2 的 EtherCAT 主站系统。你将从硬件准备出发,依次完成实时内核安装、主站编译部署、从站连接验证、用户空间应用程序编写,最终实现稳定的周期性实时通信。

9 步总览

步骤 内容 关键成果
1 硬件准备 确认网卡、从站、线缆就绪
2 安装 RT-Preempt 内核 内核启动参数包含 PREEMPT_RT
3 编译安装 IgH EtherCAT Master ec_master.koethercat 工具就绪
4 加载内核模块并绑定网卡 主站进入 IDLE 阶段
5 连接从站并验证拓扑 ethercat slaves 列出所有从站
6 编写用户空间应用程序 完成 ecrt API 初始化和周期循环
7 编译运行应用程序 主站进入 OPERATION,PDO 数据交换正常
8 使用 ethercat 工具验证 确认从站状态、域状态和通信质量
9 性能调优 SCHED_FIFO、mlockall、isolcpus 优化到位

你将完成的目标

完成本教程后,你将拥有一个完整的 EtherCAT 主站环境:从站被正确识别并进入 OP 状态,用户空间程序以 1ms 周期稳定读写 PDO 过程数据,并通过 RT-Preempt 内核保证实时性。

技术详情

端到端流程图

[流程图 - 结构化描述]

步骤 描述
1 Step 1 硬件准备
2 Step 2 安装 RT-Preempt 内核
3 Step 3 编译安装 IgH Master
4 Step 4 加载模块 / 绑定网卡
5 Step 5 连接从站 / 验证拓扑
6 Step 6 编写用户空间应用
7 Step 7 编译运行应用
8 Step 8 ethercat 工具验证
9 Step 9 性能调优

Step 1: 硬件准备

在开始之前,确保以下硬件就绪:

硬件 说明 推荐型号
EtherCAT 网卡 一块专用于 EtherCAT 的以太网卡(不能同时用作系统网络) Intel I210 / I350, Realtek r8169 系列, 板载 stmmac (RK)
EtherCAT 从站 至少一个 EtherCAT 从站设备 Beckhoff EK1100 (耦合器) + EL 系列模块 (如 EL3102, EL4102, EL2032)
Ethernet 线缆 标准 Cat5e 或以上网线,连接主站网卡与从站 屏蔽工业以太网线更佳
PC / 嵌入式板 运行 Linux 的主机,x86 或 ARM 均可 Intel i5/i7 工控机, Rockchip RK3568/RK3588

重要提示

EtherCAT 网卡必须专用于 EtherCAT 通信,不能同时承载 SSH、NFS 等系统网络流量。请确保主站有第二块网卡用于系统网络管理。

Step 2: 安装 RT-Preempt 内核

以 Debian/Ubuntu 为例,安装 PREEMPT_RT 实时内核:

# 安装 RT-Preempt 内核(Debian 仓库直接提供预编译版本)
$ sudo apt install linux-image-rt-amd64

# 或者手动编译:
# 1. 下载内核与补丁
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.tar.xz
$ tar xf linux-6.1.tar.xz && cd linux-6.1

# 2. 下载对应的 RT 补丁(版本必须精确匹配)
$ wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.1/patch-6.1.xx-rt.patch.gz
$ zcat patch-6.1.xx-rt.patch.gz | patch -p1

# 3. 配置内核
$ make menuconfig
# General setup → Preemption Model → Fully Preemptible Kernel (Real-Time)
# CONFIG_PREEMPT_RT=y
# CONFIG_HIGH_RES_TIMERS=y
# CONFIG_NO_HZ_FULL=y

# 4. 编译安装
$ make -j$(nproc)
$ sudo make modules_install install
$ sudo update-grub
$ sudo reboot

重启后验证 RT 内核:

$ uname -v
# 输出应包含 "PREEMPT RT"

$ grep PREEMPT_RT /boot/config-$(uname -r)
CONFIG_PREEMPT_RT=y

提示

如果不需要严格的硬实时保证(周期 >= 1ms),也可以使用普通内核配合 Generic 驱动先行测试,之后再切换到 RT-Preempt 内核优化性能。

Step 3: 编译安装 IgH EtherCAT Master

# 1. 安装依赖
$ sudo apt install build-essential linux-headers-$(uname -r) autoconf automake libtool ethtool

# 2. 获取源码(从 Git 仓库)
$ git clone <repository-url> motorcortex-etherlab
$ cd motorcortex-etherlab

# 3. 生成 configure 脚本(仅从仓库编译时需要)
$ ./bootstrap

# 4. 配置 — RT-Preempt + Generic 驱动(最简配置)
$ ./configure --enable-generic

# 5. 编译
$ make all modules

# 6. 安装(需要 root 权限)
$ sudo make modules_install install
$ sudo depmod

# 7. 创建 init 脚本和配置文件
$ sudo ln -s /opt/etherlab/etc/init.d/ethercat /etc/init.d/ethercat
$ sudo cp /opt/etherlab/etc/sysconfig/ethercat /etc/sysconfig/ethercat

注意

如果使用特定网卡的原生驱动(如 r8169、e1000e、igb),需要用 --enable-r8169 等选项替代 --enable-generic。Generic 驱动使用 socket 收发,兼容性最好但性能略低于原生驱动。详见 通用编译配置

Step 4: 加载内核模块并绑定网卡

首先查找 EtherCAT 网卡的 MAC 地址:

$ ip link
# 记录 EtherCAT 网卡的 link/ether 地址,例如 00:0c:29:aa:bb:cc

编辑配置文件并绑定网卡:

# 编辑配置文件(RHEL/CentOS 使用 /etc/sysconfig/ethercat)
# Debian/Ubuntu 也可使用 /etc/default/ethercat
$ sudo vi /etc/sysconfig/ethercat

# 修改以下关键参数:
# MASTER0_DEVICE="00:0c:29:aa:bb:cc"   # 你的 EtherCAT 网卡 MAC
# DEVICE_MODULES="generic"              # 使用 Generic 驱动

# 启动主站服务
$ sudo /etc/init.d/ethercat start

# 验证模块已加载
$ lsmod | grep ec
# ec_generic             16384  0
# ec_master             200704  1 ec_generic

# 手动加载方式(备选):
# $ sudo modprobe ec_master main_devices=0x00,0x0c,0x29,0xaa,0xbb,0xcc
# $ sudo modprobe ec_generic

配置文件位置

IgH 主站配置文件位置因发行版不同而异:/etc/sysconfig/ethercat(RHEL/CentOS/Fedora)或 /etc/default/ethercat(Debian/Ubuntu)。init 脚本会自动检测两个路径。

Step 5: 连接从站并验证拓扑

用以太网线连接主站 EtherCAT 网口与 EK1100 的 "IN" 端口。如果有多个从站,依次串联。然后验证:

# 列出所有从站
$ ethercat slaves
# 预期输出示例:
# 0  0:0  PREOP  +  EK1100 EtherCAT Coupler (2A E-Bus)
# 1  0:1  PREOP  +  EL3102 2Ch. Ana. Input +/-10V
# 2  0:2  PREOP  +  EL4102 2Ch. Ana. Output +/-10V
# 3  0:3  PREOP  +  EL2032 2Ch. Dig. Output 24V

# 查看主站状态
$ ethercat master
# 预期输出:
# Master0
#   Phase: Idle
#   Active: no
#   Slaves: 4

排查提示

如果 ethercat slaves 无输出:

  • 确认网线连接正确(网口灯是否亮起)
  • 确认 MAC 地址配置正确(与实际 EtherCAT 网卡一致)
  • 确认网卡没有 NetworkManager 等服务在抢占管理
  • 执行 dmesg | grep ec 查看内核日志

Step 6: 编写用户空间应用程序

以下基于 examples/user/main.c 的标准模式编写一个完整的 EtherCAT 用户空间应用。核心 API 调用序列为:

[流程图 - 结构化描述]

步骤 描述
1 ecrt_request_master(0) 请求主站
2 ecrt_master_create_domain() 创建过程数据域
3 ecrt_master_slave_config() 配置从站
4 ecrt_slave_config_pdos() 注册 PDO 映射
5 ecrt_domain_reg_pdo_entry_list() 注册 PDO 入口偏移
6 ecrt_master_activate() 激活主站
7 ecrt_domain_data() 获取过程数据指针
8 实时优化 SCHED_FIFO + mlockall
9 周期循环 receive → process → queue → send

完整示例代码:

#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
#include <sys/mman.h>
#include <sched.h>

#include "ecrt.h"

// 周期时间: 1ms
#define PERIOD_NS  (1000000)
#define NSEC_PER_SEC (1000000000)
#define MAX_SAFE_STACK (8 * 1024)

// 从站地址定义 (位置寻址)
#define BusCouplerPos  0, 0
#define DigOutPos      0, 3

// 从站 Vendor ID / Product Code
#define Beckhoff_EK1100 0x00000002, 0x044c2c52
#define Beckhoff_EL2032 0x00000002, 0x07f83052

// PDO 偏移量
static unsigned int off_dig_out = 0;

// 过程数据指针
static uint8_t *domain1_pd = NULL;

// 主站和域句柄
static ec_master_t *master = NULL;
static ec_domain_t *domain1 = NULL;

// 从站配置句柄
static ec_slave_config_t *sc_dig_out = NULL;

// ========== PDO 映射配置 ==========

// EL2032 输出 PDO 条目
static ec_pdo_entry_info_t el2032_channel1[] = {
    {0x3001, 0x01, 1},  // Output
};

// EL2032 RX-PDO (从主站到从站)
static ec_pdo_info_t el2032_pdos[] = {
    {0x1600, 1, el2032_channel1},
};

// EL2032 SyncManager 配置
static ec_sync_info_t el2032_syncs[] = {
    {0, EC_DIR_OUTPUT, 0, NULL, EC_WD_DISABLE},
    {1, EC_DIR_INPUT,  0, NULL, EC_WD_DISABLE},
    {2, EC_DIR_OUTPUT, 1, el2032_pdos, EC_WD_ENABLE},
    {3, EC_DIR_INPUT,  0, NULL, EC_WD_DISABLE},
    {0xff}
};

// PDO 入口注册表
static ec_pdo_entry_reg_t domain1_regs[] = {
    {DigOutPos, Beckhoff_EL2032, 0x3001, 1, &off_dig_out},
    {}
};

// ========== 辅助函数 ==========

static void stack_prefault(void)
{
    unsigned char dummy[MAX_SAFE_STACK];
    memset(dummy, 0, MAX_SAFE_STACK);
}

static unsigned int counter = 0;
static unsigned int blink = 0;

// ========== 周期任务 ==========

static void cyclic_task(void)
{
    // 1. 接收帧
    ecrt_master_receive(master);
    ecrt_domain_process(domain1);

    // 2. 慢速状态检查 (每 1 秒)
    if (counter) {
        counter--;
    } else {
        counter = 1000;
        blink = !blink;
    }

    // 3. 写过程数据
    EC_WRITE_U8(domain1_pd + off_dig_out, blink ? 0x01 : 0x00);

    // 4. 发送帧
    ecrt_domain_queue(domain1);
    ecrt_master_send(master);
}

// ========== 主函数 ==========

int main(int argc, char **argv)
{
    ec_slave_config_t *sc;
    struct timespec wakeup_time;
    int ret;

    printf("EtherCAT 用户空间示例启动...\n");

    // 1. 请求 EtherCAT 主站 (索引 0)
    if (!(master = ecrt_request_master(0))) {
        fprintf(stderr, "请求主站失败!\n");
        return -1;
    }

    // 2. 创建过程数据域
    if (!(domain1 = ecrt_master_create_domain(master))) {
        fprintf(stderr, "创建域失败!\n");
        return -1;
    }

    // 3. 获取从站配置句柄
    printf("配置从站...\n");
    if (!(sc = ecrt_master_slave_config(master,
            BusCouplerPos, Beckhoff_EK1100))) {
        fprintf(stderr, "配置 EK1100 失败!\n");
        return -1;
    }

    if (!(sc_dig_out = ecrt_master_slave_config(master,
            DigOutPos, Beckhoff_EL2032))) {
        fprintf(stderr, "配置 EL2032 失败!\n");
        return -1;
    }

    // 4. 配置 PDO 映射
    printf("配置 PDO 映射...\n");
    ecrt_slave_config_pdos(sc_dig_out, EC_END, el2032_syncs);

    // 5. 注册 PDO 条目偏移
    if (ecrt_domain_reg_pdo_entry_list(domain1, domain1_regs)) {
        fprintf(stderr, "PDO 入口注册失败!\n");
        return -1;
    }

    printf("激活主站...\n");

    // 6. 激活主站 (IDLE → OPERATION)
    if (ecrt_master_activate(master)) {
        fprintf(stderr, "激活主站失败!\n");
        return -1;
    }

    // 7. 获取过程数据内存指针
    if (!(domain1_pd = ecrt_domain_data(domain1))) {
        fprintf(stderr, "获取域数据失败!\n");
        return -1;
    }

    // ★ 8. 设置 SCHED_FIFO 最高实时优先级
    struct sched_param param = {};
    param.sched_priority = sched_get_priority_max(SCHED_FIFO);
    if (sched_setscheduler(0, SCHED_FIFO, &param) == -1) {
        perror("sched_setscheduler 失败");
    }

    // ★ 9. 锁定所有内存页面
    if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) {
        perror("mlockall 失败");
    }

    // ★ 10. 预触犯栈页面
    stack_prefault();

    // 11. 获取初始时间
    clock_gettime(CLOCK_MONOTONIC, &wakeup_time);
    wakeup_time.tv_sec += 1;  // 延迟 1 秒后启动周期循环
    wakeup_time.tv_nsec = 0;

    printf("进入实时周期循环 (周期 %d us)...\n", PERIOD_NS / 1000);

    // 12. 周期主循环
    while (1) {
        ret = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME,
                              &wakeup_time, NULL);
        if (ret) {
            fprintf(stderr, "clock_nanosleep 失败: %s\n",
                    strerror(ret));
            break;
        }

        cyclic_task();

        // 计算下一个唤醒时间
        wakeup_time.tv_nsec += PERIOD_NS;
        while (wakeup_time.tv_nsec >= NSEC_PER_SEC) {
            wakeup_time.tv_nsec -= NSEC_PER_SEC;
            wakeup_time.tv_sec++;
        }
    }

    // 13. 清理
    printf("释放主站资源...\n");
    ecrt_release_master(master);
    return 0;
}

Step 7: 编译运行应用程序

# 编译
$ gcc -o ecat_demo ecat_demo.c \
    -I/opt/etherlab/include \
    -L/opt/etherlab/lib \
    -lecrt -lrt -lm

# 运行(需要 root 或设备节点权限)
$ sudo LD_LIBRARY_PATH=/opt/etherlab/lib ./ecat_demo

# 输出示例:
# EtherCAT 用户空间示例启动...
# 配置从站...
# 配置 PDO 映射...
# 激活主站...
# 进入实时周期循环 (周期 1000 us)...

编译说明

关键编译标志:-lecrt 链接 IgH 用户空间库,-lrt 链接 POSIX 实时扩展库(clock_nanosleep 等)。如果 IgH 安装到非默认路径(如 /opt/etherlab),需要用 -I-L 指定头文件和库路径。

Step 8: 使用 ethercat 工具验证

在应用程序运行期间,另开终端使用 ethercat 命令行工具进行诊断:

# 1. 检查从站状态 — 应全部为 OP
$ ethercat slaves
# 0  0:0  OP  +  EK1100 EtherCAT Coupler
# 1  0:3  OP  +  EL2032 2Ch. Dig. Output

# 2. 查看域状态
$ ethercat domains
# Domain0: LogBaseAddr 0x00000000, Size 1, WorkingCounter 0/1

# 3. 查看主站详细状态
$ ethercat master
# Master0
#   Phase: Operation
#   Active: yes
#   Slaves: 2

# 4. 在线读取从站 PDO 数据
$ ethercat pdos -p 3
# SM2: PhysAddr 0x1800, DefaultSize 1, ControlRegister 0x24, Enable 1
#   TxPDO 0x1a00 "Channel 1"
#     PDO entry 0x6000:01, 1 bit

# 5. 查看从站寄存器 (AL 状态)
$ ethercat upload -p 3 0x0130 0

Step 9: 性能调优

为确保周期任务的实时性,需要进行以下三方面优化:

9.1 调度优先级 (SCHED_FIFO)

将周期任务设置为 SCHED_FIFO 最高优先级,确保不被普通任务抢占。该设置已在示例代码中完成。

9.2 内存锁定 (mlockall)

锁定所有内存页面,防止 RT 运行期间因缺页中断导致延迟。该设置已在示例代码中完成。

9.3 CPU 隔离 (isolcpus)

通过内核启动参数隔离 CPU 核心,专供 RT 任务使用:

# 编辑 GRUB 配置
$ sudo vi /etc/default/grub

# 在 GRUB_CMDLINE_LINUX 中添加:
isolcpus=1,2,3        # 隔离 CPU 1/2/3 供 RT 任务
nohz_full=1,2,3       # 禁用隔离核心的周期时钟中断
rcu_nocbs=1,2,3       # RCU 回调迁移到其他核心
irqaffinity=0         # 硬件中断绑定到非 RT 核心
intel_idle.max_cstate=0   # 禁用深度 C-State
processor.max_cstate=0
idle=poll              # 轮询空闲(低延迟但高功耗)

# 更新 GRUB 并重启
$ sudo update-grub
$ sudo reboot

重启后,使用 taskset 将应用程序绑定到隔离核心:

# 将应用绑定到 CPU 1 上运行
$ sudo taskset -c 1 LD_LIBRARY_PATH=/opt/etherlab/lib ./ecat_demo

调优优先级建议

性能调优应按以下顺序进行:(1)先确保基本功能正常运行;(2)设置 SCHED_FIFO + mlockall;(3)调整 BIOS 设置(关闭 C-State、超线程、频率调节);(4)使用 isolcpus 隔离核心。每步调整后使用 cyclictest 等工具测量实际抖动。

源码分析

ecrt API 初始化序列分析

用户空间应用程序通过 libethercat(即 ecrt API)与内核空间的主站模块交互。所有请求通过 ioctl 系统调用传递到 /dev/EtherCAT0 字符设备。

[时序图 - 参与者]

标识 名称
APP 用户空间应用
LIB libethercat (ecrt)
DEV /dev/EtherCAT0
MST ec_master (内核)
FSM Master FSM
SLV 从站

关键 API 函数说明

函数 所属阶段 作用 对应的 ioctl
ecrt_request_master() 初始化 打开主站字符设备,获取主站句柄 open("/dev/EtherCAT0")
ecrt_master_create_domain() 初始化 创建过程数据域,用于组织 PDO 数据交换 EC_IOCTL_CREATE_DOMAIN
ecrt_master_slave_config() 初始化 根据位置/VendorID/ProductCode 获取从站配置句柄 EC_IOCTL_SLAVE_CONFIG
ecrt_slave_config_pdos() 初始化 配置从站的 SyncManager 和 PDO 映射 EC_IOCTL_SLAVE_CONFIG_PDO
ecrt_domain_reg_pdo_entry_list() 初始化 注册 PDO 入口到域,获取逻辑地址偏移量 EC_IOCTL_DOMAIN_REG_PDO_ENTRY
ecrt_master_activate() 激活 分配 FMMU、域内存,进入 OPERATION 阶段 EC_IOCTL_ACTIVATE
ecrt_domain_data() 激活 获取域过程数据的 mmap 内存指针 (由 activate 时的 mmap 返回)
ecrt_master_receive() 周期循环 接收 EtherCAT 帧,更新域数据 EC_IOCTL_RECEIVE
ecrt_domain_process() 周期循环 处理域数据:计算 WorkingCounter,更新 PDO 状态 EC_IOCTL_DOMAIN_PROCESS
ecrt_domain_queue() 周期循环 将域数据标记为待发送 EC_IOCTL_DOMAIN_QUEUE
ecrt_master_send() 周期循环 发送所有已队列的域数据帧 EC_IOCTL_SEND
ecrt_release_master() 清理 释放主站资源,回到 IDLE 阶段 close(fd)

PDO 数据读写宏

过程数据通过域内存指针 + 偏移量直接读写。IgH 提供了一组便捷宏:

功能 示例
EC_READ_U8(pd + offset) 读取 1 字节无符号 uint8_t val = EC_READ_U8(domain1_pd + off_dig_in);
EC_READ_S16(pd + offset) 读取 2 字节有符号 int16_t val = EC_READ_S16(domain1_pd + off_ana_in);
EC_READ_U32(pd + offset) 读取 4 字节无符号 uint32_t val = EC_READ_U32(domain1_pd + off_enc);
EC_WRITE_U8(pd + offset, val) 写入 1 字节无符号 EC_WRITE_U8(domain1_pd + off_dig_out, 0x01);
EC_WRITE_S16(pd + offset, val) 写入 2 字节有符号 EC_WRITE_S16(domain1_pd + off_ana_out, -32768);
EC_WRITE_U32(pd + offset, val) 写入 4 字节无符号 EC_WRITE_U32(domain1_pd + off_ana_out, value);

字节序说明

所有 EC_READ_* / EC_WRITE_* 宏会自动处理 EtherCAT 小端字节序与主机字节序之间的转换。用户只需以主机字节序读写即可。

源码参考路径

文件 说明
examples/user/main.c RT-Preempt 用户空间完整示例
include/ecrt.h ecrt 用户空间 API 头文件
lib/master.c ecrt API 用户空间实现(ioctl 封装)
master/ioctl.c 内核侧 ioctl 处理函数
master/master.c 主站核心实现(activate、idle/operation 线程)
master/domain.c 域实现(PDO 注册、FMMU 分配、过程数据处理)
posted @ 2026-04-15 08:08  沐多  阅读(3231)  评论(1)    收藏  举报