02-设备树语法与实战.md 41 KB


title: 设备树语法与实战 tags: [设备树, DTS, DTB, of_*API, 嵌入式Linux, GPIO, IMX6ULL] created: 2026-09-16 updated: 2026-09-17 pdf_ref:

  • "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1_拆分/49_第四十三章 Linux设备树.pdf"
  • "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1_拆分/50_第四十四章 设备树下的LED驱动实验.pdf" ---

设备树语法与实战

关联知识:[[02-嵌入式Linux内核基础/06-设备模型与驱动框架]] | [[03-Linux驱动开发核心/07-platform驱动模型]]


一、设备树概述

1.1 什么是设备树

设备树(Device Tree)是一种描述硬件信息的数据结构,它以树状形式组织,树中每个节点描述了硬件设备的某一信息。设备树在内核启动时由 bootloader(如 U-Boot)传递给内核,内核解析设备树后即可获取板级硬件信息。

设备树相关概念:

概念 文件后缀 说明
DTS .dts 设备树源文件,文本格式,开发者编写
DTSI .dtsi 设备树头文件,可被 DTS 包含
DTB .dtb 编译后的二进制文件,bootloader 加载
DTC 设备树编译器,将 DTS 编译为 DTB
DTBO .dtbo 设备树 Overlay,运行时动态叠加

1.2 为什么需要设备树

在没有设备树的时代,Linux 内核通过 MACHINE_START / MACHINE_END 宏为每块开发板定义一个 machine_desc 结构体,把硬件信息(寄存器地址、中断号等)硬编码在内核源码中。板子一多,内核里就堆满了各种板级文件,维护成本极高。

设备树解决了这个问题:硬件描述从内核源码中剥离出来,放到独立的 .dts 文件里。同一份内核可以通过不同的设备树支持不同的板子,不需要为每块板子编译一个专用内核。

flowchart LR
    accTitle: 设备树解决的核心问题
    accDescr: 硬件信息从内核源码硬编码迁移到独立的设备树文件

    subgraph before["无设备树时代"]
        b1["内核源码"] --> b2["板级文件硬编码"]
        b2 --> b3["每块板子一个内核"]
    end

    subgraph after["设备树时代"]
        a1["内核源码"] --> a2["通用内核"]
        a3[".dts 设备树"] --> a2
        a2 --> a4["支持任意板子"]
    end

    before -->|"演进"| after

1.3 设备树在 Linux 中的作用

Linux 内核启动时的流程:

flowchart TD
    accTitle: Linux 启动时的设备树处理流程
    accDescr: 从 bootloader 传递 DTB 到内核解析设备树节点的完整流程

    start["start_kernel()"] --> setup["setup_arch()"]
    setup --> fdt["setup_machine_fdt()"]
    fdt --> match["of_flat_dt_match_machine()"]
    match --> unflatten["unflatten_device_tree()"]
    unflatten --> proc["创建 /proc/device-tree/ 目录"]
    proc --> driver["驱动通过 OF 函数读取设备信息"]

    classDef process fill:#dbeafe,stroke:#2563eb,stroke-width:1px
    classDef output fill:#dcfce7,stroke:#16a34a,stroke-width:1px

    class start,setup,fdt,match,unflatten process
    class proc,driver output

内核通过根节点 /compatible 属性值与 machine_desc 结构体的 .dt_compat 成员比较,找到匹配的设备描述:

// arch/arm/mach-imx/mach-imx6ul.c
static const char *imx6ul_dt_compat[] __initconst = {
    "fsl,imx6ul",
    "fsl,imx6ull",
    NULL,
};

DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")
    .map_io = imx6ul_map_io,
    .init_irq = imx6ul_init_irq,
    .init_machine = imx6ul_init_machine,
    .dt_compat = imx6ul_dt_compat,
MACHINE_END

匹配过程:

flowchart TD
    accTitle: 设备树节点匹配流程
    accDescr: 内核如何通过 compatible 属性找到对应的 machine_desc

    root["获取根节点 compatible"] --> compare["逐个比较 machine_desc 的 .dt_compat"]
    compare --> found{"找到匹配?"}
    found -->|"是"| desc["使用该 machine_desc"]
    found -->|"否"| fail["启动失败"]

    classDef check fill:#fef9c3,stroke:#ca8a04,stroke-width:1px
    classDef ok fill:#dcfce7,stroke:#16a34a,stroke-width:1px
    classDef err fill:#fecaca,stroke:#dc2626,stroke-width:1px

    class found check
    class desc ok
    class fail err

