--- 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` 文件里。同一份内核可以通过不同的设备树支持不同的板子,不需要为每块板子编译一个专用内核。 ```mermaid 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 内核启动时的流程: ```mermaid 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` 成员比较,找到匹配的设备描述: ```c // 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 ``` 匹配过程: ```mermaid 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` 命令即可查看属性值: ```bash # 查看根节点的 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 节点基本语法 ```dts node-name@unit-address { property-name = value; child-node { /* ... */ }; }; ``` - **node-name**:节点名,长度 1~31 个字符,可以包含字母、数字、逗号、句点和下划线 - **@unit-address**:可选,节点在地址空间中的起始地址,必须与父节点的 `#address-cells` 一致 - **标签(label)**:可选,可以给节点起一个别名,方便其他地方通过 `&label` 引用 ```dts /* 带标签的节点 */ 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 属性类型与语法 设备树属性值有多种类型: #### 字符串属性 ```dts compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart"; device_type = "cpu"; ``` #### 整数数组属性 ```dts reg = <0x02020000 0x4000>; /* 地址 + 大小 */ interrupts = ; clocks = <&clks IMX6UL_CLK_UART1_IPG>, <&clks IMX6UL_CLK_UART1_SERIAL>; ``` #### 单个整数 ```dts #address-cells = <1>; #size-cells = <1>; ``` #### 布尔属性(存在即为 true) ```dts ranges; /* 值为空表示布尔属性 */ big-endian; /* 也是布尔属性 */ ``` #### 字符串列表 ```dts clock-names = "ipg", "per"; ``` ### 2.3 核心属性详解 #### `compatible` — 兼容性属性 每个节点都有 `compatible` 属性,其值用于匹配 Linux 内核中的驱动程序。`compatible` 的值是字符串列表,第一个是设备自身的名称,后续是兼容的驱动名称。 ```dts /* 根节点 */ / { 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 位。 ```dts /* 父节点声明地址和长度各占一个 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>` | `` | 1 个 cell 地址 + 1 个 cell 长度 | | `#address-cells=<2>, #size-cells=<1>` | `` | 2 个 cell 地址 + 1 个 cell 长度 | | `#address-cells=<1>, #size-cells=<0>` | `` | 1 个 cell 地址,无长度 | ```dts /* 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>; /* 只有地址,没有长度 */ }; }; ``` ```dts /* 地址占 1 cell,长度也占 1 cell */ aips3: aips-bus@02200000 { compatible = "fsl,aips-bus", "simple-bus"; #address-cells = <1>; #size-cells = <1>; dcp: dcp@02280000 { compatible = "fsl,imx6sl-dcp"; reg = <0x02280000 0x4000>; /* address=0x02280000, length=0x4000 */ }; }; ``` #### `reg` — 寄存器地址范围 `reg` 属性的格式为 `
` 对,描述设备在父节点地址空间中的寄存器基地址和大小。 ```dts /* 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"` | 表明设备当前不可操作,但未来可以变为可操作(如热插拔设备插入后);具体含义还要看设备的绑定文档 | | `"fail"` | 表明设备不可操作,设备检测到了一系列错误,而且不大可能变得可操作 | | `"fail-sss"` | 含义和 `"fail"` 相同,后面的 sss 部分是检测到的错误内容 | > ⚠️ 上表取自原书表 43.3.3.1。`"reserved"`(设备被占用、由其他软件组件管理)属设备树规范中的扩展取值,原书未列出。 ```dts uart1: serial@02020000 { status = "disabled"; /* 默认关闭,板级 DTS 中按需开启 */ }; ``` #### `ranges` — 地址映射 `ranges` 属性值为空或包含三元组 `(child-bus-address, parent-bus-address, length)`,用于将子节点地址空间映射到父节点地址空间。 ```dts /* 值为空:子地址空间与父地址空间完全相同 */ soc { ranges; /* 子节点地址不需要转换 */ }; /* 值非空:子地址 0x0 映射到父地址 0xe0000000 */ soc { ranges = <0x0 0xe0000000 0x00100000>; /* 子节点 reg=0x4600 → 实际物理地址 = 0xe0004600 */ }; ``` #### `interrupts` 和 `interrupt-parent` ```dts uart1: serial@02020000 { interrupts = ; interrupt-parent = <&gpc>; /* 可选,默认继承父节点 */ }; ``` `` 的含义: - `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 值。 ```dts /* 定义节点并打标签 */ gpc: interrupt-controller@020dc000 { #interrupt-cells = <3>; /* ... */ }; /* 引用该节点 */ uart1: serial@02020000 { interrupt-parent = <&gpc>; /* 引用 gpc 节点 */ }; ``` #### aliases 子节点 `aliases` 节点为设备节点提供简短的别名,方便系统和用户空间程序引用: ```dts aliases { can0 = &flexcan1; ethernet0 = &fec1; gpio0 = &gpio1; i2c0 = &i2c1; spi0 = &ecspi1; }; ``` #### chosen 子节点 `chosen` 不是真正的硬件设备节点,而是 bootloader 向内核传递数据的通道。最常见的是 `bootargs` 属性: ```dts chosen { stdout-path = &uart1; }; ``` U-Boot 在启动 Linux 之前,通过 `fdt_chosen()` 函数将 `bootargs` 环境变量写入 `chosen` 节点的 `bootargs` 属性中。 ### 2.5 节点追加与修改 在板级 DTS 文件中,通过 `&label` 追加语法修改或扩展 SOC 级 DTSI 中的节点,不需要直接修改 DTSI 文件: ```dts /* 在 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 文件中追加完成。 --- ## 三、设备树编译与反编译 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。原书只介绍了 `make dtbs`/`make all` 与 DTC 工具的位置,未涉及 `dtc` 命令行、反编译及设备树 Overlay。 ### 3.1 dtc 命令 DTC(Device Tree Compiler)是设备树的编译器,将文本格式的 DTS 编译为二进制格式的 DTB。 ```bash # 编译 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` 文件,想看看里面的内容,可以用反编译命令: ```bash dtc -I dtb -O dts -o result.dts imx6ull-alientek-emmc.dtb ``` ### 3.3 设备树 Overlay(DTBO) 设备树 Overlay 允许在运行时动态叠加一段设备树到主设备树上,常用于同一块板子加载不同外设配置: ```bash # 编译 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 /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 结构体 ```c struct device_node { const char *name; /* 节点名字 */ const char *type; /* 设备类型 */ phandle phandle; const char *full_name; /* 节点全名 */ struct fwnode_handle fwnode; struct property *properties;/* 属性 */ struct property *deadprops; /* removed 属性 */ struct device_node *parent; /* 父节点 */ struct device_node *child; /* 子节点 */ struct device_node *sibling; struct kobject kobj; unsigned long _flags; void *data; #if defined(CONFIG_SPARC) const char *path_component_name; unsigned int unique_id; struct of_irq_controller *irq_trans; #endif }; ``` #### property 结构体 ```c struct property { char *name; /* 属性名字 */ int length; /* 属性长度 */ void *value; /* 属性值 */ struct property *next; /* 下一个属性 */ unsigned long _flags; unsigned int unique_id; struct bin_attribute attr; }; ``` ### 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 表示失败 | ```c 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 表示失败 | ```c 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)` | ```c struct device_node *child = NULL; while ((child = of_get_next_child(parent, child)) != NULL) { printk("child: %s\n", child->name); } ``` ### 4.4 提取属性值的 OF 函数 #### 查找属性 ```c 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 *` | 找到的属性指针 | ```c struct property *prop = of_find_property(nd, "compatible", NULL); if (prop) printk("compatible = %s\n", (char *)prop->value); ``` #### 统计属性元素数量 `of_property_count_elems_of_size` 用于获取属性中元素的数量。比如 reg 属性值是一个数组,用此函数可以拿到数组大小: ```c int of_property_count_elems_of_size(const struct device_node *np, const char *propname, int elem_size); ``` | 参数 | 类型 | 说明 | | ----------- | ---------------------------- | ------------------------ | | `np` | `const struct device_node *` | 设备节点 | | `propname` | `const char *` | 需要统计元素数量的属性名 | | `elem_size` | `int` | 元素长度 | | 返回值 | `int` | 得到的属性元素数量 | #### 按下标读取 u32 数值 `of_property_read_u32_index` 用于从属性中获取指定标号的 u32 类型(无符号 32 位)数据值: ```c int of_property_read_u32_index(const struct device_node *np, const char *propname, u32 index, u32 *out_value); ``` | 参数 | 类型 | 说明 | | ----------- | -------------- | ------------------------------------------------------------------------------------------ | | `np` | 设备节点 | 设备节点 | | `propname` | `const char *` | 要读取的属性名字 | | `index` | `u32` | 要读取的值标号 | | `out_value` | `u32 *` | 读取到的值 | | 返回值 | `int` | 0 读取成功,负值读取失败(-EINVAL 属性不存在,-ENODATA 无数据,-EOVERFLOW 属性值列表太小) | #### 读取整数属性 ```c // 读取单个 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 数组太小) | ```c 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_u32_array` 外,内核还提供读取 u8/u16/u64 数组的对应变体。多数 reg 属性都是数组数据,可用这四个函数一次读出全部数据: ```c int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz); int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz); int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz); int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz); ``` 返回值:0 读取成功;负值读取失败(-EINVAL 属性不存在,-ENODATA 无数据,-EOVERFLOW 属性值列表太小)。 #### 读取单个整数值属性 有些属性只有一个整数值,用下列函数读取 u8/u16/u32/u64: ```c int of_property_read_u8(const struct device_node *np, const char *propname, u8 *out_value); int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value); int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value); int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value); ``` #### 读取字符串属性 ```c 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 成功,负值失败 | ```c const char *str; int ret = of_property_read_string(nd, "status", &str); if (ret == 0) printk("status = %s\n", str); ``` #### 获取 #address-cells 和 #size-cells 的值 ```c 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 表示失败 | ```c /* 不再需要手动获取物理地址再 ioremap,一步完成 */ void __iomem *base = of_iomap(nd, 0); /* 映射 reg 属性第 0 段 */ ``` #### of_iomap vs ioremap 对比 | 特性 | of_iomap | ioremap | | -------- | ------------------ | ---------------------- | | 参数 | 节点 + 索引 | 物理地址 + 大小 | | 信息来源 | 从设备树读取 | 硬编码物理地址 | | 推荐度 | 设备树驱动首选 | 通用但不推荐用于新驱动 | | 适用场景 | 有设备树描述的设备 | 所有场景 | ### 4.6 其他常用 OF 函数 #### of_device_is_compatible 检查节点的 `compatible` 属性中是否包含 `compat` 指定的字符串: ```c int of_device_is_compatible(const struct device_node *device, const char *compat); ``` | 参数 | 类型 | 说明 | | -------- | ---------------------------- | -------------------------- | | `device` | `const struct device_node *` | 设备节点 | | `compat` | `const char *` | 要查看的字符串 | | 返回值 | `int` | 0 表示不包含;正数表示包含 | #### of_get_named_gpio — 获取 GPIO 编号 将设备树中形如 `<&gpio5 7 GPIO_ACTIVE_LOW>` 的属性信息转换为对应的 GPIO 编号,内核中所有 GPIO API 都使用 GPIO 编号: ```c int of_get_named_gpio(struct device_node *np, const char *propname, int index); ``` | 参数 | 类型 | 说明 | | ---------- | ---------------------- | ------------------------------------------------------------- | | `np` | `struct device_node *` | 设备节点 | | `propname` | `const char *` | 包含要获取 GPIO 信息的属性名(如 `"led-gpio"`) | | `index` | `int` | GPIO 索引;一个属性含多个 GPIO 时指定取哪一个,只有一个时为 0 | | 返回值 | `int` | 正值:获取到的 GPIO 编号;负值:失败 | ```c /* 第四十五章 gpioled 驱动片段 */ gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0); if (gpioled.led_gpio < 0) { printk("can't get led-gpio"); return -EINVAL; } ``` ```dts /* 对应的设备树属性 */ led-gpio = <&gpio1 3 GPIO_ACTIVE_LOW>; ``` #### GPIO_ACTIVE_LOW 的含义 - `GPIO_ACTIVE_LOW`:**低电平有效**。GPIO 输出低电平时对应"有效/激活"状态(例如点亮 LED)。 - `GPIO_ACTIVE_HIGH`:**高电平有效**。 - 两者定义在 `include/dt-bindings/gpio/gpio.h` 中,在 `&gpioX` 引用的 cell 里作为第三个值出现,如 `cd-gpios = <&gpio1 19 GPIO_ACTIVE_LOW>;`。 > ⚠️ **来源说明**:本小节(`of_get_named_gpio`、`GPIO_ACTIVE_LOW`)不属于第四十三章、第四十四章的内容,出自同一教材第四十五章《pinctrl和gpio子系统实验》,为扩展知识。 ### 4.7 OF 函数返回值汇总 | 返回值 | 含义 | | ------------ | ------------------ | | 0 | 成功 | | `-EINVAL` | 属性不存在 | | `-ENODATA` | 属性存在但没有数据 | | `-EOVERFLOW` | 属性值数组太小 | | NULL(指针) | 查找失败 | | 负值 | 其他错误 | --- ## 五、完整源码分析 ### 5.1 设备树节点定义 在 `imx6ull-alientek-emmc.dts` 的根节点 `/` 下添加: ```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 驱动程序完整代码 ```c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 can not found!\r\n"); return -EINVAL; } else { printk("alphaled node has been 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 ```c struct dtsled_dev { /* ... */ struct device_node *nd; /* 设备节点 */ }; ``` 如果要读取设备树某个节点的属性值,首先要获取到这个节点。在设备结构体中添加 `device_node` 指针来存放这个节点。 #### 通过 of_find_node_by_path 获取节点 ```c dtsled.nd = of_find_node_by_path("/alphaled"); ``` 路径必须是根节点下的完整路径。如果找不到节点,函数返回 NULL。 #### 通过 of_find_property 获取 compatible 属性 ```c 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 属性 ```c ret = of_property_read_string(dtsled.nd, "status", &str); printk("status = %s\r\n", str); ``` 字符串属性用 `of_property_read_string` 读取,字符串指针通过 `out_string` 输出参数返回,函数本身返回 `int`(0 成功,负值失败)。 #### 通过 of_property_read_u32_array 获取 reg 属性 ```c ret = of_property_read_u32_array(dtsled.nd, "reg", regdata, 10); ``` reg 属性包含 5 组 `
` 数据,共 10 个 u32 值。读取后存入 `regdata` 数组。 #### 两种内存映射方式对比 ```c /* 方式一:传统方式(需要先读取 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 驱动加载流程 ```mermaid 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. 编译设备树: ```bash make dtbs ``` 3. 使用新的 `imx6ull-alientek-emmc.dtb` 启动 Linux 内核(如拷贝到 SD 卡 boot 分区替换旧文件) ### 6.2 验证设备树节点 启动后在 `/proc/device-tree/` 目录下检查: ```bash # 检查节点是否存在 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 编译和测试驱动 ```bash # 编译驱动模块 make -j32 # 编译测试程序 arm-linux-gnueabihf-gcc ledApp.c -o ledApp # 拷贝到开发板 rootfs/lib/modules/4.1.15 目录 cp dtsled.ko ledApp /home/zuozhongkai/linux/nfs/rootfs/lib/modules/4.1.15/ # 加载驱动(驱动模块存放于 /lib/modules/4.1.15) depmod # 首次加载需要,生成 modules.dep modprobe dtsled.ko # 加载驱动 # 测试 ./ledApp /dev/dtsled 1 # 点亮 LED ./ledApp /dev/dtsled 0 # 关闭 LED # 卸载驱动 rmmod dtsled.ko ``` ### 6.4 预期输出 驱动加载成功后终端输出: ``` alphaled node has been found! compatible = atkalpha-led status = okay reg data: 0X20C406C 0X4 0X20E0068 0X4 0X20E02F4 0X4 0X209C000 0X4 0X209C004 0X4 dtsled major=249,minor=0 ``` --- ## 七、跨平台对比 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识。 ### 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 的解析 --- ## 八、面试精选 > ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Linux驱动开发指南》内容,为扩展知识(面试题整理)。 ### 题目 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驱动开发指南》第四十三章 Linux设备树、第四十四章 设备树下的LED驱动实验