Skip to content

LV020-基本语法

一、DTS 文件的格式

1. 根节点

设备树使用一种层次结构, 其中的根节点(Root Node) 是整个设备树的起始点和顶层节点。 根节点由一个以 /开头的标识符来表示, 然后使用{}来包含根节点所在的内容 ,DTS 文件布局(layout):

c
/dts-v1/; // 设备树版本信息
[memory reservations] // 格式为: /memreserve/ <address> <length>;
/ {
    [property definitions]
    [child nodes]
};

其中第一行的设备树中的版本信息行 dts-v1 是可选的, 可以根据需要选择是否保留。 这行注释通常用于指定设备树的语法版本。 如果不需要在设备树中指定版本信息, 可以将其删除。

2. 子节点

设备树中的子节点是根节点的直接子项, 用于描述具体的硬件设备或设备集合。 子节点采用以下特定的格式来表示 :

c
[label:] node-name@[unit-address] {
    [properties definitions]
    [child nodes]
};

(1) 节点标签(Label) (可选) : 节点标签是一个可选的标识符, 用于在设备树中引用该节点。 标签允许其他节点直接引用此节点, 以便在设备树中建立引用关系。

(2) 节点名称(Node Name) : 节点名称是一个字符串, 用于唯一标识该节点在设备树中的位置。 节点名称通常是硬件设备的名称, 但必须在设备树中是唯一的。

( 3) 单元地址(Unit Address) ( 可选) : 单元地址用于标识设备的实例。 它可以是一个整数、 一个十六进制值或一个字符串, 具体取决于设备的要求。 单元地址的目的是区分相同类型的设备的不同实例, 例如在下图中名为 cpu 的节点通过它们的单元地址值 0 和 1 来区分, 名称为 ethernet 的节点通过其单元地址值 fe002000 和 fe003000 来区分。

image-20250218090419908

( 4) 属性定义( Properties Definitions) : 属性定义是一组键值对, 用于描述设备的配置和特性。 属性可以根据设备的需求进行定义, 例如寄存器地址、 中断号、 时钟频率等, 关于这些属性会在后面的小节中学习。

( 5) 子节点( Child Nodes) : 子节点是当前节点的子项, 用于进一步描述硬件设备的子组件或配置。 子节点可以包含自己的属性定义和更深层次的子节点, 形成设备树的层次结构。

3. 一个简单示例

c
/dts-v1/;
/ {
    uart0: uart@fe001000 {
        compatible="ns16550";
        reg=<0xfe001000 0x100>;
    };
};

二、节点属性

节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用这些标准属性。

1. 属性格式

简单地说, properties 就是“name = value”, value 有多种取值方式。

1.1 Property 格式 1

c
[label:] property-name = value;

Property 取值可以有以下几种:

  • arrays of cells(1 个或多个 32 位数据, 64 位数据使用 2 个 32 位数据表示)。cell 就是一个 32 位的数据,用尖括号包围起来
c
interrupts = <17 0xc>;

64bit 数据使用 2 个 cell 来表示,用尖括号包围起来:

c
clock-frequency = <0x00000001 0x00000000>;
  • string(字符串)

A null-terminated string (有结束符的字符串),用双引号包围起来:

c
compatible = "simple-bus";
  • bytestring(1 个或多个字节)

A bytestring(字节序列) ,用中括号包围起来:

c
local-mac-address = [00 00 12 34 56 78]; // 每个 byte 使用 2 个 16 进制数来表示
local-mac-address = [000012345678]; // 每个 byte 使用 2 个 16 进制数来表示
  • 也可以是各种值的组合, 用逗号隔开:
c
compatible = "ns16550", "ns8250";
example = <0xf00f0000 19>, "a strange property format";

1.2 Property 格式 2(没有值)

c
[label:] property-name;

2. 常用属性

2.1 compatible

在设备树中, compatible 属性用于描述设备的兼容性信息。 它是设备树中重要的属性之一,用于识别设备节点与驱动程序之间的匹配关系。

compatible 属性的值是一个字符串或字符串列表, 用于指定设备节点与相应的驱动程序或设备描述符兼容的规则。 通常, compatible 属性的值由设备的厂商定义, 并且在设备树中使用。

一般格式如下:

c
"manufacturer,model"

其中 manufacturer 表示厂商, model 一般是模块对应的驱动名字。比如 imx6ull-alpha-emmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点, I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的 WM8960, sound 节点的 compatible 属性值如下:

c
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";

属性值有两个,分别为“fsl, imx6ul-evk-wm8960”和“fsl, imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。 sound 这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。

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

c
static const struct of_device_id imx_wm8960_dt_ids[] = {
  {
      .compatible = "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,
      .of_match_table = imx_wm8960_dt_ids,
  },
  .probe = imx_wm8960_probe,
  .remove = imx_wm8960_remove,
};

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

第 12 行, wm8960 采用了 platform_driver 驱动模式,关于 platform_driver 驱动后面会讲解。此行设置.of_match_table 为 imx_wm8960_dt_ids,也就是设置这个 platform_driver 所使用的 OF 匹配表。

总的来说,这个属性可能的取值有以下几种:

(1) 单个字符串值: 例如 "vendor, device", 用于指定设备节点与特定厂商的特定设备兼容。

(2) 字符串列表: 例如 ["vendor, device1", "vendor, device2"], 用于指定设备节点与多个设备兼容, 通常用于设备节点具有多种变体或配置。可以用于指定设备节点与多个设备或驱动程序的兼容性规则。

(3) 通配符匹配: 例如 " vendor,*", 用于指定设备节点与特定厂商的所有设备兼容, 不考虑具体的设备标识。

2.2 mode

在设备树中, model 属性用于描述设备的型号或者名称。 它通常作为设备节点的一个属性,用来提供关于设备的标识信息。 model 属性是可选的, 但在实际应用中经常被使用。

model 属性的值是一个字符串, 可以是设备的型号、 名称、 或者其他标识符, 用来识别设备。 该值通常由设备的厂商定义, 并且在设备树中使用。

model 属性与 compatible 属性有些类似,但是有差别。model 属性通常用于标识和区分不同的设备, 特别是当设备节点的 compatible 属性相同或相似时。 通过使用不同的 model 属性值, 可以更加准确地确定所使用的设备类型。 比如 compatible 属性是一个字符串列表,表示我们的硬件可以兼容 A、 B、 C 等驱动;model 用来准确地定义这个硬件是什么。

比如根节点中可以这样写:

c
my_device {
    compatible = "samsung,smdk2440", "samsung,mini2440";
    model = "jz2440_v3";
    // 其他属性和子节点的定义
};

它表示这个单板,可以兼容内核中的“ smdk2440”,也兼容“ mini2440”。从 compatible 属性中可以知道它兼容哪些板,但是它到底是什么板?用 model 属性来明确。

2.3 status

在设备树中, status 属性用于描述设备或节点的状态。 它是设备树中常见的属性之一, 用于表示设备或节点的可用性或操作状态。可选的状态如下表:

描述
“okay”表明设备是可操作的。
“disabled”表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档。
“fail”表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
“fail-sss”含义和“fail”相同,后面的 sss 部分是检测到的错误内容。

dtsi 文件中定义了很多设备,但是在我们的板子上某些设备是没有的。这时可以给这个设备节点添加一个 status 属性,设置为“disabled”:

c
&uart1 {
    status = "disabled"; 
}

通过使用 status 属性, 设备树可以动态地控制设备的启用和禁用状态。 这对于在系统启动过程中选择性地启用或禁用设备, 或者在运行时根据特定条件调整设备状态非常有用。

2.4 reg

reg 的本意是 register,表示寄存器地址,很容易就知道这个属性用于在设备树中指定设备的寄存器地址和大小, 提供了与设备树中的物理设备之间的寄存器映射关系。

在设备树里,它可以用来描述一段空间。反正对于 ARM 系统,寄存器和内存是统一编址的,即访问寄存器时用某块地址,访问内存时用某块地址,在访问方法上没有区别。

reg 属性可以在设备节点中有单个值格式和列表值格式这两种常见格式。

  • (1) 单个值格式如下:
c
reg = <address size>;

这种格式适用于描述单个寄存器的情况。 其中, address 是设备的起始寄存器地址, 可以是一个整数或十六进制值。 size 表示寄存器的大小, 即占用的字节数。