1.4 设备树在文件系统中的呈现

内核解析 DTB 后,会在 /proc/device-tree/ 目录下按节点结构创建对应的文件和子目录。每个属性对应一个文件,用 cat 命令即可查看属性值:

# 查看根节点的 model 属性
cat /proc/device-tree/model
# 输出:Freescale i.MX6 ULL 14x14 EVK Board

# 查看根节点的 compatible 属性
cat /proc/device-tree/compatible
# 输出:fsl,imx6ull-14x14-evkfsl,imx6ull

# 查看 soc 节点下的子节点
ls /proc/device-tree/soc/

二、设备树语法详解

2.1 节点基本语法

node-name@unit-address {
    property-name = value;
    child-node {
        /* ... */
    };
};
  • node-name:节点名,长度 1~31 个字符,可以包含字母、数字、逗号、句点和下划线
  • @unit-address:可选,节点在地址空间中的起始地址,必须与父节点的 #address-cells 一致
  • 标签(label):可选,可以给节点起一个别名,方便其他地方通过 &label 引用

    /* 带标签的节点 */
    i2c1: i2c@021a0000 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
    reg = <0x021a0000 0x4000>;
    status = "okay";
    };
    
    /* 在其他地方通过标签引用 */
    &i2c1 {
    clock-frequency = <100000>;
    };
    

2.2 属性类型与语法

设备树属性值有多种类型:

字符串属性

compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart";
device_type = "cpu";

整数数组属性

reg = <0x02020000 0x4000>;           /* 地址 + 大小 */
interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_UART1_IPG>, <&clks IMX6UL_CLK_UART1_SERIAL>;

单个整数

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

布尔属性(存在即为 true)

ranges;          /* 值为空表示布尔属性 */
big-endian;      /* 也是布尔属性 */

字符串列表

clock-names = "ipg", "per";

2.3 核心属性详解

compatible — 兼容性属性

每个节点都有 compatible 属性,其值用于匹配 Linux 内核中的驱动程序。compatible 的值是字符串列表,第一个是设备自身的名称,后续是兼容的驱动名称。

/* 根节点 */
/ {
    compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";
};

/* UART1 节点 */
uart1: serial@02020000 {
    compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";
};

Linux 内核查找驱动的流程:将设备节点的 compatible 值逐个与注册的 platform_driverof_match_table 比较,找到第一个匹配的驱动后绑定。

#address-cells#size-cells

这两个属性定义了子节点 reg 属性中地址和长度字段占用的字长(cell)。一个 cell 通常为 32 位。

/* 父节点声明地址和长度各占一个 cell */
soc {
    #address-cells = <1>;
    #size-cells = <1>;
    ranges;

    /* 子节点 reg:起始地址 0x02020000,长度 0x4000 */
    uart1: serial@02020000 {
        reg = <0x02020000 0x4000>;
    };
};

对比不同 #address-cells / #size-cells 组合:

父节点设置 子节点 reg 写法 含义
#address-cells=<1>, #size-cells=<1> <addr size> 1 个 cell 地址 + 1 个 cell 长度
#address-cells=<2>, #size-cells=<1> <hi lo size> 2 个 cell 地址 + 1 个 cell 长度
#address-cells=<1>, #size-cells=<0> <addr> 1 个 cell 地址,无长度
/* SPI 节点:地址占 1 cell,长度占 0 cell */
spi4 {
    #address-cells = <1>;
    #size-cells = <0>;
    compatible = "spi-gpio";

    gpio_spi: gpio_spi@0 {
        compatible = "fairchild,74hc595";
        reg = <0>;    /* 只有地址,没有长度 */
    };
};

reg — 寄存器地址范围

reg 属性的格式为 <address length> 对,描述设备在父节点地址空间中的寄存器基地址和大小。

/* I2C1 控制器寄存器 */
i2c1: i2c@021a0000 {
    reg = <0x021a0000 0x4000>;  /* 基地址 0x021a0000,大小 0x4000 */
};

/* 多段寄存器(如 alphaled 节点) */
alphaled {
    #address-cells = <1>;
    #size-cells = <1>;
    compatible = "atkalpha-led";
    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 */
};

status — 设备状态

