imx6ull-驱动开发篇6——Linux 设备树语法

发布于:2025-08-03 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

前言

设备树

设备树概念

DTS、 DTB 和 DTC

DTS 语法

.dtsi 头文件

设备节点

/根节点​​

节点命名与标签

节点层次结构​

属性数据类型​

标准属性

compatible 属性

model 属性

status 属性

#address-cells 和#size-cells 属性

reg 属性

ranges 属性

name 属性

device_type 属性

根节点 compatible 属性

设备匹配过程

向节点追加或修改内容


前言

在新版本的 Linux 中, ARM 相关的驱动全部采用了设备树,最新出的 CPU 其驱动开发也基本都是基于设备树的,比如 ST 新出的 STM32MP157、NXP的 I.MX8系列等。

我们所使用的正点原子I.MX6UALPHA 开发板,Linux版本为 4.1.15,其支持设备树,所以所有 Linux 驱动都是基于设备树的。

本讲实验,我们了解一下设备树的起源、重点学习一下设备树语法。

设备树

设备树概念

设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做 DTS(Device Tree Source)。

本质:硬件描述的“树状数据结构”​​

  • ​​设备(Device)​​:描述CPU、内存、外设等硬件单元。
  • ​​树(Tree)​​:以父子节点形式组织硬件关系,体现硬件拓扑。
  • ​​DTS(源文件)​​:人类可读的硬件描述文本,编译后生成二进制DTB供内核使用。

DTS文件描述了开发板上的设备信息,比如CPU 数量、 内存基地址、 IIC 接口上接了哪些设备、 SPI 接口上接了哪些设备等等,如图:

设备树如何工作?​​

  • 编写DTS​​:描述硬件连接(如UART地址、中断号)。
  • ​​编译为DTB​​:通过DTC编译器生成二进制DTB文件。
  • 内核解析DTB​​:启动时,内核读取DTB,动态生成设备节点(无需重新编译内核)。

DTS、 DTB 和 DTC

设备树源文件扩展名为.dts,但是我们在前面移植 Linux 的时候却一直在使用.dtb 文件。

DTS 是设备树源码文件, DTB 是将 DTS 编译以后得到的二进制文件。

将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb需要用到 DTC 工具。

名称

类型

用途

文件扩展名

编辑方式

DTS

文本文件

人类编写硬件描述

.dts

文本编辑器

DTB

二进制文件

内核直接读取的硬件配置

.dtb

不可直接编辑

DTC

工具

DTS 和 DTB 的转换工具

-

命令行调用

 DTC 工具源码在 Linux 内核的 scripts/dtc 目录下。

scripts/dtc/Makefile 文件内容如下:

hostprogs-y := dtc
always := $(hostprogs-y)

dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
            srcpos.o checks.o util.o
dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o
......

可以看出, DTC 工具依赖于 dtc.c、 flattree.c、 fstree.c 等文件,最终编译并链接出 DTC 这个主机文件。

如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:

make all

make all”命令是编译 Linux 源码中的所有东西,包括 zImage, .ko 驱动模块以及设备树。

如果只是编译设备树的话建议使用以下命令:

make dtbs

每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?

以 I.MX6ULL 这款芯片对应的板子为例,打开 arch/arm/boot/dts/Makefile, 在“ dtb- $(CONFIG_SOC_IMX6ULL)”配置项,有如下内容:

这个dtb就是我们之前移植Linux 系统的时候添加的设备树,可以参考:系统移植篇20——Linux 内核移植(上)

当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到I.MX6ULL 这个 SOC的板子对应的.dts 文件都会被编译为.dtb。

