RK 双板架构下的 EtherCAT 实时控制优化实践:绑核、隔离、中断亲和、硬编解码与交叉编译
0. 核心思路
在 RK 平台上做机器人、运动控制或多轴执行器控制时,最怕的不是平均 CPU 占用高,而是控制周期突然抖一下。EtherCAT 这类实时总线通常要求周期稳定,控制线程、网卡中断、PDO 收发、状态估计和安全监控都不能被视频、日志、网络、UI 或算法线程随便打断。因此优化顺序不能一上来就改业务代码,而应该先把系统资源管理清楚:CPU 不够先绑核,绑核后仍有抖动就做中断隔离,再不行才进入算法和控制代码重构。
这套打法特别适合当前“全线产品双 RK 组合”的硬件形态。双 RK 不是简单堆算力,而是把实时控制和感知计算拆成两块资源域:一块 RK 尽量靠近 EtherCAT、伺服控制、安全 IO 和底层状态机,另一块 RK 承担视觉、音视频、AI 推理、UI、网络和上层业务。这样做的目标不是让每块板都跑满,而是让实时板足够干净,让非实时板尽量把重任务吃掉,避免控制代码在 Linux 普通调度噪声里硬扛。

1. 为什么 RK 上能跑实时控制,但必须做资源隔离
以 RK3588 为例,官方资料显示它是 4 个 Cortex-A76 加 4 个 Cortex-A55 的大小核结构,并带有视频编解码、多媒体处理、NPU、双千兆网口等外设资源。这个配置对于嵌入式控制已经很强,但强不等于天然实时。Linux 默认调度会把进程、内核线程、中断、软中断和后台任务在多个 CPU 之间动态迁移,平均吞吐很好,但对 EtherCAT 这种周期性控制任务来说,随机迁移和异步中断就是 jitter 来源。
因此 RK 平台优化的第一原则是:把实时性当成资源规划问题,而不是只当成代码性能问题。控制线程本身可能只占 10% CPU,但如果它和摄像头解码、AI 推理、日志刷盘、网络收包、内核工作队列混在同一组核心上,最坏时延会被这些非实时任务拉爆。真正要优化的是最坏情况,不是平均情况,所以必须通过绑核、cpuset、IRQ affinity、nohz_full、rcu_nocbs、PREEMPT_RT 等手段把控制链路保护起来。
2. 双 RK 架构:实时板和业务板要分工清楚
双 RK 组合建议按“实时控制 RK + 业务计算 RK”的方式拆分。实时控制 RK 负责 EtherCAT 主站、伺服周期任务、安全状态机、关键传感器采样、看门狗和低延迟通信;业务计算 RK 负责相机输入、硬件解码、AI 识别、UI、日志、云端通信和上层策略。两块 RK 之间可以通过千兆以太网、PCIe、UART 或共享协议通信,但不要让业务板直接卡住控制周期,控制板只接受经过节流和时间戳管理的指令。
如果使用 RK3588 一类大小核芯片,实时板内部还要再做一次核级分工。一般可以把 A55 小核留给系统服务、日志、普通网络和 housekeeping,把 A76 大核留给 EtherCAT 周期线程、控制线程和关键计算线程。实际 CPU 编号必须以板端查询为准,不能靠经验硬写,因为不同内核和设备树可能会改变核心编号。上线前至少要用 lscpu -e 和 /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq 确认大小核对应关系。
lscpu -e
for c in /sys/devices/system/cpu/cpu[0-9]*; do
echo -n "$(basename "$c") "
cat "$c/cpufreq/cpuinfo_max_freq" 2>/dev/null || true
done
2.1 RK3588 推荐核心规划:不要先假设 CPU 编号
很多 RK3588 板卡习惯上是 CPU0-3 对应 A55,CPU4-7 对应 A76,但产品内核、设备树、调度域和频率策略可能会让这个映射表现不完全一致,所以真正上线前必须在目标镜像上确认。推荐的起步规划是:CPU0-3 做 housekeeping,承接 systemd、日志、普通网络、非实时中断和内核工作队列;CPU4-5 做业务桥接、协议解析和轻量计算;CPU6 做 EtherCAT 周期线程;CPU7 做控制计算、安全监控或 EtherCAT IRQ 备选核心。这个规划不是唯一答案,但它能把实时链路从系统噪声里先切出来,便于后续用 jitter 数据做取舍。
# RK3588 板端确认 CPU 拓扑、最大频率、当前 governor 和在线状态
lscpu -e=CPU,CORE,SOCKET,NODE,ONLINE,MAXMHZ,MINMHZ,MHZ
for cpu in /sys/devices/system/cpu/cpu[0-7]; do
echo "== $(basename "$cpu") =="
cat "$cpu/topology/core_id" 2>/dev/null
cat "$cpu/cpufreq/cpuinfo_max_freq" 2>/dev/null
cat "$cpu/cpufreq/scaling_governor" 2>/dev/null
done
cat /proc/cmdline
2.2 双 RK 通信边界:实时板只收“可控输入”
双 RK 之间的通信要有边界,不能让业务板把大图、复杂 JSON、无节流日志或不可预测的网络抖动直接打到实时板。实时板最好只接收三类输入:带时间戳的目标指令、低频状态更新和经过限幅的参数修改。业务板如果有视觉识别结果,应先做滤波、限频和时序对齐,再以固定大小结构体或 protobuf flat message 发送。控制板收到后进入双缓冲,由非实时通信线程写入 shadow buffer,再由实时线程在周期边界读取快照。这样能避免业务板偶发卡顿污染 EtherCAT 周期。

