Linux 内核编译与设备树基础
说实话,很多初学者一上来就急着写驱动,结果连内核怎么编、设备树怎么配都没搞明白。我当年也踩过这个坑——花了两天调一个GPIO,最后发现是设备树里引脚号写错了。所以这一章,咱们把地基打牢。
内核编译和设备树,说白了就是两件事:让内核认识你的硬件,和让内核知道硬件怎么连的。前者靠配置和编译,后者靠设备树描述。咱们一步步来。
2.1 下载内核源码
RK3566/RK3568 官方推荐用 Rockchip 的 BSP 内核,基于 Linux 5.10 LTS。我个人习惯直接从 Rockchip GitHub 拉取,或者用全志/瑞芯微的 SDK 包。
# 从 GitHub 克隆(推荐)
git clone -b linux-5.10.y https://github.com/rockchip-linux/kernel.git rk3568-kernel
# 或者从 SDK 包解压
tar -xvf rk3568_linux_sdk_v1.0.0.tar.gz
cd kernel
arch/arm64/boot/dts/rockchip/ 目录,确认有没有 rk3568-evb.dts 这类文件。没有的话,说明你拿到的内核版本可能不对。
2.2 配置内核
配置内核,就是告诉编译器:哪些功能要编进内核,哪些编成模块,哪些直接砍掉。RK3568 是 ARM64 架构,所以用 defconfig 起步最省事。
# 先清理
make mrproper
# 加载 RK3568 默认配置
make ARCH=arm64 rockchip_linux_defconfig
# 如果想微调,用菜单界面
make ARCH=arm64 menuconfig
menuconfig 里我经常改的几个地方:
- Device Drivers → GPIO Support —— 确认 GPIO 控制器选上
- Device Drivers → SPI / I2C Support —— 根据外设勾选
- File systems —— 如果根文件系统是 ext4,确保编进去
M 或者直接 N。
2.3 编译内核与设备树
配置好了,就该编译了。RK3568 是 4 核 Cortex-A55,我一般用 -j8 并行编译,大概 10~15 分钟搞定。
# 编译内核镜像
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j8
# 单独编译设备树
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs
# 编译内核模块
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- modules
编译完,关键产出物有这些:
| 文件 | 路径 | 说明 |
|---|---|---|
| Image | arch/arm64/boot/Image | 内核镜像(未压缩) |
| rk3568-evb.dtb | arch/arm64/boot/dts/rockchip/ | 编译好的设备树二进制 |
| *.ko | 各驱动目录 | 内核模块(.ko 文件) |
避坑指南: 我遇到过编译完发现设备树没更新,原因是只跑了 make 没跑 make dtbs。记住:make 默认不重新编译设备树,必须单独执行 make dtbs。
2.4 理解设备树语法
设备树(Device Tree)是一种描述硬件拓扑的数据结构。它用 节点(node) 和 属性(property) 来描述 CPU、内存、外设等。内核启动时解析 .dtb 文件,自动匹配驱动。
来看一个最简单的设备树片段:
/dts-v1/;
/ {
model = "Rockchip RK3568 EVB";
compatible = "rockchip,rk3568-evb", "rockchip,rk3568";
chosen {
stdout-path = &uart2;
};
memory@a00000 {
device_type = "memory";
reg = <0x0 0xa00000 0x0 0x80000000>;
};
uart2: serial@fe660000 {
compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
reg = <0x0 0xfe660000 0x0 0x100>;
interrupts = <0 252 4>;
clocks = <&cru SCLK_UART2>, <&cru PCLK_UART2>;
clock-names = "baudclk", "apb_pclk";
status = "okay";
};
};
这里有几个关键点:
- /dts-v1/; —— 版本声明,必须写在第一行
- / { }; —— 根节点,所有设备都是它的子节点
- compatible —— 驱动匹配的关键字,内核通过它找到对应的驱动
- reg —— 寄存器地址和范围,格式是 <地址 长度>
- interrupts —— 中断号、触发类型
- status —— "okay" 表示启用,"disabled" 表示禁用
compatible 一定要和驱动里的 of_match_table 完全一致,大小写都不能错。我见过有人把 "rockchip" 写成 "rockchip," 多了一个逗号,结果驱动死活匹配不上。
2.5 设备树编译与反编译
.dts 是文本文件,内核不认识。得用 dtc 工具编译成 .dtb 二进制。反过来,.dtb 也能反编译成 .dts,方便调试。
# 编译 .dts → .dtb
dtc -I dts -O dtb -o rk3568-evb.dtb rk3568-evb.dts
# 反编译 .dtb → .dts
dtc -I dtb -O dts -o rk3568-evb.dts rk3568-evb.dtb
反编译特别有用。我调试时经常把板子上的 .dtb 拷出来反编译,看看实际生效的设备树长什么样,跟源码对比找差异。
2.6 知识结构总览
下面这张图把本章的核心逻辑串起来了。你跟着走一遍,心里就有谱了。
2.7 驱动匹配机制
设备树写好了,驱动怎么找到它?内核里有一套 platform 总线匹配机制。简单说,驱动注册时声明自己支持哪些 compatible,内核遍历设备树节点,找到匹配的就调用驱动的 probe 函数。
驱动里通常这样写:
static const struct of_device_id rk3568_uart_of_match[] = {
{ .compatible = "rockchip,rk3568-uart" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, rk3568_uart_of_match);
static struct platform_driver rk3568_uart_driver = {
.probe = rk3568_uart_probe,
.remove = rk3568_uart_remove,
.driver = {
.name = "rk3568-uart",
.of_match_table = rk3568_uart_of_match,
},
};
module_platform_driver(rk3568_uart_driver);
看到没?of_match_table 里的字符串,必须和设备树里的 compatible 一模一样。这就是它们之间的「暗号」。
总结一下: 内核编译是体力活,设备树是技术活。配置别贪多,设备树别写错,编译别漏 dtbs。这三条做到了,后面写驱动就顺了。