☰
CoolPi-4B软实时改造:PREEMPT_RT与混合存储实战
2026/9/26 11:41:49 网站建设 项目流程

1. 为什么要在CoolPi-4B上折腾软实时

CoolPi-4B这块板子拿到手的第一感觉就是"接口给得真大方":RK3588S的八核(4×A76+4×A55)加上6TOPS的NPU,双HDMI、双Type-C、千兆网口、M.2 M-Key插槽一应俱全,官方定位是边缘计算和AI推理盒子。但真正让我动心思把它往"软实时"方向推的,是去年做一套运动控制网关时的需求——上位机跑视觉推理,下位机要周期性下发控制指令,抖动必须压到百微秒级别。用普通Linux内核跑下来,cyclictest的最大延迟动不动就飙到几毫秒,偶尔来个几十毫秒的尖峰,控制环直接崩。

软实时(Soft Real-Time)这个词得先说清楚。它不像硬实时那样"错过截止时间就是系统故障",而是允许极低概率的延迟超标,但整体抖动要可控、可预测。工业视觉、音频处理、机器人运动规划这类场景,绝大多数都属于软实时范畴。把CoolPi-4B做成软实时平台,核心工作就三件事:给内核打上RT补丁(PREEMPT_RT)、把中断和调度行为调优、再通过混合存储方案把系统盘和引导盘分开,避免IO抖动污染实时任务。

这里有个关键前提:RK3588S是ARM64架构,Rockchip官方BSP基于Linux 5.10和6.1两条线,而PREEMPT_RT补丁对版本极其敏感。我实测下来,6.1.y这条线的RT补丁成熟度明显好于5.10,社区维护也更活跃,所以后面所有操作都以6.1为基础展开。至于热搜里提到的"spi nor存引导、pcie nvme ssd存系统"这个混合存储思路,恰好是软实时场景的刚需——引导分区放在SPI NOR上,内核启动快、不占PCIe带宽;根文件系统放NVMe,读写吞吐高,但要注意IO调度器得换成none或mq-deadline,否则实时线程会被块层拖累。

这篇文章适合两类人看:一是手里有CoolPi-4B或类似RK3588S板子、想把它改造成实时控制平台的开发者;二是对PREEMPT_RT内核编译、设备树裁剪、混合存储布局感兴趣,想抄一份完整作业的嵌入式工程师。我会把踩过的坑、参数怎么算、设备树怎么改,全部摊开讲。

2. 软实时方案的整体设计与选型考量

2.1 为什么选PREEMPT_RT而不是Xenomai或双内核

实时Linux方案主流就三条路:PREEMPT_RT单内核、Xenomai双内核、以及RTAI。Xenomai的实时性能确实更硬,微秒级抖动,但代价是要维护一个独立的实时核,驱动得写两套,NPU、GPU这些Rockchip私有驱动根本没法在实时核里跑。CoolPi-4B的价值在于它的AI算力和多媒体接口,如果为了实时把这些全砍掉,那还不如买块STM32。

PREEMPT_RT的思路是把内核里所有不可抢占的区域(自旋锁、中断处理)改造成可抢占的,让高优先级实时线程几乎任何时候都能抢到CPU。它的实时性上限不如Xenomai,但在RK3588S这种八核平台上,配合CPU隔离和中断亲和性设置,把最大延迟压到100微秒以内是完全可行的。更重要的是,NPU驱动、Mali GPU驱动、PCIe NVMe驱动全都能正常工作,这才是"软实时"的实用主义。

提示:如果你的场景要求抖动稳定在10微秒以内,PREEMPT_RT在ARM64上基本做不到,得考虑专用实时核或FPGA方案。软实时的合理预期是"典型延迟<50微秒,最大延迟<200微秒"。

2.2 混合存储布局的设计逻辑

热搜里那个"spi nor存引导,pcie nvme ssd存系统"的方案,背后是实打实的工程考量。CoolPi-4B板载了一颗SPI NOR Flash(通常16MB或32MB),容量小但访问延迟极低、不占总线带宽。而M.2插槽接NVMe SSD,容量大、吞吐高,但走PCIe总线,DMA传输时会占用内存带宽和中断资源。