含义
"okay" 设备可用
"disabled" 设备不可用(但存在)
"reserved" 设备被占用
"fail" 设备初始化失败
uart1: serial@02020000 {
    status = "disabled";  /* 默认关闭,板级 DTS 中按需开启 */
};

ranges — 地址映射

ranges 属性值为空或包含三元组 (child-bus-address, parent-bus-address, length),用于将子节点地址空间映射到父节点地址空间。

/* 值为空:子地址空间与父地址空间完全相同 */
soc {
    ranges;   /* 子节点地址不需要转换 */
};

/* 值非空:子地址 0x0 映射到父地址 0xe0000000 */
soc {
    ranges = <0x0 0xe0000000 0x00100000>;
    /* 子节点 reg=0x4600 → 实际物理地址 = 0xe0004600 */
};

interruptsinterrupt-parent

uart1: serial@02020000 {
    interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
    interrupt-parent = <&gpc>;  /* 可选,默认继承父节点 */
};

<GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH> 的含义:

  • GIC_SPI:SPI 中断类型(共享外设中断)
  • 26:中断号
  • IRQ_TYPE_LEVEL_HIGH:高电平触发

其他常用属性

属性 说明 示例
name 节点名(已过时,不推荐使用) name = "serial";
device_type 设备类型,仅用于 cpu 和 memory 节点 device_type = "cpu";
clock-frequency 时钟频率 clock-frequency = <100000>;
phandle 节点的唯一标识符 由编译器自动生成
linux,phandle 同上,Linux 专用 由编译器自动生成

2.4 引用语法

phandle 引用

设备树中通过 &label 语法引用其他节点。&label 会被编译器自动转换为对应的 phandle 值。

/* 定义节点并打标签 */
gpc: interrupt-controller@020dc000 {
    #interrupt-cells = <3>;
    /* ... */
};

/* 引用该节点 */
uart1: serial@02020000 {
    interrupt-parent = <&gpc>;  /* 引用 gpc 节点 */
};

aliases 子节点

aliases 节点为设备节点提供简短的别名,方便系统和用户空间程序引用:

aliases {
    can0 = &flexcan1;
    ethernet0 = &fec1;
    gpio0 = &gpio1;
    i2c0 = &i2c1;
    spi0 = &ecspi1;
};

chosen 子节点

chosen 不是真正的硬件设备节点,而是 bootloader 向内核传递数据的通道。最常见的是 bootargs 属性:

chosen {
    stdout-path = &uart1;
};

U-Boot 在启动 Linux 之前,通过 fdt_chosen() 函数将 bootargs 环境变量写入 chosen 节点的 bootargs 属性中。

2.5 节点追加与修改

在板级 DTS 文件中,通过 &label 追加语法修改或扩展 SOC 级 DTSI 中的节点,不需要直接修改 DTSI 文件:

/* 在 imx6ull-alientek-emmc.dts 中追加 i2c1 的子节点 */
&i2c1 {
    clock-frequency = <100000>;
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_i2c1>;
    status = "okay";

    mag3110@0e {
        compatible = "fsl,mag3110";
        reg = <0x0e>;
        position = <2>;
    };

    fxls8471@1e {
        compatible = "fsl,fxls8471";
        reg = <0x1e>;
        position = <0>;
        interrupt-parent = <&gpio5>;
        interrupts = <0 8>;
    };
};

这样做的好处是:SOC 级 DTSI 文件保持原样,所有板级差异都在各自的 DTS 文件中追加完成。


三、设备树编译与反编译

3.1 dtc 命令

DTC(Device Tree Compiler)是设备树的编译器,将文本格式的 DTS 编译为二进制格式的 DTB。

# 编译 DTS 为 DTB
dtc -I dts -O dtb -o output.dtb input.dts

# 反编译 DTB 为 DTS
dtc -I dtb -O dts -o output.dts input.dtb

# 在内核源码中编译所有设备树
make dtbs

# 编译指定的设备树
make arch/arm/boot/dts/imx6ull-alientek-emmc.dtb

常用 dtc 参数:

参数 说明
-I dts 输入格式为 DTS
-I dtb 输入格式为 DTB
-O dtb 输出格式为 DTB
-O dts 输出格式为 DTS
-o output 指定输出文件名
-@ 生成 symbols 节点(用于 Overlay)
-W no-*-warnings 禁用特定警告

3.2 反编译实战

拿到一个 .dtb 文件,想看看里面的内容,可以用反编译命令:

dtc -I dtb -O dts -o result.dts imx6ull-alientek-emmc.dtb