如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb- $(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。

DTS 语法

.dtsi 头文件

和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。

在 imx6ull-alientekemmc.dts 中有如下所示内容:

#include <dt-bindings/input/input.h>
#include "imx6ull.dtsi"

在imx6ull-14x14-evk-gpmi-weim.dts 这个文件里:

#include "imx6ull-14x14-evk.dts"

在.dts 设备树文件中,可以通过“#include”来引用.h、 .dtsi 和.dts 文件。

这三个文件的关系:

我们在编写设备树头文件的时候最好选择.dtsi 后缀。

一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、 IIC 等等。

比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息的,内容如下:

/* 包含必要的头文件和设备树片段 */
#include <dt-bindings/clock/imx6ul-clock.h>      // i.MX6UL时钟控制宏定义
#include <dt-bindings/gpio/gpio.h>               // GPIO相关宏定义
#include <dt-bindings/interrupt-controller/arm-gic.h> // ARM中断控制器宏
#include "imx6ull-pinfunc.h"                     // i.MX6ULL引脚功能定义
#include "imx6ull-pinfunc-snvs.h"                // i.MX6ULL SNVS模块引脚定义
#include "skeleton.dtsi"                         // 设备树基础框架

/* 设备树根节点 */
/ {
    /* 设备别名定义(方便驱动引用) */
    aliases {
        can0 = &flexcan1;  // 将can0别名指向flexcan1节点
        /* 其他别名... */
    };

    /* CPU核心配置 */
    cpus {
        #address-cells = <1>;  // 子节点地址用1个u32表示
        #size-cells = <0>;     // 子节点不需要大小字段

        cpu0: cpu@0 {
            compatible = "arm,cortex-a7";  // CPU架构兼容性标识
            device_type = "cpu";            // 设备类型标识
            /* 其他CPU属性... */
        };
    };

    /* 中断控制器配置 */
    intc: interrupt-controller@00a01000 {
        compatible = "arm,cortex-a7-gic";  // ARM标准中断控制器
        #interrupt-cells = <3>;            // 中断描述需要3个参数
        interrupt-controller;              // 声明为中断控制器
        reg = <0x00a01000 0x1000>,        // 寄存器地址范围1
              <0x00a02000 0x100>;         // 寄存器地址范围2
    };

    /* 时钟系统配置 */
    clocks {
        #address-cells = <1>;
        #size-cells = <0>;

        /* 32.768kHz低速时钟 */
        ckil: clock@0 {
            compatible = "fixed-clock";    // 固定频率时钟
            reg = <0>;                     // 虚拟寄存器地址
            #clock-cells = <0>;            // 无子时钟
            clock-frequency = <32768>;     // 时钟频率32.768kHz
            clock-output-names = "ckil";   // 时钟输出名称
        };
        /* 其他时钟... */
    };

    /* SoC外设总线 */
    soc {
        #address-cells = <1>;              // 地址用1个u32表示
        #size-cells = <1>;                 // 大小用1个u32表示
        compatible = "simple-bus";         // 简单内存映射总线
        interrupt-parent = <&gpc>;         // 中断父控制器
        ranges;                            // 地址转换启用

        /* 总线频率控制节点 */
        busfreq {
            compatible = "fsl,imx_busfreq"; // NXP总线频率控制驱动
            /* 其他属性... */
        };

        /* NAND Flash控制器 */
        gpmi: gpmi-nand@01806000 {
            compatible = "fsl,imx6ull-gpmi-nand", "fsl,imx6ul-gpmi-nand"; // 兼容性
            #address-cells = <1>;         // 子地址用1个u32表示
            #size-cells = <1>;            // 子大小用1个u32表示
            reg = <0x01806000 0x2000>,    // 寄存器区域1
                  <0x01808000 0x4000>;    // 寄存器区域2
            /* 其他NAND属性... */
        };
        /* 其他外设... */
    };
};

在 imx6ull.dtsi 文件中,将 I.MX6ULL 这颗 SOC 所有的外设都描述的清清楚楚,比如 ecspi1~4、 uart1~8、 usbphy1~2、 i2c1~4等等。

设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。

以下是从imx6ull.dtsi 文件中缩减出来的设备树文件内容:

/ {
    aliases {
        can0 = &flexcan1;  // 别名:can0指向flexcan1节点
    };

    cpus {
        #address-cells = <1>;
        #size-cells = <0>;
        cpu0: cpu@0 {                  // 标签cpu0,节点名cpu@0
            compatible = "arm,cortex-a7"; // 字符串属性
            device_type = "cpu";         // 字符串属性
            reg = <0>;                   // 32位整数属性
        };
    };

    intc: interrupt-controller@00a01000 { // 标签intc
        compatible = "arm,cortex-a7-gic";
        #interrupt-cells = <3>;          // 中断描述需要3个参数
        interrupt-controller;             // 空属性(布尔值)
        reg = <0x00a01000 0x1000>,       // 地址范围数组
              <0x00a02000 0x100>;
    };
};

/根节点​​

  • 每个 .dts/.dtsi文件必须有且仅有一个根节点 /
  • ​​多文件合并规则​​:若多个文件定义根节点,编译时会将所有子节点合并到同一个根节点下(如 imx6ull.dtsi和 imx6ull-alientek-emmc.dts的根节点内容会合并)。

节点命名与标签

标准命名格式

node-name@unit-address   // 如 uart@40010000
  • node-name:描述节点功能(如 uart、gpio)。
  • unit-address:设备寄存器基地址(可省略,如 cpus节点)。

​标签(Label)​​

label: node-name@unit-address  // 如 cpu0:cpu@0
  • ​​作用​​:通过 &label快速引用节点(如 &cpu0代替 cpu@0)。
  • ​​优势​​:简化长节点名的访问(如 &intc代替 interrupt-controller@00a01000)。

节点层次结构​

可形成父子关系:

/ {                         // 根节点
    cpus {                  // 子节点
        cpu0: cpu@0 { ... }; // 子节点的子节点
    };
};

节点可嵌套,形成树状结构。

属性数据类型​

每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。

设备树源码中常用的几种数据形式如下所示:

类型​

​示例​

​说明​

​字符串​

compatible = "arm,cortex-a7";

用于驱动匹配的标识符。

​32位整数​

reg = <0>;

单个数值或寄存器地址。

​数组​

reg = <0x00a01000 0x1000>;

多值用空格分隔(如地址+长度)。

​字符串列表​

compatible = "fsl,imx6ull-nand", "fsl,imx6ul-nand";

多个兼容性标识,逗号分隔。

标准属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,有很多标准属性,用户可以自定义属性。

常用的标准属性如下:

compatible 属性

compatible 属性也叫做“兼容性”属性:

  • 核心作用:驱动与设备的“桥梁”​​
  • ​​本质​​:设备树中用于​​绑定设备与驱动​​的关键属性。
  • ​​工作原理​​:内核启动时,会遍历设备树中的 compatible值,与驱动中的 of_device_id表匹配,找到对应的驱动程序。

compatible 属性的值格式如下所示:

compatible = "manufacturer,model", "manufacturer,generic-model";

内核按顺序匹配,​​第一个值​​是设备专属驱动(如板级定制),​​第二个值​​是通用驱动(如芯片厂商提供)。

以WM8960音频芯片为例:

compatible = "fsl,imx6ul-evk-wm8960", "fsl,imx-audio-wm8960";
  • fsl,imx6ul-evk-wm8960:飞思卡尔为开发板定制的驱动。
  • fsl,imx-audio-wm8960:飞思卡尔提供的WM8960通用驱动。

一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。

比如在文件 imx-wm8960.c 中有如下内容:

/* 定义设备树兼容性匹配表 */
static const struct of_device_id imx_wm8960_dt_ids[] = {
    { .compatible = "fsl,imx-audio-wm8960", },  // 匹配设备树中兼容"fsl,imx-audio-wm8960"的节点
    { /* sentinel */ }  // 结束标记
};

/* 向内核注册设备树匹配表 */
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);

