第一章:设备树基础概念

各位同学好,我是老张。做嵌入式Linux开发十几年了,今天咱们来聊聊设备树。

说实话,我刚入行那会儿,还没有设备树这东西。那时候改个硬件配置,得改一堆C语言代码,改完还得重新编译内核。有一次我改了个GPIO编号,编译内核花了快一个小时,结果发现改错了...那滋味,真不好受。

什么是设备树

设备树,英文叫Device Tree。说白了,它就是一个描述硬件信息的文本文件。

你想想看,一个嵌入式系统里有多少硬件?CPU、内存、I2C控制器、SPI控制器、GPIO、中断控制器...每个硬件都有它的地址、中断号、时钟频率等等。设备树就是把这些信息用树形结构组织起来。

我习惯把设备树理解成硬件的「身份证」。它告诉内核:

  • 我是什么设备(比如:我是I2C控制器)
  • 我在哪里(比如:我的寄存器基地址是0x10000000)
  • 我怎么工作(比如:我支持100kHz和400kHz两种速率)
  • 我和谁连接(比如:我的中断线连到了中断控制器的第5号输入)

核心要点:设备树是硬件描述和内核驱动之间的桥梁。它让驱动代码和硬件配置解耦。

为什么需要设备树

在设备树出现之前,Linux内核是怎么知道硬件信息的?

答案是:硬编码。每个板子都在arch/arm/mach-xxx目录下有个board-xxx.c文件,里面用C语言结构体描述硬件。比如:

static struct platform_device my_device = {
    .name = "my-i2c",
    .id = 0,
    .num_resources = 2,
    .resource = (struct resource[]) {
        {
            .start = 0x10000000,
            .end = 0x10000FFF,
            .flags = IORESOURCE_MEM,
        },
        {
            .start = IRQ_I2C,
            .end = IRQ_I2C,
            .flags = IORESOURCE_IRQ,
        },
    },
};

这样做有什么问题?问题大了去了。

  • 代码冗余:每个板子都要写类似的代码,改一个引脚就得改整个文件
  • 维护困难:内核里充斥着各种板级文件,Linux 3.x时代光ARM平台就有上千个
  • 升级麻烦:换一颗Flash芯片,得重新编译整个内核

我记得有一次,客户临时换了一颗不同型号的Flash。按老办法,我得改board文件、重新配置内核、编译、烧录...折腾了大半天。后来用设备树,改一行dts文件,重新编译设备树,5分钟搞定。

我的经验:设备树最大的价值在于「配置与代码分离」。硬件变了,改dts文件就行,驱动代码不用动。这在产品快速迭代时特别有用。

设备树在CPE系统中的角色

CPE设备,就是咱们常见的家庭网关、企业路由器这类产品。这类设备有个特点:硬件配置五花八门。

同一个方案,可能衍生出十几个型号。有的带WiFi 6,有的带5G模块,有的带VoIP口...如果用老办法,每个型号都得维护一套内核代码,那工程师得疯掉。

设备树在CPE系统中的角色,我总结为三点:

角色 说明 实际例子
硬件配置表 描述CPE板上所有外设的地址、中断、时钟等 定义GMAC的MDIO引脚、PHY地址
驱动匹配器 告诉内核该用哪个驱动来驱动哪个设备 compatible = "realtek,rtl8367s" 匹配对应的switch驱动
运行时配置 uboot可以修改设备树,实现不同型号的差异化 通过修改dts使能或禁用某个USB端口

嗯,这里要注意一点。CPE设备通常有多个以太网口、多个WiFi频段、多个USB口。设备树里要清晰地描述每个接口的对应关系。我曾经见过一个项目,因为设备树里把LAN口和WAN口的GPIO搞反了,导致整批设备出厂后才发现问题...那叫一个惨。

设备树的基本结构

设备树的源文件后缀是.dts,编译后的二进制文件是.dtb。它的结构就像一棵倒挂的树:

/dts-v1/;

/ {
    model = "My CPE Router";
    compatible = "vendor,cpe-router-v1";

    chosen {
        bootargs = "console=ttyS0,115200";
    };

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

    soc {
        #address-cells = <1>;
        #size-cells = <1>;
        ranges;

        uart0: serial@10000000 {
            compatible = "ns16550a";
            reg = <0x10000000 0x1000>;
            interrupts = <0 5 4>;
            clock-frequency = <50000000>;
        };

        i2c0: i2c@10001000 {
            compatible = "vendor,i2c-controller";
            reg = <0x10001000 0x1000>;
            interrupts = <0 6 4>;
            #address-cells = <1>;
            #size-cells = <0>;

            eeprom@50 {
                compatible = "atmel,24c02";
                reg = <0x50>;
                pagesize = <8>;
            };
        };
    };
};

看到这个结构了吗?每个设备都是一个节点,节点里用属性来描述设备信息。compatible属性特别重要,它是驱动和设备匹配的关键。

避坑指南:我曾经在调试一个CPE项目时,发现I2C EEPROM死活读不到数据。查了两天才发现,是设备树里reg属性写错了地址。EEPROM的地址是0x50,我写成了0x05。这种低级错误,编译时不会报错,运行时就抓瞎了。

设备树在启动流程中的位置

CPE设备的启动流程大概是这样的:

  1. BootROM加载uboot
  2. uboot初始化基本硬件(DDR、串口等)
  3. uboot从Flash加载设备树(dtb)到内存
  4. uboot修改设备树(比如根据硬件版本调整配置)
  5. uboot加载内核,并把设备树地址传给内核
  6. 内核解析设备树,枚举设备,加载驱动

你看,设备树在uboot和内核之间扮演了信息传递的角色。uboot把硬件探测到的信息(比如内存大小、MAC地址)写进设备树,内核直接读取使用。

我个人习惯在uboot阶段加一些调试打印,确认设备树是否正确加载。比如:

# 在uboot命令行查看设备树
printenv fdtcontroladdr
fdt list /

这样能提前发现问题,不用等到内核启动失败再去排查。

小结

设备树不是什么高深的技术,它就是一套硬件描述规范。但它的出现,让嵌入式Linux开发效率提升了一大截。

说白了,设备树就是让搞硬件的人能专心改硬件,搞驱动的人能专心写驱动。各司其职,互不干扰。

下一章,咱们会深入设备树的语法细节,包括节点、属性、标签、引用这些概念。到时候我会拿一个真实的CPE设备树文件来拆解,保证让你看得明明白白。