一、设备树基础:什么是设备树?为什么需要设备树?设备树在Linux内核中的角色

各位同学,今天我们来聊聊设备树。说实话,我刚入行那会儿,设备树这个概念也让我头疼了好一阵子。但搞明白之后,你会发现它其实没那么神秘。

1.1 什么是设备树?

设备树(Device Tree),说白了就是一套描述硬件信息的“说明书”。它用树形结构,把CPU、内存、外设、中断控制器、GPIO等等硬件资源,清清楚楚地列出来。

我习惯把它想象成一张“硬件地图”。内核启动时,拿着这张地图,就知道“哦,这里有个I2C控制器,那里有个SPI设备”。

来看一个最简单的例子:

/dts-v1/;

/ {
    model = "TI AM335x BeagleBone Black";
    compatible = "ti,am335x-bone-black", "ti,am335x-bone", "ti,am335x";

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

    cpus {
        cpu@0 {
            compatible = "arm,cortex-a8";
        };
    };

    uart0: serial@44e09000 {
        compatible = "ti,am3352-uart", "ti,omap3-uart";
        reg = <0x44e09000 0x2000>;
        interrupts = <72>;
    };
};

你看,这个树形结构,根节点是“/”,下面挂着memory、cpus、uart0等子节点。每个节点都有属性,比如compatible、reg、interrupts,这些就是硬件的“身份证”。

核心要点:设备树是一种数据结构,用于描述不可枚举的硬件。它把硬件信息从内核源码中剥离出来,变成独立的数据文件。

1.2 为什么需要设备树?

这个问题,我当年也问过自己。为什么不用传统的板级文件(board file)?

嗯,这里有个历史背景。在ARM Linux早期,每个板子都要写一个board-xxx.c文件,里面塞满了各种硬件初始化代码。比如:

// 传统方式:板级文件
static struct platform_device my_device = {
    .name = "my-uart",
    .id = 0,
    .dev = {
        .platform_data = &uart_platform_data,
    },
};

static void __init my_board_init(void)
{
    platform_device_register(&my_device);
    // 还要注册时钟、GPIO、中断...
}

这样做有什么问题?我举个例子。我在项目中遇到过,一个SoC被用在几十种不同的板子上,每个板子都要写一份类似的board文件。内核源码里充斥着大量重复代码,维护起来简直是噩梦。

设备树解决了三个核心痛点:

  • 硬件描述与内核解耦:换板子不用改内核,换一个.dtb文件就行
  • 减少重复代码:同一款SoC,不同板子共用驱动,只改设备树
  • 社区统一标准:PowerPC、ARM、RISC-V都用同一套描述方式

我的经验:有一次客户要快速适配一块新板子,我花了2小时改设备树,内核代码一行没动。要是放在以前,光改board文件就得折腾一整天。

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

设备树在内核启动过程中,扮演着“硬件信息传递者”的角色。它的生命周期是这样的:

  1. 编译阶段:.dts源文件 → dtc编译器 → .dtb二进制文件
  2. 启动阶段:Bootloader加载.dtb到内存,把地址传给内核
  3. 内核解析:内核从.dtb中读取硬件信息,创建platform_device、i2c_client等
  4. 驱动匹配:驱动通过compatible属性与设备节点匹配,完成初始化

下面这张图,是我自己画的设备树在内核中的流转过程:

设备树在内核中的角色流转图 .dts 源文件 文本格式,人类可读 dtc编译 .dtb 二进制 机器可读,Bootloader加载 内核解析 内核OF API of_xxx() 函数解析 设备模型创建 platform_device / i2c_client / spi_device 中断、GPIO、时钟、DMA等资源注册 驱动匹配与初始化 compatible属性匹配 → probe() 执行 关键数据结构 • struct device_node:设备节点 • struct property:节点属性 • struct platform_device:平台设备 • struct of_device_id:匹配表 • struct resource:资源描述 • phandle:节点引用句柄 • interrupt-parent:中断父节点

你想想看,内核启动时,如果没有设备树,它怎么知道板子上有哪些外设?地址是多少?中断号是多少?

设备树就是内核的“眼睛”。它告诉内核:

  • 内存有多大,起始地址在哪
  • 哪个外设挂在哪个总线上
  • 中断是怎么路由的
  • GPIO的复用关系
  • 时钟频率是多少

我曾经踩过的坑:有一次设备树里reg属性写错了地址,结果内核访问外设时直接挂死。查了两天才发现,原来是地址偏移量少写了一个0。所以,写设备树一定要仔细核对datasheet里的地址映射。

1.4 设备树的核心语法速览

这里我快速过一下设备树的基本语法,后面章节会详细展开:

语法元素 示例 说明
节点 uart0: serial@44e09000 节点名+标签+地址
属性 compatible = "ti,am3352-uart" 驱动匹配的关键
reg reg = <0x44e09000 0x2000> 地址+长度
interrupts interrupts = <72> 中断号
phandle interrupt-parent = <&intc> 引用其他节点
include #include "imx6ul.dtsi" 包含头文件

我的习惯:写设备树时,我一般先看datasheet里的memory map,把地址范围搞清楚。然后对照参考板子的.dts文件,改改compatible和reg就行。千万别自己从头写,容易出错。

好了,这一章就到这里。设备树说白了就是硬件信息的“说明书”,它让内核和硬件解耦,让我们的开发工作更灵活。后面我们会深入每个细节,包括如何编写、如何调试、如何修复兼容性问题。

可可精品资料库