1. 设备树基础概念:什么是设备树、为什么需要设备树、设备树在Linux内核中的角色

各位同学,咱们今天聊点实在的。设备树(Device Tree)这东西,说白了就是一套描述硬件信息的“说明书”。你想想看,一个嵌入式板子上有CPU、内存、I2C控制器、SPI接口、GPIO引脚……这些东西怎么让Linux内核知道?

在早期,大家都是把硬件信息硬编码在代码里。每个板子一个board文件,里面全是平台设备、资源注册。我刚开始做嵌入式那会儿,最怕的就是换板子——换个Flash型号,得改代码重新编译内核。那叫一个痛苦。

1.1 什么是设备树

设备树是一种树形数据结构,用文本文件描述硬件信息。它的格式是.dts(Device Tree Source),编译后变成.dtb(Device Tree Blob),内核启动时解析它。

来看一个最简单的例子:

/dts-v1/;

/ {
    model = "Rockchip RK3566 EVB1";
    compatible = "rockchip,rk3566-evb1", "rockchip,rk3566";

    memory@60000000 {
        device_type = "memory";
        reg = <0x60000000 0x40000000>;
    };

    chosen {
        stdout-path = &uart2;
    };
};

这个结构很直观:根节点“/”下面挂memory、chosen等子节点。每个节点描述一个硬件设备或系统属性。

核心要点:设备树把硬件描述从内核代码中剥离出来,变成独立的数据文件。内核和硬件之间多了一层抽象。

1.2 为什么需要设备树

我遇到过不少刚入行的朋友问:以前没有设备树不也跑得好好的吗?嗯,确实能跑,但代价很大。

咱们看看没有设备树的时候,Linux是怎么处理硬件的:

对比项 传统方式(board文件) 设备树方式
硬件描述位置 arch/arm/mach-xxx/board-xxx.c 独立的.dts文件
换板子代价 改代码、重新编译内核 换.dtb文件即可
内核维护负担 每个板子一个board文件,数量爆炸 统一设备树接口,代码复用
驱动与硬件耦合度 高,驱动里写死资源 低,驱动通过设备树获取资源
多平台支持 困难,每个平台单独维护 一套内核镜像支持多个板子

说白了,设备树解决了三个核心问题:

  • 硬件与软件解耦——换硬件不用改内核代码
  • 减少内核碎片——不用每个板子一个board文件
  • 统一硬件描述语言——ARM、RISC-V、MIPS都用同一套语法
我的经验:在Rockchip平台上,同一个RK3566芯片,可以用在不同厂商的板子上。只要提供不同的.dts文件,内核就能适配。我曾经帮客户适配过一块非标板,只改了.dts里的GPIO映射和时钟配置,内核镜像完全没动。

1.3 设备树在Linux内核中的角色

设备树在内核启动过程中扮演什么角色?我画了一张图,你看完就明白了:

设备树在Linux内核启动中的角色 设备树在Linux内核启动中的角色 Bootloader 加载.dtb到内存 内核早期启动 解析DTB,建立设备树 设备树 内存中的树结构 平台设备注册 扫描节点,注册platform_device 驱动匹配 compatible匹配,probe调用 资源获取 reg、interrupts、GPIO等 硬件正常驱动 设备树驱动硬件初始化 💡 设备树是内核与硬件之间的“翻译官”,它把硬件描述从代码中解放出来。 内核启动时,设备树驱动根据设备树信息完成硬件初始化。

从这张图你能看到,设备树在内核启动中扮演了三个关键角色:

  1. 硬件信息载体——Bootloader把.dtb传给内核,内核从中读取内存布局、外设地址、中断号等
  2. 驱动匹配依据——驱动通过compatible属性与设备树节点匹配,匹配成功就调用probe
  3. 资源分配蓝图——驱动从设备树获取reg、interrupts、GPIO、时钟等资源
避坑提醒:我曾经遇到过一个问题——板子上的以太网死活不工作。查了两天,最后发现是.dts里phy的reg地址写错了。设备树里一个数字不对,硬件就驱动不起来。所以写设备树一定要对着原理图仔细核对。

1.4 设备树在Rockchip平台的特殊性

Rockchip平台对设备树的依赖非常重。为什么?因为Rockchip的SoC集成度很高,一个芯片里集成了CPU、GPU、NPU、ISP、VPU、多个显示控制器……

我列一下Rockchip设备树里常见的几大类节点:

节点类型 示例 说明
CPU cpu@0 描述CPU核心,包括频率、电压、冷却设备
内存 memory@60000000 DDR内存的起始地址和大小
时钟 cru: clock-controller@ff760000 时钟复位控制器,管理所有外设时钟
引脚控制 pinctrl GPIO复用和电气特性配置
显示 vop@fe040000 视频输出处理器,连接HDMI/DP/MIPI
多媒体 vpu@fdea0000 视频编解码单元
NPU npu@ffa00000 神经网络处理单元

你看,光是显示相关的节点就有VOP、HDMI TX、DP TX、MIPI DSI好几个。每个节点都有自己的时钟、中断、引脚配置。如果没有设备树,这些信息全塞在代码里,那维护起来简直是噩梦。

个人习惯:我拿到一块新的Rockchip板子,第一件事就是看它的.dts文件。从dts里你能读出这个板子用了哪些外设、引脚怎么分配的、内存多大。这比看原理图快多了。

1.5 设备树文件组织

在Linux内核源码里,Rockchip的设备树文件放在这里:

arch/arm64/boot/dts/rockchip/
├── rk3566.dtsi          # SoC级定义,所有RK3566芯片通用
├── rk3568.dtsi          # SoC级定义,所有RK3568芯片通用
├── rk3566-evb1.dts      # 板级定义,EVB1开发板
├── rk3566-evb2.dts      # 板级定义,EVB2开发板
├── rk3568-evb1.dts      # 板级定义,EVB1开发板
└── ...                   # 其他板子

这里有个重要的概念:.dtsi是头文件,.dts是板级文件。.dtsi定义SoC内部的所有硬件模块,.dts只定义板子特有的部分(比如用了哪个Flash、哪个GPIO接了LED)。

我举个例子,rk3566.dtsi里定义了I2C控制器:

i2c0: i2c@ff3f0000 {
    compatible = "rockchip,rk3568-i2c", "rockchip,rk3399-i2c";
    reg = <0x0 0xff3f0000 0x0 0x1000>;
    interrupts = <GIC_SPI 4 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru CLK_I2C0>, <&cru PCLK_I2C0>;
    clock-names = "i2c", "pclk";
    pinctrl-0 = <&i2c0_xfer>;
    status = "disabled";
};

然后在板级.dts里,只需要打开它并挂上设备:

&i2c0 {
    status = "okay";
    clock-frequency = <400000>;

    hym8563: rtc@51 {
        compatible = "haoyu,hym8563";
        reg = <0x51>;
        interrupts = <&gpio0 RK_PB1 IRQ_TYPE_LEVEL_LOW>;
    };
};
关键理解:.dtsi负责“有什么”,.dts负责“用什么”。这种分层设计让代码复用率极高。同一个RK3566芯片,用在路由器上、用在平板电脑上、用在工控机上,只需要写不同的.dts文件就行。

嗯,这一章的内容就到这儿。设备树的概念其实不复杂,但它是整个嵌入式Linux硬件适配的基石。后面的章节我们会深入每个细节——从语法到实战,从时钟到pinctrl,一步步把Rockchip的设备树吃透。