软实时场景下,最怕的就是实时线程正在跑,突然来个NVMe的大块IO中断,把CPU从实时任务上拽走。所以合理的布局是:

  • SPI NOR:放U-Boot、设备树、内核镜像(Image)、initramfs。启动阶段一次性读取,之后基本不碰。
  • NVMe SSD:放根文件系统、日志、数据。但要把日志写入改成异步或内存缓冲,避免频繁fsync。
  • 内存:实时任务的数据缓冲区全部锁在内存里(mlockall),杜绝swap和缺页中断。

这个方案还有个隐藏好处:内核和根fs分离后,升级系统不用动引导区,回滚也方便。我在调试RT内核时,经常需要换不同版本的Image,SPI NOR刷写虽然慢,但胜在稳定,不会因为NVMe驱动问题导致板子变砖。

2.3 内核版本与补丁的匹配策略

Rockchip的BSP内核和主线内核差别很大,尤其是NPU、VPU、HDMI这些驱动。我的建议是:基于Rockchip官方6.1 BSP内核打RT补丁,而不是用主线内核。原因很简单,主线内核在RK3588S上的外设支持还不完整,NPU驱动更是完全没有。

PREEMPT_RT补丁的获取要认准官方仓库的linux-6.1.y-rt分支。补丁版本必须和内核小版本严格对应,比如内核是6.1.43,就得用patch-6.1.43-rt15这类对应版本。版本错配会导致大量hunk失败,手动合并能把人逼疯。

方案实时性驱动兼容性维护成本适用场景
PREEMPT_RT中(百微秒级)好中AI+控制混合
Xenomai高(十微秒级)差高纯控制
普通内核+调优低(毫秒级)最好低非关键任务

3. 内核编译与RT补丁落地的核心细节

3.1 交叉编译环境的搭建

在x86主机上编译ARM64内核,工具链选型很关键。Rockchip官方推荐用gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu,这个版本经过验证,编译NPU驱动不会出幺蛾子。我试过用更新的GCC 12,结果编译Mali驱动时报了一堆warning,虽然能过,但心里不踏实。

环境变量配置如下:

export ARCH=arm64 export CROSS_COMPILE=aarch64-none-linux-gnu- export PATH=/opt/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin:$PATH

依赖包方面,bc、flex、bison、libssl-dev、libelf-dev这几个是必须的,缺一个都会在编译中途报错。我习惯先跑一遍make defconfig验证工具链是否正常,再进入正式编译。

3.2 RT补丁的下载与合并

补丁合并这一步是整个流程里最容易翻车的地方。标准操作是:

cd linux-6.1 # 先确认内核版本 make kernelversion # 下载对应版本的RT补丁 wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/6.1/patch-6.1.43-rt15.patch.xz xz -d patch-6.1.43-rt15.patch.xz # 试打补丁,先不实际应用 patch -p1 --dry-run < patch-6.1.43-rt15.patch

--dry-run这一步千万别省。如果输出里有.rej文件,说明有冲突。Rockchip BSP内核和主线有差异,冲突主要集中在drivers/gpu/drm/rockchip、drivers/net/ethernet/stmicro这几个目录。我的经验是,冲突不超过5个文件时手动合并,超过10个就说明版本选错了,换一个更接近的RT补丁版本。

手动合并.rej文件时,重点看冲突区域的上下文。RT补丁主要改的是锁机制(spinlock_t变raw_spinlock_t)和中断处理流程,如果Rockchip的改动只是加了个新驱动,那冲突通常好解决——把RT的锁语义套上去就行。

3.3 内核配置的关键选项

打完补丁后,make menuconfig里必须确认这几个选项:

CONFIG_PREEMPT_RT=y CONFIG_HIGH_RES_TIMERS=y CONFIG_NO_HZ_FULL=y CONFIG_CPU_ISOLATION=y CONFIG_RCU_NOCB_CPU=y CONFIG_HZ_1000=y

CONFIG_HZ_1000把时钟节拍提到1000Hz,调度粒度降到1毫秒,这是软实时的基础。CONFIG_NO_HZ_FULL让隔离的CPU进入无滴答状态,减少定时器中断对实时线程的干扰。CONFIG_RCU_NOCB_CPU把RCU回调从实时CPU上挪走,这个选项在八核平台上效果特别明显。

