Debian 13 上编译 RT 内核与 IgH EtherCAT 主站实战

发布时间:2026/10/24 12:01:20
Debian 13 上编译 RT 内核与 IgH EtherCAT 主站实战

1. 为什么要在 Debian 13 上折腾 RT 内核与 EtherCAT 主站

工业现场总线这块,EtherCAT 算是这几年的热门选手。它不像传统以太网那样靠 TCP/IP 协议栈层层封装,而是直接在以太网帧上跑自己的协议,靠"飞读飞写"(Processing on the Fly)的机制把每个从站的数据在帧经过时顺手处理掉,一圈下来延迟能压到微秒级。这个特性决定了它对主站的实时性要求极高——主站如果抖动个几百微秒,整个控制环就可能崩掉。

我这次做的项目,就是在一台 x86 工控机上,用 Debian 13 打底,编译一个带 RT 补丁的内核,再把 IgH EtherCAT Master 跑起来,最终实现对一个伺服驱动器的周期位置控制。选 Debian 13 是因为它自带的 GCC、glibc 版本比较新,编译 RT 内核和 IgH 源码时少踩很多依赖坑;选 RT 内核是因为标准 Linux 内核的调度延迟在几百微秒到毫秒级波动,根本喂不饱 EtherCAT 的 1ms 甚至 500us 周期。

这套方案适合谁?如果你正在做运动控制、机器人关节驱动、CNC 或者任何需要硬实时的工业项目,又不想花大价钱买商业主站授权,那 IgH 这条开源路线值得走一遍。当然,前提是你得能接受命令行编译内核、手动配置网卡驱动这些"脏活"。下面我把整个流程拆开讲,包括我踩过的坑和最后验证有效的参数。

2. 整体方案设计与选型背后的逻辑

2.1 为什么是 Debian 13 而不是 Ubuntu 或 CentOS

Debian 13(代号 Trixie)目前是 testing 分支,但它的软件包版本对编译 RT 内核非常友好。内核源码树用的是 6.12 系列,RT 补丁(PREEMPT_RT)已经基本合入主线,打补丁的冲突比 5.x 时代少了很多。Ubuntu 虽然用户多,但它的 HWE 内核和 RT 补丁经常对不上号,编译时容易报patch does not apply。CentOS 系更麻烦,glibc 版本偏旧,IgH 的configure脚本有时会卡在clock_gettime检测上。

另一个原因是 Debian 的apt源里直接有linux-source包,下载下来就是干净的内核源码,不用去 kernel.org 手动拉。我实测下来,Debian 13 的linux-source-6.12配合patch-6.12-rt补丁,一次打成功,没有出现 hunk 失败。

2.2 RT 内核补丁的选择:PREEMPT_RT 还是 Xenomai

工业实时方案里,Xenomai 是另一条路,它用双内核架构(Cobalt 核 + Linux 核)来保证硬实时。但 Xenomai 的 IgH 适配需要额外打xenomai-igh补丁,配置复杂度高一个量级。PREEMPT_RT 则是把 Linux 内核本身改造成可抢占的,虽然最坏延迟比 Xenomai 稍大(实测在 30-50us 左右),但对于 1ms 周期的 EtherCAT 应用完全够用。

我选 PREEMPT_RT 的另一个理由是调试方便。Xenomai 的双内核一旦出问题,日志分散在两个核里,排查起来很痛苦。PREEMPT_RT 就是标准内核,ftrace、perf这些工具直接能用,cyclictest跑出来的延迟直方图一目了然。

2.3 IgH EtherCAT Master 的版本考量

IgH 官方仓库的稳定版是 1.5.2,但它的网卡驱动只支持到 Linux 4.x 的内核接口。Debian 13 的内核是 6.12,直接编译 1.5.2 会报net_device结构体成员找不到。解决办法有两个:一是用社区维护的igh-ethercat-master分支(比如某些工业发行版里的补丁版),二是自己改驱动代码适配新内核。

我选了后者,因为改驱动虽然麻烦,但能彻底搞清楚 IgH 和网卡驱动的交互逻辑。具体改动集中在master/ethernet.c和devices/目录下的网卡驱动文件,主要是把ndo_start_xmit的返回值处理、netif_napi_add的参数列表按 6.12 的 API 调整。这部分后面会详细说。