3.3 设备树 Overlay(DTBO)

设备树 Overlay 允许在运行时动态叠加一段设备树到主设备树上,常用于同一块板子加载不同外设配置:

# 编译 Overlay(需要 -@ 参数生成 symbols)
dtc -@ -I dts -O dtb -o my-overlay.dtbo my-overlay.dts

# 在 U-Boot 中加载 Overlay
fdt addr 0x83000000
fdt resize 8192
load mmc 0:1 0x80800000 my-overlay.dtbo
fdt apply 0x80800000

Overlay DTS 示例:

/dts-v1/;
/plugin/;

&i2c1 {
    status = "okay";

    overlay_sensor@48 {
        compatible = "bosch,bmp280";
        reg = <0x76>;
    };
};

四、设备树 OF API 详解

Linux 内核通过一系列 of_ 前缀的函数(OF 函数)来读取设备树中的信息。这些函数定义在 include/linux/of.h 中。

4.1 核心数据结构

device_node 结构体

struct device_node {
    const char *name;           /* 节点名字 */
    const char *type;           /* 设备类型 */
    phandle phandle;
    const char *full_name;      /* 节点全名 */
    struct property *properties;/* 属性链表 */
    struct device_node *parent; /* 父节点 */
    struct device_node *child;  /* 子节点 */
    struct device_node *sibling;/* 兄弟节点 */
    void *data;
};

property 结构体

struct property {
    char *name;         /* 属性名字 */
    int length;         /* 属性长度 */
    void *value;        /* 属性值 */
    struct property *next; /* 下一个属性 */
};

4.2 查找节点的 OF 函数

函数 查找方式 原型
of_find_node_by_path 按路径 struct device_node *of_find_node_by_path(const char *path)
of_find_node_by_name 按名称 struct device_node *of_find_node_by_name(struct device_node *from, const char *name)
of_find_node_by_type 按 device_type struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
of_find_compatible_node 按 compatible struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
of_find_matching_node_and_match 按匹配表 struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)

各函数参数说明:

of_find_node_by_path — 最常用的节点查找方式

参数 类型 说明
path const char * 节点全路径,如 "/alphaled",也可使用 aliases 中的别名
返回值 struct device_node * 找到的节点指针,NULL 表示失败
struct device_node *nd = of_find_node_by_path("/alphaled");

of_find_compatible_node — 按 compatible 字符串查找

参数 类型 说明
from struct device_node * 起始搜索节点,NULL 从根节点开始
type const char * device_type 属性值,可为 NULL 表示忽略
compatible const char * 要匹配的 compatible 字符串
返回值 struct device_node * 找到的节点指针,NULL 表示失败
struct device_node *nd = of_find_compatible_node(NULL, NULL, "fsl,imx6ul-uart");

of_find_node_by_name — 按节点名称查找

参数 类型 说明
from struct device_node * 起始搜索节点,NULL 从根节点开始
name const char * 节点名称
返回值 struct device_node * 找到的节点指针,NULL 表示失败

4.3 查找父/子节点的 OF 函数

函数 用途 原型
of_get_parent 获取父节点 struct device_node *of_get_parent(const struct device_node *node)
of_get_next_child 迭代获取子节点 struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
struct device_node *child = NULL;
while ((child = of_get_next_child(parent, child)) != NULL) {
    printk("child: %s\n", child->name);
}

4.4 提取属性值的 OF 函数

查找属性

struct property *of_find_property(const struct device_node *np,
                                   const char *name,
                                   int *lenp);
参数 类型 说明
np const struct device_node * 设备节点
name const char * 属性名
lenp int * 输出属性值的字节数
返回值 struct property * 找到的属性指针
struct property *prop = of_find_property(nd, "compatible", NULL);
if (prop)
    printk("compatible = %s\n", (char *)prop->value);

读取整数属性

// 读取单个 u32 值
int of_property_read_u32(const struct device_node *np,
                          const char *propname,
                          u32 *out_value);

// 读取 u32 数组
int of_property_read_u32_array(const struct device_node *np,
                                const char *propname,
                                u32 *out_values,
                                size_t sz);
参数 类型 说明
np const struct device_node * 设备节点
propname const char * 属性名
out_values u32 * 输出数组
sz size_t 要读取的元素数量
返回值 int 0 成功,负值失败(-EINVAL 属性不存在,-ENODATA 无数据,-EOVERFLOW 数组太小)
u32 regdata[14];
int ret = of_property_read_u32_array(nd, "reg", regdata, 14);
if (ret < 0)
    printk("reg property read failed!\n");

