在 RK3588 上为 IgH 1.5.3 移植 Native stmmac:绕过 generic 收发路径
测试平台
- 开发板:正点原子 ATK-DLRK3588
- SoC:RK3588
- Kernel:Rockchip Linux 5.10.160-rt89
- EtherCAT Master:IgH EtherCAT Master 1.5.3
- EtherCAT 网口:GMAC1,fe1c0000.ethernet
- EtherCAT MAC:96:66:d2:da:00:62
- 测试周期:1 ms
- 测试从站:3 个
本文记录一次比较完整的 IgH Native 网卡驱动移植过程。
项目原本使用 ec_generic + stmmac。在已经完成 PREEMPT_RT、CPU 绑核、IRQ 亲和性和调度优化之后,我继续尝试把 EtherCAT 数据路径从 Linux 通用网络栈中拆出来,让 IgH 直接与 RK3588 的 stmmac 驱动交互。
最终 Native stmmac 成功运行,三个从站可以进入 OP,并完成了 generic/native 两种模式下的 A/B 测试。
Github: https://github.com/VictorJiaxinWang/rk3588-igh-native-stmmac
1. 为什么要做 Native stmmac
ec_generic 最大的优点是通用。
Linux 网卡驱动正常工作后,IgH 就可以通过 packet socket 使用这块网卡,不需要针对具体 MAC 驱动做适配。因此在 EtherCAT 主站开发早期,它非常适合验证协议栈、从站配置和应用程序。
但它的 EtherCAT 帧仍然要经过 Linux 通用网络路径。
Generic 发送路径大致如下:
ecrt_master_send()
-> ec_device_send()
-> ec_generic
-> kernel_sendmsg(packet socket)
-> Linux 网络发送路径
-> stmmac
-> TX DMA
接收方向:
RX DMA
-> stmmac IRQ
-> NAPI
-> skb
-> GRO / 协议分发
-> packet socket
-> ec_generic
-> ecdev_receive()
对于普通 Ethernet,这套机制非常合理;但 EtherCAT 的特点是专用网口、固定周期、小帧、高确定性,其中一些通用机制并不是必须的。
在做 Native 之前,这套系统已经完成:
- PREEMPT_RT;
- 周期线程绑定 CPU6;
- SCHED_FIFO 80;
- performance governor;
- IRQ 亲和性优化;
- stmmac TX 中断合并。
空闲状态下 wake_lateness 的平均值已经接近 2 μs。
所以这次优化的目标并不是继续改善 Linux 调度,而是缩短:
实时线程醒来
↓
EtherCAT 收发
↓
本周期处理结束
也就是主站线程醒来之后的软件执行路径。
2. Native stmmac 改变了什么
Native 的核心思路很直接:
让 IgH 直接使用 stmmac,不再经过 ec_generic 和 packet socket。
发送路径变为:
ecrt_master_send()
-> ec_device_send()
-> stmmac_xmit()
-> TX descriptor
-> DMA
接收路径变为:
ecrt_master_receive()
-> ec_device_poll()
-> ec_poll()
-> 检查 RX descriptor
-> ecdev_receive()
-> descriptor 重新交给 DMA
这样以后,周期线程在调用 ecrt_master_receive() 时直接检查 DMA ring,而不再依赖:
IRQ
NAPI
skb
GRO
packet socket
这里需要注意,“绕过 skb”并不是说 IgH 内部完全不存在 struct sk_buff。
IgH 自己仍然使用预分配的 skb 保存待发送 datagram。真正被绕开的,是 Linux 通用网络路径中的 packet socket、协议分发,以及 RX 周期中的 skb 分配和 GRO。
Native RX 还有一个特点:它不一定比 Generic RX 的 API 计时更短。
Generic 模式下,IRQ 和 NAPI 可能已经提前把数据放进队列;而 Native 模式是在周期线程中主动扫描 DMA descriptor。如果扫描时帧还没有完全返回,这部分等待时间会直接计入 RX phase。
因此可能出现:
TX 明显变快
RX 略微变慢
总 execution 仍然下降
后面的实际测试也正是这个结果。
3. 移植过程中最关键的几个问题
3.1 stmmac 必须尽量留在原 BSP 内核树中构建
最开始,我把 Rockchip stmmac 源码复制到 IgH 工程中,再作为外部模块编译。
模块可以正常链接,但装板以后两个 GMAC 都出现:
dwmac4_dma_reset() timeout
即使让 ec_master 不接管任何网卡,普通管理网口也无法正常工作。
最后改成保留 Rockchip SDK 原有目录:
kernel/drivers/net/ethernet/stmicro/stmmac/
只把修改后的:
stmmac_main.c
stmmac.h
ecdev.h
写回该目录,再使用内核原生 Kbuild:
make M=drivers/net/ethernet/stmicro/stmmac modules
问题解决。
这次踩坑之后,我对 BSP 驱动有一个比较明确的认识:
驱动源码相同,不代表换一套 Kbuild 环境后得到的二进制行为仍然相同。
厂商 BSP 往往依赖特定的编译宏、Kconfig、include 顺序和 platform glue。修改这类驱动时,最好尽量保留原来的构建上下文。
3.2 两个 GMAC 必须区分普通网络和 EtherCAT
当前 RK3588 板上有两个 GMAC:
| SSH / 普通网络 | fe1b0000.ethernet | eth0 | eth0 |
| EtherCAT | fe1c0000.ethernet | eth1 + ec_generic | IgH 独占 |
Native stmmac 在基础 DMA、MDIO 和 phylink 初始化完成后调用:
priv->ecdev = ecdev_offer(ndev, ec_poll, THIS_MODULE);
IgH 根据配置的 MAC 判断是否接管该设备。
GMAC1 被 IgH 接管后,不再注册成普通 Linux 网口;GMAC0 没被接管,继续执行 register_netdev(),所以 SSH 仍然通过 eth0 工作。
最终形成:
GMAC0 -> Linux 普通网络
GMAC1 -> IgH Native EtherCAT
这也是双网口板卡上比较实用的一种方式。
3.3 模块加载顺序必须正确
Native stmmac 会调用:
ecdev_offer()
ecdev_open()
ecdev_close()
ecdev_receive()
ecdev_set_link()
ecdev_withdraw()
因此 ec_master 必须先存在。
正确加载关系是:
ec_master
-> stmmac
-> stmmac-platform
-> dwmac-rockchip
IgH 编译后会在仓库根目录生成:
Module.symvers
构建 stmmac 时需要传给 modpost:
KBUILD_EXTRA_SYMBOLS=~/ethercat-native-stmmac/Module.symvers
如果这个路径写错,ecdev_* 会全部报 undefined symbol。
内核配置方面,stmmac 也要编译成模块:
CONFIG_STMMAC_ETH=m
CONFIG_STMMAC_PLATFORM=m
CONFIG_DWMAC_ROCKCHIP=m
这样才能先加载 ec_master,再触发两个 GMAC probe。
4. RX 和 TX 路径如何处理
Native 驱动真正麻烦的地方,不是调用 ecdev_offer(),而是 DMA buffer 和 skb 的生命周期。
RX:尽量不在实时周期分配内存
DMA 清除 OWN 位后,CPU 才可以读取 descriptor 和数据。
Native RX 中的核心处理类似:
dma_rmb();
dma_sync_single_for_cpu(...);
ecdev_receive(
priv->ecdev,
page_address(buf->page),
len
);
dma_sync_single_for_device(...);
处理完成后继续保留原来的 page 和 DMA mapping,只把 descriptor 重新交回 DMA。
因此实时周期里不会反复执行:
alloc
free
map
unmap
也不会调用:
napi_alloc_skb()
skb_put()
napi_gro_receive()
这样可以尽量避免内存分配器和通用网络栈的慢路径进入实时线程。
当前 EtherCAT 帧可以完整放入一个 RX buffer。如果以后调整 jumbo frame、buffer 大小或者 descriptor layout,需要重新验证跨 descriptor 帧处理。
TX:不能释放 IgH 的 skb
IgH 的发送 skb 是可复用缓冲区。
普通 stmmac 的 TX clean 会认为 skb 属于网络栈,因此可能执行 skb free、BQL 和 netdev queue 管理。
Native 模式不能这么做。
IgH 仍然持有 skb 的所有权,所以 TX clean 只负责:
回收 DMA descriptor
解除 DMA mapping
不能释放 skb。
否则下一周期 IgH 再次使用这个缓冲区时,就可能产生 use-after-free。
5. 构建、部署与运行验证
完整构建最终被收敛到一个脚本:
LAB="$HOME/native-package-in-tree"
SDK="$HOME/rk3588_linux_sdk"
IGH="$HOME/ethercat-native-stmmac"
OUT="$HOME/native-stmmac-in-tree-artifacts"
"$LAB/tools/build_in_tree_native_stmmac_vm.sh" \\
"$SDK" \\
"$IGH" \\
"$OUT"
最终生成:
ec_master.ko
stmmac.ko
stmmac-platform.ko
dwmac-rockchip.ko
检查 stmmac.ko:
modinfo stmmac.ko
本次结果:
depends: phylink,ec_master
vermagic: 5.10.160-rt89 SMP preempt_rt mod_unload aarch64
部署时没有直接覆盖旧环境,而是保留了完整回退机制。
先验证包:
./tools/install_native_artifacts.sh \\
/userdata/native-stmmac-in-tree-deployment \\
–verify-only
确认无误后再执行:
./tools/install_native_artifacts.sh \\
/userdata/native-stmmac-in-tree-deployment \\
–apply
./tools/select_stmmac_mode.sh native –apply
sync
reboot
另外增加了一个 fallback 机制:如果启动后 eth0 在限定时间内无法正常工作,就恢复 stock 模块。
对于依赖 SSH 调试的开发板,这一点很有必要。网卡驱动一旦出问题,没有串口的话很容易直接失联。
Native 启动成功后,可以从以下信息确认驱动路径:
stmmac depends: phylink,ec_master
ec_generic 未加载
eth0 UP
EtherCAT:
Accepting 96:66:D2:DA:00:62 as main device
fe1c0000.ethernet:
claimed by IgH native EtherCAT master
同时三个从站能够进入 OP。
至此,Native 收发路径已经真正跑通。
6. Generic 与 Native A/B 测试
为了避免不同内核、不同程序导致结果失真,A/B 测试使用:
- 同一块 RK3588;
- 同一个 5.10.160-rt89 PREEMPT_RT 内核;
- 同一份测试程序;
- 三个相同从站;
- 1 ms EtherCAT 周期;
- CPU6;
- SCHED_FIFO 80;
- 相同 IRQ 配置。
唯一变化是网卡驱动路径。
Generic:
stock stmmac
+
ec_generic
Native:
ec_master
+
native stmmac
测试重点记录四个时间:
| wake_lateness | 周期线程实际唤醒比计划晚多久 |
| receive_phase | RX、domain process 等接收阶段 |
| send_phase | domain queue 到 ecrt_master_send() 完成 |
| execution | 线程醒来到本周期发送完成 |
同时检查:
deadline miss
WKC incomplete
每组测试运行:
30 s × 3
因此每组约有:
90,003 cycles
压力测试时,在其他 CPU 上加入计算和内存负载。
最终结果如下,时间单位为 μs:
| 空闲 | generic | 1.847 / 3 / 4.764 | 1.953 / 3 / 6.711 | 3.310 / 4 / 7.878 | 5.529 / 7 / 14.006 | 0 | 5 |
| 空闲 | native | 2.020 / 3 / 7.184 | 2.212 / 3 / 5.544 | 1.810 / 3 / 2.918 | 4.275 / 5 / 7.878 | 0 | 0 |
| 压力 | generic | 1.838 / 2.667 / 12.717 | 1.973 / 3 / 9.337 | 3.101 / 5 / 16.048 | 5.364 / 7 / 21.592 | 0 | 0 |
| 压力 | native | 1.860 / 3 / 8.508 | 2.328 / 4 / 12.255 | 2.015 / 3 / 4.960 | 4.620 / 6.667 / 16.923 | 0 | 0 |
P99 为直方图统计得到的近似值,max 只是有限测试时间中的最大观测值,不能直接等同于 WCET。
7. 测试结果说明了什么
结果里最明显的是 TX。
空闲状态:
TX mean
3.310 μs -> 1.810 μs
下降 45.3%
execution:
5.529 μs -> 4.275 μs
下降 22.7%
P99:
7 μs -> 5 μs
压力状态下:
TX mean
3.101 μs -> 2.015 μs
下降约 35%
execution:
5.364 μs -> 4.620 μs
下降约 13.9%
最坏 execution:
21.592 μs -> 16.923 μs
结果基本符合预期:Native 最大的收益来自 TX。
Generic TX 需要经过 packet socket 和 Linux 通用发送路径,而 Native 可以直接进入 stmmac 和 DMA,所以这一段缩短最明显。
但 RX 的结果正好相反。
空闲状态:
1.953 μs -> 2.212 μs
压力状态:
1.973 μs -> 2.328 μs
Native RX 平均时间反而增加了约 13%~18%。
原因就是前面提到的同步 DMA poll。
Generic 模式下,IRQ/NAPI 可能已经提前处理了部分数据;Native 模式则由周期线程主动检查 DMA ring,所以一部分等待时间被直接计入 receive phase。
不过 TX 节省的时间明显大于 RX 增加的时间,所以最终 execution 仍然下降。
另一个值得注意的是 wake_lateness 基本没有改善。
这也是合理的,因为它主要由:
Linux timer
scheduler
PREEMPT_RT
CPU isolation
IRQ 干扰
决定。
Native 驱动优化的是线程醒来之后的 EtherCAT 收发,并不是 Linux 调度器。
因此不能用“wake mean 有没有继续低于 2 μs”来评价 Native 是否有效。
8. 目前结论和验证边界
这次移植已经完成:
Native stmmac 修改
内核树内构建
RK3588 部署
双 GMAC 共存
IgH 接管
三从站 OP
Generic / Native A/B 测试
从当前测试数据来看,Native stmmac 成功绕过 ec_generic 和 packet socket 后,收益主要集中在发送路径。
三个从站、1 ms 周期下:
- TX mean 下降约 35%~45%;
- execution mean 下降约 14%~23%;
- 压力状态最坏 execution 从 21.592 μs 降至 16.923 μs;
- 所有测试均没有 deadline miss。
同时也发现两个不能忽略的问题。
第一,Generic 空闲第一轮出现了 5 个 WKC incomplete,其余 Generic 和所有 Native 测试均为 0。
目前样本不足,不能据此认为 Native 消除了 WKC 异常,只能作为后续长时间测试需要关注的现象。
第二,实验期间曾出现两次 PREEMPT_RT Kernel Oops,一次处于 Native 环境,一次已经回退 Generic。由于当时没有获取完整调用栈,目前无法归因到 Native stmmac。
因此现阶段更准确的结论是:
Native stmmac 已经证明可以降低 RK3588 + IgH 主站线程醒来之后的网络发送开销,并改善整个 EtherCAT 处理阶段的平均时间和部分长尾指标,但目前的测试规模还不足以证明产品级长期可靠性。
后续如果真正准备进入产品环境,还需要继续验证:
- 小时级、天级压力测试;
- EtherCAT 反复启停;
- 网线断开和恢复;
- 从站掉电重连;
- 主站异常重启;
- 不同 PDO 数据规模;
- 500 Hz、1 kHz 以及更高频率;
- CPU 和内存高负载;
- Kernel Oops 完整串口和 pstore 抓取。
对于实时 EtherCAT 主站,平均值只是一个指标。
最终真正决定系统是否成熟的,还是:
deadline
WKC
P99 / max
链路恢复
异常恢复
长期稳定性
Native 能不能用于产品,也应该由这些数据来决定,而不是仅仅因为它比 ec_generic 的路径更短。