3. 核心细节解析与实操要点

3.1 内核编译的关键配置项

RT 内核的配置不是简单开个CONFIG_PREEMPT_RT就完事。下面这几个选项直接决定 EtherCAT 主站的抖动表现:

配置项推荐值作用
CONFIG_PREEMPT_RTy开启完全可抢占
CONFIG_HZ1000提高时钟中断频率,降低调度粒度
CONFIG_HZ_1000y同上
CONFIG_NO_HZ_FULLy隔离 CPU 上的时钟中断
CONFIG_HIGH_RES_TIMERSy高精度定时器,IgH 依赖
CONFIG_CPU_ISOLATIONy允许isolcpus隔离核心
CONFIG_RCU_NOCB_CPUy把 RCU 回调移出实时核

CONFIG_HZ这里有个坑:很多人以为设成 100 或 250 能减少开销,但 EtherCAT 的 DC(分布式时钟)同步需要高精度时间戳,HZ=1000下clock_gettime的分辨率才能到纳秒级。我试过HZ=250,cyclictest的最大延迟直接飙到 200us 以上,换成 1000 后降到 40us 左右。

3.2 网卡驱动的适配要点

IgH 主站需要直接操作网卡的 DMA 描述符,所以不能用内核自带的e1000e或igb驱动,得用 IgH 自带的ec_e1000e或ec_igb。这些驱动在 6.12 内核上编译时,主要报错集中在三处:

第一处是netif_napi_add的参数。6.12 里这个函数的第三个参数从int (*poll)(struct napi_struct *, int)变成了int (*poll)(struct napi_struct *, int),但权重参数weight被移到了netif_napi_add_weight里。改法是把netif_napi_add(dev, napi, poll, 64)改成netif_napi_add_weight(dev, napi, poll, 64)。

第二处是ndo_start_xmit的返回值。老内核返回NETDEV_TX_OK或NETDEV_TX_BUSY,新内核要求返回netdev_tx_t类型,并且NETDEV_TX_BUSY被废弃了。IgH 的驱动里如果返回NETDEV_TX_BUSY,得改成NETDEV_TX_OK并手动处理队列停止。

第三处是skb_shinfo的访问。6.12 里skb_shinfo变成了skb_shinfo宏,但内部字段nr_frags的类型从int变成了unsigned int,比较时要注意符号问题。

注意:改驱动时一定要用git diff记录每一处改动,因为 IgH 的驱动代码和内核版本强相关,下次换内核还得重新适配。

3.3 实时性调优的实操参数

内核编译完只是第一步,系统启动后的调优才是决定抖动大小的关键。我用的参数如下:

# 内核启动参数 isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3

这行参数把 CPU 2 和 3 从调度器里隔离出来,专门跑 EtherCAT 主站线程。nohz_full让这两个核上的时钟中断尽量少触发,rcu_nocbs把 RCU 回调移到其他核。

启动后还要做几件事:

  1. 关闭 CPU 频率调节:cpupower frequency-set -g performance
  2. 关闭超线程:BIOS 里禁用,或者用nosmt启动参数
  3. 把 EtherCAT 主站线程绑到隔离核:taskset -c 2 ./my_ecat_app
  4. 设置线程优先级:chrt -f 80 ./my_ecat_app

我实测下来,不做这些调优时cyclictest最大延迟在 150us 左右,做完后能压到 35us。对于 1ms 周期的 EtherCAT,这个余量足够了。

4. 实操过程与核心环节实现

4.1 编译 RT 内核的完整步骤

先装依赖:

apt update apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev \ git wget patch bc rsync kmod cpio debhelper

下载内核源码和 RT 补丁:

cd /usr/src apt source linux-source-6.12 wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.12/patch-6.12-rt.patch.xz xz -d patch-6.12-rt.patch.xz

打补丁:

cd linux-source-6.12 patch -p1 < ../patch-6.12-rt.patch

如果补丁有冲突,用patch --dry-run先试跑,看哪些文件失败。我这次只在一个Makefile的版本号行上冲突,手动改一下就行。

配置内核:

cp /boot/config-$(uname -r) .config make olddefconfig make menuconfig