3. 第一层优化:强制绑核,把控制线程固定在确定核心上
绑核是最先做的优化,因为它成本最低、收益直接、风险可控。控制进程如果不绑核,Linux 调度器可能会为了负载均衡把它从一个核心迁移到另一个核心,迁移会带来 cache miss、调度延迟和不可控抖动。对于 EtherCAT 控制周期,建议先把主控制线程、PDO 收发线程、状态估计线程固定到同一个或相邻的大核上,把日志、UI、视频、AI 推理全部排除出去。
最简单的方式是用 taskset 和 chrt 启动控制进程。taskset 设置 CPU affinity,chrt 设置实时调度优先级。比如把 EtherCAT 控制进程固定到 CPU6,并以 SCHED_FIFO 90 优先级运行,可以先这样验证。如果系统还有安全监控线程,可以放到 CPU7,并用略低优先级运行,避免它抢占主控制环路但仍能及时响应故障。
sudo chrt -f 90 taskset -c 6 ./ecat_control
sudo chrt -f 80 taskset -c 7 ./safety_monitor
taskset -a -p -c 6 "$(pidof ecat_control)"
正式产品里不建议只靠启动脚本临时绑核,因为进程重启、线程创建和 systemd 接管后可能丢失策略。更稳的做法是在程序里对关键线程调用 pthread_setaffinity_np(),并在 systemd unit 里写 CPUAffinity= 做兜底。程序内部绑线程比只绑进程更精细,因为一个进程里可能同时有实时线程、通信线程、日志线程和诊断线程,它们不应该全部挤在同一个实时核心上。
3.1 RK3588 bash 例子:把控制、通信、日志分到不同核
下面的例子假设 CPU6 是 EtherCAT 周期核,CPU7 是控制计算核,CPU4-5 是通信和协议桥,CPU0-3 是系统 housekeeping。这个脚本适合在开发阶段快速验证,不建议直接当最终产品方案。最终方案应由 systemd、程序内部线程 affinity 和启动参数共同保证。注意 taskset -a 会影响进程内所有线程,如果要精细到线程级,需要在程序里设置每个 pthread 的 affinity,或者用 ps -eLo pid,tid,psr,comm 找到线程 TID 后单独设置。
#!/usr/bin/env bash
set -euo pipefail
ECAT_BIN=/opt/robot/bin/ecat_control
BRIDGE_BIN=/opt/robot/bin/upper_bridge
LOG_BIN=/opt/robot/bin/robot_logger
# EtherCAT 周期线程所在进程:高优先级,固定 CPU6
sudo chrt -f 90 taskset -c 6 "$ECAT_BIN" &
# 上层通信桥:中等优先级,固定 CPU4-5
sudo chrt -f 50 taskset -c 4-5 "$BRIDGE_BIN" &
# 日志进程:普通调度,固定 housekeeping CPU0-3
taskset -c 0-3 "$LOG_BIN" &
sleep 2
ps -eLo pid,tid,cls,rtprio,pri,psr,comm | egrep "ecat|bridge|logger"
3.2 systemd 例子:产品化时用 CPUAffinity 兜底
systemd 的 CPUAffinity= 适合做服务级兜底,保证进程启动时落在指定 CPU 集合。它的好处是可维护、可审计、适合量产镜像;不足是线程级区分不够细,所以实时主线程仍建议在程序内部调用 affinity API。下面例子把 EtherCAT 服务限制到 CPU6-7,并开启实时优先级。不同发行版对 LimitRTPRIO、LimitMEMLOCK 和 systemd 版本支持略有差异,部署时要用 systemctl show 和 /proc/<pid>/status 验证实际生效情况。
[Unit]
Description=Robot EtherCAT Realtime Control
After=network.target
[Service]
Type=simple
ExecStart=/opt/robot/bin/ecat_control
CPUAffinity=6 7
IOSchedulingClass=realtime
IOSchedulingPriority=0
LimitRTPRIO=95
LimitMEMLOCK=infinity
Restart=always
RestartSec=1
[Install]
WantedBy=multi-user.target

