3. Linux 内核编译:从源码到设备树,一步都不能少
说实话,很多做 AI 应用层的朋友,一听到「编译内核」就觉得头大。我刚开始接触这块时也一样,觉得不就是 make 一下嘛?结果第一次编译出来的内核,板子根本起不来。嗯,从那以后我就老老实实把每一步都搞清楚了。
这一章,我们就来走一遍完整的流程:获取内核源码 → 配置内核 → 编译设备树 → 编译内核模块。每一步我都会结合自己做 NPU 驱动时的经验来讲,尤其是那些容易踩坑的地方。
3.1 内核源码获取:选对版本,省一半时间
内核源码的获取方式很多,但我的建议是:别用最新的主线内核。为什么呢?因为 NPU 驱动往往依赖厂商的补丁,主线内核可能还没合入。
我个人习惯的做法是:
- 从芯片厂商的 BSP 包获取 —— 比如 Rockchip、NXP、TI 的官方 SDK,里面已经配好了对应的内核版本和补丁。
- 使用 git clone 指定分支 —— 比如
git clone -b linux-5.10.y,然后打上厂商的补丁。 - 检查内核版本与 NPU 驱动版本的兼容性 —— 这个我吃过亏,后面会细说。
1. 芯片型号对应的内核版本(比如 RK3588 推荐 5.10)
2. NPU 驱动(如 RKNN)支持的内核范围
3. 设备树文件是否在对应版本中已有模板
举个例子,我曾经接手一个项目,用的是某厂商的 NPU 芯片。BSP 包里的内核是 4.19,但我图省事直接拉了 5.15 主线。结果编译通过,但 NPU 驱动死活加载不了。查了两天才发现,驱动里用了一个 4.19 之后被移除的 API。你说冤不冤?
3.2 内核配置(menuconfig):别偷懒,该选的都得选上
内核配置是编译前最关键的一步。很多人直接 make defconfig 然后就开始编译,结果发现缺少某些功能,又得重来。我的经验是:先基于厂商的 defconfig,再手动调整。
具体步骤:
- 加载厂商默认配置:
make ARCH=arm64 rockchip_linux_defconfig - 进入 menuconfig 微调:
make ARCH=arm64 menuconfig - 重点检查以下选项:
| 配置项 | 路径 | 说明 |
|---|---|---|
| Device Drivers → NPU support | → Rockchip NPU driver | 必须选为模块(M)或编译进内核(*) |
| Firmware Drivers | → Rockchip firmware | NPU 固件加载依赖 |
| Memory Management | → CMA size | NPU 需要连续内存,建议 256MB+ |
| Device Tree | → Flattened Device Tree support | 必须开启 |
/ 键搜索关键字,比如搜 "NPU" 或 "RKNN",能快速定位到相关配置项。省得一层层翻菜单。
配置完成后,记得保存为 .config 文件。我建议你把这个文件备份一份,命名为 config_ai_edge,下次直接复制过来用,省得重复劳动。
3.3 设备树编译(dtc):把 .dts 变成 .dtb
设备树(Device Tree)是嵌入式 Linux 里描述硬件信息的「说明书」。NPU 的寄存器地址、中断号、时钟频率,全写在这里面。
编译设备树的命令很简单:
# 编译单个设备树
make ARCH=arm64 dtbs
# 或者手动使用 dtc 工具
dtc -I dts -O dtb -o rk3588-evb1.dtb rk3588-evb1.dts
# 反编译(调试用)
dtc -I dtb -O dts -o rk3588-evb1.dts rk3588-evb1.dtb
但这里有个坑:设备树源文件(.dts)往往依赖多个 .dtsi 头文件。如果你直接手动用 dtc 编译,可能会报找不到头文件的错误。我建议用内核的构建系统来编译,它会自动处理依赖关系。
status = "okay" 写成了 okay(少了个引号),结果编译通过但驱动不加载。这种低级错误,排查起来最费时间。建议编译后用 dtc -I dtb -O dts 反编译检查一遍。
设备树中 NPU 节点的典型结构:
/ {
npu: npu@fdab0000 {
compatible = "rockchip,rk3588-npu";
reg = <0x0 0xfdab0000 0x0 0x10000>;
interrupts = <GIC_SPI 242 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru ACLK_NPU0>, <&cru ACLK_NPU1>;
clock-names = "aclk0", "aclk1";
status = "okay";
};
};
你看,这里定义了 NPU 的基地址、中断号、时钟信息。如果这些值和硬件实际不符,驱动要么加载失败,要么运行不稳定。
3.4 内核模块编译:按需加载,灵活调试
NPU 驱动我通常编译为模块(.ko),而不是直接编译进内核。为什么?因为调试方便。驱动出问题了,我只需要 insmod 或 rmmod 重新加载,不用每次都重启板子。
编译内核模块的命令:
# 编译所有模块
make ARCH=arm64 modules
# 只编译 NPU 驱动模块
make ARCH=arm64 M=drivers/soc/rockchip/npu
# 安装模块到指定目录
make ARCH=arm64 modules_install INSTALL_MOD_PATH=./modules_install
编译完成后,你会得到 rockchip-npu.ko 这样的文件。把它拷贝到目标板子上,用 insmod 加载:
insmod rockchip-npu.ko
# 查看是否加载成功
lsmod | grep npu
# 查看内核日志
dmesg | tail -20
cat /sys/kernel/debug/npu/version 查看驱动版本,确认加载的是我编译的那个版本。有时候板子上有多个版本的 .ko 文件,容易搞混。
3.5 完整编译流程:一张图看懂
说了这么多,我把整个流程画成一张图,方便你对照操作:
3.6 避坑指南:我踩过的那些坑
最后,分享几个我实际项目中遇到的坑,希望能帮你少走弯路:
- 内核版本与工具链不匹配 —— 比如用 gcc 9 编译 4.19 内核没问题,但编译 5.10 内核可能报错。我建议用芯片厂商推荐的工具链版本。
- 设备树中的 phandle 引用错误 —— 比如 NPU 节点引用了某个 clock,但那个 clock 节点没定义。编译时不会报错,但运行时驱动会挂掉。
- CMA 大小设置不足 —— NPU 推理时需要分配连续内存。如果 CMA 太小,驱动会报
alloc_contiguous_range failed。我一般设 256MB 起步。 - 模块依赖未处理 —— 有些 NPU 驱动依赖其他模块(比如 firmware 加载模块)。用
modprobe比insmod更安全,它会自动处理依赖。
V=1 参数(如 make V=1 ARCH=arm64 dtbs),这样能看到详细的编译命令,方便定位问题。另外,make -j$(nproc) 可以加速编译,但第一次编译建议不加 -j,方便看错误信息。