类似的还有 of_property_read_u8of_property_read_u16of_property_read_u64 等单值读取函数。

读取字符串属性

int of_property_read_string(struct device_node *np,
                             const char *propname,
                             const char **out_string);
参数 类型 说明
np const struct device_node * 设备节点
propname const char * 属性名
out_string const char ** 输出字符串指针
返回值 int 0 成功,负值失败
const char *str;
int ret = of_property_read_string(nd, "status", &str);
if (ret == 0)
    printk("status = %s\n", str);

获取 #address-cells 和 #size-cells 的值

int of_n_addr_cells(struct device_node *np);  // 获取 #address-cells 值
int of_n_size_cells(struct device_node *np);  // 获取 #size-cells 值

4.5 内存映射的 OF 函数

函数 用途 原型
of_get_address 获取 reg 属性中的地址 const __be32 *of_get_address(struct device_node *dev, int index, u64 *size, unsigned int *flags)
of_translate_address 将设备树地址转为物理地址 u64 of_translate_address(struct device_node *dev, const __be32 *in_addr)
of_address_to_resource 将 reg 转为 resource 结构体 int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
of_iomap 直接完成地址映射 void __iomem *of_iomap(struct device_node *np, int index)

of_iomap — 设备树推荐的内存映射方式

参数 类型 说明
np struct device_node * 设备节点
index int reg 属性中第几段(从 0 开始)
返回值 void __iomem * 映射后的虚拟地址,NULL 表示失败
/* 不再需要手动获取物理地址再 ioremap,一步完成 */
void __iomem *base = of_iomap(nd, 0);  /* 映射 reg 属性第 0 段 */

of_iomap vs ioremap 对比

特性 of_iomap ioremap
参数 节点 + 索引 物理地址 + 大小
信息来源 从设备树读取 硬编码物理地址
推荐度 设备树驱动首选 通用但不推荐用于新驱动
适用场景 有设备树描述的设备 所有场景

4.6 OF 函数返回值汇总

返回值 含义
0 成功
-EINVAL 属性不存在
-ENODATA 属性存在但没有数据
-EOVERFLOW 属性值数组太小
NULL(指针) 查找失败
负值 其他错误

五、完整源码分析

5.1 设备树节点定义

imx6ull-alientek-emmc.dts 的根节点 / 下添加:

alphaled {
    #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 */
};

节点说明:

属性 含义
#address-cells <1> reg 中地址占 1 个 cell
#size-cells <1> reg 中长度占 1 个 cell
compatible "atkalpha-led" 驱动匹配用的兼容字符串
status "okay" 设备可用
reg 5 组 <地址 长度> 5 个寄存器的物理地址和大小

5.2 驱动程序完整代码

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define DTSLED_CNT      1
#define DTSLED_NAME     "dtsled"
#define LEDOFF          0
#define LEDON           1

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* 设备结构体 */
struct dtsled_dev {
    dev_t devid;                /* 设备号 */
    struct cdev cdev;           /* cdev */
    struct class *class;        /* 类 */
    struct device *device;      /* 设备 */
    int major;                  /* 主设备号 */
    int minor;                  /* 次设备号 */
    struct device_node *nd;     /* 设备节点 */
};

struct dtsled_dev dtsled;

/* LED 开关控制 */
void led_switch(u8 sta)
{
    u32 val = 0;
    if (sta == LEDON) {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);      /* bit3 置 0,点亮 LED */
        writel(val, GPIO1_DR);
    } else if (sta == LEDOFF) {
        val = readl(GPIO1_DR);
        val |= (1 << 3);       /* bit3 置 1,关闭 LED */
        writel(val, GPIO1_DR);
    }
}

static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &dtsled;
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];
    if (ledstat == LEDON)
        led_switch(LEDON);
    else if (ledstat == LEDOFF)
        led_switch(LEDOFF);

    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
    .release = led_release,
};

