1. 设备树基础:从零认识这个嵌入式世界的“硬件说明书”

做嵌入式 Linux 开发这些年,我见过太多新手被设备树搞得一头雾水。说实话,我刚接触时也差不多——明明写驱动直接操作寄存器多痛快,怎么非要搞个 .dts 文件出来?

但干过几个项目后,我彻底服气了。设备树这东西,说白了就是一套描述硬件的“标准化语言”。你想想看,一个 SoC 上挂了多少外设?GPIO、I2C、SPI、UART、PWM……每个外设的地址、中断号、时钟频率都不一样。如果没有统一的管理方式,那代码得乱成什么样?

什么是设备树?

设备树(Device Tree)是一种描述硬件资源的数据结构。它用树形结构把 CPU、内存、总线、外设全部组织起来。内核启动时读取这个结构,就知道“哦,我这块板子上有个 UART3,地址在 0x1000A000,中断号是 42”。

它的核心文件是 .dts(Device Tree Source),编译后生成 .dtb(Device Tree Blob),内核直接解析 .dtb。

一句话总结:设备树 = 硬件的“身份证 + 户口本”,内核靠它认人。

为什么需要设备树?

在设备树出现之前,Linux 内核是怎么处理硬件的?

  • 板级文件(Board File):每个板子一个 .c 文件,里面写死所有硬件信息。比如 arch/arm/mach-xxx/ 下面一堆 board-xxx.c。
  • 问题来了:ARM 架构碎片化严重,厂商又多,每次换个板子就要改内核代码。Linux 主线内核维护者都快疯了——每天收到几百个板级补丁。

我记得有一次,帮客户移植内核到一块新板子。光改 board 文件就花了三天,还要重新编译整个内核。后来换成设备树,改个 .dts 文件,重新编译设备树就行,内核镜像都不用动。

对比项 板级文件时代 设备树时代
硬件描述位置 内核源码 .c 文件 独立 .dts 文件
更换板子 改内核、重编译 改 .dts、重编译 dtb
内核维护成本 极高 低
厂商适配效率 慢 快

个人经验:现在做 BSP 移植,我习惯先看 .dts 文件。硬件怎么接的、中断用哪个、时钟配多少,一目了然。比翻原理图快多了。

设备树源文件语法

.dts 文件的语法其实不复杂。核心就几个概念:节点(node)、属性(property)、值(value)。

来看一个最简单的例子:

/dts-v1/;

/ {
    compatible = "mycompany,myboard";
    
    cpu@0 {
        compatible = "arm,cortex-a7";
        reg = <0x00>;
    };
    
    memory@80000000 {
        device_type = "memory";
        reg = <0x80000000 0x40000000>;  /* 1GB RAM */
    };
    
    uart0: serial@1000A000 {
        compatible = "ns16550a";
        reg = <0x1000A000 0x1000>;
        interrupts = <0 33 4>;
        clock-frequency = <24000000>;
    };
};

这里我解释几个关键点:

  • /dts-v1/:版本声明,必须写在第一行。
  • / { }:根节点,整棵树从这里开始。
  • compatible:最重要的属性之一。内核靠它匹配驱动。格式是“厂商,型号”。
  • reg:地址信息。格式是 <地址 长度>。比如 uart0 的 reg = <0x1000A000 0x1000>,表示基地址 0x1000A000,地址范围 0x1000 字节。
  • interrupts:中断描述。三个值分别是中断控制器编号、中断号、触发类型。
  • uart0::标签(label),方便其他地方引用。比如 &uart0 就能引用这个节点。

避坑指南:我曾经在 reg 属性上栽过跟头。某个外设的地址范围写错了,结果驱动访问时直接挂死。后来养成习惯——每次写完 .dts 都拿原理图对一遍地址。

设备树编译与反编译

.dts 文件不能直接给内核用,得编译成二进制 .dtb。工具是 dtc(Device Tree Compiler)。

基本用法:

# 编译 .dts -> .dtb
dtc -I dts -O dtb -o myboard.dtb myboard.dts

# 反编译 .dtb -> .dts
dtc -I dtb -O dts -o myboard.dts myboard.dtb

# 查看 dtb 内容(人类可读)
dtc -I dtb -O dts myboard.dtb | less

我个人的工作流是这样的:

  1. 从厂商拿到参考 .dts 文件
  2. 根据原理图修改节点(地址、中断、时钟)
  3. 编译成 .dtb
  4. 烧录到板子,启动内核
  5. 如果启动失败,反编译 .dtb 检查是否生效

小技巧:调试时可以用 dtc -I dtb -O dts /sys/firmware/devicetree/base 查看内核当前使用的设备树。这样能确认你烧录的 dtb 是不是对的。

下面这张图是我自己总结的设备树在嵌入式系统中的完整流转路径:

设备树在嵌入式系统中的流转路径 硬件原理图 寄存器地址 / 中断号 .dts 源文件 文本格式 / 可读可改 dtc 编译 dtc -I dts -O dtb .dtb 二进制 烧录到存储介质 Bootloader 加载 U-Boot 传递 dtb 地址 内核解析 of_* API 读取节点 驱动匹配 compatible 属性匹配 外设正常工作 UART / I2C / SPI / GPIO 💡 调试常用命令 cat /proc/device-tree/ dtc -I dtb -O dts /sys/...