/* 定义平台驱动结构体 */
static struct platform_driver imx_wm8960_driver = {
    .driver = {
        .name = "imx-wm8960",  // 驱动名称
        .pm = &snd_soc_pm_ops,  // 电源管理操作集(使用标准ASoC电源管理操作)
        .of_match_table = imx_wm8960_dt_ids,  // 指向设备树匹配表
    },
    .probe = imx_wm8960_probe,  // 设备探测函数,当匹配到设备时调用
    .remove = imx_wm8960_remove,  // 设备移除函数,当设备断开时调用
};

数组 imx_wm8960_dt_ids 就是 imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的 compatible 属性值与此相等,那么这个节点就会使用此驱动文件。

wm8960 采用了 platform_driver 驱动模式,设置.of_match_table 为 imx_wm8960_dt_ids,也就是设置这个 platform_driver 所使用的OF 匹配表。

model 属性

model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如:

model = "wm8960-audio";

status 属性

status属性是设备树(Device Tree)中用于描述设备当前状态的属性,通常由操作系统或固件在运行时解析,以决定是否初始化或使用该设备。

可选的状态如表:

#address-cells 和#size-cells 属性

这两个属性用于​​描述设备树中子节点的地址和大小信息​​,通常出现在​​有子节点的设备节点​​中,用于定义子节点 reg属性的解析方式。