/* 驱动入口函数 */
static int __init led_init(void)
{
    u32 val = 0;
    int ret;
    u32 regdata[14];
    const char *str;
    struct property *proper;

    /* 1、获取设备树中的属性数据 */

    /* 1.1 获取 alphaled 节点 */
    dtsled.nd = of_find_node_by_path("/alphaled");
    if (dtsled.nd == NULL) {
        printk("alphaled node not found!\r\n");
        return -EINVAL;
    } else {
        printk("alphaled node found!\r\n");
    }

    /* 1.2 获取 compatible 属性 */
    proper = of_find_property(dtsled.nd, "compatible", NULL);
    if (proper == NULL) {
        printk("compatible property find failed\r\n");
    } else {
        printk("compatible = %s\r\n", (char *)proper->value);
    }

    /* 1.3 获取 status 属性 */
    ret = of_property_read_string(dtsled.nd, "status", &str);
    if (ret < 0) {
        printk("status read failed!\r\n");
    } else {
        printk("status = %s\r\n", str);
    }

    /* 1.4 获取 reg 属性 */
    ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);
    if (ret < 0) {
        printk("reg property read failed!\r\n");
    } else {
        u8 i = 0;
        printk("reg data:\r\n");
        for (i = 0; i < 10; i++)
            printk("%#X ", regdata[i]);
        printk("\r\n");
    }

    /* 2、初始化 LED */
    /* 方法一:传统 ioremap(不推荐) */
    #if 0
    IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);
    SW_MUX_GPIO1_IO03 = ioremap(regdata[2], regdata[3]);
    SW_PAD_GPIO1_IO03 = ioremap(regdata[4], regdata[5]);
    GPIO1_DR = ioremap(regdata[6], regdata[7]);
    GPIO1_GDIR = ioremap(regdata[8], regdata[9]);
    #else
    /* 方法二:of_iomap(推荐) */
    IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);
    SW_MUX_GPIO1_IO03 = of_iomap(dtsled.nd, 1);
    SW_PAD_GPIO1_IO03 = of_iomap(dtsled.nd, 2);
    GPIO1_DR = of_iomap(dtsled.nd, 3);
    GPIO1_GDIR = of_iomap(dtsled.nd, 4);
    #endif

    /* 3、使能 GPIO1 时钟 */
    val = readl(IMX6U_CCM_CCGR1);
    val &= ~(3 << 26);     /* 清除以前的设置 */
    val |= (3 << 26);      /* 设置新值 */
    writel(val, IMX6U_CCM_CCGR1);

    /* 4、设置 GPIO1_IO03 的复用功能,复用为 GPIO1_IO03 */
    writel(5, SW_MUX_GPIO1_IO03);

    /* 寄存器 SW_PAD_GPIO1_IO03 设置 IO 属性 */
    writel(0x10B0, SW_PAD_GPIO1_IO03);

    /* 5、设置 GPIO1_IO03 为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3);      /* 清除以前的设置 */
    val |= (1 << 3);       /* 设置为输出 */
    writel(val, GPIO1_GDIR);

    /* 6、默认关闭 LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);

    /* 7、注册字符设备驱动 */
    if (dtsled.major) {
        dtsled.devid = MKDEV(dtsled.major, 0);
        register_chrdev_region(dtsled.devid, DTSLED_CNT, DTSLED_NAME);
    } else {
        alloc_chrdev_region(&dtsled.devid, 0, DTSLED_CNT, DTSLED_NAME);
        dtsled.major = MAJOR(dtsled.devid);
        dtsled.minor = MINOR(dtsled.devid);
    }

    printk("dtsled major=%d,minor=%d\r\n", dtsled.major, dtsled.minor);

    dtsled.cdev.owner = THIS_MODULE;
    cdev_init(&dtsled.cdev, &dtsled_fops);
    cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_CNT);

    dtsled.class = class_create(THIS_MODULE, DTSLED_NAME);
    if (IS_ERR(dtsled.class))
        return PTR_ERR(dtsled.class);

    dtsled.device = device_create(dtsled.class, NULL, dtsled.devid,
                                  NULL, DTSLED_NAME);
    if (IS_ERR(dtsled.device))
        return PTR_ERR(dtsled.device);

    return 0;
}

