
1. 为什么要在 Jetson Orin Nano 上折腾内核编译拿到 Jetson Orin Nano 的第一天大多数人都是插上电源、烧录官方镜像、跑通 Hello AI World然后心满意足地收工。但只要你打算接一个官方 BSP 里没带驱动的外设——比如某款特定的网卡、CAN 收发器、工业相机或者自定义的 GPIO 扩展板——很快就会撞上那堵墙驱动源码编译通过了insmod却报invalid module format或者干脆在dmesg里看到一堆Unknown symbol。这不是你代码写错了而是内核模块与运行中的内核版本、配置、符号表不匹配。Jetson Orin Nano 跑的是 NVIDIA 定制的 Linux 内核基于 5.10 或 5.15 系列具体取决于 JetPack 版本它和你在 Ubuntu 主机上apt install linux-headers-$(uname -r)拿到的那套东西完全是两码事。NVIDIA 对内核做了大量下游修改尤其是 Tegra 平台相关的驱动、设备树和内存管理部分。你从 kernel.org 拉一个 vanilla 内核编译出来的模块几乎不可能直接加载到 Orin Nano 上。所以要加驱动就得编译和板子上运行的内核完全同源的那份源码。这件事的难点不在于编译本身——make谁都会敲——而在于整条链路上有太多容易踩的坑源码版本对不上、交叉编译工具链选错、localversion没设置导致模块路径错位、Module.symvers缺失、设备树没同步更新、打包时--append-to-version用错……任何一个环节出问题你拿到的就是一堆废铁。我前后在 Orin Nano 上完整走过五六遍这个流程从 JetPack 5.1 到 6.x踩过的坑足够写一本小册子。这篇就把整条链路拆开从依赖安装一直讲到驱动打包把每个环节的为什么和怎么做都讲透。适合读这篇的人手里有 Orin Nano 开发板、需要编译自定义内核模块或修改内核配置的嵌入式工程师正在做边缘 AI 设备驱动适配的开发者以及任何被invalid module format折磨过、想搞清楚背后原理的人。不需要你是内核专家但得会用 Linux 命令行、看得懂 Makefile。2. 编译前的环境盘点主机、工具链与源码版本的三方对齐2.1 主机环境的选择与最小依赖清单编译 Jetson 内核主机建议用 Ubuntu 20.04 或 22.04 的 x86_64 机器。为什么强调这个因为 NVIDIA 官方文档里给的依赖包列表和构建脚本都是在这两个版本上验证过的。你用 Ubuntu 24.04 或者别的发行版大概率会在某个apt install步骤卡住或者遇到 GCC 版本不兼容的问题。我试过在 Fedora 上跑光是libncurses和libssl的包名差异就折腾了半天不划算。依赖安装这一步很多人直接复制官方文档的一条命令就完事但实际执行时经常报无法定位软件包。原因通常是apt源没更新或者某些包在新版本里改了名字。下面是我在 Ubuntu 22.04 上实测可用的完整依赖清单sudo apt update sudo apt install -y \ build-essential \ bc \ flex \ bison \ libssl-dev \ libncurses5-dev \ libncursesw5-dev \ libelf-dev \ dwarves \ python3 \ python3-pip \ device-tree-compiler \ rsync \ cpio \ kmod \ git \ wget \ curl \ zstd \ liblz4-tool这里有几个包值得单独说。dwarves提供pahole工具内核编译开启DEBUG_INFO_BTF时需要它生成 BTF 信息缺了会直接编译失败。device-tree-compiler提供dtc处理设备树必备。libssl-dev是内核模块签名和证书处理需要的。zstd和liblz4-tool则是内核镜像压缩格式的支持——Orin Nano 的默认配置可能用 zstd 压缩主机上没有对应工具就会在压缩阶段报错。提示如果你在虚拟机里编译确保磁盘空间至少留 50GB。内核源码本身约 1.5GB编译产物加上中间文件轻松超过 20GB再加上交叉工具链和后续打包空间不够会在链接阶段莫名其妙失败。2.2 交叉编译工具链用官方的还是自己装Orin Nano 是 aarch64 架构主机是 x86_64所以必须用交叉编译工具链。这里有个分岔路一是用 NVIDIA 官方 JetPack 里自带的工具链二是用 Ubuntu 源里的gcc-aarch64-linux-gnu。我的建议是优先用官方工具链。原因很直接NVIDIA 的内核源码是用他们自己的工具链验证的GCC 版本、binutils 版本都经过匹配测试。Ubuntu 源里的交叉编译器版本可能偏新或偏旧编译 Tegra 特定代码时偶尔会触发一些奇怪的警告甚至错误。官方工具链通常在你安装 JetPack 的主机上位于/usr/bin/aarch64-linux-gnu-*或者通过 NVIDIA 的 SDK Manager 安装后放在~/nvidia/nvidia_sdk/下面。如果你手头没有官方工具链用 Ubuntu 源的也能跑通但要确认版本sudo apt install -y gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu aarch64-linux-gnu-gcc --version记下这个版本号后面配置内核时CROSS_COMPILE前缀要和它对应。工具链的路径一定要加到PATH里或者在内核make时用绝对路径指定否则会出现command not found或者更隐蔽的——找到了错误的编译器。2.3 源码版本必须和板子上的内核严格一致这是整个流程里最关键、也最容易翻车的一步。你怎么知道板子上跑的内核是什么版本在 Orin Nano 上执行uname -r你会看到类似5.10.104-tegra或5.15.136-tegra的输出。这个字符串由三部分组成上游内核版本号5.10.104、localversion-tegra、以及可能的构建后缀。你编译时用的源码必须能生成完全相同的版本字符串否则模块加载时内核会直接拒绝。源码从哪来两个途径JetPack 自带的 kernel 源码通过 SDK Manager 安装 JetPack 时如果勾选了 Jetson Linux 或源码组件会在~/nvidia/nvidia_sdk/JetPack_*/Linux_for_Tegra/source/下找到kernel_src.tbz2之类的压缩包。这是最稳妥的来源。NVIDIA 官方 Git 仓库在https://github.com/NVIDIA/linux或 NVIDIA 的公开源码站点上按 JetPack 版本找对应的分支比如jetson-linux-r35对应 JetPack 5.x。用 Git 的好处是方便切换版本和查看提交历史。拿到源码后第一件事是确认它的版本号。解压后进入源码根目录看Makefile顶部的VERSION 5 PATCHLEVEL 10 SUBLEVEL 104 EXTRAVERSION 如果这里的版本和板子上uname -r的前三段对不上后面全白搭。EXTRAVERSION通常是空的-tegra这个后缀是通过内核配置里的CONFIG_LOCALVERSION加进去的这个后面会细说。注意不要试图用uname -r的完整字符串去反推源码版本然后随便下载一个看起来差不多的内核。Tegra 的下游补丁差异很大5.10.104 和 5.10.120 之间的 Tegra 驱动代码可能完全不同。版本对不上编译出来的模块符号表就对不上加载必失败。3. 内核配置从 defconfig 到 localversion 的每一个关键开关3.1 选对 defconfig 并理解它的来源Jetson 内核的配置不是从零开始make menuconfig一项项选的而是基于 NVIDIA 提供的 defconfig 文件。在源码目录下执行ls arch/arm64/configs/ | grep tegra你会看到类似defconfig、tegra_defconfig这样的文件。对于 Orin Nano通常用defconfig即可它已经包含了 Tegra 平台所需的全部基础配置。但要注意不同 JetPack 版本的 defconfig 名字可能不同有的版本里叫jetson_defconfig。以实际源码里的文件为准。配置命令export CROSS_COMPILEaarch64-linux-gnu- export ARCHarm64 make defconfig这里的ARCHarm64必须设置否则内核会按主机的 x86 架构去解析配置直接报错。CROSS_COMPILE的前缀要和你的工具链名字匹配如果你用的是官方工具链可能是aarch64-linux-gnu-或者aarch64-none-linux-gnu-用ls看一下工具链目录里的文件名前缀就知道了。3.2 localversion 设置决定模块能不能被加载的隐藏开关这是最容易被忽略、但后果最严重的一个配置项。前面说过板子上uname -r输出的是5.10.104-tegra这个-tegra就是CONFIG_LOCALVERSION的值。如果你编译时没有设置它生成的内核版本就是5.10.104模块会被安装到/lib/modules/5.10.104/下而板子上的内核去/lib/modules/5.10.104-tegra/找模块——路径对不上加载失败。设置方法有两种。一是在make menuconfig里找到General setup→Local version - append to kernel release填入-tegra。二是直接改.config文件CONFIG_LOCALVERSION-tegra改完之后验证一下最终版本字符串make kernelrelease这个命令会输出编译后将生成的内核版本。它必须和板子上uname -r的输出完全一致一个字符都不能差。如果板子上是5.10.104-tegra你输出的是5.10.104-tegra-custom那也不行。有些 JetPack 版本的 localversion 可能更复杂比如带构建日期或配置后缀这时候你得从板子上/proc/version或者/lib/modules/$(uname -r)/build/Makefile里反查出准确的值。提示如果板子上有/lib/modules/$(uname -r)/build这个符号链接指向内核源码目录那说明板子本身带了编译环境你可以直接在板子上编译模块省去交叉编译的麻烦。但 Orin Nano 的 eMMC 或 SD 卡空间通常紧张而且板子上编译速度慢所以大多数情况下还是主机交叉编译更实际。3.3 必须确认的几个内核选项除了 localversion还有几个配置项直接影响模块加载和设备驱动工作配置项推荐值作用CONFIG_MODULESy启用模块支持不开启无法加载任何 .koCONFIG_MODULE_SIG按需模块签名验证开启后未签名模块会被拒绝CONFIG_MODULE_SIG_FORCEn强制签名设为 y 则只加载已签名模块CONFIG_MODVERSIONSy启用模块版本控制影响符号 CRC 校验CONFIG_DEBUG_INFO_BTFy生成 BTF 信息eBPF 相关工具需要CONFIG_MODULE_SIG_FORCE这个尤其要注意。有些安全加固的内核配置会把它设为y这时候你编译的未签名模块根本加载不了dmesg里会看到module verification failed: signature and/or required key missing。如果你只是做开发调试把它设为n最省事。如果必须开启签名那就得生成密钥对、编译时签名、并把公钥烧到板子的内核密钥环里流程复杂得多。CONFIG_MODVERSIONS开启后内核会为每个导出符号生成 CRC 校验值模块加载时校验。这本来是好事能防止版本不匹配的模块被加载。但如果你编译模块时用的Module.symvers和板子内核的不一致就会报disagrees about version of symbol错误。所以编译完内核后一定要把生成的Module.symvers保留好编译外部模块时要用到它。4. 编译与模块打包从 make 到 .ko 的完整链路4.1 编译内核镜像与模块的正确姿势配置确认无误后就可以开始编译了。命令本身很简单make -j$(nproc) Image modules dtbs但这里有几个细节值得展开。Image是 aarch64 架构的内核镜像格式不是 x86 的bzImagemodules编译所有配置为模块的驱动dtbs编译设备树二进制文件。三个目标可以一起编也可以分开编。-j$(nproc)用满所有 CPU 核心加速编译。但如果你主机内存不足比如只有 8GB并行度太高会导致 OOM。这时候把-j的值降到核心数的一半或-j4。我在一台 16GB 内存的机器上用-j16编译大概 15-20 分钟能完成内存小的机器可能要半小时以上。编译过程中如果报错最常见的原因是依赖缺失。比如HOSTCC scripts/dtc/dtc.o /bin/sh: 1: flex: not found这就是flex没装。或者*** No rule to make target debian/canonical-certs.pem这是 Ubuntu 内核源码里常见的证书路径问题在 Jetson 源码里一般不会遇到但如果遇到了检查.config里CONFIG_SYSTEM_TRUSTED_KEYS和CONFIG_SYSTEM_REVOCATION_KEYS是否指向了不存在的文件把它们清空即可。编译完成后检查关键产物ls arch/arm64/boot/Image ls arch/arm64/boot/dts/nvidia/*.dtb find . -name *.ko | head -20Image是内核镜像.dtb是设备树.ko是内核模块。三者缺一不可。4.2 外部模块的编译Makefile 怎么写才不出错如果你只是要编译一个外部驱动模块比如自己写的字符设备驱动不需要重新编译整个内核但需要内核源码目录里已经完成过一次完整编译生成了Module.symvers和配置好的构建环境。外部模块的 Makefile 典型写法obj-m my_driver.o my_driver-objs : main.o helper.o KDIR : /path/to/kernel/source PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) ARCHarm64 CROSS_COMPILEaarch64-linux-gnu- modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean关键点是-C $(KDIR)指定内核源码路径M$(PWD)指定模块源码路径ARCH和CROSS_COMPILE必须和编译内核时一致。编译成功后当前目录下会生成my_driver.ko。这里有个坑KDIR指向的源码目录必须是已经编译过的里面要有.config、Module.symvers、include/generated/等生成文件。如果你拿一份刚解压、没编译过的源码去编译外部模块会报Module.symvers not found或者include/config/auto.conf not found。解决办法就是先在KDIR里跑一遍make defconfig make modules_preparemodules_prepare会生成编译外部模块所需的最小环境。4.3 模块安装路径与 depmod 的配合编译出来的.ko文件需要放到板子上的正确位置。标准路径是/lib/modules/$(uname -r)/kernel/drivers/子系统/比如一个网络驱动放在kernel/drivers/net/下一个字符设备驱动放在kernel/drivers/char/下。放好之后在板子上执行sudo depmod -adepmod会扫描/lib/modules/$(uname -r)/下的所有模块解析它们之间的依赖关系生成modules.dep和modules.alias等文件。没有这一步modprobe找不到模块只能手动insmod并自己解决依赖顺序。如果你在主机上交叉编译可以用INSTALL_MOD_PATH指定安装根目录把模块安装到一个临时目录再整体拷贝到板子上make INSTALL_MOD_PATH/tmp/jetson_modules modules_install这样模块会安装到/tmp/jetson_modules/lib/modules/5.10.104-tegra/下保持目录结构完整。然后用scp或读卡器拷贝到板子的/lib/modules/下再在板子上跑depmod。注意modules_install默认会尝试更新主机上的/lib/modules/如果你不加INSTALL_MOD_PATH可能把交叉编译的 aarch64 模块装到 x86 主机上造成混乱。养成加INSTALL_MOD_PATH的习惯。5. 驱动打包与部署让模块在板子上真正跑起来5.1 打包成 deb 包比手动拷贝更靠谱的方式手动拷贝.ko文件到板子上虽然简单但有几个问题容易漏文件、权限可能不对、升级和卸载麻烦。更工程化的做法是打成.deb包用dpkg安装和管理。NVIDIA 的源码树里通常带了打包脚本在debian/或scripts/目录下找找。如果没有可以自己写一个简单的打包流程# 创建包目录结构 mkdir -p my-module/DEBIAN mkdir -p my-module/lib/modules/5.10.104-tegra/kernel/drivers/misc # 拷贝模块 cp my_driver.ko my-module/lib/modules/5.10.104-tegra/kernel/drivers/misc/ # 写控制文件 cat my-module/DEBIAN/control EOF Package: my-driver Version: 1.0.0 Architecture: arm64 Maintainer: your_name Description: Custom driver for Jetson Orin Nano EOF # 写 postinst 脚本安装后自动 depmod cat my-module/DEBIAN/postinst EOF #!/bin/sh depmod -a modprobe my_driver exit 0 EOF chmod x my-module/DEBIAN/postinst # 打包 dpkg-deb --build my-module my-driver_1.0.0_arm64.deb这个.deb包拷到板子上dpkg -i安装postinst脚本会自动跑depmod并加载模块。卸载时dpkg -r my-driver也能干净地移除文件。比手动rm靠谱得多。5.2 设备树同步驱动能加载不等于硬件能用很多驱动除了.ko文件还需要设备树里对应的节点描述。比如你接了一个 I2C 设备驱动加载了但设备树里没有这个 I2C 设备的节点驱动就找不到硬件probe函数根本不会被调用。设备树的修改流程在源码的arch/arm64/boot/dts/nvidia/下找到对应板子的.dts文件Orin Nano 通常是tegra234-p3767-0000-p3768-0000-a0.dts之类的名字在里面添加或修改节点。改完后重新编译dtbsmake dtbs生成的.dtb文件需要放到板子的/boot/目录下并确保引导配置extlinux.conf或 UEFI 配置指向了正确的.dtb。这一步如果搞错板子可能直接黑屏起不来——这也是热词里Jetson Orin Nano 启动后黑屏的常见原因之一。提示修改设备树之前先把原始的.dtb备份一份。如果新设备树导致无法启动可以通过串口或恢复模式把备份的.dtb刷回去。Orin Nano 的恢复模式进入方法是按住 Force Recovery 按钮再上电然后用主机上的flash.sh或 SDK Manager 重新烧录。5.3 验证模块加载从 dmesg 到 lsmod 的排查链路模块部署到板子上后加载并验证sudo modprobe my_driver lsmod | grep my_driver dmesg | tail -30lsmod显示模块已加载说明insmod成功了。但加载成功不等于工作正常。真正的验证要看dmesg里驱动probe函数的输出以及设备节点是否创建ls /dev/my_device如果probe失败dmesg里会有详细错误信息。常见的几类Unknown symbol in module模块依赖的符号在内核里找不到。检查Module.symvers是否匹配或者依赖的模块是否已加载。invalid module format版本字符串不匹配回到第 3.2 节检查localversion。disagrees about version of symbolCONFIG_MODVERSIONS的 CRC 校验失败说明编译模块时用的内核源码和板子上的内核不是同一份。probe failed with error -ENODEV设备树里没有对应节点或者硬件没接好。排查的时候dmesg -w实时看日志最方便一边插拔硬件一边观察输出。6. 那些年我踩过的坑从依赖缺失到启动黑屏的完整复盘6.1 依赖包缺失导致的编译中断第一次编译时我在make modules阶段遇到pahole not found。当时不知道dwarves包提供这个工具在网上搜了半天有人说是内核版本太新、有人说是 GCC 问题绕了一大圈才定位到就是一个包没装。后来我养成了一个习惯编译前先跑一遍make defconfig make prepare看它报什么错缺什么装什么比对着文档一条条核对快得多。另一个坑是libssl-dev的版本问题。Ubuntu 22.04 默认的 OpenSSL 3.0 和某些老版本内核的签名脚本不兼容会报openssl: error while loading shared libraries。解决办法是装libssl-dev的同时确保openssl命令行工具也是匹配的版本或者在内核配置里关掉模块签名相关选项。6.2 localversion 不一致引发的模块加载失败这个坑我踩了两次。第一次是忘了设CONFIG_LOCALVERSION编译出来的模块版本是5.10.104板子上是5.10.104-tegrainsmod直接报invalid module format。第二次是设了-tegra但板子上的实际版本是5.10.104-tegra后面还跟了一个构建后缀某些 JetPack 版本会有还是对不上。后来我学乖了编译前先在板子上执行cat /proc/version把完整的版本字符串记下来然后在主机上make kernelrelease对比。两者完全一致才继续编译。这个检查花不了 10 秒钟但能省掉后面半小时的排查。6.3 设备树改错导致的黑屏有一次我为了加一个 SPI 设备在设备树里改了一个引脚复用配置结果板子启动到一半就黑屏了。串口日志显示内核在初始化某个时钟时卡死。原因是那个引脚被其他外设占用了我改的配置产生了冲突。教训是改设备树之前先搞清楚每个引脚、每个时钟、每个电源域的归属。Tegra 的引脚复用和时钟树非常复杂一个引脚可能同时被多个控制器引用。改之前用dtc把原始设备树反编译成.dts搜索相关节点看清楚有没有冲突。改完之后如果条件允许先用fdtdump或dtc验证新设备树的语法正确性再刷到板子上。如果已经黑屏了别慌。Orin Nano 可以通过恢复模式重新烧录。按住 Force Recovery 按钮上电主机上lsusb能看到 NVIDIA 的设备然后用 SDK Manager 或flash.sh重新刷入官方镜像即可。数据会丢但板子能救回来。6.4 Module.symvers 丢失导致的符号校验失败编译外部模块时如果KDIR指向的内核源码目录里没有Module.symvers编译会报一堆undefined符号警告生成的.ko加载时也会失败。Module.symvers是内核编译过程中生成的记录了所有导出符号的 CRC 值。如果你把内核源码清理过make clean或make mrproper这个文件就没了。解决办法编译完内核后把Module.symvers单独备份一份。编译外部模块时确保KDIR目录里有这个文件。如果没有重新编译一次内核的modules目标即可生成。7. 把流程固化成脚本一次编写多次复用7.1 编译脚本的骨架设计每次手动敲一堆命令容易出错我把整个流程写成了一个 shell 脚本放在主机上需要时改几个变量就能跑。脚本的核心结构#!/bin/bash set -e # 可配置变量 KERNEL_SRC/path/to/kernel/source CROSS_COMPILEaarch64-linux-gnu- ARCHarm64 LOCALVERSION-tegra INSTALL_PATH/tmp/jetson_modules JOBS$(nproc) # 进入源码目录 cd ${KERNEL_SRC} # 配置 export ARCH CROSS_COMPILE make defconfig sed -i s/^CONFIG_LOCALVERSION.*/CONFIG_LOCALVERSION\${LOCALVERSION}\/ .config # 验证版本 EXPECTED_VERSION$(make kernelrelease) echo Kernel release will be: ${EXPECTED_VERSION} read -p Does this match the board? (y/n) confirm if [ ${confirm} ! y ]; then echo Version mismatch, aborting. exit 1 fi # 编译 make -j${JOBS} Image modules dtbs # 安装模块到临时目录 make INSTALL_MOD_PATH${INSTALL_PATH} modules_install echo Build complete. Modules at ${INSTALL_PATH}这个脚本的关键点是set -e任何命令失败就退出和版本验证环节人工确认后再继续。read -p那一步虽然看起来笨但能有效防止版本不匹配的模块被编译出来。7.2 部署脚本与板端验证主机上编译完成后用scp或rsync把模块目录同步到板子上rsync -avz /tmp/jetson_modules/lib/modules/5.10.104-tegra/ \ jetsonboard_ip:/lib/modules/5.10.104-tegra/然后在板子上执行sudo depmod -a sudo modprobe my_driver dmesg | tail -20如果一切正常dmesg里会看到驱动的初始化日志。如果失败根据错误信息回到前面的章节排查。提示板子上的/lib/modules/目录可能需要 root 权限才能写入。用rsync时加--rsync-pathsudo rsync或者先同步到用户目录再sudo cp。7.3 版本管理与回滚策略内核和模块的版本管理很重要。我的做法是每次编译成功后把Image、.dtb、Module.symvers和模块目录一起打包用版本号命名比如jetson_kernel_5.10.104-tegra_20240115.tar.gz。板子上保留最近两三个版本出问题可以快速回滚。回滚时把旧的Image和.dtb放回/boot/旧的模块目录放回/lib/modules/重启即可。注意extlinux.conf里的内核和设备树路径要对应修改否则引导的还是新版本。这套流程跑熟之后从修改代码到板子上验证大概 20-30 分钟能完成一轮。比起第一次折腾时花了一整天效率提升非常明显。核心就是把每个环节的为什么搞清楚然后固化成脚本减少人为失误。