4. 第二层优化:ECAT 核心隔离,把 EtherCAT 周期从系统噪声里拔出来
绑核只能告诉调度器“这个进程尽量跑在哪些核心上”,但不能保证这些核心没有别的内核噪声。Linux 内核文档把 CPU isolation 定义为让某个 CPU 尽可能只服务指定工作负载,不被中断、定时器、workqueue、kthread 等噪声干扰。对 EtherCAT 来说,真正有效的做法是把控制核心从普通调度域里隔离出来,把系统 housekeeping 留给其他核心。
一个常见 RK3588 规划是:CPU0-3 留给系统服务和普通后台,CPU4-5 留给业务通信、轻量感知或协议桥,CPU6 留给 EtherCAT 周期线程,CPU7 留给控制计算和安全监控。启动参数可以先从保守方案开始,例如隔离 CPU6-7,并把默认 IRQ 亲和性限制到 CPU0-5。实际是否使用 nohz_full 要看控制线程是否频繁系统调用,如果控制线程大部分时间在用户态周期循环中运行,收益更明显。
isolcpus=domain,managed_irq,6-7 nohz_full=6-7 rcu_nocbs=6-7 irqaffinity=0-5
这里要注意,isolcpus 并不是万能开关。内核文档也明确提醒,CPU isolation 有代价:隔离核心越干净,housekeeping 核心压力越大;如果隔离核心频繁进入内核、触发 page fault 或被中断打入,仍然会产生抖动。因此控制线程要配合 mlockall() 锁内存、启动前预热堆栈和缓存、避免实时环里动态分配内存、避免实时环里写文件或打印日志。
4.1 RK3588 bootargs 例子:先隔离 CPU6-7
如果使用 extlinux 或 U-Boot 环境变量,可以把隔离参数写进内核启动参数。下面是一个思路示例,不同板厂镜像可能使用 /boot/extlinux/extlinux.conf、/boot/firmware/extlinux/extlinux.conf、U-Boot env 或 vendor boot 分区,路径不能直接照抄。建议先在开发板上做一版可回滚镜像,确认串口可进 U-Boot,再改启动参数。隔离 CPU6-7 后,普通 IRQ 默认进 CPU0-5,RCU 回调也从 CPU6-7 挪走,减少实时核上的内核噪声。
# 查看当前启动参数
cat /proc/cmdline
# extlinux 示例:找到 APPEND 行后追加以下参数
sudo cp /boot/extlinux/extlinux.conf /boot/extlinux/extlinux.conf.bak
grep -n "^[[:space:]]*APPEND" /boot/extlinux/extlinux.conf
sudo perl -0pi -e 's/(^[ \t]*APPEND[^\n]*)/$1 isolcpus=domain,managed_irq,6-7 nohz_full=6-7 rcu_nocbs=6-7 irqaffinity=0-5/m' \
/boot/extlinux/extlinux.conf
# 重启后验证
sudo reboot
cat /proc/cmdline
cat /sys/devices/system/cpu/isolated 2>/dev/null || true
4.2 cpuset 运行时隔离:不用重启也能做分区验证
如果暂时不想改 bootargs,可以先用 cpuset/cgroup 做运行时隔离验证。cpuset 的作用是限制一组进程能在哪些 CPU 上运行,内核文档也说明任务的 affinity 会被 cpuset 允许范围过滤。它不如启动级 isolation 干净,因为中断、内核线程和 workqueue 仍需额外处理,但很适合做 A/B 实验:先把控制进程放入实时 cpuset,把业务进程放入普通 cpuset,看 EtherCAT jitter 是否明显改善。
# cgroup v1 cpuset 示例,具体路径取决于系统挂载方式
sudo mount -t cgroup -o cpuset cpuset /sys/fs/cgroup/cpuset 2>/dev/null || true
cd /sys/fs/cgroup/cpuset
sudo mkdir -p rt_housekeeping rt_ecat
echo 0-5 | sudo tee rt_housekeeping/cpuset.cpus
echo 0 | sudo tee rt_housekeeping/cpuset.mems
echo 6-7 | sudo tee rt_ecat/cpuset.cpus
echo 0 | sudo tee rt_ecat/cpuset.mems
# 把当前 shell 或指定 PID 放入实时组
echo $$ | sudo tee rt_ecat/tasks
sudo chrt -f 90 ./ecat_control

