Linux 设备树详解:从基础到实践
一、设备树概述
设备树(Device Tree)是一种描述硬件配置的树形数据结构,其源文件称为 DTS(DeviceTree Source)。DTS 文件采用层级结构来描述嵌入式系统的硬件组件,特别是板级外设。这种结构清晰地展示了系统总线与连接在其上的各种外设控制器之间的关系,例如 IIC、GPIO 和 SPI 控制器。
例如,IIC 控制器可以细分为 IIC1 和 IIC2,每个 IIC 总线上又可以挂载不同的设备,如 FT5206、AT24C02 或 MPU6050。
通常,.dts 文件用于描述特定开发板的硬件信息,而 .dtsi 文件则用于描述片上系统(SOC)的通用外设信息,如 CPU 数量、主频和外设控制器等。
引入设备树的主要动机之一是简化 Linux 内核中针对不同硬件平台的配置,避免为每个板子维护大量冗余的板级信息文件。
二、DTS、DTB 与 DTC
DTS 是设备树的源码文件,DTB(DeviceTree Blob)则是 DTS 编译后的二进制文件。将 DTS 编译为 DTB 需要使用 DTC(DeviceTree Compiler)工具。
在 Linux 源码根目录下,可以通过以下命令编译设备树:
cd /path/to/linux-source
make dtbs
上述命令会编译所有选中的设备树文件。若仅需编译特定的设备树文件,例如 stm32mp157d-ed1.dts,可执行:
make stm32mp157d-ed1.dtb
Linux 内核如何确定编译哪个 DTS 文件呢?这通常通过 arch/arm/boot/dts/Makefile 文件中的配置来指定。例如,对于 STM32MP1 SOC,该文件中会列出所有基于此 SOC 的板子对应的 DTB 文件:
dtb-$(CONFIG_ARCH_STM32) += \
stm32mp157d-ed1.dtb \
stm32mp157f-ev1.dtb \
# ... 其他板子
当内核配置中启用 CONFIG_ARCH_STM32 时,所有列出的 DTB 文件都会被编译。若要为新的 STM32MP1 开发板添加支持,只需创建对应的 .dts 文件,并将其名称添加到上述列表中。
编译生成的 DTB 文件在系统启动时由 U-Boot 传递给 Linux 内核,内核通过解析 DTB 来了解硬件配置。
三、DTS 语法
学习 DTS 语法有助于理解和修改设备树文件。通常,我们会在 SOC 厂商提供的 DTS 文件基础上进行修改。
- dtsi 头文件
设备树支持包含 .h、.dtsi 和 .dts 文件。推荐使用 .dtsi 作为头文件后缀。
例如,STM32MP1 系列包含 stm32mp151、stm32mp153 和 stm32mp157 等型号。其中,stm32mp151 是基础型号,stm32mp153 和 stm32mp157 在其基础上增加了更多外设。因此,stm32mp151.dtsi 文件描述了这三个 SOC 共有的外设资源。
/ {
#address-cells = <1>;
#size-cells = <1>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
...
};
};
spi2: spi@4000b000 {
compatible = "st,stm32h7-spi";
reg = <0x4000b000 0x400>;
...
};
}
根节点 / 包含 cpus 和 spi2 等子节点。每个节点用花括号括起,并包含一系列属性。
- 设备节点
设备树中的每个硬件组件都表示为一个节点,称为设备节点。节点通过属性(键值对)来描述其特性。
以下是简化后的设备树模板:
/ {
#address-cells = <1>;
#size-cells = <1>;
aliases {
serial0 = &uart4;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
...
};
};
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges;
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x10000000 0x60000>;
...
};
};
}
节点格式为 node-name@unit-address,其中 node-name 是节点名称,unit-address 是设备地址或寄存器基地址。
- 标准属性
① compatible 属性
compatible 属性是设备与驱动匹配的关键。其值是一个字符串列表,格式通常为 "manufacturer,model"。
例如:
compatible = "cirrus,cs42l51";
如果设备节点的 compatible 属性值与驱动中的 OF 匹配表(of_device_id)中的任何值匹配,则该设备将使用该驱动。
② model 属性
model 属性描述开发板或设备模块的名称:
model = "STMicroelectronics STM32MP157C-DK2 Discovery Board";
③ status 属性
status 属性表示设备的状态:
"okay": 设备可操作。"disabled": 设备当前不可操作,但未来可能变为可操作。"fail": 设备不可操作,且不大可能恢复。
④ #address-cells 和 #size-cells 属性
这两个属性定义子节点的地址信息格式。#address-cells 指定地址占用的字长,#size-cells 指定地址长度占用的字长。
例如:
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
reg = <0>; // 地址为 0,无长度
};
};
sram: sram@10000000 {
reg = <0x10000000 0x60000>; // 起始地址和长度
};
⑤ reg 属性
reg 属性描述设备的地址空间资源,通常为 (address, length) 对。
⑥ ranges 属性
ranges 属性定义地址映射,格式为 (child-bus-address, parent-bus-address, length)。若为空,表示子地址空间与父地址空间相同。
四、创建小型设备树模板
以下是一个基于 STM32MP157 SOC 的简化设备树示例,仅用于演示语法。
- 添加 cpus 节点
/ {
compatible = "st,stm32mp157d-atk", "st,stm32mp157";
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
cpu1: cpu@1 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <1>;
};
}
}
- 添加 soc 节点
/ {
...
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x10000000 0x60000>;
};
timers6: timer@40004000 {
compatible = "st,stm32-timers";
reg = <0x40004000 0x400>;
};
spi2: spi@4000b000 {
compatible = "st,stm32h7-spi";
reg = <0x4000b000 0x400>;
};
usart2: serial@4000e000 {
compatible = "st,stm32h7-uart";
reg = <0x4000e000 0x400>;
};
i2c1: i2c@40012000 {
compatible = "st,stm32mp15-i2c";
reg = <0x40012000 0x400>;
};
};
}
五、设备树在系统中的体现
Linux 内核启动时会解析设备树,并在 /proc/device-tree 目录下创建与节点对应的目录结构。
例如,根节点的属性会以文件形式存在:
/proc/device-tree/
├── #address-cells
├── #size-cells
├── compatible
├── model
└── name
子节点则作为子目录存在,例如 /proc/device-tree/soc/。
六、特殊节点
- aliases 子节点
aliases 节点用于定义节点别名,方便通过别名访问节点:
aliases {
serial0 = &uart4;
};
- chosen 子节点
chosen 节点用于 U-Boot 向 Linux 内核传递数据,特别是 bootargs 参数:
chosen {
bootargs = "console=ttymxc0,115200 root=/dev/mmcblk1p2 rootwait";
};
七、设备树常用 OF 操作函数
Linux 内核提供了一系列以 of_ 为前缀的函数,用于从设备树中提取节点和属性信息。
- 查找节点的 OF 函数
of_find_node_by_name: 通过节点名称查找节点。of_find_node_by_type: 通过device_type属性查找节点。of_find_compatible_node: 通过compatible属性查找节点。of_find_node_by_path: 通过路径查找节点。
- 查找父/子节点的 OF 函数
of_get_parent: 获取父节点。of_get_next_child: 迭代查找子节点。
- 提取属性值的 OF 函数
of_find_property: 查找指定属性。of_property_read_u32_index: 读取指定索引的 u32 值。of_property_read_u8_array: 读取 u8 数组。of_property_read_string: 读取字符串值。
- 其他常用 OF 函数
of_device_is_compatible: 检查设备兼容性。of_get_address: 获取地址属性。of_translate_address: 将设备树地址转换为物理地址。of_address_to_resource: 将reg属性转换为resource结构。of_iomap: 将reg属性映射为虚拟地址。