在menuconfig里按前面表格的配置项逐项检查。特别注意CONFIG_PREEMPT_RT在General setup -> Preemption Model里选Fully Preemptible Kernel (Real-Time)。

编译:

make -j$(nproc) deb-pkg

用deb-pkg而不是make install,因为前者会生成.deb包,方便在多台机器上部署。编译时间取决于 CPU,我用的 8 核机器大概 25 分钟。

安装:

dpkg -i ../linux-image-6.12-rt_*.deb ../linux-headers-6.12-rt_*.deb update-grub reboot

重启后uname -a应该能看到6.12-rt字样。

4.2 编译 IgH EtherCAT Master

从官方仓库拉源码:

git clone https://gitlab.com/etherlab.org/ethercat.git cd ethercat git checkout stable-1.5

配置:

./bootstrap ./configure --prefix=/usr/local/ethercat \ --with-linux-dir=/usr/src/linux-source-6.12 \ --enable-generic \ --enable-8139too=no \ --enable-e1000e=yes \ --enable-igb=yes

--enable-generic是通用网卡驱动,适合没有专用驱动的场景,但性能不如e1000e或igb。我用的 Intel I210 网卡,对应igb驱动。

编译:

make -j$(nproc) make modules make install

make modules会编译网卡驱动模块,make install把主站库和工具装到/usr/local/ethercat。

加载模块:

modprobe ec_master modprobe ec_igb

如果ec_igb加载失败,用dmesg | tail看报错。常见问题是网卡被内核自带的igb驱动占用了,需要先rmmod igb。

4.3 配置主站和从站

主站配置文件在/usr/local/ethercat/etc/sysconfig/ethercat,关键内容:

MASTER0_DEVICE="00:1b:21:xx:xx:xx" DEVICE_MODULES="igb"

MASTER0_DEVICE填网卡的 MAC 地址,用ip link show查。

启动主站:

/usr/local/ethercat/etc/init.d/ethercat start

用ethercat master命令查看状态,应该输出Master0和Phase: Idle。

扫描从站:

ethercat slaves

如果能看到从站列表,说明物理层通了。我接的是一个伺服驱动器,显示0 0:0 PREOP EK1100之类的信息。

4.4 写一个简单的周期控制程序

IgH 提供了ecrt.h库,下面是一个最小化的周期控制示例:

#include <ecrt.h> #include <stdio.h> #include <unistd.h> #include <time.h> #define PERIOD_NS 1000000 // 1ms int main() { ec_master_t *master = ecrt_request_master(0); ec_domain_t *domain = ecrt_master_create_domain(master); ec_slave_config_t *sc = ecrt_master_slave_config(master, 0, 0, 0x00000002); // 配置 PDO 映射 ecrt_slave_config_pdos(sc, EC_END, my_pdo_config); ecrt_domain_reg_pdo_entry_list(domain, my_pdo_entries); ecrt_master_activate(master); struct timespec next; clock_gettime(CLOCK_MONOTONIC, &next); while (1) { next.tv_nsec += PERIOD_NS; if (next.tv_nsec >= 1000000000) { next.tv_nsec -= 1000000000; next.tv_sec++; } clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL); ecrt_master_receive(master); ecrt_domain_process(domain); // 读写 PDO 数据 EC_WRITE_S32(domain_data + offset, target_pos); ecrt_domain_queue(domain); ecrt_master_send(master); } }

编译:

gcc -o ecat_app ecat_app.c -I/usr/local/ethercat/include \ -L/usr/local/ethercat/lib -lethercat -lrt

运行:

chrt -f 80 taskset -c 2 ./ecat_app

这个程序每 1ms 发一帧,读写伺服的位置指令和实际位置。实测下来,ecrt_master_send到ecrt_master_receive的往返时间在 80-120us 之间,抖动小于 20us。

5. 常见问题与排查技巧实录

5.1 主站启动失败:Failed to open /dev/EtherCAT0

这个报错通常是ec_master模块没加载,或者设备节点没创建。先lsmod | grep ec_确认模块在不在。如果不在,modprobe ec_master手动加载。如果加载时报Unknown symbol,说明内核版本和模块不匹配,需要重新编译 IgH。