5. 第三层优化:中断隔离,把网卡 IRQ 和非实时 IRQ 分开
如果绑核后仍然抖,下一步就是看中断。EtherCAT 走以太网,网卡 IRQ、软中断、NAPI 轮询、驱动线程和控制线程之间的关系会直接影响周期稳定性。Linux 的 /proc/irq/*/smp_affinity 可以设置某个 IRQ 允许在哪些 CPU 上运行,内核文档也给出了通过该文件改变 IRQ affinity 的方式。实际调优时,应先查出 EtherCAT 网卡对应 IRQ,再决定它和控制线程是同核、邻核还是专用核。
对 EtherCAT 来说,网卡 IRQ 有两种常见策略。第一种是 IRQ 和 EtherCAT 周期线程放在同一个隔离核,减少跨核唤醒和 cache 传递;第二种是 IRQ 放在相邻隔离核,控制线程独占一个核,避免 IRQ 打断控制计算。哪种更好要以实测 jitter 为准,不能只凭理论判断。普通摄像头、USB、Wi-Fi、存储、显示、非实时网口的 IRQ 应全部排到 housekeeping 核心,绝不能和 EtherCAT 控制核混在一起。
ECAT_IRQ=123 # 用 /proc/interrupts 查到的 EtherCAT 网卡 IRQ 号
NONRT_IRQ=456 # 摄像头、USB、显示等非实时设备 IRQ 号示例
grep -E "eth|enP|gmac|r8169|igb|stmmac" /proc/interrupts
cat "/proc/irq/${ECAT_IRQ}/smp_affinity_list"
echo 6 | sudo tee "/proc/irq/${ECAT_IRQ}/smp_affinity_list"
echo 0-5 | sudo tee "/proc/irq/${NONRT_IRQ}/smp_affinity_list"
sudo systemctl stop irqbalance
如果系统里开了 irqbalance,它可能会自动重新分配 IRQ,直接破坏手工隔离策略。产品系统里建议禁用 irqbalance,或者明确配置 banirq 和 CPU mask。还要检查 /sys/devices/virtual/workqueue/cpumask,把 unbound workqueue 从隔离核移走。否则即使应用绑核了,内核工作队列仍可能在控制核上执行,造成很难解释的随机尖峰。
cat /sys/devices/virtual/workqueue/cpumask
echo 0-5 | sudo tee /sys/devices/virtual/workqueue/cpumask
5.1 RK3588 bash 例子:自动找出高频 IRQ 和网卡 IRQ
调 IRQ 之前不要直接猜,先观察 /proc/interrupts 在压力下的增长速度。下面这个脚本每秒采样一次中断计数,帮助找出增长最快的 IRQ。摄像头、USB、VPU、RGA、GPU、显示和非实时网口如果增长很快,应该优先排出实时核。EtherCAT 网卡 IRQ 则单独测试:同核策略和邻核策略各跑一轮 cyclictest 与 EtherCAT 周期统计,比较 Max 和 P99.99,而不是只看平均值。
#!/usr/bin/env bash
set -euo pipefail
echo "Top IRQ deltas, press Ctrl+C to stop"
while true; do
awk 'NR>1 && $1 ~ /^[0-9]+:/ {
irq=$1; gsub(":","",irq);
sum=0;
for (i=2;i<=NF;i++) if ($i ~ /^[0-9]+$/) sum+=$i;
name="";
for (i=NF-2;i<=NF;i++) if (i>0) name=name" "$i;
print irq, sum, name;
}' /proc/interrupts | sort -k1,1n > /tmp/irq.now
if [ -f /tmp/irq.prev ]; then
join /tmp/irq.prev /tmp/irq.now | \
awk '{delta=$3-$2; if(delta>0) print delta, "IRQ="$1, substr($0,index($0,$4))}' | \
sort -nr | head -20
echo "----"
fi
mv /tmp/irq.now /tmp/irq.prev
sleep 1
done
5.2 两种 EtherCAT IRQ 策略的实测脚本
下面的脚本把 EtherCAT 网卡 IRQ 分别放到 CPU6 和 CPU7,各跑一次调度延迟测试。真实项目里还要同步记录 EtherCAT 主站周期、从站 WKC、Distributed Clocks 偏差和伺服报警。这个脚本的价值不是直接给答案,而是让团队建立“IRQ 策略必须实测”的习惯。很多时候同核策略平均值更好,但最大值更差;邻核策略平均值略高,却能把控制线程从突发 IRQ 中保护出来。
#!/usr/bin/env bash
set -euo pipefail
ECAT_IRQ="${1:?usage: $0 <ecat_irq_number>}"
for cpu in 6 7; do
echo "== Test ECAT IRQ on CPU${cpu} =="
echo "$cpu" | sudo tee "/proc/irq/${ECAT_IRQ}/smp_affinity_list"
cat "/proc/irq/${ECAT_IRQ}/smp_affinity_list"
sudo cyclictest --mlockall --priority=90 --interval=1000 --affinity=6 --duration=120s \
| tee "cyclictest_irq_cpu${cpu}.log"
done

6. PREEMPT_RT 与 EtherCAT 主站:不是“装了 RT 就完事”
PREEMPT_RT 的价值在于降低内核不可抢占区域,把很多中断处理线程化,并让实时任务有更稳定的调度响应。但它不是魔法,不能替代绑核、IRQ 亲和性、内存锁定和驱动选择。Linux 内核实时文档说明 PREEMPT_RT 会改变锁、中断和调度语义,因此做 EtherCAT 时要把 RT 内核、主站驱动、网卡驱动和线程优先级一起验证,而不是只看 uname -a 里有没有 PREEMPT_RT。
IgH EtherCAT Master 是 Linux 上常见的 EtherCAT 主站方案,EtherLab 页面说明它可用于 Linux 实时应用,并支持 RT-Preempt、Xenomai、RTAI 等实时环境。工程上要把配置阶段和实时周期阶段分开:SDO、PDO 映射、从站扫描、状态切换这些配置动作放在普通上下文,实时周期里只做必要的 receive -> process -> send,避免在实时环里做字符串处理、动态申请、复杂日志和阻塞等待。
实时优先级建议形成层级,而不是所有线程都设成 99。比如 EtherCAT 周期线程 90,伺服控制计算 85,安全监控 80,通信桥 60,日志和诊断保持普通优先级。这样可以避免多个 RT 线程互相饿死,也方便分析谁抢占了谁。上线前必须用 cyclictest、EtherCAT 周期统计和应用内部时间戳一起验证,不能只看业务表现“感觉不卡”。
sudo cyclictest --mlockall --smp --priority=80 --interval=200 --distance=0
sudo cyclictest --mlockall --priority=90 --interval=1000 --affinity=6
6.1 RK3588 bash 例子:确认 RT 内核、调度策略和内存锁定
在 RK3588 上验证 PREEMPT_RT,不能只看内核名字。要同时确认 PREEMPT_RT 配置、线程调度策略、实时优先级、内存锁定上限和实际运行 CPU。很多现场问题来自 ulimit -l 太小导致 mlockall() 失败,或者 systemd 没放开 LimitRTPRIO,导致程序以为自己在实时调度,实际仍是普通 SCHED_OTHER。下面这些命令适合写入产测脚本,启动后自动输出实时环境状态。
uname -a
zcat /proc/config.gz 2>/dev/null | egrep "CONFIG_PREEMPT|CONFIG_PREEMPT_RT|CONFIG_HZ="
ulimit -r
ulimit -l
ps -eLo pid,tid,cls,rtprio,pri,psr,comm | egrep "ecat|IRQ|cyclic|control"
chrt -p "$(pidof ecat_control)" 2>/dev/null || true
grep -E "Cpus_allowed_list|Mems_allowed_list|voluntary_ctxt_switches|nonvoluntary_ctxt_switches" \
/proc/"$(pidof ecat_control)"/status 2>/dev/null || true

7. 硬件编解码必须全上:视频不能再用 CPU 硬扛
在双 RK 产品里,摄像头、录制、推流、识别预处理和 UI 显示经常和控制系统同时存在。如果视频解码、编码、缩放、颜色转换还在 CPU 上跑,很容易把 A76 核吃满,间接影响控制周期。Rockchip 官方 MPP 文档说明 MPP 是 Rockchip 平台的视频编解码 parser 和硬件抽象层,RK3588 资料也列出 8K 视频解码、8K 视频编码、JPEG 编解码等多媒体能力,所以视频链路必须优先走 MPP/RGA/硬件零拷贝。
实际工程里要避免“看起来用了硬解,实际还在 CPU 拷贝”的假加速。视频链路最好采用 MPP 解码、RGA 缩放或颜色转换、DMA-BUF/DRM PRIME 零拷贝,再把结果送到显示、编码或 AI 预处理。跨 RK 传输时也不要传原始大图,能传 H.264/H.265 就硬编后传,能传 ROI、结构化结果和时间戳就不要传全帧,实时控制板只接收轻量指令和状态,不参与重视频处理。
如果使用 FFmpeg/GStreamer,要确认实际 codec 是否走 rkmpp、V4L2 M2M 或板厂提供的硬件插件,并用 top、perf top、/proc/interrupts 和温度频率记录验证。硬件编解码不是为了让视频更快,而是为了把 CPU 从非实时任务里释放出来。只要视频软件解码占了控制板的大核,就算平均帧率正常,也可能在某个关键控制周期制造不可接受的抖动。
7.1 RK3588 bash 例子:检查 MPP/RGA/VPU 设备和 CPU 占用
…详情请参照古月居
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)