例如, 假设有一个设备节点 my_device, 使用单个值格式的 reg 属性来描述一个 4 字节寄存器的地址和大小, 可以这样定义:

c
my_device {
    compatible = "vendor,device";
    reg = <0x1000 0x4>;
    // 其他属性和子节点的定义
}

在这个示例中, my_device 设备节点的 reg 属性值为 <0x1000 0x4>, 表示从地址 0x1000 开始的 4 字节寄存器区域。

  • (2) 列表值格式如下:
c
reg = <address1 size1 address2 size2 ...>;

当设备具有多个寄存器区域时, 可以使用列表值格式的 reg 属性来描述每个寄存器区域的地址和大小。 通过这种方式, 可以指定多个寄存器的位置和大小, 以描述设备的完整寄存器映射。

例如, 考虑一个设备节点 my_device, 它具有两个寄存器区域, 分别是 8 字节和 4 字节大小的寄存器。 可以使用列表值格式的 reg 属性来描述这种情况:

c
my_device {
    compatible = "vendor,device";
    reg = <0x1000 0x8 0x2000 0x4>;
    // 其他属性和子节点的定义
}

在这个示例中, my_device 设备节点的 reg 属性值为 < 0x1000 0x8 0x2000 0x4 >, 表示设备有两个寄存器区域。 第一个寄存器区域从地址 0x1000 开始, 大小为 8 字节; 第二个寄存器区域从地址 0x2000 开始, 大小为 4 字节。

通过使用 reg 属性, 设备树可以提供有关设备寄存器布局和寄存器访问方式的信息。 这对于操作系统的设备驱动程序很重要, 因为它们需要了解设备的寄存器映射以正确地与设备进行交互和配置。

2.5 address-cells 和 size-cells

#address-cells 和 #size-cells 这两个属性可以用在任何拥有子节点的设备中, 用于指定 reg 属性中要设置的设备树中地址单元和大小单元的位数。 它们提供了设备树解析所需的元数据, 以正确解释设备的地址和大小信息。

Tips:总的来说,就是#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值 。

  • cell 指一个 32 位的数值
  • address-cells: address 要用多少个 32 位数来表示
  • size-cells: size 要用多少个 32 位数来表示

也就是说#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位), #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。

  • (1) #address-cells 属性

#address-cells 属性是一个位于设备树根节点的特殊属性, 它指定了设备树中地址单元的位数。 地址单元是设备树中用于表示设备地址的单个单位。 它通常是一个整数, 可以是十进制或十六进制值。

#address-cells 属性的值告诉解析设备树的软件在解释设备地址时应该使用多少位来表示一个地址单元。默认情况下, #address-cells 的值为 2, 表示使用两个单元来表示一个设备地址。 这意味着设备的地址将由两个整数(每个整数使用指定位数的位) 组成。

例如, 对于一个使用两个 32 位(4 字节) 整数表示地址的设备, 可以在设备树的根节点中设置 #address-cells 属性为 <2>。

c
/ {
    #address-cells = <2>;
    //......
};
  • (2) #size-cells 属性

#size-cells 属性也是一个位于设备树根节点的特殊属性, 它指定了设备树中大小单元的位数。 大小单元是设备树中用于表示设备大小的单个单位。 它通常是一个整数, 可以是十进制或十六进制值。#size-cells 属性的值告诉解析设备树的软件在解释设备大小时应该使用多少位来表示一个大小单元。

默认情况下, #size-cells 的值为 1, 表示使用一个单元来表示一个设备的大小。 这意味着设备的大小将由一个整数(使用指定位数的位) 表示。

例如, 对于一个使用一个 32 位(4 字节) 整数表示大小的设备, 可以在设备树的根节点中设置 #size-cells 属性为 < 1 >。

这两个属性的存在是为了使设备树能够灵活地描述各种设备的地址和大小表示方式。 通过在设备树的根节点中设置适当的 #address-cells 和 #size-cells 值, 设备树解析软件能够正确地解释设备节点中的地址和大小信息。

示例 1:

c
node1 {
    #address-cells = <1>;
    #size-cells = <1>;
    node1-child {
        reg = <0x02200000 0x4000>;
        // 其他属性和子节点的定义
    };
};

