一、设备树的前世今生:为什么需要设备树?从板级文件到设备树的演进

1.1 那个没有设备树的年代

说实话,我刚入行那会儿,Linux内核里还没有设备树这东西。那时候我们做嵌入式开发,最头疼的事情之一就是——改板子。

你想想看,一个芯片方案,今天客户要换颗传感器,明天要改个GPIO复用,后天又想把LCD接口从RGB换成LVDS。每次硬件改版,内核代码就得跟着改。改什么?改那个叫board-xxx.c的板级文件。

我记得2010年左右,我在一家做安防摄像头的公司。当时我们用的是一颗ARM9的芯片,每次硬件改版,我都要在arch/arm/mach-xxx目录下翻半天,找到对应的板级文件,然后手动修改寄存器地址、中断号、GPIO配置。改完还得重新编译整个内核。那感觉,就像每次搬家都要重新装修房子一样痛苦。

板级文件时代的痛点:
  • 硬件改动 → 内核代码改动 → 重新编译 → 烧录验证
  • 每个板子一个board-xxx.c,代码冗余严重
  • 驱动代码和硬件信息高度耦合,可移植性极差
  • 不同厂商的板级文件风格各异,维护成本高

说白了,那时候的Linux内核,就像一个把所有家具都焊死在墙上的房子。你想换个沙发?对不起,得把墙拆了重砌。

3.2 设备树的诞生:把硬件信息从代码中剥离

为什么会这样?因为内核里同时包含了「驱动逻辑」和「硬件配置信息」。这两者本该是分开的。

设备树(Device Tree)的出现,就是为了解决这个问题。它的核心思想很简单:把硬件描述从内核代码中抽离出来,放到一个独立的数据结构中

这个数据结构,就是设备树源文件(.dts)。它用树形结构描述硬件:CPU、内存、外设、中断、GPIO、时钟……所有硬件资源,都在这里定义。内核启动时,解析这个文件,动态构建硬件信息,然后驱动去匹配、去使用。

嗯,这里要注意:设备树不是Linux独有的。它最早源于PowerPC架构,后来被ARM社区采纳,再后来几乎成了嵌入式Linux的标准做法。

我个人习惯:把设备树想象成硬件的「身份证」。它只描述「你是谁、你在哪、你怎么用」,至于怎么驱动你,那是驱动代码的事。两者各司其职,互不干扰。

3.3 从板级文件到设备树的演进路线

这个演进过程,我把它总结为三个阶段:

阶段 代表方式 特点 典型问题
第一阶段 板级文件(board-xxx.c) 硬件信息硬编码在C代码中 改硬件=改内核,维护噩梦
第二阶段 平台设备(platform_device) 用结构体描述硬件资源 代码量依然大,冗余多
第三阶段 设备树(Device Tree) 独立描述文件,内核动态解析 学习曲线陡,但一劳永逸

我曾经在一个项目里,从板级文件迁移到设备树。那是一个基于i.MX6Q的工控主板,原来有6个板级文件,每个文件2000多行。迁移后,变成了1个.dts文件和几个.dtsi头文件,总共不到800行。而且,后续硬件改版,只需要改.dts,内核代码完全不用动。

你说爽不爽?

3.4 设备树的核心结构:一个简单的例子

来看一个最简单的设备树片段:

/dts-v1/;

/ {
    model = "MyBoard v1.0";
    compatible = "vendor,myboard", "vendor,myboard-v1";

    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x10000000>;  // 256MB内存
    };

    uart0: serial@10000000 {
        compatible = "vendor,uart";
        reg = <0x10000000 0x1000>;
        interrupts = <0 31 4>;
        clock-frequency = <24000000>;
    };

    gpio0: gpio@20000000 {
        compatible = "vendor,gpio";
        reg = <0x20000000 0x1000>;
        gpio-controller;
        #gpio-cells = <2>;
    };
};

这个例子展示了设备树的几个核心要素:

  • 根节点/,代表整个板子
  • 属性compatiblereginterrupts 等,描述硬件特性
  • 节点memoryuart0gpio0,每个外设一个节点
  • 标签uart0:,方便其他节点引用
避坑指南:我曾经在调试一个SPI设备时,死活读不到数据。查了两天,最后发现是reg属性里的地址写错了——少了一个0。设备树里地址写错,驱动是不会有任何提示的,它只会默默地读到错误的数据。所以,写设备树一定要对着原理图逐字核对

3.5 设备树如何映射到硬件寄存器?

这个问题,说白了就是:设备树里写的那些地址,怎么变成驱动里能用的寄存器?

流程是这样的:

  1. 内核启动时,解析.dtb(编译后的设备树),构建device_node
  2. 驱动通过of_match_table匹配compatible属性
  3. 匹配成功后,驱动调用of_iomap(),把reg里的物理地址映射到虚拟地址
  4. 然后驱动就可以通过这个虚拟地址,直接读写硬件寄存器了

我画了一张图,帮你理解这个流程:

设备树到硬件寄存器映射流程 设备树源文件 myboard.dts 编译 设备树二进制 myboard.dtb 解析 设备节点树 device_node 结构体 驱动匹配 驱动匹配成功 of_match_table 匹配 of_iomap() 物理地址 → 虚拟地址映射 ioremap / of_iomap 最终:驱动通过虚拟地址读写硬件寄存器

这个流程里,最关键的一步就是of_iomap()。它把设备树里写的物理地址,映射到内核能用的虚拟地址。驱动拿到这个虚拟地址后,就可以用readl()/writel()直接操作寄存器了。

我建议:刚开始学设备树的时候,别急着写复杂的.dts。先拿一个简单的板子,对着原理图,把UART、GPIO、I2C这几个基本外设的设备树写出来。然后写一个简单的驱动,用of_iomap()映射寄存器,看看能不能点亮一个LED。这一步走通了,后面就顺了。

3.6 设备树的真正价值

说了这么多,设备树到底解决了什么问题?我总结三点:

  • 硬件与驱动解耦:同一个内核镜像,可以支持不同硬件配置的板子。只需要换.dtb文件就行。
  • 减少内核代码量:ARM架构从板级文件迁移到设备树后,内核代码量减少了约30%。
  • 硬件描述标准化:所有厂商都使用同一套语法描述硬件,降低了学习成本。

嗯,当然设备树也不是完美的。它的语法有点啰嗦,调试起来也不方便。但相比板级文件时代,已经是天壤之别了。

我记得有一次,客户临时要改一个GPIO的复用功能。放在以前,我得改板级文件、重新编译内核、烧录、重启。现在呢?改一行.dts,重新编译.dtb,替换文件,重启。前后不到5分钟。

这就是设备树的价值。


本章小结:

  • 设备树的核心目的:将硬件描述从内核代码中分离
  • 演进路径:板级文件 → 平台设备 → 设备树
  • 映射流程:.dts → .dtb → device_node → of_iomap → 寄存器访问
  • 设备树不是银弹,但它解决了嵌入式Linux开发中最头疼的硬件耦合问题

下一章,我们会深入设备树的语法细节,看看.dts文件到底怎么写,那些compatiblereginterrupts属性到底是什么意思。到时候,我会拿一个真实的板子来拆解。


公众号:蓝海资料掘金营,微信deep3321