嵌入式Linux驱动开发——设备树语法与编译工具——读懂这张"藏宝图"

嵌入式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 目录下。和所有编译器一样,它分为三个阶段:

  1. 词法分析dtc-lexer.l):把文本切分成 Token,比如把 compatible = "fsl,imx6ull"; 识别为标识符、赋值号、字符串和分号。
  2. 语法分析dtc-parser.y):根据文法规则把 Token 流构建成内存中的树形结构。
  3. 代码生成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 字符串,比如 uart1i2c0
  • unit-address:设备的寄存器首地址,可省略。

实际文件中,我们经常看到带标签的写法:

dts 复制代码
cpu0: cpu@0
intc: interrupt-controller@00a01000

格式是 label: node-name@unit-address。冒号前面的 cpu0intc节点标签 ,方便后面通过 &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>;
    };
}

几个关键点:

  1. 根节点 /:整个树的起点,所有其他节点都是它的子孙。
  2. 节点嵌套cpus 是根节点的子节点,cpu@0 又是 cpus 的子节点,反映了硬件的层级结构。
  3. 标签的使用cpu0intc 是标签,可以在文件其他地方通过 &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 文件,也能写出规范的设备树代码了。

相关推荐
原来是猿2 小时前
网络计算器:理解序列化与反序列化(下)
linux·开发语言·网络·网络协议·json·php
木木_王2 小时前
嵌入式学习 | STM32裸板驱动开发(Day01)入门学习笔记(超详细完整版|点灯实验 + 库函数代码 + 原理全解)
linux·驱动开发·笔记·stm32·学习
小锋学长生活大爆炸2 小时前
【教程】树莓派驱动 0.96 寸 SSD1315 OLED 屏幕完整指南
单片机·嵌入式硬件·嵌入式·教程·树莓派·oled·屏幕
勤自省2 小时前
ROS2从入门到“重启解决”:21讲8~12章踩坑血泪史与核心总结
linux·开发语言·ubuntu·ssh·ros
原来是猿2 小时前
Linux守护进程(Daemon)完全指南:从原理到实战
linux·运维·服务器·网络·php
阡陌..2 小时前
如何使用samba为Linux设置一个局域网共享盘
linux·运维·服务器
霞姐聊IT3 小时前
三大并发技术—进程、线程和协程
linux·运维·网络·操作系统
南境十里·墨染春水3 小时前
linux学习进展 网络编程——HTTPS (补充)
linux·网络·学习
t5y223 小时前
【Linux】学习小计
linux