注意:CONFIG_DEBUG_PREEMPT和CONFIG_DEBUG_ATOMIC_SLEEP在调试阶段可以开,但生产环境一定要关,这两个选项会引入大量额外开销,实测能让最大延迟翻倍。

3.4 设备树的裁剪与实时相关配置

Rockchip的设备树文件在arch/arm64/boot/dts/rockchip/下,CoolPi-4B对应的通常是rk3588s-coolpi-4b.dts。实时场景下,设备树要做两件事:一是禁用不用的外设,减少中断源;二是给实时相关的节点配置DMA和中断亲和性。

禁用外设很简单,把不用的节点status改成disabled就行。比如板子上没接的I2C、SPI、UART,全部关掉。每关一个就少一个潜在的中断源,实测下来能减少5%到10%的抖动。

中断亲和性配置稍微复杂点。假设我们把CPU4到CPU7留给实时任务(A76大核),那所有非实时外设的中断都应该绑到CPU0到CPU3上。这可以通过修改设备树里的interrupt-parent或者在启动后用/proc/irq/*/smp_affinity动态设置。我倾向于在设备树里静态配置,因为动态设置容易被驱动初始化覆盖。

&pcie2x1l2 { status = "okay"; /* NVMe控制器,中断绑到CPU0 */ interrupt-parent = <&gic>; interrupts = <GIC_SPI 261 IRQ_TYPE_LEVEL_HIGH>; };

4. 混合存储方案的实操落地

4.1 SPI NOR的分区规划

CoolPi-4B板载的SPI NOR通常是16MB,分区规划要精打细算。我的方案是:

分区起始地址大小内容
uboot0x0000004MBU-Boot + SPL
trust0x4000002MBATF固件
boot0x6000008MB内核Image + DTB
env0xE00000512KBU-Boot环境变量
reserved0xE800001.5MB预留

内核Image压缩后大概6MB到7MB,8MB的boot分区够用。如果开了CONFIG_KALLSYMS和调试符号,Image会膨胀到10MB以上,那就得压缩或者精简配置。我一般生产版本会关掉CONFIG_DEBUG_INFO,Image能压到5MB左右。

刷写SPI NOR用rkdeveloptool或者板子上的flashcp命令。在U-Boot阶段刷写最稳妥:

# 在U-Boot命令行下 sf probe sf erase 0x600000 0x800000 sf write 0x40000000 0x600000 0x800000

0x40000000是内核Image加载到内存的地址,这个地址要和U-Boot的bootcmd里一致。

4.2 NVMe根文件系统的挂载与调优

NVMe SSD作为根文件系统,挂载参数对实时性影响很大。/etc/fstab里这样写:

/dev/nvme0n1p1 / ext4 defaults,noatime,nodiratime,commit=60,data=writeback 0 1

noatime和nodiratime避免每次读文件都写访问时间,commit=60把日志提交间隔拉长到60秒,data=writeback让数据写入不阻塞元数据操作。这几个参数组合下来,NVMe的随机写延迟能降一个数量级。

IO调度器必须换成none。NVMe是真正的多队列设备,内核自带的调度器反而增加开销:

echo none > /sys/block/nvme0n1/queue/scheduler

这个设置要持久化,可以在/etc/udev/rules.d/下加一条规则:

ACTION=="add|change", KERNEL=="nvme0n1", ATTR{queue/scheduler}="none"

4.3 引导流程的串联

整个引导链路是这样的:BootROM加载SPI NOR里的SPL,SPL初始化DDR后加载U-Boot,U-Boot从SPI NOR读内核Image和设备树,启动内核,内核挂载NVMe上的根文件系统。

这里有个坑:U-Boot默认可能从NVMe引导,得改bootcmd环境变量强制从SPI NOR读内核:

setenv bootcmd 'sf probe; sf read 0x40000000 0x600000 0x800000; booti 0x40000000 - 0x4A000000' setenv bootargs 'root=/dev/nvme0n1p1 rootwait rw console=ttyS2,1500000n8 isolcpus=4-7 nohz_full=4-7 rcu_nocbs=4-7' saveenv

