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,再手动调整。

具体步骤:

  1. 加载厂商默认配置:make ARCH=arm64 rockchip_linux_defconfig
  2. 进入 menuconfig 微调:make ARCH=arm64 menuconfig
  3. 重点检查以下选项:
配置项 路径 说明
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 必须开启
小技巧: 我习惯在 menuconfig 里用 / 键搜索关键字,比如搜 "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 编译,可能会报找不到头文件的错误。我建议用内核的构建系统来编译,它会自动处理依赖关系。

注意: 我曾经在调试 NPU 驱动时,发现设备树里 NPU 节点的 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 完整编译流程:一张图看懂

说了这么多,我把整个流程画成一张图,方便你对照操作:

Linux内核编译与设备树编译流程 Linux 内核编译与设备树编译完整流程 1. 获取内核源码 2. 内核配置 (menuconfig) 3. 编译内核与模块 4. 设备树编译 (dtc) 5. 内核模块编译 输出:.dtb 文件 输出:.ko 模块文件 输出:zImage/Image 部署到目标板:烧录 / 拷贝 .dtb + .ko + Image 验证:dmesg | lsmod | /sys/kernel/debug/npu 说明:设备树和内核模块可以独立编译,不必每次都全量编译内核。调试阶段建议只编译修改的部分,节省时间。 完整部署时,需要确保 .dtb、.ko、Image 三者的版本匹配,否则可能导致系统无法启动或驱动异常。

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,方便看错误信息。