属性​

​数据类型​

​作用​

#address-cells

u32(无符号32位整数)

指定子节点 reg属性中​​地址字段​​的字长(以32位为单位)。

#size-cells

u32(无符号32位整数)

指定子节点 reg属性中​​大小字段​​的字长(以32位为单位)。

  • 字长(cell)​​:1 cell = 32 bits(4字节)。
  • ​​影响范围​​:这两个属性仅影响​​当前节点的子节点​​,不会影响兄弟节点或父节点。

举例:

/* SPI4 控制器节点,使用 GPIO 模拟 SPI */
spi4 {
    compatible = "spi-gpio";  // 使用 GPIO 模拟 SPI 总线
    #address-cells = <1>;     // 子节点 reg 属性中的地址部分占 1 个 32 位字
    #size-cells = <0>;        // 子节点 reg 属性不包含大小信息

    /* SPI 设备节点:74HC595 移位寄存器 */
    gpio_spi: gpio_spi@0 {
        compatible = "fairchild,74hc595";  // 设备兼容性标识
        reg = <0>;  // 设备片选号(CS),由于 #size-cells=0,只包含地址部分
    };
};

/* AIPS3 总线节点 */
aips3: aips-bus@02200000 {
    compatible = "fsl,aips-bus", "simple-bus";  // AIPS 总线兼容性标识
    #address-cells = <1>;  // 子节点 reg 属性中的地址部分占 1 个 32 位字
    #size-cells = <1>;    // 子节点 reg 属性中的大小部分占 1 个 32 位字

    /* DCP 加密模块节点 */
    dcp: dcp@02280000 {
        compatible = "fsl,imx6sl-dcp";  // i.MX6SL 的 DCP 模块
        reg = <0x02280000 0x4000>;  // 寄存器基地址 0x02280000,大小 16KB
    };
};

reg 属性

reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。

格式如下:

reg = <address1 length1 address2 length2 ...>;
  • address​​:起始地址(由 #address-cells决定占用的 cell 数量)。
  • ​​length​​:地址范围长度(由 #size-cells决定占用的 cell 数量)。

举例:

soc {
    #address-cells = <1>;
    #size-cells = <1>;

    ethernet@10000000 {
        reg = <0x10000000 0x1000>;  // 起始地址 0x10000000,长度 4KB
    };
};

ranges 属性

ranges是设备树(Device Tree)中用于​​描述子节点地址空间如何映射到父节点地址空间​​的关键属性,本质是一个​​地址转换表​​。其核心作用是定义​​子总线地址​​与​​父总线地址​​之间的映射关系。

每个 ranges条目由 ​​3 部分组成​​,格式如下:

ranges = <child-addr parent-addr length>;

每个条目表示一段地址空间的映射关系:

​字段​

​描述​

​长度由谁决定​

child-bus-address

子总线地址空间的物理地址

父节点的 #address-cells

parent-bus-address

父总线地址空间的物理地址

父节点的 #address-cells

length

子地址空间的长度(范围)

父节点的 #size-cells

举例:

/* 空 ranges(无需转换) */

soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;  // 子地址 = 父地址,无需转换

    uart1: serial@02020000 {
        reg = <0x02020000 0x4000>;  // 地址 0x02020000 直接对应 CPU 物理地址
    };
};


/* 非空 ranges(地址转换)  */

pcie {
    #address-cells = <2>;  // 子地址 64 位
    #size-cells = <1>;     // 长度 32 位
    ranges = <0x00 0x00000000 0x80000000 0x10000000>;  // 子地址 0x0_00000000 → 父地址 0x80000000,长度 256MB

    device@0 {
        reg = <0x00 0x00000000 0x10000000>;  // 子设备地址范围
    };
};

name 属性

name 属性值为字符串, name 属性用于记录节点名字。

name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。

device_type 属性

device_type 属性值为字符串, IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。i

mx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:

cpu0: cpu@0 {
        compatible = "arm,cortex-a7";
        device_type = "cpu";
        reg = <0>;
......
};

根节点 compatible 属性

每个节点都有 compatible 属性,根节点“/”也不例外,

imx6ull-alientek-emmc.dts 文件中根节点的 compatible 属性内容如下所示:

/ {
     model = "Freescale i.MX6 ULL 14x14 EVK Board";
     compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
......
}

设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,compatible 有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。

Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。

设备匹配过程

Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程如图:

1) 入口:start_kernel(初始化内核)​​

  • 位置:init/main.c
  • 调用 setup_arch()进入架构相关初始化。

