OrangePi 5 Plus 构筑双 EtherCAT 与 6 路 CAN 软实时主站实践
把 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. 编译带实时补丁的内核RK35883.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 里补上。我实际用的是 isolcpus2,3 nohz_full2,3 rcu_nocbs2,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 主站软件我选的是 IgHEtherLab原因有两个一是它的应用层 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 的 MACMASTER1_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 的 DCDistributed 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_RTA76 隔离核最大 35us / -32us从站 DC SYNC0 抖动8 从站链逻辑分析仪抓 DO 翻转300ns 以内6 路 CAN 同时收发500kbps每路每秒 1000 帧丢帧 0CPU 占用额外增加 15%CAN 总线错误帧双绞屏蔽线 10 米终端电阻正常运行 8 小时 0 错误帧系统稳定性双 EtherCAT 链 6 CAN 满载连续 48 小时无掉线压测时有一点要注意抖动数据会随着 CPU 温度变化而漂移。A76 大核负载高时发热明显如果散热片没贴好CPU 降频会让周期线程的唤醒时间变长。我后来给 RK3588 加了一个主动散热片抖动数据明显稳定下来这也是性能调优里容易被忽视但真实存在的因素。这台板子在我手里跑了两个多月踩过的坑基本都在上面积累了。后续我还打算把 CAN 接收的报文加上时间戳后做回放用来模拟现场从站的突发报文测试 EtherCAT 主站的抗抖动能力方法不算新但在这个平台上实测下来很有效。如果你也在折腾 OrangePi 5 Plus 做多总线控制欢迎把你的抖动数据拿来一起对比看看不同内核版本、不同扩展方案之间的差距到底有多大。