Linux设备树(dts/dtsi/dtb、设备树概念,设备树解析,驱动匹配)
设备树
文章目录
- 设备树
- I 设备树基本概念
- 1.设备树的作用
- 2.为什么需要设备树
- 3.设备树基本结构
- 4.常用节点类型
- 5.常用属性
- 6.设备树的编译
- II 内核对设备树文件的解析
- 1.内核启动时,从 uboot 获取.dtb的内存地址
- 2.解析.dtb为内部数据结构(device_node)
- 3.根据compatible属性匹配驱动,创建platform_device等设备对象
- 4.驱动通过platform_device获取设备属性,完成硬件初始化
- III 总结
I 设备树基本概念
1.设备树的作用
- 设备树 (Device Tree) 是描述硬件资源的数据结构,实现硬件配置与内核分离
- 核心作用:向内核驱动程序提供硬件信息
- 系统启动时,会先运行 uboot,uboot 加载内核和设备树到内存,启动内核并传递设备树内存地址给内核,内核解析设备树获取硬件信息
2.为什么需要设备树
- 在传统嵌入式开发中,硬件信息(如外设地址、中断号等)通常硬编码到内核源码中。当硬件发生微小变化(如同一型号外设的地址偏移),就需要重新修改并编译内核,极大增加了内核适配不同硬件的复杂度。
- 设备树将所有硬件信息从内核中剥离,以独立的设备树文件(.dts)描述,内核通过解析设备树动态获取硬件信息,实现了 “一份内核适配多份硬件”,大幅简化了嵌入式系统的移植与维护。
3.设备树基本结构
一个简单的设备树文件包含版本声明、根节点及若干子节点,每个节点都包含各种属性信息。
/dts-v1/; // 版本声明,固定格式#include "xxx.dtsi" // 可引用其他设备树片段(.dtsi为通用片段文件)/ { // 根节点,唯一的顶级节点#address-cells = <1>; // 地址字段长度(单位:32位),用于子节点reg属性#size-cells = <1>; // 大小字段长度(单位:32位),用于子节点reg属性node1: device@12340000 { // 子节点,命名格式:"设备类型@地址"(地址用于区分同类型设备)compatible = "vendor,node1-v1"; // 兼容属性,用于驱动匹配reg = <0x12340000 0x1000>; // 地址范围:起始地址 长度status = "okay"; // 设备状态:启用};node2 {compatible = "vendor,node2-v2";pinctrl-names = "default"; // 引脚配置名称pinctrl-0 = <&pinctrl_node2>; // 引用引脚配置节点interrupt-parent = <&intc>; // 中断父节点(通常为中断控制器)interrupts = <5 0x04>; // 中断号 触发方式(0x04为高电平触发)};};
节点命名规则:
节点名通常采用device-type@address
格式。其中:
-
device-type
:描述设备类型(如 uart、led、i2c 等); -
address
:设备在父总线中的地址(如寄存器起始地址),用于区分同一总线上的同类型设备(如uart@12340000
和uart@12350000
)。
4.常用节点类型
(1)根节点: /
所有设备树的顶级节点,必须包含#address-cells
和#size-cells
属性,用于定义子节点reg
属性的地址和大小字段长度。
/ {#address-cells = <2>; // 地址由2个32位字段组成(如高32位+低32位)#size-cells = <1>; // 大小由1个32位字段组成};
(2)CPU 节点: cpus
描述系统中的 CPU 核心信息,包含cpu
子节点(每个核心一个)。
cpus {#address-cells = <1>;#size-cells = <0>; // CPU地址无大小,设为0cpu@0 { // 第一个CPU核心compatible = "arm,cortex-a7"; // CPU型号reg = <0>; // CPU编号clocks = <&cpu_clk>; // CPU时钟源operating-points-v2 = <&cpu_opp_table>; // 工作点(频率/电压)表};};
(3)内存节点: memory
描述系统物理内存,必须包含reg
属性定义内存地址范围。
memory@80000000 {device_type = "memory"; // 固定属性,标识为内存节点reg = <0x80000000 0x20000000>; // 内存范围:起始地址0x80000000,大小512MB(0x20000000)};
(4)总线节点: amba
, soc
, i2c
, spi
描述系统总线(如 I2C、SPI、AMBA 总线),通常包含总线时钟、速率等属性。
i2c@12360000 {compatible = "vendor,i2c-v3";reg = <0x12360000 0x1000>;clock-frequency = <100000>; // I2C总线频率100kHz#address-cells = <1>; // I2C设备地址长度(7位或10位)#size-cells = <0>;sensor@48 { // I2C总线上的传感器设备(地址0x48)compatible = "vendor,temp-sensor";reg = <0x48>; // I2C设备地址};};
(5)外设节点
描述具体硬件设备(如 UART、LED、LCD 等)
led@0 {compatible = "gpio-leds";label = "user-led"; // LED标签(用于用户空间识别)gpios = <&gpio1 3 GPIO_ACTIVE_HIGH>; // 引脚:GPIO控制器 引脚号 有效电平default-state = "off"; // 默认状态};
5.常用属性
(1)compatible
-
作用:用于驱动与设备的匹配(“设备找驱动”)。
-
格式:
compatible = "厂商,设备型号", "通用型号";
(从具体到通用,驱动匹配时优先匹配前面的)。 -
示例:
compatible = "nvidia,tegra20-uart", "ns16550"; // 英伟达Tegra20的UART,兼容通用ns16550串口
(2)reg
-
作用:描述设备在父总线中的地址范围(如寄存器地址、内存地址)。
-
格式:
reg = <address1 length1 [address2 length2 ...]>;
(地址和长度的数量由父节点#address-cells
和#size-cells
定义)。 -
示例:
reg = <0x12340000 0x1000>; // 起始地址0x12340000,长度0x1000(4KB)
(3)status
-
作用:描述设备状态。
-
常用值:
-
"okay"
:设备启用; -
"disabled"
:设备禁用(内核不解析); -
"fail"
:设备存在但不可用。
-
(4)interrupts
-
作用:描述设备中断信息(依赖
interrupt-parent
指定中断控制器)。 -
格式:
interrupts = <中断号 触发方式 [优先级]>;
-
触发方式常用值:
0x01
:上升沿触发;0x02
:下降沿触发;0x04
:高电平触发;0x08
:低电平触发。
-
示例:
interrupt-parent = <\&intc>; // 中断控制器为intc节点interrupts = <5 0x04>; // 中断号5,高电平触发
(5)clocks / clock-names
-
作用:描述设备使用的时钟源。
-
格式:
clocks = <&clock_node1>, <&clock_node2>;
;clock-names = "clk1", "clk2";
(用于区分多个时钟)。 -
示例:
clocks = <\&clk\_pll 0>, <\&clk\_ahb 2>;clock-names = "core-clk", "bus-clk"; // 核心时钟、总线时钟
(6)gpios
-
作用:描述设备使用的 GPIO 引脚。
-
格式:
gpios = <&gpio_controller pin_number [flags]>;
-
flags 常用值:
GPIO_ACTIVE_HIGH
:高电平有效;GPIO_ACTIVE_LOW
:低电平有效。
(7)model
-
作用:描述设备 / 系统的型号(比 compatible 更简洁)。
-
示例:
model = "raspberrypi,3-model-b";
(树莓派 3B 型号)。
(8)phandle / 引用
-
作用:实现节点间的引用(如中断父节点、引脚配置节点等)。
-
用法:通过
&节点标签
引用(节点需先定义标签,如node1: uart@12340000
,引用时用&node1
)。
6.设备树的编译
(1)编译工具:dtc
(Device Tree Compiler)
-
功能:将
.dts
(设备树源文件)编译为.dtb
(设备树二进制文件,内核可解析)。 -
常用命令:
dtc -I dts -O dtb -o output.dtb input.dts # 编译:输入.dts,输出.dtbdtc -I dtb -O dts -o input.dts output.dtb # 反编译:从.dtb还原为.dts
(2)设备树文件类型:
.dts
:针对具体板卡的设备树文件(包含板级特有信息);*.dtsi
:通用设备树片段(如 SOC 级通用硬件描述,可被多个.dts
引用)。
II 内核对设备树文件的解析
1.内核启动时,从 uboot 获取.dtb的内存地址
- uboot 在引导内核启动时,通过特定寄存器(如 ARM 架构的 r2 寄存器)将.dtb 的物理内存地址传递给内核。例如在 ARM 平台中,内核启动入口函数会从 r2 寄存器读取该地址。
2.解析.dtb为内部数据结构(device_node)
-
内核会遍历.dtb 中的节点和属性,将其转换为device_node结构体链表
struct device_node {const char *name; // 节点名称(如"uart@12340000")const char *type; // 节点类型(由device_type属性指定)phandle phandle; // 节点句柄(用于节点引用)struct device_node *parent; // 父节点指针struct device_node *child; // 子节点指针struct device_node *sibling; // 兄弟节点指针struct property *properties; // 属性链表};
-
每个节点的属性会被解析为property结构体,包含属性名、值长度和值数据
struct property {const char *name; // 属性名(如"compatible")int length; // 属性值长度void *value; // 属性值数据(如"vendor,uart"的二进制表示)struct property *next; // 下一个属性};
-
解析完成后,内核会构建以根节点(/)为起点的device_node树状结构,所有硬件信息都以节点和属性的形式保存在该结构中
3.根据compatible属性匹配驱动,创建platform_device等设备对象
-
设备对象创建:
- 内核遍历device_node树,对每个具备硬件驱动需求的节点创建platform_device对象。
- platform_device与device_node存在关联,前者的dev.of_node成员指向对应的device_node,便于驱动通过设备对象反向查询设备树信息。
-
compatible属性驱动匹配:
- 驱动程序通过of_device_id结构体声明支持的设备类型,其中包含与设备树compatible属性对应的字符串
static const struct of_device_id uart_of_match[] = {{ .compatible = "nvidia,tegra20-uart" },{ .compatible = "ns16550" },{ /* 终止符 */ }};MODULE_DEVICE_TABLE(of, uart_of_match);
- 内核通过of_match_device函数比对设备节点的compatible属性与驱动的of_device_id列表,找到匹配的驱动
4.驱动通过platform_device获取设备属性,完成硬件初始化
- 内核提供了一系列of_xxx接口函数,用于从device_node中读取属性值,
- 获取 compatible 属性:of_get_property(node, “compatible”, &len)
- 获取 reg 属性(地址信息):of_address_to_resource(node, index, &res),将 - reg 属性解析为resource结构体(包含起始地址和长度)。
- 获取中断属性:irq_of_parse_and_map(node, index),返回中断号。
- 获取 GPIO 属性:of_get_named_gpio(node, “gpios”, index),返回 GPIO 编号。
static int uart_probe(struct platform_device *pdev) {struct device_node *node = pdev->dev.of_node;struct resource res;int irq;// 获取寄存器地址of_address_to_resource(node, 0, &res);void __iomem *base = ioremap(res.start, resource_size(&res));// 获取中断号irq = irq_of_parse_and_map(node, 0);// 初始化硬件(如设置波特率、中断使能等)uart_hw_init(base, irq);return 0;}
- 驱动获取寄存器地址、中断号、时钟频率等硬件参数后,调用驱动的probe函数完成初始化,例如内存映射(ioremap)、中断注册(request_irq)、时钟使能等操作,最终实现硬件设备的正常工作。
III 总结
- 设备树是嵌入式系统中连接硬件与内核的关键桥梁,通过结构化的节点和属性描述硬件信息,实现了 “硬件描述与内核解耦”。
- 设备树的结构、节点类型、核心属性及编译方法,是嵌入式开发中适配硬件、调试驱动的基础。
- 实际开发中,需结合具体 SOC 和板卡的 datasheet 编写设备树,并通过
dtc
工具验证语法正确性,最终实现内核对硬件的自动识别与驱动。