​​(2) 解析设备树:setup_arch​​

  • 位置:arch/arm/kernel/setup.c
  • 调用 setup_machine_fdt()匹配设备树根节点的 compatible。

​​(3) 匹配 machine_desc:setup_machine_fdt​​

  • 位置:arch/arm/kernel/devtree.c

关键步骤:

  • 通过 of_flat_dt_match_machine()匹配设备树根节点的 compatible。
  • 返回匹配的 machine_desc结构体。

​​(4) 匹配逻辑:of_flat_dt_match_machine​​

  • 位置:drivers/of/fdt.c
  • 遍历内核预定义的 machine_desc列表(通过 DT_MACHINE_START宏注册),与设备树的 compatible字符串比较。
  • 匹配优先级:​​完全匹配 > 部分匹配​​(按 compatible列表顺序)。

​​(5) 返回 machine_desc​​

  • 匹配成功后,内核会执行 machine_desc中定义的平台初始化函数(如 init_machine)。

向节点追加或修改内容

产品开发过程中可能面临着频繁的需求更改,怎么修改或者新增内容呢?

方法一,修改通用SOC文件,比如imx6ull.dtsi 文件:

原始代码如下:

i2c1: i2c@021a0000 {
    #address-cells = <1>;                // 子节点 reg 地址字段占 1 个 cell(32位)
    #size-cells = <0>;                   // 子节点 reg 不包含长度字段
    compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";  // 驱动兼容性标识
    reg = <0x021a0000 0x4000>;           // 寄存器基地址 0x021a0000,长度 16KB
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;  // 中断号 36,高电平触发
    clocks = <&clks IMX6UL_CLK_I2C1>;    // 时钟源引用
    status = "disabled";                  // 默认禁用,需在板级 DTS 中启用
};

现在要在 i2c1 节点下创建一个子节点,这个子节点就是 fxls8471,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点,如下所示:

i2c1: i2c@021a0000 {
    #address-cells = <1>;                // 子设备地址用1个32位单元表示
    #size-cells = <0>;                   // 子设备不需要地址空间大小
    compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";  // 兼容的驱动程序
    reg = <0x021a0000 0x4000>;           // 控制器寄存器地址范围
    interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;  // 中断配置
    clocks = <&clks IMX6UL_CLK_I2C1>;    // 时钟源
    status = "disabled";                  // 默认禁用控制器

    /* FXLS8471加速度传感器子节点 */
    fxls8471@1e {
        compatible = "fsl,fxls8471";     // 设备驱动兼容性
        reg = <0x1e>;                    // I2C设备地址0x1E
    };
};

这样非常简单,但是imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。

直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是不是每个板子都有该设备。

所以方法二,在修改板级设备树文件中,追加设备:

I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alientek-emmc.dts,因此我们需要在imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:

&i2c1 {
    // 添加 FXLS8471 传感器子节点
    fxls8471@1e {
        compatible = "nxp,fxls8471";    // 驱动匹配字符串
        reg = <0x1e>;                   // I2C 设备地址
        interrupt-parent = <&gpio5>;    // 中断引脚配置
        interrupts = <0 IRQ_TYPE_LEVEL_HIGH>;
    };
};

修改imx6ull-alientek-emmc.dts 这个文件内的代码,不会对使用 I.MX6ULL 这颗 SOC 的其他板子造成任何影响。

这个就是向节点追加或修改内容的方法,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。


网站公告

今日签到

点亮在社区的每一天
去签到