2. 设备树基础:起源、语法规则与编译方法
设备树(Device Tree)是嵌入式 Linux 系统中用于描述硬件拓扑结构的数据结构,它替代了早期 ARM Linux 内核中大量硬编码的 board-*.c 文件,实现了内核与硬件描述的分离。本章将系统讲解设备树的起源、语法规则、文件格式及编译方法,为后续 DRM 显示驱动的设备树配置打下坚实基础。
2.1 设备树的起源与设计动机
在 Linux 内核早期(2.6 及之前版本),ARM 架构的板级支持包(BSP)通常在内核源码中直接包含 arch/arm/mach-* 目录下的 board-*.c 文件,这些文件通过静态结构体描述硬件资源(如中断号、内存地址、时钟频率等)。这种方式的弊端随着 ARM SoC 的多样化而愈发明显:
- 代码冗余:每增加一款开发板,就需要新增或修改大量 C 代码。
- 耦合度高:硬件描述与内核驱动代码紧密绑定,升级内核时需同步维护板级文件。
- 二进制 Blob 依赖:部分厂商通过 ATAG(ARM Tagged List)传递参数,缺乏标准化。
设备树(Device Tree)最初源于 Open Firmware(IEEE 1275)规范,PowerPC 架构率先采用。2011 年,ARM Linux 社区决定全面引入设备树,从 Linux 3.x 开始逐步淘汰 board-*.c。设备树的核心思想是:将硬件描述从内核源码中剥离,以树形结构文本文件(.dts)的形式独立存在,由 bootloader(如 U-Boot)传递给内核解析。
关键优势:同一份内核镜像可以支持多种硬件平台,只需更换设备树文件即可。DRM 显示驱动中,管脚复用、显示接口类型(HDMI/DSI/LVDS)、背光控制等硬件参数均通过设备树节点描述。
2.2 设备树语法规则
设备树源文件(.dts)采用树形结构,由节点(node)、属性(property)和标签(label)组成。每个节点代表一个硬件设备或总线,属性描述设备的配置参数。
2.2.1 节点(Node)
节点是设备树的基本单元,格式为:
[label:] node-name[@unit-address] {
[property-definitions]
[child-nodes]
};
- node-name:节点名称,通常为通用设备类型(如
i2c、spi、display)。 - @unit-address:可选,表示设备在总线上的地址(如寄存器基址、I2C 从机地址)。
- label:可选,为节点定义唯一标识符,便于其他节点引用(如
&dsi0)。
DRM 驱动常见节点示例:显示控制器节点通常命名为 display-controller@xxxx,DSI 接口节点命名为 dsi@xxxx,HDMI 发送器节点命名为 hdmi@xxxx。
2.2.2 属性(Property)
属性是键值对,用于描述节点的硬件特性。常用属性类型如下表:
| 属性类型 | 语法示例 | 说明 | DRM 相关场景 |
|---|---|---|---|
| 字符串 | compatible = "rockchip,rk3399-dsi"; |
设备兼容性标识,驱动匹配的关键 | DRM 桥接芯片、显示控制器匹配 |
| 32 位整数 | reg = <0xff960000 0x1000>; |
寄存器基址和长度 | 显示控制器寄存器映射 |
| 整数数组 | interrupts = <0 45 4>; |
中断号、触发类型 | 显示控制器中断配置 |
| 布尔值 | status = "okay"; 或 "disabled" |
设备启用状态 | 控制显示接口是否使能 |
| phandle 引用 | panel = &lcd_panel; |
引用其他节点 | 连接显示面板节点 |
| GPIO 描述 | reset-gpios = <&gpio3 RK_PB5 GPIO_ACTIVE_LOW>; |
GPIO 引脚描述 | 显示复位、背光控制 |
2.2.3 标签(Label)
标签为节点提供别名,便于在设备树包含文件(.dtsi)或覆盖文件(.dtso)中引用。标签定义在节点名称前,使用 &label 语法引用:
// 定义标签
dsi0: dsi@ff960000 {
compatible = "rockchip,rk3399-dsi";
reg = <0xff960000 0x1000>;
...
};
// 在其他文件中引用标签并追加属性
&dsi0 {
status = "okay";
panel = <&lcd_panel>;
};
注意:标签引用必须在编译时解析,不能循环引用。DRM 驱动中常用标签连接显示控制器、桥接芯片和面板节点,形成完整的显示链路。
2.3 dts / dtsi / dtb 文件格式
设备树涉及三种主要文件格式,它们之间的关系如下表:
| 文件扩展名 | 全称 | 格式 | 用途 | 是否可编译 |
|---|---|---|---|---|
| .dts | Device Tree Source | 文本(ASCII) | 顶层设备树源文件,描述具体板级硬件 | 是,编译为 .dtb |
| .dtsi | Device Tree Source Include | 文本(ASCII) | 设备树包含文件,描述 SoC 公共部分或 IP 核 | 不单独编译,被 .dts 包含 |
| .dtb | Device Tree Blob | 二进制(FDT 格式) | 编译后的设备树,由 bootloader 加载并传递给内核 | 由 .dts 编译生成 |
| .dtbo | Device Tree Blob Overlay | 二进制 | 设备树覆盖文件,用于运行时动态修改设备树 | 由 .dtso 编译生成 |
DRM 驱动开发中的典型文件组织:SoC 厂商提供 rk3399.dtsi 定义显示控制器、DSI、HDMI 等 IP 核的默认配置;板级厂商在 my-board.dts 中 #include "rk3399.dtsi",然后通过 &dsi0 标签追加具体面板参数、GPIO 引脚和时钟频率。
2.4 设备树编译方法
设备树源文件需要通过 Device Tree Compiler(dtc)编译为二进制 .dtb 文件。dtc 工具包含在 Linux 内核源码的 scripts/dtc/ 目录中,也可通过系统包管理器安装。
2.4.1 安装 dtc
# Ubuntu / Debian
sudo apt-get install device-tree-compiler
# Fedora / RHEL
sudo dnf install dtc
# 从内核源码编译
cd linux/scripts/dtc
make
sudo make install
2.4.2 基本编译命令
# 编译 .dts 为 .dtb
dtc -I dts -O dtb -o my-board.dtb my-board.dts
# 反编译 .dtb 为 .dts(调试用)
dtc -I dtb -O dts -o my-board-decompiled.dts my-board.dtb
# 编译时输出依赖信息
dtc -I dts -O dtb -o my-board.dtb my-board.dts -d my-board.d
# 检查 .dtb 语法(不输出文件)
dtc -I dts -O dtb my-board.dts -o /dev/null 2>&1
2.4.3 内核编译系统中的设备树编译
在内核源码树中,设备树编译集成在 Kbuild 系统中。通常通过以下方式编译:
# 编译指定板级的设备树
make ARCH=arm64 my-board.dtb
# 编译所有设备树
make ARCH=arm64 dtbs
# 编译内核时同时编译设备树
make ARCH=arm64 defconfig
make ARCH=arm64 -j4 Image dtbs
DRM 调试技巧:在开发阶段,可以使用 dtc -I dtb -O dts 反编译内核使用的 .dtb 文件,检查显示控制器节点是否被正确使能(status = "okay"),以及管脚复用(pinctrl)配置是否符合预期。
2.5 设备树在 DRM 显示驱动中的典型应用
在 DRM 框架中,设备树节点用于描述显示链路中的各个硬件组件。以下是一个简化的 DRM 显示链路设备树示例:
// 显示控制器节点
display-controller@ff8f0000 {
compatible = "rockchip,rk3399-vop";
reg = <0xff8f0000 0x400>;
interrupts = <GIC_SPI 119 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&cru ACLK_VOP0>, <&cru HCLK_VOP0>;
clock-names = "aclk", "hclk";
status = "disabled";
// 显示输出端口
port {
#address-cells = <1>;
#size-cells = <0>;
vop0_out_dsi: endpoint@0 {
reg = <0>;
remote-endpoint = <&dsi0_in_vop0>;
};
};
};
// DSI 控制器节点
dsi0: dsi@ff960000 {
compatible = "rockchip,rk3399-dsi";
reg = <0xff960000 0x1000>;
clocks = <&cru SCLK_DSI0>, <&cru PCLK_DSI0>;
clock-names = "ref", "pclk";
status = "disabled";
ports {
#address-cells = <1>;
#size-cells = <0>;
port@0 {
reg = <0>;
dsi0_in_vop0: endpoint {
remote-endpoint = <&vop0_out_dsi>;
};
};
port@1 {
reg = <1>;
dsi0_out_panel: endpoint {
remote-endpoint = <&panel_in_dsi>;
};
};
};
};
// 显示面板节点
panel: panel@0 {
compatible = "boe,tv080wum-nl0";
reg = <0>;
enable-gpios = <&gpio1 RK_PB3 GPIO_ACTIVE_HIGH>;
backlight = <&backlight>;
status = "disabled";
port {
panel_in_dsi: endpoint {
remote-endpoint = <&dsi0_out_panel>;
};
};
};
常见错误:remote-endpoint 引用的 phandle 必须存在且类型匹配;status 属性必须显式设置为 "okay" 才能启用设备;时钟名称(clock-names)必须与驱动中 devm_clk_get() 使用的名称一致。
2.6 本章小结
设备树是嵌入式 Linux 显示驱动开发的基石。本章涵盖了以下核心知识点:
- 起源:从
board-*.c到设备树的演进,解决了硬件描述与内核代码耦合的问题。 - 语法规则:节点(node)、属性(property)、标签(label)的定义与使用,以及
compatible、reg、interrupts、status等关键属性。 - 文件格式:.dts(源文件)、.dtsi(包含文件)、.dtb(二进制 Blob)的区别与协作关系。
- 编译方法:使用 dtc 工具编译/反编译设备树,以及内核 Kbuild 系统中的集成方式。
- DRM 应用:通过显示控制器、DSI 控制器、面板节点的
port和endpoint连接,构建完整的显示链路描述。