1. 设备树基础:什么是设备树?为什么需要设备树?设备树文件结构(dts、dtsi、dtb)
大家好,我是你们的嵌入式系统讲师。今天咱们来聊聊设备树——这个在Linux内核开发中绕不开的话题。
说实话,我刚开始接触嵌入式Linux那会儿,也被设备树搞得一头雾水。那时候还是古老的板级文件(board file)时代,每个板子都要写一堆C代码来描述硬件。后来设备树出现了,很多人觉得麻烦,但用久了你会发现——这玩意儿真香。
1.1 什么是设备树?
设备树,英文叫Device Tree,简称DT。说白了,它就是一个描述硬件信息的配置文件。你想想看,一个嵌入式板子上有CPU、内存、I2C控制器、SPI控制器、GPIO、中断控制器……这些硬件怎么连接?地址是多少?中断号是多少?设备树就是干这个的。
设备树的核心思想是:把硬件描述从内核代码中分离出来。这样,同一份内核镜像可以跑在不同硬件上,只需要更换设备树文件就行。
重要概念:设备树是一个树形结构的数据格式,每个硬件设备都是一个"节点"(node),节点包含若干"属性"(property),用来描述设备的配置信息。
1.2 为什么需要设备树?
在设备树出现之前,Linux内核是怎么处理硬件的?
- 板级文件时代(2.6内核及之前):每个板子都在arch/arm/mach-xxx/目录下有一个board-xxx.c文件,里面用C代码硬编码了所有硬件信息。比如内存基地址、GPIO引脚分配、时钟频率等。
- 问题来了:如果你有100种板子,就要写100个board文件。而且每次硬件改动,都要重新编译内核。我在一个项目中就遇到过,客户换了颗Flash芯片,我不得不改board文件、重新编译、烧录……折腾了一整天。
设备树解决了这个问题:
- 硬件描述与内核代码解耦:内核不需要知道具体硬件细节,它只管解析设备树。
- 减少内核代码量:大量板级文件被移除,内核变得更干净。
- 支持多平台:同一份内核镜像,通过更换dtb文件就能适配不同硬件。
- 社区标准化:设备树规范由ePAPR标准定义,各大厂商都遵循。
我的经验:有一次调试一块新板子,硬件工程师说"GPIO引脚改了",我只需要改dts文件,重新编译dtb,然后替换掉启动分区里的dtb文件就行。内核都不用重新编译,省了不少时间。
1.3 设备树文件结构
设备树涉及三种文件格式:dts、dtsi、dtb。很多人一开始搞不清它们的区别,我来捋一捋。
| 文件类型 | 全称 | 说明 | 是否可读 |
|---|---|---|---|
| .dts | Device Tree Source | 设备树源文件,文本格式,开发者编写 | 是(文本) |
| .dtsi | Device Tree Source Include | 设备树头文件,可被dts包含,用于复用公共定义 | 是(文本) |
| .dtb | Device Tree Blob | 设备树二进制文件,由dts编译生成,供内核使用 | 否(二进制) |
嗯,这里要注意:dtsi文件是给dts文件include用的。比如,一个SoC厂商会提供一个通用的soc.dtsi,里面定义了该SoC所有内置外设。然后各个板卡厂商在自己的board.dts里include这个soc.dtsi,再添加板级特有的设备。
举个例子,看看一个简单的dts文件长什么样:
/dts-v1/;
#include "imx6ul.dtsi"
/ {
model = "Freescale i.MX6 UltraLite Board";
compatible = "fsl,imx6ul-14x14-evk", "fsl,imx6ul";
memory {
device_type = "memory";
reg = <0x80000000 0x20000000>; /* 512MB RAM */
};
chosen {
stdout-path = &uart1;
};
&uart1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_uart1>;
status = "okay";
};
};
这段代码描述了什么?
/dts-v1/;—— 版本声明,表示使用设备树v1格式#include "imx6ul.dtsi"—— 包含SoC级定义/ { ... }—— 根节点,整个设备树的起点model和compatible—— 板子名称和兼容性字符串memory节点 —— 描述内存大小和起始地址chosen节点 —— 内核启动参数,比如指定控制台串口&uart1 { ... }—— 引用并修改uart1节点,配置引脚复用和使能
避坑指南:我曾经在调试时发现串口不工作,查了半天发现是dts里忘了写status = "okay"。默认情况下,很多外设节点是disabled状态,必须显式使能。这个坑我踩过,你们别踩。
1.4 设备树编译与反编译
dts文件不能直接给内核用,需要编译成dtb二进制文件。工具链里有个叫dtc(Device Tree Compiler)的工具,就是干这个的。
常用命令:
# 编译dts为dtb
dtc -I dts -O dtb -o board.dtb board.dts
# 反编译dtb为dts(调试时很有用)
dtc -I dtb -O dts -o board.dts board.dtb
# 查看dtb内容(人类可读)
fdtdump board.dtb
我个人习惯在调试时先用fdtdump看看dtb里到底有什么,确认编译是否正确。有一次我发现编译出来的dtb大小不对,一查发现是dts里有个语法错误,dtc报了个warning但我没注意……嗯,从那以后我每次编译都会检查返回值。
1.5 设备树的核心语法速览
这里我快速过一下设备树的基本语法规则,后面章节会详细展开:
- 节点命名:
node-name@unit-address,比如uart@021e8000 - 属性格式:
property-name = <value>; - 常见属性类型:
<u32>—— 32位整数,如reg = <0x80000000 0x20000000>;string—— 字符串,如compatible = "fsl,imx6ul";phandle—— 指针引用,如&uart1bytestring—— 字节数组,如local-mac-address = [00 11 22 33 44 55];
- 标签(label):给节点起别名,方便引用,如
uart1: serial@021e8000
小技巧:写dts时,我习惯先用dtc -@选项开启符号支持,这样可以在编译时保留标签信息,方便后续overlay(设备树叠加层)使用。
1.6 设备树在启动流程中的角色
设备树在系统启动时扮演什么角色?我画了一张图来说明:
从这张图可以看出,设备树贯穿了整个启动流程:
- 开发阶段:编写dts/dtsi源文件
- 编译阶段:用dtc编译成dtb二进制文件
- 启动阶段:Bootloader(如U-Boot)将dtb加载到内存,并把地址传给内核
- 内核阶段:内核解析dtb,创建device_node结构体,然后与驱动匹配
- 驱动阶段:驱动probe时,从设备树中读取硬件配置参数
一句话总结:设备树就是硬件的"身份证",内核通过它来认识板子上有什么、在哪里、怎么用。
好了,这一章我们聊了设备树是什么、为什么需要它、以及三种文件格式的区别。下一章我会带大家深入设备树的语法细节,包括节点、属性、中断映射、GPIO控制等实战内容。到时候我会拿一个真实的板子dts文件来逐行分析,保证让你看得明明白白。
公众号:蓝海资料掘金营,微信deep3321