/* 驱动出口函数 */
static void __exit led_exit(void)
{
    /* 取消映射 */
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO03);
    iounmap(SW_PAD_GPIO1_IO03);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    /* 注销字符设备驱动 */
    cdev_del(&dtsled.cdev);
    unregister_chrdev_region(dtsled.devid, DTSLED_CNT);
    device_destroy(dtsled.class, dtsled.devid);
    class_destroy(dtsled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

5.3 代码逐段解析

设备结构体中新增 device_node

struct dtsled_dev {
    /* ... */
    struct device_node *nd;     /* 设备节点 */
};

如果要读取设备树某个节点的属性值,首先要获取到这个节点。在设备结构体中添加 device_node 指针来存放这个节点。

通过 of_find_node_by_path 获取节点

dtsled.nd = of_find_node_by_path("/alphaled");

路径必须是根节点下的完整路径。如果找不到节点,函数返回 NULL。

通过 of_find_property 获取 compatible 属性

proper = of_find_property(dtsled.nd, "compatible", NULL);
printk("compatible = %s\r\n", (char *)proper->value);

of_find_property 返回 property 结构体指针,通过 ->value 成员获取属性值。

通过 of_property_read_string 获取 status 属性

ret = of_property_read_string(dtsled.nd, "status", &str);
printk("status = %s\r\n", str);

字符串属性用 of_property_read_string 读取,返回值直接就是字符串指针。

通过 of_property_read_u32_array 获取 reg 属性

ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10);

reg 属性包含 5 组 <address length> 数据,共 10 个 u32 值。读取后存入 regdata 数组。

两种内存映射方式对比

/* 方式一:传统方式(需要先读取 reg 值) */
IMX6U_CCM_CCGR1 = ioremap(regdata[0], regdata[1]);

/* 方式二:of_iomap(直接从设备树读取并映射) */
IMX6U_CCM_CCGR1 = of_iomap(dtsled.nd, 0);

of_iomap 内部会自动读取 reg 属性中第 index 段的地址和大小,然后调用 ioremap 完成映射。驱动中推荐使用 of_iomap,代码更简洁,也不需要手动读取 reg 数据。

5.4 驱动加载流程

flowchart TD
    accTitle: dtsled 驱动加载流程
    accDescr: 从驱动入口函数到 LED 初始化完成的完整流程

    init["led_init()"] --> find["of_find_node_by_path('/alphaled')"]
    find --> compat["of_find_property() 获取 compatible"]
    compat --> status["of_property_read_string() 获取 status"]
    status --> reg["of_property_read_u32_array() 获取 reg"]
    reg --> map["of_iomap() 完成内存映射"]
    map --> clk["使能 GPIO1 时钟"]
    clk --> mux["配置 GPIO 复用功能"]
    mux --> dir["设置 GPIO 为输出"]
    dir --> off["默认关闭 LED"]
    off --> chardev["注册字符设备驱动"]

    classDef step fill:#dbeafe,stroke:#2563eb,stroke-width:1px
    classDef final fill:#dcfce7,stroke:#16a34a,stroke-width:1px

    class init,find,compat,status,reg,map,clk,mux,dir,off step
    class chardev final

六、实验验证

6.1 设备树修改步骤

  1. 编辑 imx6ull-alientek-emmc.dts,在根节点 / 最后添加 alphaled 节点
  2. 编译设备树:

    make dtbs
    
  3. 将生成的 imx6ull-alientek-emmc.dtb 拷贝到 SD 卡的 boot 分区

6.2 验证设备树节点

启动后在 /proc/device-tree/ 目录下检查:

# 检查节点是否存在
ls /proc/device-tree/alphaled/

# 查看属性值
cat /proc/device-tree/alphaled/compatible
# 输出:atkalpha-led

cat /proc/device-tree/alphaled/status
# 输出:okay

如果找不到 alphaled 节点,检查两点:

  1. 设备树修改是否成功,节点是否为根节点 / 的子节点
  2. 是否使用了新的 DTB 文件启动 Linux 内核

6.3 编译和测试驱动

# 编译驱动模块
make -j32

# 编译测试程序
arm-linux-gnueabihf-gcc ledApp.c -o ledApp

# 拷贝到开发板
cp dtsled.ko ledApp /home/root/

# 加载驱动
depmod              # 首次加载需要
modprobe dtsled     # 加载驱动

# 测试
./ledApp /dev/dtsled 1     # 点亮 LED
./ledApp /dev/dtsled 0     # 关闭 LED

# 卸载驱动
rmmod dtsled

6.4 预期输出

驱动加载成功后终端输出:

alphaled node found!
compatible = atkalpha-led
status = okay
reg data:
0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4
dtsled major=249,minor=0
dtsled init!

七、跨平台对比

7.1 IMX6ULL vs STM32 vs RK3568 设备树差异

