4、内核配置与设备树:Linux内核源码管理、内核配置(make linux-menuconfig)、设备树(DTS)基础与修改、内核模块编译

好,咱们进入第四章。这一章可以说是整个嵌入式Linux开发的核心环节之一。内核怎么管?配置怎么调?设备树怎么改?模块怎么编?这些事儿,说白了就是你能不能把Linux跑在你那块板子上的关键。

我记得刚入行那会儿,觉得内核配置就是改几个开关,设备树就是写几行描述。结果呢?板子起不来,串口没输出,网卡不认。后来才明白,这里面的门道,远比想象中深。

4.1 Linux内核源码管理

先聊聊内核源码怎么管。Buildroot本身会帮你下载和解压内核,但你得知道它放在哪儿、怎么维护自己的修改。

管理方式 适用场景 我的建议
Buildroot内置下载 快速原型、学习验证 适合刚开始接触时用
本地Git仓库 团队协作、版本追溯 我强烈推荐这种方式
补丁文件(patch) 少量修改、保持干净 适合给上游贡献代码

我个人习惯用本地Git仓库。在Buildroot的board/目录下,我会建一个linux-patches/文件夹,把内核补丁放进去。Buildroot会自动应用这些补丁。这样,内核源码是干净的,我的修改是独立的。

小技巧: 在 buildroot/configs/ 里,你可以看到类似 myboard_defconfig 的文件。里面有一行 BR2_LINUX_KERNEL_PATCH,指向你的补丁目录。这样每次构建都会自动打上你的修改。

4.2 内核配置:make linux-menuconfig

配置内核,最直观的方式就是make linux-menuconfig。这玩意儿会弹出一个图形界面,让你勾选各种功能。

但这里有个坑。你直接在菜单里改了配置,Buildroot下次重新构建时,这些修改可能会被覆盖。为什么会这样?因为Buildroot会从board/目录下的内核配置片段(linux.config或linux.fragment)重新生成完整配置。

注意: 在 make linux-menuconfig 里修改后,一定要执行 make linux-update-config 把配置保存回你的配置片段文件。否则,下次 make 时修改就丢了。我曾经因为这个浪费了一整天。

正确的流程是这样的:

  1. 执行 make linux-menuconfig 进入配置界面
  2. 按需勾选或取消功能(比如添加USB支持、去掉蓝牙)
  3. 保存退出
  4. 执行 make linux-update-config 将配置写回 board/yourboard/linux.config
  5. 提交这个配置文件到版本管理

嗯,这里要注意,内核配置项成千上万。你不需要全部搞懂。我一般只关注这几类:

  • 架构相关:CPU类型、内存布局、启动方式
  • 驱动支持:串口、网卡、I2C、SPI、USB、显示
  • 文件系统:ext4、FAT、NFS、OverlayFS
  • 网络协议:TCP/IP、IPv6、Netfilter
  • 调试工具:printk、kgdb、trace

4.3 设备树(DTS)基础与修改

设备树,说白了就是描述硬件的一张表。它告诉内核:你的板子上有哪些外设、它们连在哪个地址上、用哪个中断。

我刚开始学设备树时,觉得语法挺绕。其实核心就几个东西:

概念 说明 示例
节点(node) 表示一个设备或总线 uart0: serial@10000000
属性(property) 描述设备的参数 compatible = "ns16550";
reg 地址和大小 reg = <0x10000000 0x1000>;
interrupts 中断号 interrupts = <0 33 4>;

一个典型的设备树文件(.dts)长这样:

/dts-v1/;
#include "imx6ul.dtsi"

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

    chosen {
        stdout-path = &uart1;
    };

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

&iomuxc {
    pinctrl_uart1: uart1grp {
        fsl,pins = <
            MX6UL_PAD_UART1_TX_DATA__UART1_DCE_TX 0x1b0b1
            MX6UL_PAD_UART1_RX_DATA__UART1_DCE_RX 0x1b0b1
        >;
    };
};

你看,这里先包含了芯片级的基础设备树(.dtsi),然后覆盖或添加板级特有的配置。这种分层设计,让代码复用性很高。

核心原则: 设备树只描述硬件,不描述软件行为。不要在里面写驱动逻辑,那是C代码的事。

修改设备树的步骤,我一般这么来:

  1. 找到对应芯片的 .dtsi 文件(在 linux/arch/arm/boot/dts/ 下)
  2. 复制一份类似的 .dts 作为模板
  3. 修改 model 和 compatible 属性
  4. 使能需要用到的外设节点(把 status 从 disabled 改为 okay)
  5. 配置引脚复用(pinctrl)
  6. 在Buildroot里指定使用这个新的 .dts 文件

我曾经在一个项目里,因为设备树里一个中断号写错了,导致触摸屏死活没反应。查了两天才发现,是interrupts属性的第二个参数少了个0。这种坑,真的只有踩过才知道疼。

4.4 内核模块编译

内核模块,就是可以动态加载到内核里的代码。好处是不用每次改驱动都重新编译整个内核,重启机器。

在Buildroot里编译内核模块,有两种方式:

  • 内置编译:在 linux-menuconfig 里把驱动设为 <*>(编译进内核)
  • 模块编译:设为 <M>(编译成模块),然后 make 会生成 .ko 文件

我个人更倾向于把不常用的驱动编译成模块。比如Wi-Fi驱动、USB摄像头驱动。这样内核镜像小,启动快,需要时再加载。

模块编译完成后,Buildroot会把 .ko 文件安装到目标文件系统的 /lib/modules/<kernel-version>/ 目录下。你可以在板子上用 modprobe 或 insmod 加载它们。

调试技巧: 如果你在开发自己的模块,可以在Buildroot的 package/ 目录下创建一个包,用 KERNEL_MODULE_AUTOLOAD 让模块在启动时自动加载。这样就不用每次手动 insmod 了。

下面这张图,把内核配置、设备树、模块编译的关系串起来了:

内核配置与设备树工作流程 内核配置与设备树工作流程 Linux内核源码 (Git仓库 / 补丁) 内核配置 (make linux-menuconfig) 设备树(DTS) (硬件描述) 内核编译 (zImage + dtb) 内核模块(.ko) (动态加载) 目标板启动 (加载dtb + 模块) 关键输出文件 • zImage: 压缩内核镜像 • .dtb: 编译后的设备树 • .ko: 可加载模块

你看,整个流程是串起来的。源码管理好了,配置对了,设备树写准了,模块编好了,最后才能在板子上跑起来。任何一个环节出问题,都可能导致系统起不来。

嗯,这一章的内容就这些。内核配置和设备树,是嵌入式Linux开发的基本功。多练、多踩坑、多总结,慢慢就熟了。