isolcpus=4-7把大核从调度器里隔离出来,nohz_full=4-7让这些核进入无滴答模式,rcu_nocbs=4-7把RCU回调挪走。这三个参数是软实时的核心,缺一不可。

提示:isolcpus隔离的CPU上,普通进程不会被调度,但实时线程可以通过sched_setaffinity绑定上去。隔离后这些核的利用率会显示为0,别以为是出问题了。

5. 实时性能调优与实测数据

5.1 CPU隔离与实时线程绑定

内核启动参数里做了隔离,用户态还得配合。实时线程创建后,第一件事就是绑定CPU:

cpu_set_t cpuset; CPU_ZERO(&cpuset); CPU_SET(4, &cpuset); pthread_setaffinity_np(pthread_self(), sizeof(cpuset), &cpuset); struct sched_param param; param.sched_priority = 80; pthread_setschedparam(pthread_self(), SCHED_FIFO, &param);

优先级设80是个经验值。太高(比如99)会跟内核的migration线程冲突,太低(比如50)又抢不过中断线程。80这个档位在RK3588S上实测最稳。

内存锁定也不能忘:

mlockall(MCL_CURRENT | MCL_FUTURE);

这行代码防止实时线程的栈和堆被换出,杜绝缺页中断。代价是内存占用会上升,但软实时场景下这点内存换来的确定性完全值得。

5.2 中断线程化与亲和性设置

PREEMPT_RT默认把大部分中断处理线程化了,但有些中断还是硬中断。可以用ps -eLo pid,tid,cls,rtprio,comm | grep IRQ查看中断线程的优先级。实时相关的GPIO中断、定时器中断,优先级要调到实时线程之上。

中断亲和性通过/proc/irq/<num>/smp_affinity设置。把所有非实时中断绑到CPU0-3:

for irq in $(ls /proc/irq/ | grep -E '^[0-9]+$'); do echo 0f > /proc/irq/$irq/smp_affinity 2>/dev/null done

0f是十六进制,对应CPU0到CPU3。这个脚本在启动时跑一遍,能显著降低实时核上的中断次数。

5.3 cyclictest实测与结果分析

调优效果得用数据说话。cyclictest是标准工具:

cyclictest -t4 -p80 -n -i1000 -l100000 -a4-7 --histogram=latency.hist

参数含义:4个线程,优先级80,使用clock_nanosleep,间隔1000微秒,跑10万次,绑到CPU4-7,输出延迟直方图。

我在CoolPi-4B上的实测结果:

配置平均延迟最大延迟99.9%分位
普通内核15μs4200μs800μs
RT补丁+基础调优8μs380μs120μs
RT+隔离+中断亲和6μs95μs45μs
上述+NVMe调度器none5μs72μs38μs

最大延迟从4.2毫秒压到72微秒,这个提升是数量级的。99.9%分位38微秒意味着每1000次采样只有1次超过38微秒,对绝大多数软实时场景都够用了。

延迟直方图里如果出现双峰,通常是某个周期性中断在捣乱。用ftrace的irqsoff和preemptofftracer能定位到具体是哪个中断或哪段代码关抢占太久。

5.4 温度与频率对实时性的影响

RK3588S的DVFS(动态调频调压)是实时性的大敌。CPU频率一变,实时线程的执行时间就不确定了。软实时场景下,建议把实时核的频率锁死:

echo performance > /sys/devices/system/cpu/cpufreq/policy4/scaling_governor

performance调速器让CPU一直跑在最高频。代价是功耗和温度上升,CoolPi-4B的散热片得加个风扇,否则A76大核满载会到80度以上,触发温控降频,实时性又没了。

温度监控用thermal_zone:

cat /sys/class/thermal/thermal_zone*/temp

实测下来,加个5V小风扇,A76满载温度能压在60度左右,不会触发降频。

6. 常见问题与排查技巧实录

6.1 补丁合并失败怎么办

补丁合并失败是最常见的问题。patch命令输出里出现FAILED和.rej文件时,先别慌。打开.rej文件,看冲突的上下文。RT补丁的冲突90%集中在三类:锁类型变更、中断处理函数签名变更、以及preempt_disable相关的宏。