特性 IMX6ULL STM32MP1 RK3568
SOC 厂商 NXP ST Rockchip
设备树位置 arch/arm/boot/dts/imx6ull*.dts* arch/arm/boot/dts/stm32mp1*.dts* arch/arm64/boot/dts/rockchip/rk3568*.dts*
架构 ARM 32-bit ARM 32-bit ARM 64-bit
#address-cells <1> <1> <2>(64 位系统)
时钟框架 fsl,imx*-clk st,stm32mp1-clk rockchip,rk3568-cru
中断控制器 GIC GIC GIC
GPIO 控制器 fsl,imx-gpio st,stm32-gpio rockchip,gpio-bank

7.2 设备树编写注意事项

  1. 寄存器地址必须正确:从芯片数据手册中查找,写错会导致驱动无法工作
  2. compatible 字符串要匹配:驱动中的 of_match_table 必须与设备树一致
  3. status 要设为 "okay":否则内核认为设备不可用
  4. 不要直接修改 DTSI:通过 &label 追加方式在 DTS 文件中修改
  5. cells 数量要正确#address-cells / #size-cells 影响 reg 的解析

八、面试精选

题目 1:设备树解决了什么问题?

考察点:设备树概念理解

设备树解决了 Linux 内核中板级硬件信息硬编码的问题。在没有设备树的时代,每块开发板的硬件信息(寄存器地址、中断号、时钟配置等)都通过 MACHINE_START 宏硬编码在内核源码中,导致内核代码膨胀、维护困难。

设备树将硬件描述从内核源码中剥离,放在独立的 .dts 文件里。同一份内核通过不同的设备树文件支持不同的硬件平台,无需为每块板子编译一个专用内核。这种方式让内核代码更干净,板级支持更灵活。

题目 2:DTS、DTSI、DTB、DTBO 分别是什么?

考察点:设备树文件类型

  • DTS(Device Tree Source):设备树源文件,文本格式,开发者直接编写
  • DTSI(Device Tree Source Include):设备树头文件,可以被 DTS 包含,通常存放 SOC 级的通用信息
  • DTB(Device Tree Blob):编译后的二进制文件,由 bootloader 加载传递给内核
  • DTBO(Device Tree Blob Overlay):设备树 Overlay,用于运行时动态叠加一段设备树,常用于外设扩展

编译关系:DTS + DTSI → (DTC 编译器) → DTB → (bootloader) → 内核

题目 3:of_iomap 和 ioremap 有什么区别?什么时候用哪个?

考察点:内存映射 API

ioremap 是通用的物理地址到虚拟地址映射函数,需要手动提供物理地址和大小。of_iomap 是设备树专用的映射函数,内部自动从设备树节点的 reg 属性中读取物理地址和大小,然后调用 ioremap 完成映射。

推荐使用 of_iomap:代码更简洁,不需要手动读取 reg 数据,也更符合设备树驱动的开发规范。但如果设备没有设备树描述(如旧平台),仍然需要使用 ioremap

题目 4:设备树中 &label 追加语法有什么好处?

考察点:设备树组织结构

&label 追加语法允许在板级 DTS 文件中修改或扩展 SOC 级 DTSI 中定义的节点,而不需要直接修改 DTSI 文件。好处包括:

  1. SOC 级 DTSI 保持原样:多个板级 DTS 可以共享同一份 DTSI
  2. 板级差异集中管理:所有板级特有的配置都在各自的 DTS 中
  3. 避免合并冲突:不同板子修改同一个节点时不会产生冲突
  4. 维护方便:SOC 厂商更新 DTSI 时,板级 DTS 通常不需要改动

题目 5:Linux 内核如何通过设备树匹配设备?

考察点:设备树匹配机制

内核启动时,首先通过 setup_machine_fdt() 函数读取 bootloader 传来的 DTB 数据。然后调用 of_flat_dt_match_machine() 函数,将根节点 /compatible 属性值逐个与内核中已注册的 machine_desc 结构体的 .dt_compat 成员比较,找到第一个完全匹配的 machine_desc 后使用该描述。

对于驱动匹配,内核通过 of_driver_match_device() 函数,将设备节点的 compatible 值与驱动的 of_match_table 进行比较。匹配成功后,内核调用驱动的 probe() 函数完成设备初始化。

匹配流程:start_kernel()setup_arch()setup_machine_fdt()of_flat_dt_match_machine() → 找到匹配的 machine_desc


参考资料:正点原子 I.MX6U 嵌入式 Linux 驱动开发指南 第四十三章、第四十四章