Jetson Orin NX实时内核编译与烧录实战:从PREEMPT_RT补丁到系统调优
1. 项目缘起:为什么要在边缘设备上折腾实时内核?
最近在搞一个基于NVIDIA Jetson的边缘计算项目,涉及到高速视觉检测和机械臂的协同控制。项目原型在标准Linux内核上跑得还行,但一到高负载、多线程并发的时候,偶尔就会出现几毫秒甚至十几毫秒的延迟抖动。对于需要精确时序控制的场景,比如要求每10毫秒稳定触发一次拍照和计算,这种抖动是致命的——它直接导致图像采集与机械臂动作不同步,轻则产品良率下降,重则可能引发设备碰撞。
排查了一圈,从应用层代码优化到系统负载监控,最终问题指向了内核调度器的非确定性延迟。标准Linux内核(比如Ubuntu默认的通用内核)为了追求整体吞吐量和公平性,其调度、中断处理、内存管理等行为并非完全“实时”。这就引出了我们今天要折腾的主角:PREEMPT_RT(Real-Time)补丁。简单说,它通过一系列深度修改,将Linux内核变成一个“硬实时”或“软实时”系统,大幅降低任务调度的最坏情况延迟,使其变得可预测。
我手头的设备是Seeed推出的reComputer Jetson系列,搭载了NVIDIA最新的JetPack 6.2.1(基于Ubuntu 22.04)。官方镜像默认不包含RT内核。虽然NVIDIA为部分Jetson模块提供了预编译的RT内核镜像,但对于reComputer这类载板,或者需要自定义内核配置的情况,从源码开始交叉编译和烧录是更稳妥、也更深入理解系统的方式。这个过程涉及BSP(Board Support Package)获取、内核配置、设备树编译、模块打包等一系列步骤,任何一个环节出错都可能导致设备无法启动。接下来,我就把在reComputer Jetson Orin NX上成功烧录PREEMPT_RT内核的完整过程、踩过的坑以及核心原理梳理出来。
2. 环境准备与核心概念澄清
在动手之前,我们需要准备好“战场”,并理解几个关键概念,这能帮你避开很多后续的迷惑。
2.1 硬件与软件清单
- 目标设备(Target): Seeed reComputer Jetson Orin NX(16GB版本)。其核心是Jetson Orin NX模组。不同Jetson模组(如Orin NX, Orin Nano, AGX Orin)的BSP和内核配置可能有差异,本文流程主要针对Orin NX系列,但思路通用。
- 开发主机(Host): 一台x86_64架构的Ubuntu 22.04 LTS电脑。这是NVIDIA官方推荐的开发环境。虚拟机理论上可行,但涉及USB烧录和大量编译,物理机或性能强大的虚拟机更佳。
- 关键软件:
- JetPack 6.2.1 BSP 和根文件系统: 这是为你的特定Jetson模组定制的软件包,包含U-Boot引导程序、内核源码、设备树、驱动模块和基本的根文件系统。你需要从NVIDIA开发者网站下载。
- L4T(Linux for Tegra)工具链: 主要是
nvflash和nv_boot_control等工具,用于将编译好的镜像烧录到设备。 - ARM64交叉编译工具链: 因为Jetson是ARM64(aarch64)架构,我们需要在x86主机上安装对应的gcc等工具来编译内核。
- PREEMPT_RT补丁: 对应特定Linux内核版本的实时补丁文件。
注意:确保开发主机有充足的磁盘空间(建议至少100GB空闲),因为内核源码、编译中间文件和根文件系统会占用大量空间。
2.2 PREEMPT_RT 补丁与内核版本绑定
这是第一个容易踩坑的点。PREEMPT_RT补丁高度依赖特定的Linux内核版本。你不能随便拿一个5.15版本的补丁打到5.10的内核上。JetPack 6.2.1 默认使用的内核版本是5.10.120-tegra(可以通过uname -r在设备上查看)。因此,我们必须寻找与5.10.120版本号精确匹配的PREEMPT_RT补丁。
补丁通常以.patch.xz或.patch.gz格式发布在 kernel.org 或实时内核的维基页面上。你需要找到类似patch-5.10.120-rt.patch.xz这样的文件。如果找不到完全一致的版本,可能需要寻找最接近的版本(如5.10.119-rt)并尝试手动调整,但这会引入风险,强烈建议使用完全匹配的版本。
2.3 烧录模式:Force Recovery Mode 是关键
Jetson设备有两种主要的启动模式:
- 正常模式(Normal Mode): 从eMMC或NVMe存储中加载系统。
- 强制恢复模式(Force Recovery Mode): 设备上电时,按住Force Recovery按钮(在reComputer上通常是一个标有“REC”或“FRC”的小按钮)不松,然后短按一下Reset按钮,再松开Force Recovery按钮。此时设备会进入一个特殊的USB恢复状态,等待主机通过
nvflash工具发送镜像。
我们整个烧录过程,包括刷写引导程序、内核、设备树等,都必须在强制恢复模式下进行。这是与普通PC刷BIOS或树莓派烧录SD卡截然不同的地方。
3. 一步步获取与准备源码
有了清晰的概念,我们开始实操。第一步是获取所有必要的源代码和工具。
3.1 下载 JetPack 6.2.1 BSP
- 访问 NVIDIA 开发者网站的 Jetson 下载中心。
- 找到JetPack 6.2.1的下载链接。对于 Orin NX,你需要下载
Jetson Orin NX/T234 BSP包,文件名可能类似于Jetson_Linux_R36.2.1_aarch64.tbz2。这个压缩包包含了我们需要的所有基础组件。 - 同时,下载对应的根文件系统(Root Filesystem),例如
Tegra_Linux_Sample-Root-Filesystem_R36.2.1_aarch64.tbz2。
# 在开发主机上创建一个工作目录 mkdir -p ~/jetson-rt-kernel cd ~/jetson-rt-kernel # 假设你将下载的BSP和根文件系统放到了当前目录 # 解压BSP包,这会创建一个 `Linux_for_Tegra` 目录 tar -xjf Jetson_Linux_R36.2.1_aarch64.tbz2 # 解压根文件系统到指定位置 cd Linux_for_Tegra/rootfs/ sudo tar -xjf ../../Tegra_Linux_Sample-Root-Filesystem_R36.2.1_aarch64.tbz2 cd ..3.2 获取并应用 PREEMPT_RT 补丁
进入内核源码目录。在
Linux_for_Tegra/source/public/下,你应该能找到kernel_src.tbz2,解压它。cd ~/jetson-rt-kernel/Linux_for_Tegra/source/public tar -xjf kernel_src.tbz2 cd kernel/kernel-5.10这个
kernel-5.10目录就是标准内核源码。下载对应版本的 PREEMPT_RT 补丁。你可以从 https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/ 寻找。假设我们找到了
patch-5.10.120-rt.patch.xz。wget https://cdn.kernel.org/pub/linux/kernel/projects/rt/5.10/patch-5.10.120-rt.patch.xz应用补丁。务必在干净的源码上操作。
# 解压补丁文件 xz -cd patch-5.10.120-rt.patch.xz | patch -p1如果补丁应用成功,你会看到大量文件被修改的提示。如果出现大量“FAILED”或“Hunk”错误,说明补丁版本不匹配,需要解决冲突,这非常棘手。因此版本匹配至关重要。
3.3 配置交叉编译环境
我们需要安装ARM64的交叉编译工具链。NVIDIA推荐使用Linaro或Bootlin的工具链。这里以Bootlin的稳定版本为例:
# 安装必要的依赖 sudo apt-get update sudo apt-get install build-essential bc kmod cpio flex libncurses5-dev libssl-dev # 下载并解压aarch64工具链 wget https://toolchains.bootlin.com/downloads/releases/toolchains/aarch64/tarballs/aarch64--glibc--stable-2023.08-1.tar.bz2 tar -xjf aarch64--glibc--stable-2023.08-1.tar.bz2 -C ~/ # 将工具链路径加入环境变量(临时,或写入~/.bashrc) export CROSS_COMPILE=~/aarch64--glibc--stable-2023.08-1/bin/aarch64-linux- export ARCH=arm644. 内核配置、编译与打包
这是最核心也最耗时的步骤,配置选项直接决定了内核的特性和能否正常启动。
4.1 获取并调整内核配置
Jetson BSP通常提供了一个默认的配置文件。我们基于它进行修改。
# 回到内核源码目录 cd ~/jetson-rt-kernel/Linux_for_Tegra/source/public/kernel/kernel-5.10 # 获取NVIDIA提供的默认配置 make O=$PWD/build-tegra ARCH=arm64 CROSS_COMPILE=$CROSS_COMPILE tegra_defconfig # 进入交互式配置菜单,在默认配置上启用RT特性 make O=$PWD/build-tegra ARCH=arm64 CROSS_COMPILE=$CROSS_COMPILE menuconfig在menuconfig界面中,你需要重点关注以下几个配置项:
General setup -> Preemption Model:
- 这是RT内核的核心。将选项从默认的
Preemptible Kernel (Low-Latency Desktop)修改为Fully Preemptible Kernel (Real-Time)。这个选项会强制开启一系列子选项,为RT内核铺平道路。
- 这是RT内核的核心。将选项从默认的
Kernel Features -> Timer frequency:
- 提高定时器中断频率(HZ)可以降低调度粒度,提高响应性。对于实时系统,通常设置为1000 HZ。但注意,更高的频率会增加一点系统开销。在Jetson上,1000 HZ是个不错的起点。
确保关键驱动被编译:
- 使用
/键搜索你的Jetson模组关键驱动,如TEGRA、PCIe、USB、GPU(NVIDIA Tegra GPU)、ETHERNET等。确保它们被启用(y)或编译为模块(m)。特别是显示、网络和存储驱动,必须包含。
- 使用
排查冲突选项:
- 应用RT补丁后,有些调试或特性选项可能与实时性冲突。留意配置中的警告信息。一个常见的需要关闭的选项是
CONFIG_DEBUG_PREEMPT,它在生产环境可能会引入额外开销,可以先关掉。
- 应用RT补丁后,有些调试或特性选项可能与实时性冲突。留意配置中的警告信息。一个常见的需要关闭的选项是
配置完成后,保存退出。配置文件会保存在build-tegra/.config。
4.2 编译内核与模块
接下来开始漫长的编译过程。利用你主机所有核心可以加快速度(-j$(nproc))。
# 编译内核Image和压缩的Image.gz make O=$PWD/build-tegra ARCH=arm64 CROSS_COMPILE=$CROSS_COMPILE -j$(nproc) Image Image.gz # 编译设备树(Device Tree Blobs)。设备树描述了硬件的拓扑结构,对启动至关重要。 make O=$PWD/build-tegra ARCH=arm64 CROSS_COMPILE=$CROSS_COMPILE -j$(nproc) dtbs # 编译所有内核模块 make O=$PWD/build-tegra ARCH=arm64 CROSS_COMPILE=$CROSS_COMPILE -j$(nproc) modules # 创建模块安装的临时目录 mkdir -p $PWD/build-tegra/modules_install # 安装模块到临时目录 make O=$PWD/build-tegra ARCH=arm64 CROSS_COMPILE=$CROSS_COMPILE modules_install INSTALL_MOD_PATH=$PWD/build-tegra/modules_install编译成功后,关键产出物在build-tegra/arch/arm64/boot/下(Image和Image.gz),设备树在build-tegra/arch/arm64/boot/dts/nvidia/下(如tegra234-p3767-0000-p3768-0000-a0.dtb),模块则在build-tegra/modules_install/lib/modules/5.10.120-rt-...下。
4.3 替换BSP中的内核文件并打包模块
现在我们需要用新编译的文件替换掉BSP准备的标准文件。
# 回到Linux_for_Tegra根目录 cd ~/jetson-rt-kernel/Linux_for_Tegra # 备份原始内核(可选但建议) cp kernel/Image kernel/Image.orig cp kernel/Image.gz kernel/Image.gz.orig cp kernel/dtb/tegra234-p3767-0000-p3768-0000-a0.dtb kernel/dtb/tegra234-p3767-0000-p3768-0000-a0.dtb.orig # 复制新编译的内核Image和设备树 cp source/public/kernel/kernel-5.10/build-tegra/arch/arm64/boot/Image kernel/ cp source/public/kernel/kernel-5.10/build-tegra/arch/arm64/boot/Image.gz kernel/ cp source/public/kernel/kernel-5.10/build-tegra/arch/arm64/boot/dts/nvidia/tegra234-p3767-0000-p3768-0000-a0.dtb kernel/dtb/ # 处理内核模块:先清理根文件系统中的旧模块,再安装新模块 sudo rm -rf rootfs/lib/modules/* sudo cp -rav source/public/kernel/kernel-5.10/build-tegra/modules_install/lib/modules/* rootfs/lib/modules/5. 烧录镜像到 reComputer Jetson
所有文件准备就绪,进入最紧张的烧录环节。
5.1 进入强制恢复模式并连接
- 断开reComputer Jetson的电源。
- 用Micro-USB数据线(注意,是用于烧录的USB口,不是普通的USB-A口)将reComputer的Recovery USB口连接到开发主机。
- 按住reComputer板子上的Force Recovery按钮不放。
- 保持按住Force Recovery按钮,短按一下Reset按钮。
- 等待2秒左右,松开Force Recovery按钮。
- 在开发主机上执行
lsusb命令,你应该能看到一个NVIDIA Corp.相关的设备(ID可能为0955:7323),这表明设备已成功进入恢复模式并被主机识别。
5.2 执行烧录脚本
NVIDIA BSP提供了方便的烧录脚本。在烧录前,建议先尝试“试运行”模式,检查配置是否正确。
cd ~/jetson-rt-kernel/Linux_for_Tegra # 首先,运行flash.sh脚本,但指定--no-flash参数,它会准备所有镜像文件但不真正烧写 sudo ./flash.sh jetson-orin-nx-devkit mmcblk0p1 # 如果上一步没有报错,并且生成了所有必要的镜像文件,则可以开始正式烧录。 # 确保设备处于强制恢复模式,然后执行(不加--no-flash) sudo ./flash.sh jetson-orin-nx-devkit mmcblk0p1flash.sh脚本会自动调用nvflash工具,依次烧写引导加载程序(U-Boot)、内核、设备树、根文件系统等到设备的eMMC存储中。整个过程会在终端显示进度,耗时几分钟。期间绝对不要断开USB连接或给设备断电。
5.3 首次启动与验证
烧录完成后,脚本通常会提示你给设备重新上电。断开USB烧录线,连接显示器和键盘(或通过串口调试),然后给reComputer上电。
- 观察启动日志:如果一切顺利,你会看到U-Boot的启动信息,接着内核开始解压、加载驱动,最后进入Ubuntu登录界面。
- 验证内核版本:登录系统后,打开终端,执行:
输出应该包含uname -rrt字样,例如5.10.120-rt-...,这表明实时内核正在运行。 - 测试实时性(基础):安装
cyclictest工具进行简单的延迟测试。
观察输出的sudo apt-get update sudo apt-get install rt-tests # 运行一个简单的测试,持续10秒,优先级80 sudo cyclictest -t -p 80 -n -i 1000 -l 10000Max Latency(最大延迟)值。在标准内核上,这个值可能在几十到几百微秒甚至毫秒级波动。在正确配置的RT内核上,最大延迟应该显著降低且更加稳定(理想情况下在几十微秒以内)。注意:cyclictest本身运行在高优先级,可能会影响系统其他任务,这只是一个初步的定性测试。
6. 烧录后的关键配置与性能调优
成功启动RT内核只是第一步。要让实时应用真正受益,还需要进行一系列系统配置。
6.1 隔离CPU核心
为了防止非实时任务(如桌面环境、后台服务)干扰实时线程,标准的做法是将一个或多个CPU核心隔离出来,专供实时任务使用。
修改内核启动参数。编辑
/boot/extlinux/extlinux.conf(在Jetson上通常是这个路径):sudo vim /boot/extlinux/extlinux.conf在
APPEND那一行,找到root=参数后面,添加isolcpus=1-3(假设你想隔离CPU1, CPU2, CPU3)。例如:APPEND ${cbootargs} quiet root=/dev/mmcblk0p1 rw rootwait rootfstype=ext4 console=ttyTCU0,115200n8 console=tty0 fbcon=map:0 isolcpus=1-3重启生效。
使用
taskset将实时进程绑定到隔离的CPU上。例如,将你的实时应用进程绑定到CPU1:taskset -c 1 ./your_rt_application
6.2 调整进程调度策略与优先级
对于关键的实时线程,在编程时应该使用SCHED_FIFO或SCHED_RR调度策略,并赋予较高的静态优先级(1-99,数字越大优先级越高)。
// C语言示例代码片段 #include <sched.h> #include <pthread.h> void set_realtime_priority() { struct sched_param param; param.sched_priority = 80; // 设置优先级为80 if (sched_setscheduler(0, SCHED_FIFO, ¶m) == -1) { perror("sched_setscheduler failed"); // 处理错误,可能需要CAP_SYS_NICE能力 } }记得给执行此代码的程序赋予相应的Linux能力(Capability),或者直接以root权限运行(不推荐生产环境)。
6.3 禁用频率调节与Turbo模式
CPU频率的动态调节(DVFS)和自动降频会引入不可预测的延迟。对于实时核心,可以将其调节器设置为performance模式,并禁用Turbo Boost(如果存在)以获得更稳定的性能。
# 查看所有CPU的当前调节器 cat /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor # 将隔离的CPU(例如cpu1)设置为performance模式 echo performance | sudo tee /sys/devices/system/cpu/cpu1/cpufreq/scaling_governor # 对于Jetson Orin,可能需要通过nvpmodel来固定功耗和频率模式 sudo /usr/sbin/nvpmodel -m 0 # 模式0通常是MAX-N模式,所有核心最高性能注意:固定高性能模式会显著增加功耗和发热,需要做好散热。
6.4 内存锁定与避免换出
实时任务的内存页应该被锁定在物理RAM中,防止被换出到交换分区,否则换入换出操作会导致巨大的延迟。
在程序中使用mlockall()系统调用:
#include <sys/mman.h> if (mlockall(MCL_CURRENT | MCL_FUTURE) == -1) { perror("mlockall failed"); }同样,这需要CAP_IPC_LOCK能力或root权限。
7. 实战排坑:我遇到的那些“坑”与解决方案
没有一次成功的烧录是不踩坑的。下面是我在过程中遇到的几个典型问题及其解决方法。
7.1 坑一:设备树编译错误或版本不匹配
- 现象:
make dtbs时出现大量未定义的引用错误,或者烧录后设备无法启动,串口日志卡在某个驱动初始化。 - 根因:内核源码目录下的设备树源文件(
.dts或.dtsi)可能与当前内核版本或BSP不兼容。特别是如果你从其他渠道获取了内核源码或补丁。 - 解决:
- 使用BSP包内的原生源码:这是最稳妥的。确保你的
kernel-5.10目录完全来自kernel_src.tbz2,没有混入其他版本的代码。 - 检查补丁应用:确保RT补丁应用成功,没有因为冲突而部分失败。可以尝试
patch -p1 --dry-run < patchfile先进行试运行。 - 对比官方配置:使用
tegra_defconfig作为基础,只增量修改RT相关选项,不要随意改动其他平台相关的配置。
- 使用BSP包内的原生源码:这是最稳妥的。确保你的
7.2 坑二:烧录后卡在“Starting kernel ...”或黑屏
- 现象:U-Boot阶段正常,显示“Starting kernel ...”后就没有任何输出,屏幕黑屏。
- 根因:这通常是由编译的内核或设备树与硬件不匹配导致。最常见的是:
- 错误的设备树文件:
flash.sh脚本烧录了错误的.dtb文件。Jetson Orin NX 和 Orin Nano 的设备树名称不同,reComputer的载板也可能有细微差别。 - 显示驱动问题:内核中显示驱动(如NVIDIA Tegra DRM)未正确编译或配置。
- 错误的设备树文件:
- 解决:
- 确认设备树名称:查看
Linux_for_Tegra/bootloader/t186ref/或相关目录下的配置文件,确认你的设备对应的确切设备树名称。对于reComputer Jetson Orin NX,通常是tegra234-p3767-0000-p3768-0000-a0.dtb。 - 检查内核配置:重新运行
make menuconfig,确保以下关键驱动已启用:CONFIG_TEGRA_HOST1X,CONFIG_DRM_TEGRACONFIG_PCIE_TEGRA194(对于PCIe设备)- 你的具体存储控制器驱动(如
CONFIG_MMC_SDHCI_TEGRA)
- 使用串口调试:通过UART串口连接reComputer的调试口(通常标有
DEBUG),在电脑上用串口工具(如minicom,screen)查看更详细的启动日志,这能精准定位卡在哪一步。
- 确认设备树名称:查看
7.3 坑三:系统启动后,cyclictest延迟依然很高
- 现象:RT内核成功启动,但
cyclictest测试的最大延迟(Max Latency)仍在几百微秒以上,没有达到预期效果。 - 根因:实时内核只是提供了低延迟的基础,但系统上运行的其他中断和任务仍然会抢占CPU。常见干扰源包括:
- 其他高优先级中断:如网络中断(
irq/eth0)、USB控制器中断等。 - 内核线程:一些内核后台线程(
ksoftirqd,rcu_sched等)可能运行在隔离的CPU上。 - 电源管理特性:
intel_idle(x86)或ARM的CPU空闲状态(cpuidle)在进入深睡眠状态(C-state)后,唤醒需要时间。 - BIOS/Firmware设置:在某些平台上,需要禁用C-state和P-state调节。
- 其他高优先级中断:如网络中断(
- 解决:
- 中断隔离:将除了实时任务所需之外的所有中断,都绑定到非隔离的CPU上。例如,将网络中断
irq/eth0绑定到CPU0:
需要先echo 1 | sudo tee /proc/irq/<eth0_irq_num>/smp_affinitycat /proc/interrupts找到对应中断号。 - 禁用看门狗:
/dev/watchdog相关的内核线程可能会定时唤醒。可以尝试临时禁用或将其绑定到非实时CPU。 - 调整内核启动参数:尝试添加
rcu_nocbs=1-3(将RCU回调从隔离CPU卸载)、nohz_full=1-3(在隔离CPU上启用完全无滴答)、irqaffinity=0(将所有中断默认绑定到CPU0)。 - 使用
tuna或chrt工具:在运行时动态调整进程的调度策略和优先级,确保实时进程获得最高调度权。 - 进行更专业的基准测试:
cyclictest是基础工具。对于更严谨的评估,可以考虑使用stress工具施加系统负载,同时运行cyclictest来观察在最坏情况下的延迟表现。
- 中断隔离:将除了实时任务所需之外的所有中断,都绑定到非隔离的CPU上。例如,将网络中断
烧录PREEMPT_RT内核到嵌入式边缘设备如Jetson,是一个从系统底层提升确定性的有效手段,但它不是“银弹”。它需要你深入理解整个软件栈——从引导加载程序、内核配置、设备树到应用层的调度策略。整个过程就像给一台高性能跑车更换了更精准的赛车级电控系统和悬挂,但你需要一名专业的技师(开发者)来调校它,才能让它在赛道上(你的实时应用场景)发挥出全部潜力。希望这份详细的记录能帮你少走弯路,顺利踏上边缘实时计算的道路。如果在具体操作中遇到新的问题,多查看串口日志、多分析内核配置,社区的资源和NVIDIA的官方论坛也是很好的求助渠道。