嵌入式Linux驱动开发------设备树语法与编译工具------读懂这张"藏宝图"
仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.0的Linux!欢迎各位大佬观摩!喜欢的话点个⭐!
仓库地址:github.com/Awesome-Emb...
静态网页:awesome-embedded-learning-studio.github.io/imx-forge/
上一章我们讲了设备树的历史背景和基本概念,现在手里应该有一个模糊的概念了:设备树是一种描述硬件的数据结构,内核通过它来了解板子上有什么设备。
这一章,我们先简单了解一下 DTC 编译器是如何把 .dts 文本变成内核能识别的 .dtb 二进制的,然后重点讲解设备树的语法规则------看到任何一个 .dts 文件,都能一眼看穿它的结构和含义。
DTC 编译器:文本到二进制的魔法
dts / dtsi / dtb 三兄弟
在设备树的生态里,你会经常看到三种文件后缀,搞清楚它们的区别是第一步:
.dts:设备树源文件(Device Tree Source),给人看的文本格式。内核不会直接读取它,必须先编译。.dtsi:设备树头文件(Device Tree Source Include),类似 C 的.h,包含通用的硬件定义,被多个.dts引用。永远不要直接修改厂商的.dtsi文件!.dtb:设备树二进制文件(Device Tree Blob),内核真正加载的格式,由 DTC 编译器从.dts生成。
DTC 工作原理
DTC(Device Tree Compiler)本质上就是一个编译器,它的源码在内核的 scripts/dtc 目录下。和所有编译器一样,它分为三个阶段:
- 词法分析 (
dtc-lexer.l):把文本切分成 Token,比如把compatible = "fsl,imx6ull";识别为标识符、赋值号、字符串和分号。 - 语法分析 (
dtc-parser.y):根据文法规则把 Token 流构建成内存中的树形结构。 - 代码生成 (
flattree.c):遍历语法树,"拍扁"成线性的 DTB 二进制格式。
DTB 二进制结构
DTB 文件由四部分组成:
diff
+------------------+
| fdt_header | 文件头(40字节,含魔术数字 0xd00dfeed)
+------------------+
| memory reserve | 内存保留区域
+------------------+
| structure block| 结构块(节点和属性,用 FDT_BEGIN_NODE / FDT_PROP 等标记)
+------------------+
| strings block | 字符串块(属性名,共享存储以节省空间)
+------------------+
DTB 使用大端序(Big Endian),所以用 hexdump 看到的魔术数字是 d0 0d fe ed。
常用命令
bash
# 编译 DTS → DTB
dtc -I dts -O dtb -o output.dtb input.dts
# 反编译 DTB → DTS(调试利器)
dtc -I dtb -O dts /boot/imx6ull-14x14-evk.dtb > current.dts
# 查看完整 DTB 信息
fdtdump output.dtb
在内核构建中,make dtbs 会根据 arch/arm/boot/dts/Makefile 中的配置自动选择并编译对应的 DTS 文件。
以上就是 DTC 和 DTB 的核心要点。理解了编译链路,接下来我们进入正题------设备树的语法。
节点语法:树的枝干
设备树的核心结构就是树 。每个设备都是一个节点,每个节点里是一堆键值对,我们称之为属性。
节点命名规则
节点的命名格式通常如下:
css
node-name@unit-address
node-name:节点名字,ASCII 字符串,比如uart1、i2c0。unit-address:设备的寄存器首地址,可省略。
实际文件中,我们经常看到带标签的写法:
dts
cpu0: cpu@0
intc: interrupt-controller@00a01000
格式是 label: node-name@unit-address。冒号前面的 cpu0 和 intc 是节点标签 ,方便后面通过 &label 引用,不用每次敲那长长的一串名字。
我们来看一个从 I.MX6ULL 设备树截取的完整例子:
dts
/ {
aliases {
can0 = &flexcan1;
};
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
};
intc: interrupt-controller@00a01000 {
compatible = "arm,cortex-a7-gic";
reg = <0x00a01000 0x1000>;
interrupt-controller;
#interrupt-cells = <2>;
};
}
几个关键点:
- 根节点
/:整个树的起点,所有其他节点都是它的子孙。 - 节点嵌套 :
cpus是根节点的子节点,cpu@0又是cpus的子节点,反映了硬件的层级结构。 - 标签的使用 :
cpu0和intc是标签,可以在文件其他地方通过&cpu0或&intc引用。
节点引用:&label 机制
这是设备树中最实用的语法糖。假设 imx6ull.dtsi 里定义了:
dts
i2c1: i2c@021a0000 {
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
status = "disabled";
};
你想启用 I2C1 并挂两个设备,只需要在 .dts 里写:
dts
&i2c1 {
clock-frequency = <100000>;
status = "okay";
mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
};
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>;
};
};
既启用了控制器,又挂上了从设备,而且不会污染原始 .dtsi。这种"通过引用标签来追加内容"的机制,是设备树移植中最常用的操作。
属性类型:数据的各种面孔
节点里面全是属性,属性就是键值对。DTS 支持的数据类型非常直观。
字符串属性
dts
model = "Freescale i.MX6 ULL 14x14 EVK Board";
status = "okay";
device_type = "cpu";
字符串列表用逗号分隔,非常关键------它表示"先试着找前一个驱动,找不到就找后一个":
dts
compatible = "fsl,imx6ull-gpmi-nand", "fsl,imx6ul-gpmi-nand";
数值属性
数值用尖括号 <> 包裹,默认是 32 位无符号整数,支持十进制和十六进制:
dts
reg = <0x02020000 0x4000>;
前一个是地址,后一个是长度(16KB)。
引用属性(phandle)
一个节点需要引用另一个节点时,用 &label:
dts
gpio1: gpio@0209c000 {
#gpio-cells = <2>;
gpio-controller;
};
some-device {
gpios = <&gpio1 12 0>;
};
编译后的 DTB 里,&gpio1 会被替换为该节点的唯一数字 ID(phandle)。
空属性
有些属性不需要值,它们的存在本身就表示某种含义:
dts
gpio-controller;
interrupt-controller;
标准属性:那些你一定会用到的
Linux 内核定义了一些标准属性,驱动程序会对它们有特定的预期处理。
compatible:驱动匹配的灵魂
这是最重要的属性,没有之一。格式为 "manufacturer,model",它是驱动和硬件绑定的"红娘":
dts
compatible = "fsl,imx6ul-evk-wm8960", "fsl,imx-audio-wm8960";
内核会拿着这个字符串去驱动的 of_match_table 里匹配:
c
static const struct of_device_id imx_wm8960_dt_ids[] = {
{ .compatible = "fsl,imx-audio-wm8960", },
{ /* sentinel */ }
};
只要有一个能对上,驱动就会被触发。
status:设备的生死开关
"okay":设备可操作,内核加载驱动。"disabled":设备禁用,内核忽略它。"fail"/"fail-sss":设备有严重错误。
移植时最常改的属性。很多外设在 .dtsi 里默认 disabled,我们在 .dts 里改成 okay 来激活。
reg:地址与长度
描述设备寄存器地址范围,格式由父节点的 #address-cells 和 #size-cells 决定:
dts
uart1: serial@02020000 {
compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";
reg = <0x02020000 0x4000>;
};
#address-cells 和 #size-cells:地址的度量衡
这两个属性出现在父节点里,指导子节点如何写 reg:
#address-cells:地址字段占多少个 32 位整数。#size-cells:长度字段占多少个 32 位整数。
dts
spi4 {
#address-cells = <1>;
#size-cells = <0>; // 没有长度字段
gpio_spi@0 {
reg = <0>; // 只有一个地址
};
};
再看:
dts
aips-bus@02200000 {
#address-cells = <1>;
#size-cells = <1>;
dcp@02280000 {
reg = <0x02280000 0x4000>; // 地址 + 长度
};
};
ranges:地址翻译
ranges 是一个翻译函数:ranges = <child-bus-address parent-bus-address length>。
空 ranges 意味着子地址和父地址一一对应,不需要翻译,这在 SoC 内部总线中很常见:
dts
soc {
ranges;
};
如果不为空,比如 ranges = <0x0 0xe0000000 0x00100000>,则子总线地址 0x4600 会被翻译为 0xe0004600。大部分普通开发板移植不需要关心这个。
语法糖:让代码更简洁
include 机制
dts
#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"
DTC 不仅能 include .dtsi,连 .h 文件也支持。约定上:.h 放宏定义(把"魔术数字"变成可读常量),.dtsi 放节点定义。
/delete-node/:删除节点
dts
/delete-node/ &sim2;
把不需要的节点彻底删除,而不是让它以 disabled 状态存在。
节点追加与覆盖
dts
&i2c1 {
status = "okay"; // 覆盖原来的 "disabled"
clock-frequency = <100000>; // 新增属性
};
如果原节点已有该属性则覆盖,没有则新增。不用修改原始文件,就能完全定制设备树行为。
实战示例:一个完整的设备树文件
下面是一个用于 Alpha 开发板的 LED 驱动设备树文件:
dts
/dts-v1/;
#include "imx6ull.dtsi"
#include "imx6ull-aes.dtsi"
/ {
model = "Awesome Embedded Studio IMX6ULL Example Driver";
compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
imx_aes_led {
#address-cells = <1>;
#size-cells = <1>;
compatible = "atkalpha-led";
status = "okay";
reg = < 0X020C406C 0X04 /* CCM_CCGR1_BASE */
0X020E0068 0X04 /* SW_MUX_GPIO1_IO03_BASE */
0X020E02F4 0X04 /* SW_PAD_GPIO1_IO03_BASE */
0X0209C000 0X04 /* GPIO1_DR_BASE */
0X0209C004 0X04 >; /* GPIO1_GDIR_BASE */
};
};
逐行解析:
/dts-v1/;:声明设备树版本,必须写。#include:引入芯片级头文件和板级配置。- 根节点的
compatible:必须和内核里某个DT_MACHINE_START的.dt_compat匹配,否则内核启动失败。 imx_aes_led:自定义节点,没有@address后缀因为它只是描述寄存器的"容器"。compatible = "atkalpha-led":驱动匹配的关键字符串。reg:列出了驱动需要的五个寄存器地址,每个由物理地址和长度组成。
踩坑预警
根节点 compatible 拼写错误
手滑多打一个字母(比如 "fsl,imx6ullll"),内核启动后只显示 Starting kernel ... 然后就没了。内核启动不了,第一件事检查根节点 compatible 拼写!
忘记写 ranges
定义总线节点时忘了 ranges,内核映射外设寄存器时可能拿到错误地址。SoC 内部总线通常用空 ranges 表示直接映射。
#address-cells 和 #size-cells 不匹配
父节点定义了 #address-cells = <1> 和 #size-cells = <1>,子节点 reg 却只写了一个数字。DTC 可能不报错,但内核解析时会出问题。
直接修改 .dtsi 文件
新手最容易犯的错误。.dtsi 是公用的,改了会影响所有引用它的项目。正确做法是在 .dts 里通过 &label 引用修改。
小结
这一章我们先把 DTC 编译器的原理过了一遍------词法分析、语法分析、二进制生成三阶段,以及 DTB 的文件头、结构块、字符串块布局。然后重点讲解了设备树的语法:节点命名、属性类型、标准属性、语法糖,最后通过一个完整的 LED 驱动示例串联了所有知识点。
掌握这些,你就能看懂任何 .dts 文件,也能写出规范的设备树代码了。