4. 内核配置:Linux Kernel版本选择、内核配置片段(defconfig)的使用、设备树(Device Tree)的集成

内核配置这块,说难不难,说简单也容易踩坑。我见过不少新手,Buildroot 编译都过了,板子启动到一半就卡死,最后发现是内核版本选错了,或者设备树没对上。今天咱们就把这几个关键点捋清楚。

4.1 Linux Kernel 版本选择:别盲目追新

选内核版本,我个人习惯是「稳」字当头。你想想看,嵌入式产品要的是长期稳定,不是跑分。

场景 推荐策略 我踩过的坑
量产产品 选 LTS(长期支持)版本,比如 5.10.x、6.1.x 曾经追新用了 5.15-rc,结果某个 SPI 驱动在正式版改了 API,项目延期两周
原型验证 可以用最新稳定版,但别用 -rc 或 -next —
BSP 强制要求 优先用芯片厂商提供的内核分支 某次用主线内核跑全志芯片,MIPI DSI 怎么调都不亮,换回厂商 4.9 分支就好了

在 Buildroot 里,内核版本在 BR2_LINUX_KERNEL_CUSTOM_VERSION_VALUE 里设置。我个人建议:先查芯片厂商的官方 BSP 用了哪个内核版本,然后选对应的 LTS。别问我为什么强调这个——我曾经为了省事,给 i.MX6 平台选了 5.4 主线内核,结果 GPU 驱动死活起不来,最后发现 NXP 官方 BSP 还在用 4.19。

小技巧:在 Buildroot 的 linux/Config.in 里,你可以看到所有预置的内核版本。如果列表里没有你想要的,用 BR2_LINUX_KERNEL_CUSTOM_VERSION 选项手动指定 Git 标签或 commit hash。

4.2 内核配置片段(defconfig)的使用

说白了,内核配置片段就是你的「配置补丁」。Buildroot 本身会先加载一个基础 defconfig,然后你用片段往里加东西、改东西。这样做的好处是:你不用维护一份完整的内核 .config,只需要记录你的改动点。

4.2.1 配置片段的加载机制

Buildroot 处理内核配置的顺序是这样的:

  1. 加载 BR2_LINUX_KERNEL_DEFCONFIG 指定的基础 defconfig
  2. 依次应用 BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES 里列出的所有片段文件
  3. 如果启用了 BR2_LINUX_KERNEL_USE_CUSTOM_CONFIG,则直接用自定义 .config 覆盖

嗯,这里要注意:片段文件是增量叠加的。如果你在片段里写了 CONFIG_A=y,它就会把基础配置里的 CONFIG_A 改成 y。但如果基础配置里没有 CONFIG_A,它就新增一行。

4.2.2 片段文件的写法规范

片段文件本质上就是内核的 .config 片段,但有几个硬性要求:

  • 每行一个配置项,格式为 CONFIG_XXX=y 或 CONFIG_XXX=m
  • 可以用 # CONFIG_XXX is not set 来禁用某个选项
  • 注释行以 # 开头
  • 不要包含 # Automatically generated file; DO NOT EDIT. 这种头部
# 我的片段文件:enable_qt5_fb.frag
# 启用帧缓冲控制台
CONFIG_FRAMEBUFFER_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y
# 禁用 HDMI 音频(板子上没接喇叭)
# CONFIG_DRM_I915 is not set
# 启用 Qt5 需要的 evdev 输入
CONFIG_INPUT_EVDEV=y
警告:片段文件里不要写 CONFIG_LOCALVERSION 或 CONFIG_LOCALVERSION_AUTO,这些应该在 Buildroot 的 BR2_LINUX_KERNEL_LOCALVERSION 里统一管理。我曾经在片段里改了 LOCALVERSION,结果每次 make linux-reconfigure 都触发完整重编,浪费了半小时排查。

4.2.3 多平台共享配置的技巧

我在项目中遇到过需要维护 3 款硬件变体的场景。我的做法是:

  • 一个公共片段 common.frag,放所有平台都需要的配置(比如 ext4、网络、串口)
  • 每个平台一个专属片段,比如 board_a.frag、board_b.frag
  • 在 Buildroot 的 board/xxx/ 目录下放一个 readme.txt,说明每个片段的作用

然后在 buildroot.config 里这样引用:

BR2_LINUX_KERNEL_CONFIG_FRAGMENT_FILES="board/common/common.frag board/board_a/board_a.frag"

4.3 设备树(Device Tree)的集成

设备树,说白了就是硬件的「说明书」。内核通过它知道你的板子上有哪些外设、接在哪个引脚上、用哪个驱动。没有设备树,内核就是个瞎子。

4.3.1 Buildroot 中的设备树配置

在 Buildroot 里,设备树的配置主要涉及这几个变量:

配置项 作用 典型值
BR2_LINUX_KERNEL_DTS_SUPPORT 启用设备树支持 y
BR2_LINUX_KERNEL_INTREE_DTS_NAME 使用内核源码树内的 .dts 文件 imx6ull-14x14-evk
BR2_LINUX_KERNEL_CUSTOM_DTS_PATH 使用自定义的 .dts/.dtsi 文件 board/myboard/myboard.dts
BR2_LINUX_KERNEL_DTB_OVERLAY_SUPPORT 启用设备树叠加层 按需启用

我个人强烈建议:除非你的板子和官方评估板完全一样,否则一定要用自定义设备树。为什么?因为官方 .dts 里可能启用了你板子上没有的硬件(比如 HDMI、WiFi 模块),内核在初始化时如果找不到对应硬件,可能会卡住或报错。

4.3.2 自定义设备树的集成步骤

  1. 在 board/<company>/<boardname>/ 下创建你的 .dts 文件
  2. 在 .dts 里 #include 芯片厂商提供的 .dtsi 文件(通常在内核源码的 arch/arm/boot/dts/ 下)
  3. 在 Buildroot 配置中设置 BR2_LINUX_KERNEL_CUSTOM_DTS_PATH 指向你的 .dts
  4. 设置 BR2_LINUX_KERNEL_INTREE_DTS_NAME 为你的 .dts 文件名(不含路径和扩展名)
# 假设你的板子叫 myboard,基于 i.MX6ULL
# 1. 创建 board/mycompany/myboard/myboard.dts
/dts-v1/;
#include "imx6ull.dtsi"

/ {
    model = "MyCompany MyBoard";
    compatible = "mycompany,myboard", "fsl,imx6ull";

    chosen {
        stdout-path = &uart1;
    };

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x20000000>;  /* 512 MB */
    };
};

&uart1 {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_uart1>;
    status = "okay";
};

# 2. 在 Buildroot 配置中设置
# BR2_LINUX_KERNEL_CUSTOM_DTS_PATH="board/mycompany/myboard/myboard.dts"
# BR2_LINUX_KERNEL_INTREE_DTS_NAME="myboard"

4.3.3 设备树编译的常见问题

我遇到过最坑的一个问题:.dts 里引用了不存在的 .dtsi 文件。编译时 DTC(设备树编译器)会报错,但 Buildroot 的日志里可能只显示一行 Error: ...,很容易被忽略。我的排查方法是:

  • 先手动编译一次:make linux-dirclean && make linux,然后看完整输出
  • 检查 output/build/linux-xxx/arch/arm/boot/dts/ 下是否生成了你的 .dtb 文件
  • 用 dtc -I dtb -O dts xxx.dtb 反编译生成的 .dtb,确认内容正确
核心原则:设备树是硬件和内核之间的「契约」。契约写错了,内核要么不认硬件,要么启动到一半崩溃。每次修改 .dts 后,建议先用 make linux-rebuild 单独编译内核和设备树,确认无误后再 make 完整构建。

4.4 本章知识体系

下面这张图把内核配置、defconfig 片段、设备树三者的关系串起来了。你可以看到,从 Buildroot 配置到最终生成内核镜像和 .dtb 文件的完整流程。

内核配置与设备树集成流程 内核配置与设备树集成流程 Buildroot 配置 内核源码树 自定义 .dts 文件 加载基础 defconfig 应用配置片段 DTC 编译 .dts → .dtb 内核镜像 zImage/Image 内核模块(.ko) 设备树 .dtb 文件 部署到目标板:/boot/zImage + /boot/*.dtb 💡 配置片段是增量叠加的,顺序很重要:后面的片段会覆盖前面的同名配置项。 设备树文件必须与内核版本匹配,否则可能出现驱动不识别或引脚冲突。

4.5 避坑指南:我踩过的三个坑

最后分享几个我实际遇到过的教训,希望能帮你省点时间:

  • 坑一:内核版本与设备树不匹配。我曾经把 5.10 内核的设备树拿到 5.4 内核上用,结果 GPIO 编号全乱了。后来我养成了习惯:每次换内核版本,都重新检查 arch/arm/boot/dts/ 下的 .dtsi 文件有没有变化。
  • 坑二:配置片段里写了冲突的选项。比如一个片段里 CONFIG_FOO=y,另一个片段里 # CONFIG_FOO is not set。Buildroot 不会报错,但最终生效的是最后一个片段里的值。我排查了整整一天才发现是两个片段顺序搞反了。
  • 坑三:忘了把 .dts 文件加入版本控制。有一次我重装了开发环境,从 Git 拉下来直接编译,结果板子起不来。查了半天才发现自定义 .dts 文件根本没提交到仓库。从那以后,我把所有板级文件都放在 board/ 目录下,并且写了个脚本检查是否有未跟踪的文件。
我的工作流:每次修改内核配置或设备树后,我会执行 make linux-dirclean && make linux 来确保干净编译。然后检查 output/images/ 下是否生成了新的 zImage 和 .dtb 文件。最后用 file 命令确认文件类型正确——这个习惯帮我避免过好几次「编译成功但文件没更新」的尴尬。