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=yCONFIG_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,分区规划要精打细算。我的方案是:
| 分区 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| uboot | 0x000000 | 4MB | U-Boot + SPL |
| trust | 0x400000 | 2MB | ATF固件 |
| boot | 0x600000 | 8MB | 内核Image + DTB |
| env | 0xE00000 | 512KB | U-Boot环境变量 |
| reserved | 0xE80000 | 1.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 0x8000000x40000000是内核Image加载到内存的地址,这个地址要和U-Boot的bootcmd里一致。
4.2 NVMe根文件系统的挂载与调优
NVMe SSD作为根文件系统,挂载参数对实时性影响很大。/etc/fstab里这样写:
/dev/nvme0n1p1 / ext4 defaults,noatime,nodiratime,commit=60,data=writeback 0 1noatime和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' saveenvisolcpus=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, ¶m);优先级设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 done0f是十六进制,对应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μs | 4200μs | 800μs |
| RT补丁+基础调优 | 8μs | 380μs | 120μs |
| RT+隔离+中断亲和 | 6μs | 95μs | 45μs |
| 上述+NVMe调度器none | 5μs | 72μs | 38μ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_governorperformance调速器让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主站方向扩展,把网口的实时性再压一压,那就是另一个话题了。