在这个示例中, node1-child 节点的 reg 属性使用了 < 0x02200000 0x4000 > 表示地址和大小。由于 #address-cells 的值为 < 1 >, 表示使用一个单元来表示地址。 #size-cells 的值也为 < 1 >, 表示使用一个单元来表示大小。解释后的地址和大小值如下:

txt
地址部分: 0x02200000 被解释为一个地址单元, 地址为 0x02200000。
大小部分: 0x4000 被解释为一个大小单元, 大小为 0x4000。

示例 2:

c
node1 {
    #address-cells = <2>;
    #size-cells = <0>;
    node1-child {
        reg = <0x0000 0x0001>;
        // 其他属性和子节点的定义
    };
};

在这个示例中, node1-child 节点的 reg 属性使用了 < 0x0000 0x0001 > 表示地址。 由于 #address-cells 的值为 < 2 >, 表示使用两个单元来表示地址。 #size-cells 的值为 < 0 >, 表示不使用单元来表示大小。解释后的地址值如下:

txt
地址部分:0x0000 0x0001 被解释为两个地址单元, 其中第一个地址单元为 0x0000, 第二个地址单元为 0x0001。
大小部分:#size-cells为0,表示没有大小部分

示例 3:

一段内存,怎么描述它的起始地址和大小?如下:

c
/ {
    #address-cells = <1>;
    #size-cells = <1>;
    memory {
        reg = <0x80000000 0x20000000>;
    };
};

# address-cells 为 1,所以 reg 中用 1 个数来表示地址,即用 0x80000000 来表示地址; #size-cells 为 1,所以 reg 中用 1 个数来表示大小,即用 0x20000000 表示大小

这种使用 #address-cells 和 #size-cells 属性的方式使得设备树可以适应不同设备的寄存器映射和大小表示方式, 并确保设备树解析软件能够正确解释设备的地址和大小信息。

2.6 ranges

ranges 属性值可以为空或者按照(child-bus-address, parent-bus-address, length)格式编写的数字矩阵, ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:

child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。

parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。

length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。

如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi 中找到大量的值为空的 ranges 属性:

c
soc {
	#address-cells = <1>;
	#size-cells = <1>;
	compatible = "simple-bus";
	interrupt-parent = <&gpc>;
	ranges;
	//......
}

ranges 属性不为空的示例代码如下:

c
soc {
	compatible = "simple-bus";
	#address-cells = <1>;
	#size-cells = <1>;
	ranges = <0x0 0xe0000000 0x00100000>;

	serial {
		device_type = "serial";
		compatible = "ns16550";
		reg = <0x4600 0x100>;
		clock-frequency = <0>;
		interrupts = <0xA 0x8>;
		interrupt-parent = <&ipic>;
	};
};

第 5 行:节点 soc 定义的 ranges 属性,值为 < 0x0 0xe0000000 0x00100000 >,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。

第 10 行, serial 是串口设备节点, reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。经过地址转换, serial 设备可以从 0xe0004600 开始进行读写操作, 0xe0004600 = 0x4600 + 0xe0000000。

2.7 name

name 属性值为字符串, name 属性用于记录节点名字, name 属性已经被弃用,不推荐使用 name 属性,一些老的设备树文件可能会使用此属性。

2.8 device_type

device_type 属性值为字符串, IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:

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

device_type 节点的存在有助于操作系统或其他软件识别和处理设备。 它提供了设备的基本分类信息, 使得驱动程序、 设备树解析器或其他系统组件能够根据设备的类型执行相应的操作。常见的设备类型包括但不限于:

(1) cpu: 表示中央处理器。

(2) memory: 表示内存设备。

(3) display: 表示显示设备, 如液晶显示屏。

(4) serial: 表示串行通信设备, 如串口。

(5) ethernet: 表示以太网设备。

(6) usb: 表示通用串行总线设备。

(7) i2c: 表示使用 I2C (Inter-Integrated Circuit) 总线通信的设备。

(8) spi: 表示使用 SPI (Serial Peripheral Interface) 总线通信的设备。

(9) gpio: 表示通用输入/输出设备。

(10) pwm: 表示脉宽调制设备。

