Contents

设备树

设备树

在嵌入式 Linux 系统中,设备树(Device Tree, DT) 是一套用来描述硬件结构的数据结构。

背景:在早期的 Linux 内核中,硬件的细节(比如某个控制器的寄存器地址、中断号)都硬编码在 C 代码中。这导致内核代码非常臃肿,且不同型号的开发板需要重新编译内核。

作用:设备树实现了**“硬件描述”与“内核代码”的分离**。内核镜像(zImage)是通用的,通过加载不同的设备树文件(DTB),内核就能知道当前运行的硬件环境。

表现形式:

DTS (Device Tree Source):人类可读的文本文件(类似 JSON 或 C 结构体)。

DTC (Device Tree Compiler):编译器。

DTB (Device Tree Blob):编译后的二进制文件,由引导程序(如 U-Boot)传递给内核。

工作流程

设备树是一种用来描述硬件配置的数据结构。它本质上是一棵“树”,由节点和属性组成,用来向操作系统(特别是 Linux)传递硬件的详细信息,从而实现硬件与操作的解耦。

设备树的框架可以从源码组织逻辑结构关键语法三个维度来理解:


1. 源码组织形式

在开发过程中,设备树源码(DTS, Device Tree Source)通常遵循以下文件组织框架:

  • .dts 文件
    • 这是板级描述文件。每一块不同的开发板对应一个 .dts 文件。
    • 描述了该开发板特有的硬件配置(如内存大小、板载外设接口等)。
  • .dtsi 文件
    • 这是头文件或包含文件(类似 C 语言的 .h)。
    • 通常描述 SoC(片上系统)级的通用硬件配置(如 CPU 核心数量、通用 GPIO 控制器、中断控制器等)。
    • 不同的开发板如果共用同一款 SoC,可以包含同一个 .dtsi 文件,避免代码重复。
  • 编译产物 .dtb (Device Tree Blob)
    • .dts 文件经过编译器(dtc)编译后,生成二进制的 .dtb 文件。
    • Bootloader(如 U-Boot)在启动内核时,会将 .dtb 加载到内存,并将其地址传递给 Linux 内核。

2. 逻辑结构(树状图)

设备树在逻辑上是一个树状结构,有一个唯一的根节点(/),根节点下包含若干子节点,子节点还可以再包含子节点。

典型框架如下:

/ (根节点)
├── model = "My Board";          /* 板子名称 */
├── compatible = "vendor,board"; /* 兼容性字符串 */
├── #address-cells = <1>;        /* 地址编码格式 */
├── #size-cells = <1>;           /* 长度编码格式 */
│
├── cpus {                       /* CPU 节点 */
│   ├── cpu@0 { ... };
│   └── cpu@1 { ... };
│}
│
├── memory@40000000 {            /* 内存节点 */
│   reg = <0x40000000 0x80000000>;
│}
│
├── chosen {                     /* 系统启动参数 */
│   bootargs = "console=ttyS0,115200 root=/dev/ram";
│}
│
├── soc {                        /* SoC 节点(通常来自 .dtsi)*/
│   ├── compatible = "vendor,soc-name";
│   │
│   ├── serial@... {             /* 串口设备 */
│   │    status = "okay";
│   │    ...属性...
│   };
│   │
│   ├── i2c@... {                /* I2C 总线控制器 */
│   │    status = "okay";
│   │    /* I2C 总线下的从设备 */
│   │    sensor@50 {
│   │        compatible = "vendor,sensor";
│   │        reg = <0x50>;
│   │    };
│   │ };
│   │
│   ├── spi@... { ... };
│   └── gpio@... { ... };
│}

关键区域说明:

  1. 根节点 (/):树的起点。
  2. CPU 节点 (cpus):描述多核处理器的拓扑结构。
  3. 内存节点 (memory):告诉内核物理内存的起始地址和大小。
  4. Chosen 节点 (chosen):传递内核启动参数或引导加载程序的数据(如 bootargs)。
  5. SoC/Bus 节点 (soc, amba, pci):作为总线节点,连接各种外设控制器。

3. 核心语法构成

设备树的“叶子”是节点,节点的内容是属性

A. 节点

节点用名称标识,格式通常为 name@unit-address

  • name:节点名称(如 serial, gpio, i2c)。
  • unit-address:该节点在寄存器总线上的地址。如果节点没有 reg 属性,通常省略 @unit-address

B. 属性

属性是键值对,用于描述硬件的具体信息。主要有以下几种形式:

  1. 字符串属性

    • 最重要的是 compatible(兼容性)。
    • 格式:"manufacturer,model"
    • 作用:内核驱动通过匹配这个字符串来绑定驱动程序。
    • 示例:compatible = "arm,cortex-a9";
  2. 数组属性 (reg)

    • 描述寄存器地址或内存映射区域。
    • 格式:一组 address length
    • 示例:reg = <0x101F2000 0x1000>; (起始地址 0x101F2000,长度 0x1000)。
  3. 逻辑属性

    • 只有属性名,没有值。如果属性存在,则表示“真/开启”;如果不存在,表示“假/关闭”。
    • 示例:status = "okay";status = "disabled"; (注意 status 也是字符串,但像 interrupts-extended 这种逻辑常用于开启某功能)。
  4. 引用属性 (phandle)

    • 用于节点之间的连接。例如,一个 GPIO 设备需要引用另一个 GPIO 控制器节点。
    • 使用 &label 的方式引用。
    • 示例:pinctrl-0 = <&uart0_pins>;

4. 设备树如何与内核交互(工作原理)

理解框架的最后,需要知道数据是怎么流动的:

  1. 解析:内核启动时,解析 .dtb 文件,将其在内存中展开为链表/树形结构。
  2. 匹配:内核初始化各个驱动时,驱动程序中定义的 of_match_table 会去遍历设备树中的 compatible 属性。
  3. 获取资源:一旦匹配成功,驱动通过 API(如 of_iomap, of_get_interrupt 等)读取节点中的 reg(寄存器地址)、interrupts(中断号)等属性,从而操作硬件。

总结

设备树的框架可以概括为:

  • 形式上:分层的源码(.dts 包含 .dtsi)编译成二进制(.dtb)。
  • 结构上:以 / 为根,向下分支出 CPU、内存、总线、外设。
  • 内容上:节点负责分类(是谁),属性负责描述(有什么、在哪、怎么配)。