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内核中的角色

设备树在内核启动过程中扮演什么角色?我画个图给大家看。

设备树在Linux内核启动中的角色 Bootloader 加载 .dtb 到内存 内核解压,解析dtb 驱动匹配 & 设备注册 系统启动完成 关键点 • dtb由Bootloader加载 • 内核不直接访问硬件 • 驱动通过dtb获取资源

这张图展示了设备树在内核启动中的完整链路。我来拆解一下每个环节:

  1. Bootloader加载dtb:U-Boot或其他bootloader把.dtb文件从Flash读到内存,然后把dtb地址通过寄存器传给内核。
  2. 内核解析dtb:内核启动早期,在setup_arch()函数里调用unflatten_device_tree(),把扁平化的dtb展开成树状结构。
  3. 驱动匹配:内核遍历设备树中的每个节点,根据compatible属性找到对应的驱动,然后调用驱动的probe()函数。
  4. 资源分配:驱动从设备树节点里读取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属性建立联系。