1、设备树基础概念:什么是设备树、为什么需要设备树、设备树在Linux内核中的角色
各位同学,咱们今天聊聊设备树。说实话,我刚入行那会儿,Linux内核里还没有设备树这东西。那时候板级文件满天飞,每次换个Flash型号都要改arch/arm/mach-xxx下的代码,改完还得重新编译内核,烦得很。
设备树(Device Tree)到底是什么?我给它下个定义:设备树是一种描述硬件资源的数据结构。它用文本文件把CPU、内存、外设、中断、GPIO这些硬件信息写清楚,内核启动时解析这个文件,就知道自己跑在什么板子上了。
一句话总结:设备树就是硬件的“身份证”和“说明书”。内核不看板子长什么样,只看.dtb文件里写了什么。
1.1 为什么需要设备树?
在设备树出现之前,Linux内核是怎么处理硬件差异的?我给大家讲讲历史。
早期ARM Linux内核里,每个板子都有自己的board-xxx.c文件。比如你要支持三块开发板,就得在arch/arm/mach-xxx目录下放三个板级文件。每个文件里硬编码了GPIO号、中断号、时钟频率。这导致什么问题?
- 代码冗余:同样的驱动代码,因为板级信息不同,要复制多份
- 维护噩梦:换一个Flash型号,要改arch层的C代码,还要重新编译内核
- 厂商碎片化:每个芯片厂商都往内核里塞自己的板级文件,内核越来越臃肿
我记得2011年左右,我在一家做工业控制板的公司。当时我们同时维护三款产品,每款产品的GPIO分配都不一样。每次内核版本升级,我们都要花两周时间合并板级文件的改动。那滋味,真不好受。
后来设备树被引入ARM社区,情况就变了。硬件描述从C代码里剥离出来,变成独立的.dts文件。内核只负责解析,不负责硬编码。你想想看,这有多清爽?
| 对比项 | 板级文件时代 | 设备树时代 |
|---|---|---|
| 硬件描述位置 | arch/arm/mach-xxx/*.c | arch/arm/boot/dts/*.dts |
| 修改硬件信息 | 改C代码,重新编译内核 | 改.dts,重新编译dtb |
| 内核镜像通用性 | 一个内核只支持一块板 | 一个内核支持多块板 |
| 社区维护成本 | 高,每个板子都要review | 低,硬件描述与驱动分离 |
我的经验:设备树最大的价值不是“少写代码”,而是“分离关注点”。硬件工程师改板子,软件工程师不用动内核,改个.dts就行。这在产品快速迭代时特别有用。
1.2 设备树在Linux内核中的角色
设备树在内核启动过程中扮演什么角色?我画个图给大家看。
这张图展示了设备树在内核启动中的完整链路。我来拆解一下每个环节:
- Bootloader加载dtb:U-Boot或其他bootloader把.dtb文件从Flash读到内存,然后把dtb地址通过寄存器传给内核。
- 内核解析dtb:内核启动早期,在
setup_arch()函数里调用unflatten_device_tree(),把扁平化的dtb展开成树状结构。 - 驱动匹配:内核遍历设备树中的每个节点,根据
compatible属性找到对应的驱动,然后调用驱动的probe()函数。 - 资源分配:驱动从设备树节点里读取reg、interrupts、gpios等属性,完成硬件初始化。
我曾经踩过的坑:有一次我改了.dts里的reg地址,但忘了重新编译dtb。结果内核启动时驱动拿到的地址还是旧的,外设根本没法工作。排查了半天才发现是dtb没更新。所以大家记住:改完.dts一定要重新编译dtb,并且确认bootloader加载的是新文件。
1.3 设备树的核心文件类型
设备树涉及三种文件格式,我列个表给大家看:
| 文件类型 | 扩展名 | 格式 | 作用 |
|---|---|---|---|
| 设备树源文件 | .dts | 文本 | 开发者编写的硬件描述文件 |
| 设备树头文件 | .dtsi | 文本 | 公共硬件描述,被.dts包含 |
| 设备树二进制文件 | .dtb | 二进制 | 内核实际解析的文件 |
简单来说,.dts是源码,.dtsi是公共头文件,.dtb是编译后的产物。编译命令也很简单:
# 编译单个dts
dtc -I dts -O dtb -o myboard.dtb myboard.dts
# 在内核源码树中编译
make dtbs
我个人的习惯:在项目里,我会把SoC共用的部分放在soc.dtsi里,把板级差异放在board.dts里。这样换板子时只需要改board.dts,soc.dtsi基本不动。说白了,这就是设备树版的“模块化设计”。
1.4 设备树与驱动的交互方式
驱动怎么从设备树里拿信息?我给大家看个实际例子。
假设设备树里有一个UART节点:
/ {
soc {
uart0: serial@10000000 {
compatible = "myvendor,uart";
reg = <0x10000000 0x1000>;
interrupts = <0 33 4>;
clock-frequency = <24000000>;
};
};
};
驱动里这样读取:
static int my_uart_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct resource *res;
void __iomem *base;
int irq;
u32 clk;
// 获取寄存器地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(dev, res);
// 获取中断号
irq = platform_get_irq(pdev, 0);
// 获取时钟频率
of_property_read_u32(dev->of_node, "clock-frequency", &clk);
// 注册设备
return 0;
}
你看,驱动完全不关心硬件地址是怎么来的。它只管调用platform_get_resource()和of_property_read_u32(),内核会自动从设备树里把数据取出来。这就是设备树的精髓——硬件描述与驱动逻辑彻底解耦。
核心思想:设备树是“数据”,驱动是“逻辑”。数据变了,逻辑不用改。逻辑要升级,数据不用动。两者通过compatible属性建立联系。