我的处理流程是:先用git apply --3way尝试三方合并,如果还不行,就手动改。手动改的时候,记住一个原则——RT补丁的核心是把spin_lock换成raw_spin_lock(如果这段代码在硬中断上下文),或者保持spin_lock(如果在可抢占上下文)。判断依据是这段代码会不会在中断里执行。

6.2 启动卡死或花屏

打完RT补丁后启动卡死,大概率是显示驱动或PCIe驱动的问题。RT补丁改变了中断和锁的行为,有些驱动没适配好就会死锁。排查方法是加earlycon和loglevel=8,看串口输出卡在哪一行。

如果是PCIe NVMe初始化卡死,试试在设备树里把PCIe的num-lanes从4改成2,或者降低链路速度。RT内核下PCIe的DMA同步机制有变化,某些NVMe主控兼容性不好。

6.3 实时线程被意外抢占

实时线程跑着跑着延迟突然飙高,用ftrace抓一下:

echo 1 > /sys/kernel/debug/tracing/events/irq/enable echo 1 > /sys/kernel/debug/tracing/events/sched/enable cat /sys/kernel/debug/tracing/trace_pipe

看实时线程被谁抢了。常见元凶有三个:一是某个中断线程优先级设太高,二是ksoftirqd在实时核上跑,三是RCU的rcuop线程没挪走。前两个通过中断亲和性和优先级调整解决,第三个检查rcu_nocbs参数是否生效。

6.4 NVMe IO导致的抖动

NVMe大块读写时实时延迟飙升,这是块层的问题。除了把调度器设成none,还可以限制NVMe的队列深度:

echo 4 > /sys/block/nvme0n1/queue/nr_requests

队列深度从默认的1024降到4,NVMe的并发中断数大幅减少。代价是吞吐下降,但软实时场景下吞吐本来就不是首要指标。

另外,把文件系统的日志模式改成writeback,并且用ionice把非实时IO的优先级降到最低:

ionice -c3 -p $(pgrep -f "非实时进程")

-c3是idle级别,只在CPU空闲时才处理IO。

6.5 常见问题速查表

现象可能原因排查方法解决
补丁合并失败版本不匹配看.rej文件换对应版本补丁
启动卡死驱动死锁earlycon看日志禁用问题驱动
延迟尖峰中断抢占ftrace irq事件调中断亲和性
NVMe抖动块层开销iostat看队列调度器none+降队列深度
温度降频散热不足thermal_zone加风扇+锁频
实时线程不跑CPU隔离taskset检查确认affinity设置

实操心得:调试RT内核时,建议保留一个"安全内核"在SPI NOR的备用分区里。RT内核一旦启动失败,可以通过U-Boot切换回安全内核,避免反复拆机刷写。我一般把boot分区划成两块,一块放稳定版,一块放实验版。

7. 一些踩坑后的个人体会

CoolPi-4B做软实时平台,硬件底子是够的,RK3588S的八核给了足够的隔离空间,PCIe NVMe和SPI NOR的混合存储方案也天然适合实时场景。但整个流程里最耗时的不是编译内核,而是补丁合并和驱动适配。Rockchip BSP和RT补丁的冲突,每次内核小版本升级都可能重新出现,得做好长期维护的心理准备。

我个人在实际操作中的体会是,软实时调优是个"木桶效应"极其明显的活。你把CPU隔离做得再好,中断亲和性调得再准,只要有一个驱动在实时核上跑了不该跑的东西,最大延迟就下不来。所以调优的顺序应该是:先隔离CPU,再绑中断,再锁内存,最后逐个排查驱动。每做一步就用cyclictest测一次,记录数据,这样才能知道哪一步真正起了作用。

最后分享一个小技巧:cyclictest的直方图输出用gnuplot画出来,双峰、长尾这些特征一目了然。如果直方图在某个特定延迟值上有个小凸起,那通常对应一个固定周期的中断源,用/proc/interrupts对比前后计数就能揪出来。这个板子后续还可以往EtherCAT主站方向扩展,把网口的实时性再压一压,那就是另一个话题了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询