• 正文
  • 相关推荐
申请入驻 产业图谱

RK双板架构下的EtherCAT实时控制优化实践:绑核、隔离、中断亲和、硬编解码与交叉编译

4小时前
61
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

转载自公众号:敢敢AUTOHUB

核心思路

在 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 占用

RK3588 上做硬编解码,第一步是确认设备节点、内核驱动和用户态库是否存在。不同板厂镜像设备节点名称会有差异,常见包括 /dev/mpp_service、/dev/rga、/dev/dri/*、/dev/dma_heap/*、/dev/rkvdec、/dev/rkvenc 等。真正验证是否硬件生效,不能只看程序参数,要同时看 CPU 占用、VPU/RGA 中断增长、温度和帧率。如果 CPU 上出现大量 H.264/H.265 解码函数,说明链路可能退回软件解码。

ls -l /dev | egrep "mpp|rga|vpu|rkv|dri|dma_heap|iep|hevc|h265|jpeg" || true
ldconfig -p 2>/dev/null | egrep "rockchip|mpp|rga" || true
grep -E "mpp|rga|vpu|rkv|jpeg|dri" /proc/interrupts || true

# 运行视频任务时观察 CPU 和中断
VIDEO_PROCESS=video_service
top -H -p "$(pidof "${VIDEO_PROCESS}")" 2>/dev/null
watch -n 1 'grep -E "mpp|rga|vpu|rkv|jpeg|dri" /proc/interrupts'

7.2 GStreamer/FFmpeg 思路:目标是零拷贝和少 CPU

不同系统对 RKMPP、V4L2 request、GStreamer 插件和 FFmpeg 编译选项支持差异很大,所以这里不写死唯一命令。工程上要抓住三个判断:第一,解码是否走硬件;第二,缩放和颜色转换是否走 RGA 或硬件路径;第三,帧在解码、推理、显示或编码之间是否发生多次 CPU memcpy。只要链路里出现大分辨率 NV12/RGB 来回拷贝,CPU 就会被视频任务吃掉,实时控制核即使隔离了,也可能因为内存带宽和 DDR 争用产生尾延迟。

# GStreamer 插件探测
gst-inspect-1.0 2>/dev/null | egrep -i "mpp|rga|rockchip|v4l2|kmssink"

# FFmpeg 编译能力探测
ffmpeg -hide_banner -hwaccels 2>/dev/null
ffmpeg -hide_banner -decoders 2>/dev/null | egrep -i "rkmpp|v4l2|h264|hevc"
ffmpeg -hide_banner -encoders 2>/dev/null | egrep -i "rkmpp|v4l2|h264|hevc"

8. 交叉编译全上:构建链路要为 RK 平台专门优化

RK 产品不要长期依赖板端编译。板端编译慢、依赖不可控、版本容易漂移,也会让发布包难以复现。更合理的方式是在 x86 主机上使用固定 sysroot 和交叉编译工具链,针对 aarch64 生成控制程序、EtherCAT 用户态组件、业务服务和多媒体模块。所有产物都要记录编译器版本、glibc 版本、内核头文件版本和目标板 rootfs 版本,避免“开发板能跑,产线镜像不能跑”的问题。

交叉编译优化不等于盲目 -O3。控制代码更重视确定性和可调试性,默认可以用 -O2 -pipe -fno-omit-frame-pointer,关键数学模块再单独评估 -O3、NEON、LTO 或手写 SIMD。多媒体和 AI 模块可以更激进,但实时控制模块不要引入不可控的异常、隐式分配和复杂运行时依赖。最终要用同一个工具链生成 release、debug symbols 和 perf 可解析符号,方便线上定位。

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR aarch64)
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
set(CMAKE_SYSROOT /opt/rk/sysroot)
set(CMAKE_C_FLAGS_RELEASE "-O2 -pipe -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -pipe -fno-omit-frame-pointer")

8.1 RK3588 bash 例子:导出 sysroot 并验证动态库依赖

交叉编译最容易踩的坑是 sysroot 和目标板运行环境不一致。建议从目标 RK3588 镜像导出 sysroot,或由同一套 rootfs 构建系统生成 sysroot,然后在 CI 中固定版本。编译完成后,要用 file、readelf、ldd 或目标板上的 lddtree 检查产物架构和动态库依赖。控制程序最好减少动态依赖,尤其不要让实时主进程依赖巨大图形、多媒体或 Python 运行时。

# 从目标板同步 sysroot 到构建机
rsync -a --delete root@rk3588:/lib ./sysroot/
rsync -a --delete root@rk3588:/usr/include ./sysroot/usr/
rsync -a --delete root@rk3588:/usr/lib ./sysroot/usr/

# 检查交叉编译产物
file build/ecat_control
aarch64-linux-gnu-readelf -d build/ecat_control | egrep "NEEDED|RPATH|RUNPATH"
aarch64-linux-gnu-objdump -x build/ecat_control | grep NEEDED

9. 一套可落地的调优顺序

推荐的落地顺序是固定的:第一步先建立基线,记录 CPU 占用、频率、温度、IRQ 分布、EtherCAT 周期最大抖动和 cyclictest 最大延迟;第二步做进程和线程绑核,把控制链路固定在指定 A76 核;第三步把非实时任务迁走,让视频、AI、UI 和日志远离控制核;第四步做 IRQ 亲和性,把 EtherCAT 网卡 IRQ 和其他设备 IRQ 分开;第五步再上 isolcpus/nohz_full/rcu_nocbs/irqaffinity 等启动参数。

第六步验证 PREEMPT_RT 和 EtherCAT 主站组合,把主站线程优先级、网卡驱动和实时周期跑满压力测试;第七步把所有视频编解码、缩放、颜色转换迁到 MPP/RGA/硬件链路;第八步统一交叉编译和发布工具链,确保性能优化可以复现;第九步才改实时代码,把动态内存、阻塞 IO、日志和锁竞争从控制周期里清出去。每一步都要有数据,不要凭感觉跳级。

# 1. 看 CPU、频率和中断
lscpu -e
watch -n 1 'cat /proc/interrupts | egrep "eth|gmac|usb|mipi|rga|vpu|gpu"'

# 2. 固定控制进程
sudo chrt -f 90 taskset -c 6 ./ecat_control

# 3. 测调度延迟
sudo cyclictest --mlockall --priority=90 --interval=1000 --affinity=6

# 4. 固定 EtherCAT 网卡 IRQ
ECAT_IRQ=123
grep -E "eth|gmac|stmmac" /proc/interrupts
echo 6 | sudo tee "/proc/irq/${ECAT_IRQ}/smp_affinity_list"

9.1 一键采集脚本:每次优化前后都保存证据

实时优化必须留下证据,否则很容易变成“感觉好了”。下面脚本把 CPU 拓扑、启动参数、IRQ、调度器、频率、温度、进程分布和 cyclictest 结果打包到一个目录。每次修改绑核、IRQ、bootargs、视频链路或交叉编译参数后,都运行一次并归档。这样后面出现现场问题时,可以对比哪一次改动让最大延迟下降,哪一次改动引入了新的中断尖峰。

#!/usr/bin/env bash
set -euo pipefail
OUT="rt_report_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$OUT"

uname -a > "$OUT/uname.txt"
cat /proc/cmdline > "$OUT/cmdline.txt"
lscpu -e > "$OUT/lscpu.txt"
cat /proc/interrupts > "$OUT/interrupts_before.txt"
ps -eLo pid,tid,cls,rtprio,pri,psr,comm > "$OUT/threads.txt"
for f in /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor; do
  echo "$f $(cat "$f" 2>/dev/null)" >> "$OUT/governor.txt"
done

sudo cyclictest --mlockall --priority=90 --interval=1000 --affinity=6 --duration=60s 
  | tee "$OUT/cyclictest_cpu6.txt"
cat /proc/interrupts > "$OUT/interrupts_after.txt"
tar czf "${OUT}.tar.gz" "$OUT"
echo "saved ${OUT}.tar.gz"

9.2 验证标准:看最大延迟,不只看平均 CPU

实时优化的验收指标必须看 worst-case,而不是平均值。CPU 平均占用从 70% 降到 40% 没有意义,如果 EtherCAT 周期最大抖动仍然偶发 3 ms,伺服就可能报警。建议记录至少四类数据:cyclictest 最大延迟、EtherCAT 周期实际间隔最大值、控制计算执行时间最大值、IRQ 在各 CPU 上的分布变化。每次改 bootargs、绑核策略、IRQ 亲和性或硬件编解码链路,都要重新跑同一套压力测试。

压力测试不能只空载跑。真实产品里要同时打开相机、推流、AI 推理、日志上传、UI、网络通信和 EtherCAT 多轴运动,让系统进入最坏组合。只有在这种状态下 EtherCAT 周期仍然稳定,才说明优化有效。最终报告建议用 P99、P99.9、P99.99 和 Max 记录,而不是只写平均延迟。机器人现场出问题,往往就是那一次 Max,而不是平均值。

10. 总结

RK 平台做 EtherCAT 控制的关键,不是盲目相信芯片算力,也不是一开始就重写控制算法,而是先建立实时边界。CPU 不够先绑核,把关键线程固定住;绑核还抖就做中断隔离,把 IRQ、workqueue 和系统噪声赶出控制核;系统层面仍不稳,再用 PREEMPT_RT、内存锁定、频率固定和主站优先级继续压 jitter;最后才改代码,把实时路径做成无阻塞、预分配、少锁、少系统调用的确定性链路。

双 RK 架构的价值在于把问题拆开:实时板要干净,业务板要能吞重任务;控制链路要确定,视频和 AI 要硬件加速;开发链路要可复现,交叉编译和 sysroot 要统一。只要按这个顺序推进,RK 上跑 EtherCAT 并不是靠“硬扛”,而是靠系统资源规划、实时调度、硬件加速和工程纪律一起把最坏延迟压下来。

参考资料

    • Linux Kernel Documentation: CPU Isolation (https://docs.kernel.org/admin-guide/cpu-isolation.html)• Linux Kernel Documentation: SMP IRQ affinity (https://www.kernel.org/doc/html/v6.2/core-api/irq/irq-affinity.html)• Linux Kernel Documentation: Real-time preemption (https://docs.kernel.org/next/core-api/real-time/index.html)• Linux Foundation Wiki: cyclictest (https://wiki.linuxfoundation.org/realtime/documentation/howto/tools/cyclictest/start)• Linux Kernel Documentation: CPUSETS (https://docs.kernel.org/admin-guide/cgroup-v1/cpusets.html)• EtherLab / IgH EtherCAT Master (https://etherlab.org/en_GB/ethercat)• EtherCAT Technology Group: EtherCAT Master for Linux as part of EtherLab (https://www.ethercat.org/en/products/263FCDB7639F4DDFB3587C18F46BA289.htm)• Rockchip Open Source Documentation: MPP (https://opensource.rock-chips.com/wiki_Mpp)• Rockchip Open Source Documentation: RGA (https://opensource.rock-chips.com/wiki_RGA)• Rockchip RK3588 Brief Datasheet (https://www.rock-chips.com/uploads/pdf/2022.8.26/191/RK3588%20Brief%20Datasheet.pdf)• Linux manual page: sched_setaffinity (https://man7.org/linux/man-pages/man2/sched_setaffinity.2.html)

相关推荐