设备节点/dev/EtherCAT0由ec_master模块创建,如果udev规则没生效,可以手动mknod /dev/EtherCAT0 c 10 0。

5.2 从站扫描不到:No slaves found

先检查网线是不是插在正确的网口上。IgH 会接管指定的网卡,如果插错口,ethercat slaves就是空的。用ethtool -i eth0确认网卡驱动是ec_igb而不是igb。

如果驱动对了还是扫不到,用tcpdump -i eth0 -XX抓包,看有没有 EtherCAT 帧发出。正常情况下应该看到0x88a4的以太网类型。如果没有,说明主站没在发帧,检查ethercat master的Phase是不是Idle。

5.3 周期抖动大:cyclictest最大延迟超过 100us

先确认isolcpus和nohz_full生效了。cat /proc/cmdline看启动参数,cat /sys/devices/system/cpu/isolated看隔离核列表。

如果参数对了但抖动还是大,检查 BIOS 里的 C-State 和 P-State。C-State 深度睡眠会导致唤醒延迟,在 BIOS 里禁用C1E和C6。P-State 频率调节也会引入延迟,设成Performance模式。

还有一个容易被忽略的点:irqbalance服务会把中断分配到隔离核上。systemctl stop irqbalance并禁用,然后手动把网卡中断绑到非隔离核:

echo 0 > /proc/irq/$(cat /sys/class/net/eth0/device/irq)/smp_affinity

5.4 从站进入 OP 状态失败

从站状态机从PREOP到OP需要主站正确配置 PDO 映射和 DC 同步。如果卡在PREOP,用ethercat pdos看 PDO 配置对不对,ethercat sdos看 SDO 读写是否正常。

DC 同步失败常见于MASTER0_DEVICE的 MAC 地址填错,或者从站的 DC 周期和主站不匹配。用ethercat config看 DC 配置,确保AssignActivate和Sync0Cycle参数正确。

5.5 常见问题速查表

现象可能原因排查命令解决
主站启动失败模块未加载lsmod | grep ec_modprobe ec_master
扫不到从站网卡驱动不对ethtool -i eth0重编译 IgH 驱动
抖动大中断未隔离cat /proc/interrupts绑中断到非隔离核
从站不进 OPPDO 映射错ethercat pdos检查ecrt_slave_config_pdos
DC 同步失败MAC 地址错ethercat master改MASTER0_DEVICE

提示:每次改完配置,先ethercat master看主站状态,再ethercat slaves看从站状态,最后ethercat pdos看 PDO 映射。这个顺序能快速定位问题在哪一层。

6. 几个容易被忽略的实操心得

第一个心得是关于内核编译的。很多人编译 RT 内核时喜欢用make localmodconfig来精简配置,但这个命令会关掉很多 IgH 需要的选项,比如CONFIG_UIO和CONFIG_EXTRA_FIRMWARE。我建议直接用发行版的config-$(uname -r)做基础,只改实时相关的选项,别贪图编译快。

第二个心得是关于网卡选择的。IgH 对 Intel 的e1000e和igb支持最好,Realtek 的r8169虽然也能用generic驱动跑,但抖动明显大一圈。我试过用板载 Realtek 网卡,cyclictest最大延迟比 Intel I210 高了 60us。如果项目对实时性要求高,花几十块钱买张 Intel 网卡是值得的。

第三个心得是关于 DC 同步的。EtherCAT 的分布式时钟能让所有从站的时钟对齐到主站,但前提是主站的参考时钟要稳。如果主站跑在隔离核上,clock_gettime(CLOCK_MONOTONIC)的抖动会小很多。我实测过,不隔离核时 DC 同步误差在 200ns 左右,隔离后降到 50ns 以内。

第四个心得是关于代码结构的。IgH 的ecrt_master_receive和ecrt_master_send必须成对出现,中间不能有阻塞操作。我见过有人在中间加printf调试,结果周期直接崩掉。调试信息应该用环形缓冲区记下来,在非实时线程里打印。

这套方案我前后调了大概两周,从内核编译到从站跑起来,中间踩的坑基本都记在上面了。如果你也在做类似的项目,建议先把cyclictest跑稳,再上 EtherCAT 主站,这样能少走很多弯路。