这些只是一些常见的设备类型示例, 实际上, 设备类型可以根据具体的硬件和设备树的使用情况进行自定义和扩展。 根据设备类型, 操作系统或其他软件可以加载适当的驱动程序、 配置设备资源、 建立设备之间的连接等。

2.9 自定义属性

设备树中的自定义属性是用户根据特定需求添加的属性。 这些属性可以用于提供额外的信息、 配置参数或元数据, 以满足设备或系统的特定要求。

在设备树中添加自定义属性时, 可以在设备节点或其他适当的节点下定义新的属性。 自定义属性可以是整数、 字符串、 布尔值或其他数据类型。 它们的命名应遵循设备树的命名约定,并且应该与已有的属性名称避免冲突。

例如可以在设备树中自定义一个管脚标号的属性 pinnum, 添加好的设备树源码如下所示 :

c
my_device {
    compatible = "my_device";
    pinnum = <0 1 2 3 4>;
};

在上述示例中, my_device 是一个自定义设备节点, 并添加了一个自定义属性 pinnum。 该属性的值 < 0 1 2 3 4 > 是一个整数数组, 表示管脚的标号( PIN number) 。

通过这样定义 pinnum 属性, 可以在设备树中为特定设备指定管教标号, 以便操作系统、驱动程序或其他软件组件使用。 这可以用于在设备初始化或配置过程中对特定管教进行操作或控制。

3. 修改节点

3.1 修改方式

产品开发过程中可能面临着频繁的需求更改,设备树节点可能也需要进行修改,可以有以下两种方式:

c
// 在根节点之外使用 label 引用 node:
&uart0 {
	status = “disabled”;
};

//或在根节点之外使用全路径:
&{/uart@fe001000} {
	status = “disabled”;
}

3.2 修改实例

比如第一版硬件上有一个 IIC 接口的六轴芯片 MPU6050,第二版硬件又要把这个 MPU6050 更换为 MPU9250 等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片 fxls8471, fxls8471 要接到 I.MX6U-ALPHA 开发板的 I2C1 接口上,那么相当于需要在 i2c1 这个节点上添加一个 fxls8471 子节点。 先看一下 I2C1 接口对应的节点,打开文件 imx6ul.dtsi 文件,找到如下所示内容:

c
i2c1: i2c@21a0000 {
    #address-cells = <1>;
    #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";
};

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

c
i2c1: i2c@21a0000 {
    #address-cells = <1>;
    #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>;
    };
};

第 10 ~ 12 行就是添加的 fxls8471 这个芯片对应的子节点。但是这样会有个问题! i2c1 节点是定义在 imx6ul.dtsi 文件中的,而 imx6ul.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL 这颗 SOC 的板子都会引用 imx6ul.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备!因此,按照上面这样写肯定是不行的。

这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向 i2c1 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 I.MX6ULL 的板子。I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alpha-emmc.dts,因此我们需要在 imx6ull-alpha-emmc.dts 文件中完成数据追加的内容,方式如下:

c
&i2c1 {
	/* 要追加或修改的内容 */
};

&i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ul.dtsi 中的“i2c1: i2c@021a0000”。花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。

c
&i2c1 {
	clock-frequency = <100000>;
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_i2c1>;
	status = "okay";
	//fxls8471 子节点
	fxls8471@1e {
		compatible = "fsl,fxls8471";
		reg = <0x1e>;
	};
};

因为示例代码中的内容是 imx6ull-alpha-emmc.dts 这个文件内的,所以不会对使用 I.MX6ULL 这颗 SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。

三、特殊节点

1. 根节点

dts 文件中必须有一个根节点:

c
/dts-v1/;
/ {
    model = "SMDK24440";
    compatible = "samsung,smdk2440";
    #address-cells = <1>;
    #size-cells = <1>;
}

根节点中至少应该有这些属性:

c
compatible  // 定义一系列的字符串, 用来指定内核中哪个 machine_desc 可以支持本设备
			// 即这个板子兼容哪些平台
			// uImage : smdk2410 smdk2440 mini2440 ==> machine_desc
model       // 这个板子是什么
			// 比如有 2 款板子配置基本一致, 它们的 compatible 是一样的
			// 那么就通过 model 来分辨这 2 款板子

2. CPU 节点

