把 OrangePi 5 Plus 塞进运动控制项目的现场,同时扛 2 路 EtherCAT 从站链和 6 路 CAN 总线,听起来有点胡闹——一块几百块的开发板,凭什么去顶工业现场的实时任务?这篇记录就是我完整搭建这套软实时 Linux 系统的过程,包括内核编译、EtherCAT 主站部署、CAN 通道扩展,以及那些只在现场才会暴露出来的坑。先说结论:只要把 PREEMPT_RT 内核、CPU 隔离、线程优先级这套组合拳打好,1ms 周期的 EtherCAT 转发抖动可以控制在几十微秒以内,6 路 CAN 同时收发也不会互相拖后腿。这套方案适合正在评估 RK3588 平台做产线设备原型、想用低成本 ARM 板验证多总线运动控制的工程师,也适合刚接触 EtherCAT 和 SocketCAN 的入门玩家照着抄作业。
1. 为什么选 OrangePi 5 Plus 当软实时主站
1.1 硬件底子:双网口和 RK3588 的优势
做 EtherCAT 主站,最重要的硬件资源不是算力,而是网卡。EtherCAT 本质上是以太网帧的实时搬运,主站侧需要一个标准网卡,所以板子上网口越多、越独立,后面能玩的拓扑就越多。OrangePi 5 Plus 自带两个 2.5G 网口,其中一个走 PCIe,另一个是 GMAC 引出的,两个网口可以分别绑定为独立的 EtherCAT 主站段,正好对应标题里"2 EtherCAT"的需求。如果用只有一个千兆口的树莓派,想挂两组从站链就必须外接 USB 网卡,延迟和稳定性都要打折扣。
另外一个关键点是它的 SoC 是 Rockchip RK3588,四颗 A76 大核加四颗 A55 小核。A76 跑主站周期任务、A55 跑 CAN 收发和日志写入,天然可以分核隔离。我实测下来,EtherCAT 周期任务绑在 A76 上,CAN 收发线程绑在 A55 上,两边互不干扰,比单核板子把所有中断和任务挤在一起舒服太多。
1.2 软实时不是玄学:PREEMPT_RT 能做到什么程度
很多人一听到"软实时"就摇头,觉得既然不是硬实时,那项目上了也是白搭。我的看法是得分场景。如果你的从站是伺服驱动器,且要求循环周期 250us 以下、抖动不超过正负 5us,那确实得上专用实时网卡和硬实时操作系统。但很多现场应用其实是 1ms 或 2ms 周期的 IO 刷新、阀岛控制、扫码枪触发,这类场景对绝对延迟不敏感,更关心"时序抖动是否超过控制算法容忍范围"。PREEMPT_RT 补丁就是把标准 Linux 内核改造成"几乎可以抢占任何地方"的内核,配合 SCHED_FIFO 实时线程,确实能让 1ms 任务的抖动压在几十微秒这个量级。
举个生活化的类比:硬实时像地铁时刻表,晚一秒就是事故;软实时像快递当日达,偶尔晚半小时能接受,但不能天天晚。工业上很多 EtherCAT 从站本身有 DC 分布式时钟,主站只要周期性发帧,真正精确的同步由从站之间完成,主站的"软实时"只要保证帧不被调度器卡住太久就行。这套思路在 IgH 官方文档里也反复提到,PREEMPT_RT 是它支持的主流运行环境之一。
2. 硬件方案与 6 路 CAN 的扩展布局
2.1 两块网卡怎样切成两路 EtherCAT
先说清楚"2 EtherCAT"的两种理解:一种是两个网口做冗余,同一个从站链两头各接一根线,断一条另一条顶上;另一种是两个网口各自接一组从站链,成为完全独立的两套总线。我这边做的是第二种,因为现场有两台设备,一台挂 8 个 IO 从站,另一台挂 6 个伺服,混在一条链上调试时互踢网线太痛苦。IgH 主站支持一个网卡绑定一个 master,通过 /etc/ethercat.conf 里依次写 MASTER0_DEVICE 和 MASTER1_DEVICE,就能在一个内核模块里同时管两条链。
如果你确实想做网卡冗余,IgH 在 configure 阶段要额外开 redundancy 选项,并且两个网口会被绑定成同一段总线,从站数量不变只是链路有了备份。两种思路没有谁更好,只看现场拓扑需要。我的经验是:从站数量不多、链路距离短的场景优先选两条独立链,调试效率高得多;从站链拉得长、环境电磁干扰强的场景才值得上冗余。
2.2 原生 CAN + MCP2515 的通道分配
6 路 CAN 的构成是这样安排的:RK3588 芯片内部自带两个 CAN 控制器,在设备树里对应 can0 和 can1,这两路走原生 SocketCAN 驱动,延迟最低、稳定性最好。剩下的 4 路靠 SPI 总线外扩 MCP2515 芯片,OrangePi 5 Plus 的 40pin 排针上有 SPI0 和 SPI1 两组控制器,每组拉两个片选信号,正好挂 4 个 MCP2515。
需要注意的是原生 CAN 控制器在 RK3588 的引脚上做了引脚复用,默认可能被配置成普通 GPIO 或 UART,得在设备树 overlay 里主动把它切到 CAN 功能。MCP2515 是 SPI 从设备,通信速率受限于 SPI 时钟,一般 MCP2515 的 SPI 时钟能跑到 10MHz,换算下来单路 CAN 跑 500kbps 的波特率没问题,但不建议上 1Mbps,实测丢帧概率会明显上升。另外 MCP2515 内部只有 CAN 2.0 控制器,不支持 CAN FD,如果现场需要 CAN FD 就得换成 MCP2518FD 或直接上 USB 转 CAN FD 设备。
2.3 布线和供电的细节处理
这块板子对供电比较敏感,特别是同时插两块 EtherCAT 从站链和 6 路 CAN 收发器时。官方推荐 USB-C PD 供电,但我实测用 5V 4A 的 DC 电源接排针上的 5V 引脚更稳,因为 USB-C 供电握手失败时会反复重启。CAN 收发器那边,MCP2515 必须外接 TJA1050 或 SN65HVD230 这类收发器芯片,一个 MCP2515 配一个收发器,收发器的 VCC 建议单独供电,别直接从排针 3.3V 拉,否则总线短路时很容易把 SoC 的电源轨拉垮。
我的接线顺序是:先把两组 EtherCAT 网线固定好,再做 CAN 总线干线。CAN 干线两头各加一个 120Ω 终端电阻,注意不是每个节点都加,而是物理两端各一个。如果 6 路 CAN 走同一个线槽,尽量每路单独用双绞屏蔽线,屏蔽层单端接地。这些看着是老生常谈,但我在这个项目里至少被 CAN_H 和 CAN_L 接反、终端电阻忘加这两个低级错误耗掉过一整个下午。
3. 编译带实时补丁的内核(RK3588)
3.1 内核源码和 RT 补丁的准备
编译内核的第一步是把 OrangePi 官方维护的 Linux 内核源码拉下来,这套源码里已经包含了 RK3588 的大部分驱动和设备树,比直接去 kernel.org 拉主线要省事得多。需要说明一下,并不是所有内核版本都有对应的 PREEMPT_RT 补丁,OrangePi 官方源码基于 5.10 内核,而 5.10 系列正好有 rt 补丁,这是选择 5.10 而不是更新内核版本的重要原因。
具体流程是:先克隆源码到工作目录,确认源码根目录下的 makefile 里的版本号,比如 5.10.160,然后去内核组织的 RT 补丁发布目录下载对应版本,文件名类似 patch-5.10.160-rtXXX.patch.gz。下载后先解压,再用 patch 工具在源码根目录执行合并操作。合并过程中如果报错,多半是源码版本和补丁版本不一致,先检查 makefile 大版本号,然后换严格对应的补丁文件。
3.2 关键配置项与编译要点
合并完补丁后,用 OrangePi 官方提供的默认配置做基础,这一步不能少,否则 RK3588 的设备树和驱动配置全靠手写会疯掉。执行 make 配置命令后,会进入一个基于文本菜单的配置界面。在 Kernel Features 菜单下找到 Preemption Model,把选项从 Voluntary Kernel Preemption 切到 Fully Preemptible Kernel (Real-Time),这一项就是 PREEMPT_RT 软实时的核心开关。
还有一些我建议顺手改掉的配置:把 CPU 调频策略设为 performance,否则内核会动态调整 CPU 频率,实时任务的延迟峰值会跟着频率变化一起波动;打开 High Resolution Timer Support,正常情况下它已经是默认开启的;如果板子上的闲置 CPU 比较多,可以开启 CPU isolation 相关的配置项,但更直接的方法是在内核启动参数里用 isolcpus 和 nohz_full 手动隔离核心,省得为了一些实验性的内核配置折腾设备树。
编译命令需要注意交叉编译工具链,不能在板子上直接编内核,算力勉强够但浪费时间。在 PC 上安装 aarch64 交叉编译工具链后,设置 ARCH 和 CROSS_COMPILE 环境变量,然后分步编译内核映像、设备树和内核模块。编出来的内核映像放到 boot 分区,设备树文件放到 boot 分区的 dtb 目录,内核模块安装在根文件系统的 lib/modules 目录下。模块很多,复制文件时最好打包传输再解压,比一条条 cp 快得多。
3.3 烧录与启动参数调整
烧录完成后第一次启动,最好先确认内核确实进入了 RT 模式。登录后查看内核配置里 CONFIG_PREEMPT_RT 是否为 y,或者运行 uname -a 看内核版本后面有没有 -rt 后缀。如果启动参数里没加 isolcpus,现在就得在 /boot 下的 extlinux.conf 或 boot.cmd 里补上。我实际用的是 isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3,意思是大核 2 和 3 不让内核调度普通任务,也不让 RCU 回调和时钟中断打扰它们,专门留给实时线程。
这里有个容易踩的坑:isolcpus 隔离的是 CPU,不是中断,如果网卡中断本来被分配到隔离核上,EtherCAT 主站收发还是会被中断打扰。我在 /etc/default/irqbalance 里把 irqbalance 打开,让它自动把中断赶到非隔离核,或者手动改 /proc/irq 下的 smp_affinity,把两个网卡的中断绑到 CPU0 和 CPU1 上。这一步不做好,后面测抖动时你会看到明显的周期性毛刺,那是中断抢占了你的 EtherCAT 周期线程。
4. IgH EtherCAT 主站:两路从站链的搭建
4.1 主站安装与 sysconfig 绑定
EtherCAT 主站软件我选的是 IgH(EtherLab),原因有两个:一是它的应用层 API 比较成熟,周期任务的结构清晰,网上资料也最多;二是它支持多主站,一个内核模块可以同时注册两个 master,正好对应两个物理网卡。SOEM 其实也能做,但它是轻量级实现,主站状态机和应用层代码需要自己写不少,不适合做产品原型。
编译安装前先装好 autoconf、automake、libtool 这些工具。从 git 仓库拉下源码后,先运行 bootstrap 脚本生成 configure 文件。configure 参数里最关键的是 --disable-rtdm,因为我们用的是 PREEMPT_RT 加普通应用层线程,不需要额外的 RTDM 实时驱动框架。加上 --enable-generic,让主站代码使用通用网卡驱动。安装完成后,内核模块在 lib/modules 对应目录下,工具命令在 /usr/local/sbin 下。
然后修改 /etc/ethercat.conf,把两个网卡的 MAC 地址填进去。MASTER0_DEVICE 填 eth0 的 MAC,MASTER1_DEVICE 填 eth1 的 MAC。DEVICE_MODULES 填 generic,意思是使用通用网卡模块而不是特定厂商驱动。改完后加载 ec_master 内核模块并启动 ethercat 服务。这时运行 ethercat slaves,能看到所有从站按拓扑顺序列出来,如果只看到一个空的主站信息,多半是网卡绑定失败,用 dmesg 看一下是不是 MAC 地址写错了。
4.2 周期任务怎么写才能压得住
IgH 的应用层编程有一个标准的循环骨架:周期循环里要做这么几件事:取当前时间戳并传给主站、调用同步参考时钟和从站时钟的接口、发送帧、接收帧、处理域数据、重新队列域数据,然后等到下一个周期点。代码结构不复杂,真正难的是让这个循环稳定地跑在 1ms 周期上。我的做法是给这个线程设置 SCHED_FIFO 实时调度策略,优先级设成 80,然后用 clock_nanosleep 做绝对时间定时,等循环末尾根据预计时间休眠到下一个唤醒点。
这样写下来,线程从头到尾都不会被普通进程抢占,只有更高优先级的硬中断才会插一脚。在实际测试中,1ms 周期的主循环抖动大概在正负 35us 左右,这个数据是在内核没做额外优化、网卡中断在另一个核上的情况下测的。如果对抖动要求更苛刻,可以把主循环挪到隔离核上,同时把两个网卡的中断用 smp_affinity 强制分到另外两个非隔离核,这样 EtherCAT 线程几乎不会被打断。
4.3 DC 同步和抖动数据
EtherCAT 的 DC(Distributed Clocks)机制本质上和主站的实时性没有强绑定,从站之间的同步靠的是每帧报文里的时钟信息,主站只是负责周期性地给所有从站发送系统时间,然后在应用层周期性地调用同步接口,让从站时间基准对齐到参考从站。伺服驱动的插补周期就是靠 DC 对齐的,如果 DC 没配好,各轴的 SYNC 信号会差出几百纳秒甚至微秒级,运行时就可能出现随机的电流波动和位置误差。
我这边把两个 master 的 DC 同步都在应用层开了,从站的 SYNC0 周期设成 1ms,用逻辑分析仪抓从站 DO 口翻转输出,测出来的 SYNC 抖动比主站循环抖动还小,因为 DC 是在从站硬件上生成的,主站抖动会被从站时钟缓冲吸收一部分。实测数据:两个 master 下各 8 个从站,SYNC0 输出抖动正负 120ns 到 300ns,这在大部分非超高精度运动控制场景里都够用了。
5. 6 路 CAN 从 SocketCAN 到应用层
5.1 设备树里把 CAN 口都点起来
RK3588 自带的两路 CAN 不是默认启用的,需要在设备树 overlay 里把对应节点的 status 改为 okay。每个 CAN 节点还要配置引脚 mux,把对应 GPIO 组切换到 CAN 功能。改完设备树后重新编译 dtb 并覆盖 boot 分区的旧文件,重启后检查 /sys/class/net 下是否出现 can0 和 can1。
MCP2515 那 4 路要麻烦一点,每个芯片都要在设备树里有一个节点,主要填三样东西:reg 是 SPI 片选号,interrupts 是连接到 SoC 的中断引脚,clocks 是 MCP2515 的外部晶振频率。MCP2515 的 SPI 通信有最大时钟限制,我按数据手册保守地设成 5MHz,避免高速 SPI 下出现偶发 CRC 错误。设备树改完重启后,如果 can2 到 can5 没出现,先看 dmesg 里有没有 spi 注册失败的信息,再用 i2cdetect 或 spi 设备列表确认 MCP2515 是否在总线上被枚举到。
5.2 六路 CAN 的线程调度与读写模型
6 路 CAN 全部启动后,设备名依次是 can0 到 can5,每路用 ip 命令设置波特率并 up。我统一用的 500kbps,能满足现场大多数设备。应用层读写 SocketCAN 的方式有两种:阻塞读和 epoll 事件驱动。如果 6 路 CAN 各自独立工作,最简单可靠的做法是每路一个线程,线程里做阻塞 read,收到报文后丢到带锁的环形缓冲区;如果 CAN 报文要跟 EtherCAT 周期联动,就必须用 epoll 把所有 fd 集中起来,在 EtherCAT 周期循环的同一个线程里非阻塞监听。
我测试下来,每路一个阻塞线程的方式在报文量不大时最省心,因为阻塞 read 自带流量控制,不会出现某个 fd 的缓冲队列堆积。但要注意线程优先级不能太高,我把 6 个 CAN 线程都设成 SCHED_FIFO 优先级 50,低于 EtherCAT 线程的 80。这样即使 CAN 总线风暴来了,也只是占用 A55 核,不会影响 EtherCAT 主循环,实测同时 6 路各 1000 帧每秒收发时,EtherCAT 周期线程的抖动只增加了不到 10us。
5.3 CAN 总线故障排查三板斧
做 6 路 CAN 免不了遇到信号问题,我总结的三板斧是:先量静态电压、再查终端电阻、最后看错误计数器。
静态电压这一步,用万用表分别测 CAN_H 对地、CAN_L 对地的电压。正常空闲状态下 CAN_H 在 2.5V 左右,CAN_L 也在 2.5V 左右,两个电压差接近 0V;如果 CAN_H 或 CAN_L 明显偏离,比如一个 3.3V 一个 1.5V,说明收发器供电或接地有问题。然后量 CAN_H 和 CAN_L 之间的电阻,在断电状态下,总线两端如果有终端电阻,量到的应该是 60Ω 左右;如果量到 120Ω,说明只加了一个终端电阻或者根本没组网。
最后看错误计数器,SocketCAN 会在内核里维护 CAN 总线的错误统计。出现大量错误帧时先看是不是波特率不匹配,再看是哪个节点在报错。我遇到过一种诡异的情况:总线报文一多就出现偶发错误帧,排查到最后发现是 MCP2515 的 SPI 中断脚接在了和某个 CAN 收发器共用的电源轨上,收发器瞬间拉电流时 SPI 中断被干扰,误触发了一次读操作。这类问题只有靠逻辑分析仪同时抓 CAN 总线和 SPI 引脚才能定位,所以布线早期就把信号完整性测试做掉,能省不少后面的事。
6. 常见问题与排查技巧实录
6.1 从站偶发掉线和 MAC 绑定
EtherCAT 从站掉线是主站调试里最常遇到的问题,表现是 ethercat slaves 时好时坏,或者运行中从站突然从拓扑里消失,过几秒又恢复。我这边遇到的坑有两个:一是两个网口在系统启动时顺序会变,eth0 和 eth1 偶尔对调,导致主站绑定到了错误的网卡上。解决办法是不依赖 eth 名称,直接在 /etc/ethercat.conf 里写死 MAC 地址;二是网线质量问题,EtherCAT 的帧走的是标准以太网物理层,但对时序敏感,现场随手抓的一根普通网线可能就有串扰,我换了屏蔽工业网线后问题消失。
另外一个不太起眼但严重影响稳定性的因素是网卡的电源管理。默认情况下网卡为了省电会在空闲时进入低功耗状态,唤醒延迟会导致第一帧丢失。我通过在启动脚本里用 ethtool 关掉两个网卡的节能特性,并且在内核启动参数里加上禁用网卡电源管理的配置,EtherCAT 主站启动后从站链的扫描成功率明显提高。
6.2 bus-off 和错误帧处理
CAN 总线如果长期存在严重干扰,控制器会进入 bus-off 状态,自动断开总线,此时该路 CAN 不再收发任何报文。SocketCAN 应用层程序读到的错误码会显示为 -ENETDOWN 之类的异常,但更麻烦的是系统不会自动恢复。我处理的办法是在应用层加一个监视线程,定期读取每个 CAN 通道的错误状态,如果发现 bus-off 标志,就把对应网卡 down 再 up,重新初始化控制器。
这个"自动重启"逻辑看着简单,实际有一个坑:如果干扰源还在,重启后可能立刻又 bus-off,形成不断重启的死循环。所以我在重启策略里加了一个退避机制,第一次重启后若 500ms 内再次 bus-off,就等 1 秒再重启,仍然失败就等 2 秒,最多等 8 秒。这样既能让总线自行恢复,又不会疯狂抖动干扰其他通道。
6.3 中断扎堆和 CPU 隔离
软实时系统的性能瓶颈很多时候不在 CPU 算力,而在中断处理。Linux 的网卡中断默认全部落在 CPU0 上,如果 EtherCAT 周期线程也跑在 CPU0,那么每来一包数据,中断就会打断周期线程的操作,抖动马上就上去了。我一开始没做隔离时,1ms 周期抖动能到几百微秒,完全不可用。后来用 isolcpus 隔离了大核 2 和 3,把 EtherCAT 主线程固定在 CPU2,网卡中断通过 smp_affinity 强制分到 CPU0 和 CPU1,抖动立刻降了一个数量级。
还要注意别让 irqbalance 把你的配置覆盖掉。我后来发现,即使手动设置了中断亲和性,irqbalance 后台服务每隔一段时间还是会重新平衡中断。最简单的处理是把 irqbalance 服务关掉,或者在它的配置文件里把隔离核排进黑名单。这个细节不太容易第一时间想到,但它是软实时调优里收益最明显的一项。
7. 文末彩蛋:一键部署脚本与压测原始记录
7.1 快速部署脚本
每次重新刷系统后都要手动配置一大堆东西,我干脆写了一个部署脚本,把内核确认、网卡优化、IgH 配置、CAN 通道启动都串起来。脚本逻辑不复杂,但能把环境复现时间从一下午压缩到十分钟。下面是核心片段:
#!/bin/bash # 检查 RT 内核 grep -q "PREEMPT_RT" /proc/version || { echo "Not RT kernel"; exit 1; } # 两个 EtherCAT 网卡去节能 for eth in eth0 eth1; do ethtool --offload $ eth rx off tx off ethtool -s $ eth speed 1000 duplex full autoneg off done # 启动 CAN 通道 for can in can0 can1 can2 can3 can4 can5; do ip link set $ can type can bitrate 500000 restart-ms 1000 ip link set $ can up done # 加载 IgH 主站模块 modprobe ec_master systemctl start ethercat脚本里有个细节值得说一下:ethtool 把网卡速率固定在千兆全双工,避免自动协商阶段产生的短暂链路抖动。有些人会觉得 2.5G 网卡跑千兆是浪费,但 EtherCAT 实际只需要很小的带宽,牺牲速率换稳定性是划算的。
7.2 压测记录
最后放一份我在这个平台上跑过的压测原始记录,方便你有参照:
| 测试项目 | 配置条件 | 实测结果 |
|---|---|---|
| EtherCAT 周期抖动 | 1ms 周期,PREEMPT_RT,A76 隔离核 | 最大 +35us / -32us |
| 从站 DC SYNC0 抖动 | 8 从站链,逻辑分析仪抓 DO 翻转 | 300ns 以内 |
| 6 路 CAN 同时收发 | 500kbps,每路每秒 1000 帧 | 丢帧 0,CPU 占用额外增加 15% |
| CAN 总线错误帧 | 双绞屏蔽线 10 米,终端电阻正常 | 运行 8 小时 0 错误帧 |
| 系统稳定性 | 双 EtherCAT 链 + 6 CAN 满载 | 连续 48 小时无掉线 |
压测时有一点要注意:抖动数据会随着 CPU 温度变化而漂移。A76 大核负载高时发热明显,如果散热片没贴好,CPU 降频会让周期线程的唤醒时间变长。我后来给 RK3588 加了一个主动散热片,抖动数据明显稳定下来,这也是性能调优里容易被忽视但真实存在的因素。
这台板子在我手里跑了两个多月,踩过的坑基本都在上面积累了。后续我还打算把 CAN 接收的报文加上时间戳后做回放,用来模拟现场从站的突发报文测试 EtherCAT 主站的抗抖动能力,方法不算新,但在这个平台上实测下来很有效。如果你也在折腾 OrangePi 5 Plus 做多总线控制,欢迎把你的抖动数据拿来一起对比,看看不同内核版本、不同扩展方案之间的差距到底有多大。