目录
#address-cells 和#size-cells 属性
前言
在新版本的 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 |
文本文件 |
人类编写硬件描述 |
|
文本编辑器 |
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 { ... }; // 子节点的子节点
};
};
节点可嵌套,形成树状结构。
属性数据类型
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。
设备树源码中常用的几种数据形式如下所示:
类型 |
示例 |
说明 |
---|---|---|
字符串 |
|
用于驱动匹配的标识符。 |
32位整数 |
|
单个数值或寄存器地址。 |
数组 |
|
多值用空格分隔(如地址+长度)。 |
字符串列表 |
|
多个兼容性标识,逗号分隔。 |
标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,有很多标准属性,用户可以自定义属性。
常用的标准属性如下:
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属性的解析方式。
属性 |
数据类型 |
作用 |
---|---|---|
|
|
指定子节点 |
|
|
指定子节点 |
- 字长(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>;
每个条目表示一段地址空间的映射关系:
字段 |
描述 |
长度由谁决定 |
---|---|---|
|
子总线地址空间的物理地址 |
父节点的 |
|
父总线地址空间的物理地址 |
父节点的 |
|
子地址空间的长度(范围) |
父节点的 |
举例:
/* 空 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 来访问节点,然后直接在里面编写要追加或者修改的内容。