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 时修改就丢了。我曾经因为这个浪费了一整天。
正确的流程是这样的:
- 执行
make linux-menuconfig进入配置界面 - 按需勾选或取消功能(比如添加USB支持、去掉蓝牙)
- 保存退出
- 执行
make linux-update-config将配置写回board/yourboard/linux.config - 提交这个配置文件到版本管理
嗯,这里要注意,内核配置项成千上万。你不需要全部搞懂。我一般只关注这几类:
- 架构相关: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),然后覆盖或添加板级特有的配置。这种分层设计,让代码复用性很高。
修改设备树的步骤,我一般这么来:
- 找到对应芯片的
.dtsi文件(在linux/arch/arm/boot/dts/下) - 复制一份类似的
.dts作为模板 - 修改
model和compatible属性 - 使能需要用到的外设节点(把
status从disabled改为okay) - 配置引脚复用(
pinctrl) - 在Buildroot里指定使用这个新的
.dts文件
我曾经在一个项目里,因为设备树里一个中断号写错了,导致触摸屏死活没反应。查了两天才发现,是interrupts属性的第二个参数少了个0。这种坑,真的只有踩过才知道疼。
4.4 内核模块编译
内核模块,就是可以动态加载到内核里的代码。好处是不用每次改驱动都重新编译整个内核,重启机器。
在Buildroot里编译内核模块,有两种方式:
- 内置编译:在
linux-menuconfig里把驱动设为<*>(编译进内核) - 模块编译:设为
<M>(编译成模块),然后make会生成.ko文件
我个人更倾向于把不常用的驱动编译成模块。比如Wi-Fi驱动、USB摄像头驱动。这样内核镜像小,启动快,需要时再加载。
模块编译完成后,Buildroot会把 .ko 文件安装到目标文件系统的 /lib/modules/<kernel-version>/ 目录下。你可以在板子上用 modprobe 或 insmod 加载它们。
package/ 目录下创建一个包,用 KERNEL_MODULE_AUTOLOAD 让模块在启动时自动加载。这样就不用每次手动 insmod 了。
下面这张图,把内核配置、设备树、模块编译的关系串起来了:
你看,整个流程是串起来的。源码管理好了,配置对了,设备树写准了,模块编好了,最后才能在板子上跑起来。任何一个环节出问题,都可能导致系统起不来。
嗯,这一章的内容就这些。内核配置和设备树,是嵌入式Linux开发的基本功。多练、多踩坑、多总结,慢慢就熟了。