设备树
在嵌入式 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文件,避免代码重复。
- 这是头文件或包含文件(类似 C 语言的
- 编译产物
.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@... { ... };
│}
关键区域说明:
- 根节点 (
/):树的起点。 - CPU 节点 (
cpus):描述多核处理器的拓扑结构。 - 内存节点 (
memory):告诉内核物理内存的起始地址和大小。 - Chosen 节点 (
chosen):传递内核启动参数或引导加载程序的数据(如bootargs)。 - SoC/Bus 节点 (
soc,amba,pci):作为总线节点,连接各种外设控制器。
3. 核心语法构成
设备树的“叶子”是节点,节点的内容是属性。
A. 节点
节点用名称标识,格式通常为 name@unit-address。
- name:节点名称(如
serial,gpio,i2c)。 - unit-address:该节点在寄存器总线上的地址。如果节点没有
reg属性,通常省略@unit-address。
B. 属性
属性是键值对,用于描述硬件的具体信息。主要有以下几种形式:
-
字符串属性
- 最重要的是
compatible(兼容性)。 - 格式:
"manufacturer,model"。 - 作用:内核驱动通过匹配这个字符串来绑定驱动程序。
- 示例:
compatible = "arm,cortex-a9";
- 最重要的是
-
数组属性 (
reg)- 描述寄存器地址或内存映射区域。
- 格式:一组
address length。 - 示例:
reg = <0x101F2000 0x1000>;(起始地址 0x101F2000,长度 0x1000)。
-
逻辑属性
- 只有属性名,没有值。如果属性存在,则表示“真/开启”;如果不存在,表示“假/关闭”。
- 示例:
status = "okay";或status = "disabled";(注意 status 也是字符串,但像interrupts-extended这种逻辑常用于开启某功能)。
-
引用属性 (
phandle)- 用于节点之间的连接。例如,一个 GPIO 设备需要引用另一个 GPIO 控制器节点。
- 使用
&label的方式引用。 - 示例:
pinctrl-0 = <&uart0_pins>;
4. 设备树如何与内核交互(工作原理)
理解框架的最后,需要知道数据是怎么流动的:
- 解析:内核启动时,解析
.dtb文件,将其在内存中展开为链表/树形结构。 - 匹配:内核初始化各个驱动时,驱动程序中定义的
of_match_table会去遍历设备树中的compatible属性。 - 获取资源:一旦匹配成功,驱动通过 API(如
of_iomap,of_get_interrupt等)读取节点中的reg(寄存器地址)、interrupts(中断号)等属性,从而操作硬件。
总结
设备树的框架可以概括为:
- 形式上:分层的源码(
.dts包含.dtsi)编译成二进制(.dtb)。 - 结构上:以
/为根,向下分支出 CPU、内存、总线、外设。 - 内容上:节点负责分类(是谁),属性负责描述(有什么、在哪、怎么配)。