title: 设备树语法与实战 tags: [设备树, DTS, DTB, of_*API, 嵌入式Linux, GPIO, IMX6ULL] created: 2026-09-16 updated: 2026-09-17 pdf_ref:
关联知识:[[02-嵌入式Linux内核基础/06-设备模型与驱动框架]] | [[03-Linux驱动开发核心/07-platform驱动模型]]
设备树(Device Tree)是一种描述硬件信息的数据结构,它以树状形式组织,树中每个节点描述了硬件设备的某一信息。设备树在内核启动时由 bootloader(如 U-Boot)传递给内核,内核解析设备树后即可获取板级硬件信息。
设备树相关概念:
| 概念 | 文件后缀 | 说明 |
|---|---|---|
| DTS | .dts |
设备树源文件,文本格式,开发者编写 |
| DTSI | .dtsi |
设备树头文件,可被 DTS 包含 |
| DTB | .dtb |
编译后的二进制文件,bootloader 加载 |
| DTC | — | 设备树编译器,将 DTS 编译为 DTB |
| DTBO | .dtbo |
设备树 Overlay,运行时动态叠加 |
在没有设备树的时代,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
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
内核解析 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/
node-name@unit-address {
property-name = value;
child-node {
/* ... */
};
};
#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>;
};
设备树属性值有多种类型:
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>;
ranges; /* 值为空表示布尔属性 */
big-endian; /* 也是布尔属性 */
clock-names = "ipg", "per";
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_driver 的 of_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 */
};
interrupts 和 interrupt-parentuart1: 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 专用 | 由编译器自动生成 |
设备树中通过 &label 语法引用其他节点。&label 会被编译器自动转换为对应的 phandle 值。
/* 定义节点并打标签 */
gpc: interrupt-controller@020dc000 {
#interrupt-cells = <3>;
/* ... */
};
/* 引用该节点 */
uart1: serial@02020000 {
interrupt-parent = <&gpc>; /* 引用 gpc 节点 */
};
aliases 节点为设备节点提供简短的别名,方便系统和用户空间程序引用:
aliases {
can0 = &flexcan1;
ethernet0 = &fec1;
gpio0 = &gpio1;
i2c0 = &i2c1;
spi0 = &ecspi1;
};
chosen 不是真正的硬件设备节点,而是 bootloader 向内核传递数据的通道。最常见的是 bootargs 属性:
chosen {
stdout-path = &uart1;
};
U-Boot 在启动 Linux 之前,通过 fdt_chosen() 函数将 bootargs 环境变量写入 chosen 节点的 bootargs 属性中。
在板级 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 文件中追加完成。
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 |
禁用特定警告 |
拿到一个 .dtb 文件,想看看里面的内容,可以用反编译命令:
dtc -I dtb -O dts -o result.dts imx6ull-alientek-emmc.dtb
设备树 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>;
};
};
Linux 内核通过一系列 of_ 前缀的函数(OF 函数)来读取设备树中的信息。这些函数定义在 include/linux/of.h 中。
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;
};
struct property {
char *name; /* 属性名字 */
int length; /* 属性长度 */
void *value; /* 属性值 */
struct property *next; /* 下一个属性 */
};
| 函数 | 查找方式 | 原型 |
|---|---|---|
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 表示失败 |
| 函数 | 用途 | 原型 |
|---|---|---|
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);
}
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_u8、of_property_read_u16、of_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);
int of_n_addr_cells(struct device_node *np); // 获取 #address-cells 值
int of_n_size_cells(struct device_node *np); // 获取 #size-cells 值
| 函数 | 用途 | 原型 |
|---|---|---|
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 | ioremap |
|---|---|---|
| 参数 | 节点 + 索引 | 物理地址 + 大小 |
| 信息来源 | 从设备树读取 | 硬编码物理地址 |
| 推荐度 | 设备树驱动首选 | 通用但不推荐用于新驱动 |
| 适用场景 | 有设备树描述的设备 | 所有场景 |
| 返回值 | 含义 |
|---|---|
| 0 | 成功 |
-EINVAL |
属性不存在 |
-ENODATA |
属性存在但没有数据 |
-EOVERFLOW |
属性值数组太小 |
| NULL(指针) | 查找失败 |
| 负值 | 其他错误 |
在 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 个寄存器的物理地址和大小 |
#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");
struct dtsled_dev {
/* ... */
struct device_node *nd; /* 设备节点 */
};
如果要读取设备树某个节点的属性值,首先要获取到这个节点。在设备结构体中添加 device_node 指针来存放这个节点。
dtsled.nd = of_find_node_by_path("/alphaled");
路径必须是根节点下的完整路径。如果找不到节点,函数返回 NULL。
proper = of_find_property(dtsled.nd, "compatible", NULL);
printk("compatible = %s\r\n", (char *)proper->value);
of_find_property 返回 property 结构体指针,通过 ->value 成员获取属性值。
ret = of_property_read_string(dtsled.nd, "status", &str);
printk("status = %s\r\n", str);
字符串属性用 of_property_read_string 读取,返回值直接就是字符串指针。
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 数据。
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
imx6ull-alientek-emmc.dts,在根节点 / 最后添加 alphaled 节点编译设备树:
make dtbs
将生成的 imx6ull-alientek-emmc.dtb 拷贝到 SD 卡的 boot 分区
启动后在 /proc/device-tree/ 目录下检查:
# 检查节点是否存在
ls /proc/device-tree/alphaled/
# 查看属性值
cat /proc/device-tree/alphaled/compatible
# 输出:atkalpha-led
cat /proc/device-tree/alphaled/status
# 输出:okay
如果找不到 alphaled 节点,检查两点:
/ 的子节点# 编译驱动模块
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
驱动加载成功后终端输出:
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!
| 特性 | 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 |
of_match_table 必须与设备树一致&label 追加方式在 DTS 文件中修改#address-cells / #size-cells 影响 reg 的解析考察点:设备树概念理解
设备树解决了 Linux 内核中板级硬件信息硬编码的问题。在没有设备树的时代,每块开发板的硬件信息(寄存器地址、中断号、时钟配置等)都通过 MACHINE_START 宏硬编码在内核源码中,导致内核代码膨胀、维护困难。
设备树将硬件描述从内核源码中剥离,放在独立的 .dts 文件里。同一份内核通过不同的设备树文件支持不同的硬件平台,无需为每块板子编译一个专用内核。这种方式让内核代码更干净,板级支持更灵活。
考察点:设备树文件类型
编译关系:DTS + DTSI → (DTC 编译器) → DTB → (bootloader) → 内核
考察点:内存映射 API
ioremap 是通用的物理地址到虚拟地址映射函数,需要手动提供物理地址和大小。of_iomap 是设备树专用的映射函数,内部自动从设备树节点的 reg 属性中读取物理地址和大小,然后调用 ioremap 完成映射。
推荐使用 of_iomap:代码更简洁,不需要手动读取 reg 数据,也更符合设备树驱动的开发规范。但如果设备没有设备树描述(如旧平台),仍然需要使用 ioremap。
&label 追加语法有什么好处?考察点:设备树组织结构
&label 追加语法允许在板级 DTS 文件中修改或扩展 SOC 级 DTSI 中定义的节点,而不需要直接修改 DTSI 文件。好处包括:
考察点:设备树匹配机制
内核启动时,首先通过 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 驱动开发指南 第四十三章、第四十四章