一般不需要我们设置,在 dtsi 文件中都定义好了:

c
cpus {
    #address-cells = <1>;
    #size-cells = <0>;
    cpu0: cpu@0 {
    	// .......
    };
};

3. memory 节点

芯片厂家不可能事先确定我们的板子使用多大的内存,所以 memory 节点需要板厂设置,比如:

c
memory {
	reg = <0x80000000 0x20000000>;
};

4. chosen 节点

4.1 节点说明

chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。它位于设备树的根部, 并具有路径/chosen。

chosen 节点通常包含以下子节点和属性:

(1) bootargs: 用于存储引导内核时传递的命令行参数。 它可以包含诸如内核参数、 设备树参数等信息。 在引导过程中, 操作系统或引导加载程序可以读取该属性来获取启动参数。

(2) stdout-path: 用于指定用于标准输出的设备路径。 在引导过程中, 操作系统可以使用该属性来确定将控制台输出发送到哪个设备, 例如串口或显示屏。

( 3) firmware-name: 用于指定系统固件的名称。 它可以用于标识所使用的引导加载程序或固件的类型和版本。

( 4) linux, initrd-start 和 linux, initrd-end: 这些属性用于指定 Linux 内核初始化 RAM 磁盘( initrd) 的起始地址和结束地址。 这些信息在引导过程中被引导加载程序使用, 以将 initrd 加载到内存中供内核使用。

( 5) 其他自定义属性: chosen 节点还可以包含其他自定义属性, 用于存储特定于系统引导和配置的信息。 这些属性的具体含义和用法取决于设备树的使用和上下文。

通过使用 chosen 节点, 系统引导过程中的相关信息可以方便地传递给操作系统或引导加载程序。 这样, 系统引导和配置的各个组件可以共享和访问这些信息, 从而实现更灵活和可配置的系统引导流程。 chosen 节点提供了一种通用的机制, 使得不同的设备树和引导系统可以在传递信息方面保持一致性, 并且可以根据具体需求扩展和自定义。

4.2 节点示例

关于 chosen 节点的实际例子如下所示:

c
chosen {
        bootargs="quiet initcall_debug=y earlycon console=ttyS0,115200";
        stdout-path = &uart0;
    };

我们可以看一下 chosen 节点中 bootargs 的值:

shell
cat /proc/device-tree/chosen/bootargs
image-20250218185719092

但是有一些 chosen 节点是这样的,可以看一下 imx6ul-14x14-evk.dtsi,在一开始的时候,这个评估板默认的设备树中这个节点是这样的:

c
chosen {
    stdout-path = &uart1;
};

可以看出, chosen 节点仅仅设置了属性“stdout-path”,表示标准输出使用 uart1。但是当我们进入到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个属性,输入 cat 命令查看 bootargs 这个文件的内容,结果如图:

image-20250309151938826

4.3 参数的传递

上面 imx6ull 的例子中,bootargs 这个文件的内容为“console = ttymxc0,115200……”,仔细一看,这个不就是我们在 uboot 中设置的 bootargs 环境变量的值吗?

image-20250309152429056

根据设备树的 chosen 节点情况,现在有两个疑点 :

(1)我们并没有在设备树中设置 chosen 节点的 bootargs 属性,那么图 43.6.2.1 中 bootargs 这个属性是怎么产生的?

(2)为何 bootargs 文件的内容和 uboot 中 bootargs 环境变量的值一样?它们之间有什么关系?

前面学习 uboot 的时候应该有了解过, uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux 内核, bootargs 会作为 Linux 内核的命令行参数, Linux 内核启动的时候会打印出命令行参数(也就是 uboot 传递进来的 bootargs 的值),如图:

image-20250309152522796

既然 chosen 节点的 bootargs 属性不是我们在设备树里面设置的,那么只有一种可能,那就是 uboot 自己在 chosen 节点里面添加了 bootargs 属性!并且设置 bootargs 属性的值为 bootargs 环境变量的值。因为在启动 Linux 内核之前,只有 uboot 知道 bootargs 环境变量的值,并且 uboot 也知道.dtb 设备树文件在 DRAM 中的位置。在 uboot 源码中全局搜索“ chosen”这个字符串,果然不出所料,在 fdt_support.c - common/fdt_support.c 文件中发现了“chosen”的身影,文件中有个 fdt_chosen 函数,此函数内容如下所示:

c
int fdt_chosen(void *fdt)
{
	int   nodeoffset;
	int   err;
	char  *str;		/* used to set string properties */

	err = fdt_check_header(fdt);
	if (err < 0) {
		printf("fdt_chosen: %s\n", fdt_strerror(err));
		return err;
	}

	/* find or create "/chosen" node. */
	nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
	if (nodeoffset < 0)
		return nodeoffset;

	str = env_get("bootargs");
	if (str) {
		err = fdt_setprop(fdt, nodeoffset, "bootargs", str,
				  strlen(str) + 1);
		if (err < 0) {
			printf("WARNING: could not set bootargs %s.\n",
			       fdt_strerror(err));
			return err;
		}
	}

	return fdt_fixup_stdout(fdt, nodeoffset);
}

287 行,调用函数 fdt_find_or_add_subnode 从设备树(.dtb)中找到 chosen 节点,如果没有找到的话就会自己创建一个 chosen 节点。

291 行,读取 uboot 中 bootargs 环境变量的内容。

293 行,调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值就是环境变量 bootargs 的内容。

就是 uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs 属性,并且还设置了 bootargs 属性值。fdt_chosen 函数调用流程 如下:

image-20250218191119287

框起来的部分就是函数 do_bootm_linux() 函数的执行流程,也就是说 do_bootm_linux() 函数会通过一系列复杂的调用,最终通过 fdt_chosen() 函数在 chosen 节点中加入了 bootargs 属性。而我们通过 bootz 命令启动 Linux 内核的时候会运行 do_bootm_linux() 函数,而运行到这个函数,是因为启动的时候是这样的:

shell
bootz 80800000 83000000

当我们输入上述命令并执行以后, do_bootz() 函数就会执行,然后一切就按照上图中所示的流程开始运行。

5. aliases 节点

aliases 节点是一个特殊的节点, 用于定义设备别名。 该节点位于设备树的根部, 并具有节点路径 /aliases。

aliases 节点是一个容器节点, 包含一组属性, 每个属性都代表一个设备别名。 每个属性的名称是别名的标识符, 而属性的值是被引用设备节点的路径或设备树中其他节点的路径。

如何在设备树中使用 aliases 节点呢,可以看一下下面的例子:

c
aliases {
	mmc0 = &sdmmc0;
    mmc1 = &sdmmc1;
    mmc2 = &sdhci;
    serial0 = "/simple@fe000000/seria1@11c500";
};

(1) mmc0 别名与设备树中的 sdmmc0 节点相关联。 通过使用别名 mmc0, 其他设备节点或客户端程序可以更方便地引用 sdmmc0 节点, 而不必直接使用其完整路径。

(2) mmc1 别名与设备树中的 sdmmc1 节点相关联。 通过使用别名 mmc1, 其他设备节点或客户端程序可以更方便地引用 sdmmc1 节点, 而不必直接使用其完整路径。

(3) mmc2 别名与设备树中的 sdhci 节点相关联。 通过使用别名 mmc2, 其他设备节点或客户端程序可以更方便地引用 sdhci 节点, 而不必直接使用其完整路径。

(4) serial0 别名与设备树中的路径 /simple@fe000000/seria1@11c500 相关联。 通过使用别名 serial0, 其他设备节点或客户端程序可以更方便地引用该路径, 而不必记住整个路径字符串

在别名的定义中, & 符号用于引用设备树中的节点。 别名的目的是提供可读性更高的名称,使设备树更易于理解和维护。 通过使用别名, 可以简化设备节点之间的关联, 并减少重复输入设备节点的路径。不过我们一般会在节点命名的时候会加上 label,然后通过&label 来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。

客户端程序可以使用别名属性名称来引用完整的设备路径或部分路径。 当客户端程序将别名字符串视为设备路径时, 应检测并使用别名。 这样, 设备树的使用者可以更方便地引用设备节点, 而不必记住复杂的路径结构。

需要注意的是, aliases 节点中定义的别名只在 设备树内部可见, 不能在设备树之外引用。它们主要用于设备树的内部组织和引用, 以提高可读性和可维护性。