本文约3800字,在嵌入式Linux开发中,设备树(Device Tree,简称DT)是贯穿内核、驱动与硬件的核心组件,尤其在ARM、RISC-V等架构中,更是实现硬件与驱动解耦的关键技术。对于嵌入式开发者而言,掌握设备树不仅能解决驱动移植的痛点,更能大幅提升硬件适配的效率。本文整理了设备树的起源、核心组成、工作原理,以及实战编写与调试方法。
关注公众号, 即可获得与Linux相关的电子书籍以及常用开发工具,文末有文档清单。
一 为什么需要设备树?—— 解决传统嵌入式开发的痛点
在设备树出现之前,嵌入式Linux内核中充斥着大量板级支持包(BSP)硬编码代码,这些代码主要用于描述特定开发板的硬件信息,比如寄存器地址、中断号、GPIO引脚配置等,集中在`arch/arm/mach-xxx/`目录下。这种模式存在三大核心痛点:
[1].内核冗余严重:不同开发板即便基于同一CPU架构,也需要编写独立的板级代码,导致内核源码臃肿,维护成本极高;
[2].可移植性差:新增或更换硬件平台时,需修改内核源码并重新编译,无法实现内核镜像的复用;
[3].耦合度极高:硬件描述与驱动逻辑深度绑定,驱动开发者需频繁修改硬件相关的硬编码,开发效率低下。
为解决这些问题,设备树应运而生。其核心目标是将硬件描述与内核代码分离,让同一个内核镜像(针对特定CPU架构),只需加载不同的设备树二进制文件,就能适配多种不同的硬件平台,大幅简化内核移植与硬件适配流程。
说明:设备树的本质是“硬件描述文件”,它不参与驱动逻辑实现,仅负责告诉内核“硬件是什么、在哪里、如何配置”,实现“一次编译内核,多平台适配”。
二 设备树核心组成:4个关键文件与核心语法
设备树的完整工作链路涉及4类核心文件,以及一套标准化的语法规则,掌握这些是编写设备树的基础。
[1]. 四类核心文件(从源码到二进制)
设备树的开发流程遵循“编写→编译→加载→解析”的链路,对应以下4类文件,其关系类似C语言的“源码→编译器→可执行文件”:
>>设备树源文件(.dts):人类可读的文本文件,是设备树的核心源码,用于描述特定开发板的完整硬件拓扑结构和配置,比如CPU、内存、外设的连接关系与参数。通常一个开发板对应一个.dts文件,命名格式如`imx6ull-xxx.dts`(基于NXP i.MX6ULL芯片的开发板)。
>>设备树头文件(.dtsi):类似C语言的头文件,用于提炼多个.dts文件的公共部分(如SoC内部通用外设、总线控制器的描述),通过`#include`指令被.dts文件引用,实现代码复用。例如`imx6ull.dtsi`包含了i.MX6ULL芯片的核心硬件描述,所有基于该芯片的开发板.dts文件都可引用它。
>>设备树编译器(dtc):将.dts和.dtsi文件编译为机器可识别的二进制文件的工具,相当于C语言的gcc编译器。编译命令格式为:`dtc -I dts -O dtb -o 目标.dtb 源.dts`,其中`-I`指定输入格式,`-O`指定输出格式,`-o`指定输出文件。
>>设备树二进制文件(.dtb):由dtc编译生成的二进制文件,是最终被Bootloader加载并传递给内核的硬件描述数据。其文件结构包含四部分:头部(标识文件信息)、内存保留块、结构块(节点信息)、字符串块(属性名称),内核启动时会解析该文件获取硬件信息。
[2]. 核心语法:节点与属性
设备树以树形结构组织硬件信息,核心语法元素是节点(Node)和属性(Property),所有硬件描述都围绕这两个元素展开,以下结合实例详解关键语法:
>>节点:硬件的“容器”
节点对应一个硬件设备或总线(如CPU、I2C控制器、LED外设),以树形结构嵌套,根节点`/`是所有节点的父节点,唯一且必须存在。节点的标准格式为:
dtsnode-label: node-name@unit-address {属性键 = 属性值;子节点 { ... };};
各部分含义:
node-label(标签):可选,用于快速引用该节点,格式为`标签名:`,后续可通过`&标签名`引用该节点,避免重复编写路径;
node-name@unit-address(节点名+地址):节点的唯一标识,`node-name`是设备类型(如i2c、led、uart),`unit-address`是设备的基地址(如寄存器地址、I2C从地址),用于区分同一总线上的多个同类设备,例如`i2c@11000000`表示基地址为0x11000000的I2C控制器;
子节点:嵌套在父节点中,对应挂载在该设备/总线上的外设,例如I2C控制器节点下可嵌套温度传感器子节点。
>>属性:硬件的“说明书”
属性是节点的核心,以“键值对”形式存在,用于描述节点的硬件特性和配置参数,分为标准属性(由设备树规范定义,具有固定含义)和自定义属性(由驱动开发者定义,通常带厂商前缀)。
常用标准属性(必掌握):
compatible:最核心属性,用于驱动与设备节点的匹配,值为字符串或字符串列表,格式通常为“厂商,设备型号”,例如`compatible = "ti,tmp102"`表示该节点对应TI公司的TMP102温度传感器,驱动程序通过匹配该属性与设备绑定;
reg:描述设备的地址范围(如寄存器地址、内存地址),值为32位整数列表,格式为`<地址 长度>`,例如`reg = <0x10000000 0x100>`表示基地址为0x10000000,长度为0x100(256字节)的寄存器空间;
interrupts:描述设备的中断信息,值为整数列表,包含中断控制器、中断号、触发方式等,例如`interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>`表示中断类型为SPI中断,中断号为5,高电平触发;
status:描述设备的使能状态,常用值为`"okay"`(使能)和`"disabled"`(禁用),未指定时默认禁用;
#address-cells和#size-cells:仅存在于父节点,用于描述子节点`reg`属性的格式,`#address-cells`指定子节点`reg`中地址部分的32位整数个数,`#size-cells`指定长度部分的32位整数个数,默认均为1。
自定义属性:通常以厂商前缀开头,用于传递驱动所需的自定义配置,例如`fsl,clk-freq = <100000000>`表示飞思卡尔(FSL)芯片的时钟频率为100MHz,`temp-high-threshold = <30000>`表示温度传感器的高温阈值为30℃(单位:毫摄氏度)。
>>关键语法补充
引用节点:通过`&标签名`引用已定义的节点,用于关联父设备、中断控制器等,例如`interrupt-parent = <&gic>`表示该设备的中断父节点是中断控制器gic(需提前为gic节点定义标签);
版本声明:.dts文件开头需添加`/dts-v1/;`,声明设备树版本为v1,否则编译会报错;
注释:与C语言一致,单行注释用`//`,多行注释用`/* ... */`。
>>完整设备树片段示例
dts/dts-v1/; // 设备树版本声明#include"imx6ull.dtsi"// 引用SoC公共头文件/ {model = "NXP i.MX6ULL 开发板"; // 开发板型号compatible = "nxp,imx6ull-xxx", "nxp,imx6ull"; // 平台兼容属性#address-cells = <1>; // 子节点reg地址部分占1个32位整数#size-cells = <1>; // 子节点reg长度部分占1个32位整数soc { // SOC节点,包含所有内部外设compatible = "simple-bus";ranges; // 子节点地址无需转换// LED控制器节点(标签led_ctl)led_ctl: led-controller@10000000 {compatible = "nxp,imx6ull-led-ctl";reg = <0x10000000 0x100>; // 寄存器地址+长度interrupt-parent = <&gic>; // 引用中断控制器interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;status = "okay"; // 使能设备};// I2C控制器节点(标签i2c1)i2c1: i2c@11000000 {compatible = "nxp,imx6ull-i2c";reg = <0x11000000 0x100>;status = "okay";// I2C总线上的温度传感器子节点tmp102@48 {compatible = "ti,tmp102";reg = <0x48>; // I2C从地址temp-alert-gpios = <&gpio1 20 GPIO_ACTIVE_LOW>; // 自定义属性};};};};
三 设备树编译到与驱动匹配
设备树的完整工作流程贯穿嵌入式系统启动的全过程,从文件编译到驱动初始化,共分为5个关键步骤,清晰理解流程能帮助开发者定位适配问题:
步骤1:编译设备树
开发者编写.dts文件(引用必要的.dtsi文件)后,使用dtc编译器将其编译为.dtb二进制文件。编译命令示例:
dtc -I dts -O dtb -o imx6ull-xxx.dtb imx6ull-xxx.dts编译成功后生成.dtb文件,若语法错误(如节点未闭合、属性格式错误),dtc会提示具体错误位置,需修改后重新编译。
步骤2:加载设备树
系统启动时,Bootloader(如U-Boot)首先将.dtb文件从存储介质(SD卡、Flash、网络)加载到内存的指定位置,同时加载Linux内核镜像。
步骤3:传递设备树地址
Bootloader启动内核时,通过约定的机制将.dtb文件在内存中的物理地址传递给内核(例如ARM架构中通过r2寄存器传递),告知内核“硬件描述文件在哪里”。
步骤4:内核解析设备树
Linux内核启动早期(setup_arch阶段),会解压并解析.dtb文件,在内存中构建一个结构化的设备树数据库(Flattened Device Tree,FDT),遍历所有节点,提取硬件资源信息(地址、中断、GPIO等),并构建内核设备模型。
步骤5:驱动匹配与设备初始化
这是设备树发挥作用的核心步骤,具体流程如下:
>>内核遍历设备树数据库中的所有节点,检查每个节点的`compatible`属性;
>>内核在已编译的驱动模块或内核源码中,查找`of_match_table`(设备树匹配表)中包含该`compatible`字符串的驱动;
>>找到匹配的驱动后,内核调用该驱动的`probe`函数;
>>驱动在`probe`函数中,通过设备树API(`of_*`系列函数,如`of_get_property`、`of_iomap`、`of_get_gpio`)解析节点属性,获取硬件资源(如寄存器地址、中断号);
>>驱动利用解析到的资源完成硬件初始化,并注册设备(如字符设备、网络设备),最终实现硬件与驱动的联动。
注意:驱动与设备节点的匹配核心是`compatible`属性,两者必须完全一致(区分大小写),否则驱动无法匹配设备,设备无法正常工作。
四 设备树编写与调试技巧
掌握理论后,实战中常遇到编译报错、驱动不匹配、硬件无法识别等问题,以下是高频实战要点与调试技巧,帮助开发者避坑:
[1]. 编写规范
>>节点命名遵循“设备类型@地址”格式,标签名简洁明了(如`led_ctl`、`i2c1`),便于引用;
>>`compatible`属性格式统一为“厂商,设备型号”,避免自定义无意义字符串,可参考Linux内核`Documentation/devicetree/bindings/`目录下的绑定文档(设备树绑定文档定义了特定设备的属性规范);
>>尽量复用.dtsi文件中的公共节点,避免重复编写,减少维护成本;
>>`reg`属性的地址和长度需与硬件手册一致,地址错误会导致驱动无法访问设备寄存器。
[2]. 常见报错与解决方法
>>编译报错:“syntax error”:语法错误,如节点未闭合、属性后缺少分号、`<>`使用错误,根据dtc提示的行号,检查语法格式;
>>驱动不匹配:内核日志中出现“no matching driver found”,检查节点`compatible`属性与驱动`of_match_table`中的字符串是否一致,区分大小写;
>>硬件无法识别:节点`status`属性未设为`"okay"`,或`reg`地址错误,可通过内核日志查看设备树解析情况;
>>中断异常:`interrupts`属性格式错误,或`interrupt-parent`引用错误,需核对硬件手册中的中断号和中断控制器节点。
[3]. 调试工具与命令
>>dtc工具:除了编译,还可反编译.dtb文件为.dts文件,用于查看内核加载的设备树内容,命令:
dtc -I dtb -O dts -o test.dts test.dtb>>内核日志:通过`dmesg | grep OF:`查看设备树解析过程的日志,定位解析错误(如节点加载失败、属性解析异常);
>>/proc/device-tree:系统启动后,内核会将解析后的设备树信息挂载到该目录下,可通过`ls`、`cat`命令查看节点和属性,例如`cat /proc/device-tree/model`可查看开发板型号,`ls /proc/device-tree/soc`可查看SOC节点下的所有子节点。
五 总结
设备树的核心价值是“硬件与驱动解耦”,通过标准化的硬件描述,实现内核镜像复用和快速移植,是嵌入式Linux开发的必备技能。本文梳理了设备树的核心概念、组成、工作流程及实战技巧,掌握这些内容,可应对大部分基础的设备树编写与适配需求。
以上为全文内容。
这里是女程序员的笔记本
15年+嵌入式软件工程师兼二胎宝妈
分享读书心得、工作经验,自我成长和生活方式。
希望我的文字能对你有所帮助