LV06-10-设备树-02-设备树语法

怎么写一个设备树文件,怎么修改?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、基本语法

1. DTS 文件的格式

1.1 根节点

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

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

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

1.2 子节点

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

1
2
3
4
[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) : 子节点是当前节点的子项, 用于进一步描述硬件设备的子组件或配置。 子节点可以包含自己的属性定义和更深层次的子节点, 形成设备树的层次结构。

1.3 一个简单示例

1
2
3
4
5
6
7
/dts-v1/;
/ {
uart0: uart@fe001000 {
compatible="ns16550";
reg=<0xfe001000 0x100>;
};
};

2. 节点属性

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

2.1 属性格式

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

2.1.1 Property 格式 1

1
[label:] property-name = value;

Property 取值可以有以下几种:

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

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

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

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

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

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

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

2.1.2 Property 格式 2(没有值)

1
[label:] property-name;

2.2 常用属性

2.2.1 compatible

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

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

一般格式如下:

1
"manufacturer,model"

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

1
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 中有如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.2 mode

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

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

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

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

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

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

2.2.3 status

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

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

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

1
2
3
&uart1 {
status = "disabled";
}

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

2.2.4 reg

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

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

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

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

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

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

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

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

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

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

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

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

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

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

2.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>。

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

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

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

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

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

示例 1:

1
2
3
4
5
6
7
8
node1 {
#address-cells = <1>;
#size-cells = <1>;
node1-child {
reg = <0x02200000 0x4000>;
// 其他属性和子节点的定义
};
};

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

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

示例 2:

1
2
3
4
5
6
7
8
node1 {
#address-cells = <2>;
#size-cells = <0>;
node1-child {
reg = <0x0000 0x0001>;
// 其他属性和子节点的定义
};
};

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

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

示例3:

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

1
2
3
4
5
6
7
/ {
#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.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 属性:

1
2
3
4
5
6
7
8
soc {
#address-cells = <1>;
#size-cells = <1>;
compatible = "simple-bus";
interrupt-parent = <&gpc>;
ranges;
//......
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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.2.7 name

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

2.2.8 device_type

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

1
2
3
4
5
6
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.2.9 自定义属性

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

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

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

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

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

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

2.3 修改节点

2.3.1 修改方式

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

1
2
3
4
5
6
7
8
9
// 在根节点之外使用 label 引用 node:
&uart0 {
status = “disabled”;
};

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

2.3.2 修改实例

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

1
2
3
4
5
6
7
8
9
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 的子节点,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 文件中完成数据追加的内容,方式如下:

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

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

1
2
3
4
5
6
7
8
9
10
11
&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 来访问节点,然后直接在里面编写要追加或者修改的内容。

3. 特殊节点

3.1 根节点

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

1
2
3
4
5
6
7
/dts-v1/;
/ {
model = "SMDK24440";
compatible = "samsung,smdk2440";
#address-cells = <1>;
#size-cells = <1>;
}

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

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

3.2 CPU 节点

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

1
2
3
4
5
6
7
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
// .......
};
};

3.3 memory 节点

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

1
2
3
memory {
reg = <0x80000000 0x20000000>;
};

3.4 chosen 节点

3.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 节点提供了一种通用的机制, 使得不同的设备树和引导系统可以在传递信息方面保持一致性, 并且可以根据具体需求扩展和自定义。

3.4.2 节点示例

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

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

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

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

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

1
2
3
chosen {
stdout-path = &uart1;
};

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

3.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 函数,此函数内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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() 函数,而运行到这个函数,是因为启动的时候是这样的:

1
bootz 80800000 – 83000000

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

3.5 aliases 节点

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

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

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

1
2
3
4
5
6
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 节点中定义的别名只在设备树内部可见, 不能在设备树之外引用。它们主要用于设备树的内部组织和引用, 以提高可读性和可维护性。

二、实例分析

这里我们对着设备树中不同的节点做具体的分析,来了解一下常用的节点的一些属性的写法。这里我是看的rk3568的教程学习的,这里就以rk3568为例,虽然芯片不同,但是设备树的语法都是一样的。

1. 中断

1.1 中断相关属性

下面展示的是 iTOP-RK3568 开发板 SDK 源码中的 ft5x06 设备树, 其中就是关于中断相关的描述, 包括了 interrupts、 interrupt-controller、 #interrupt-cells、 interrupt-parent四种常见属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPI00>, <&pmucru DBCLK_GPI00>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
ft5x06: ft5x06@38 {
status = "disabled";
compatible = "edt,edt-ft5306";
reg = <0x38>;
touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <800>;
touchscreen-size-y = <1280>;
touch_type = <1>;
};

1.1.1 interrupts

interrupts 属性用于指定设备的中断相关信息。 它描述了中断控制器的类型、 中断号以及中断触发类型。前面列举的设备树源码中的gpio0节点和ft5x06节点都涉及到了interrupts 属性,如下所示:

1
2
3
4
5
6
7
8
9
10
gpio0: gpio@fdd60000 {
//....
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
//....
};
ft5x06: ft5x06@38 {
//....
interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
//....
};

gpio0 节点的 interrupts 具有三个参数, 分别表示中断控制器类型、 中断号和中断触发类型,每个参数的具体描述如下所示:

  • (1) 中断控制器类型

interrupts 属性的第一个参数指定了中断控制器的类型。 常见的类型包括 GIC (Generic Interrupt Controller)、 IRQ (Basic Interrupt Handling) 等。 例如, 在给定的代码片段中, GIC_SPI 表示中断控制器的类型为 GIC SPI 中断。

中断控制器负责管理系统中的中断信号, 它可以是硬件中的专用中断控制器, 也可以是处理器内部的中断控制器。

  • (2)中断号

interrupts 属性的第二个参数指定了设备所使用的中断号。 中断号是一个唯一标识符, 用于区分不同的中断信号源。 系统使用中断号来识别中断源并进行相应的中断处理。

中断号可以是一个整数值, 也可以是一个宏定义或符号引用。 在给定的代码片段中, 33 表示该设备使用的中断号为 33。

  • (3) 中断触发类型

interrupts 属性的第三个参数指定了中断的触发类型, 即中断信号的触发条件。 常见的触发类型包括边沿触发和电平触发。

边沿触发表示中断信号在从低电平到高电平或从高电平到低电平的变化时触发。 触发类型可以是上升沿触发、 下降沿触发或双边沿触发。 电平触发表示中断信号在保持特定电平状态时触发, 可以是高电平触发或低电平触发。

在给定的代码片段中, IRQ_TYPE_LEVEL_HIGH 表示中断的触发类型为高电平触发。 触发类型的宏定义在内核源码“irq.h - include/dt-bindings/interrupt-controller/irq.h” 目录下, 具体内容如下所示:

1
2
3
4
5
6
#define IRQ_TYPE_NONE 0 		// 无中断触发类型
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)// 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发

而在 ft5x06 节点中只有中断号和中断触发类型两个参数, 这是为什么呢?我们继续往下学习。

1.1.2 interrupt-controller

interrupt-controller 属性是设备树中用于描述中断控制器的属性之一。 它提供了关于中断控制器的相关信息, 以便操作系统和其他设备能够正确配置和使用中断系统。

interrupt-controller 属性用于标识当前节点所描述的设备是一个中断控制器。 中断控制器是硬件或软件模块, 负责管理和分发中断信号。 它接收来自各种设备的中断请求, 并根据优先级和配置规则分发中断给相应的处理器或设备。

interrupt-controller 属性本身没有特定的属性值, 只需出现在节点的属性列表中即可。 出现该属性的存在即表示该节点描述的设备是中断控制器。

1.1.3 interrupt-parent

interrupt-parent 属性是设备树中用于建立中断信号源与中断控制器之间关联的属性。 它指定了中断信号源所属的中断控制器节点, 以确保正确的中断处理和分发。

interrupt-parent 属性用于指定中断信号源所属的中断控制器。 中断信号源是产生中断的设备或其他中断源节点。 通过指定中断控制器, 操作系统可以正确地将中断请求传递给相应的中断控制器节点进行处理和分发。

interrupt-parent 属性值是一个引用, 它指向中断控制器节点的路径或标签。 可以使用路径来引用中断控制器节点, 如/interrupt-controller-node, 或使用标签来引用中断控制器节点, 如&interrupt-controller-label, 在前面例子中的 ft5x06 就是通过中断控制器节点和 gpio0 中断控制器建立了联系, 如下所示:

1
2
3
4
ft5x06: ft5x06@38 {
//....
interrupt-parent = <&gpio0>;
};

中断信号源节点(例如设备节点或其他中断源节点) 中的 interrupt-parent 属性用于指定中断信号源所属的中断控制器节点。 这样, 中断信号源就可以将中断请求传递给正确的中断控制器进行处理。 中断信号源节点的 interrupts 属性中的中断号和其他相关信息将与指定的中断控制器关联起来。

在某些情况下, 中断控制器可以形成多级结构, 其中一个中断控制器节点可能是另一个中断控制器的父节点。 在这种情况下, interrupt-parent 属性可以用于指定层次结构中的上级中断控制器。

1.1.4 #interrupt-cells

#interrupt-cells 属性用于描述中断控制器中每个中断信号源的中断编号单元的数量。 中断编号单元是指用于表示中断号和其他相关信息的固定大小的单元。 通过指定中断编号单元的数量, 操作系统可以正确解析和处理中断信息, 并将其与中断控制器和中断信号源进行关联。

#interrupt-cells 属性的值是一个整数, 表示中断编号单元的数量。 通常, 这个值是一个正整数, 例如 1、 2 或 3, 取决于中断控制器和设备的要求。

在 gpio0 的中断控制器为 gic, 在 gic 节点中#interrupt-cells 属性被设置为 3, 这也就是为什么在 gpio0 节点中 interrupts 属性有三个值, 而 ft5x06 的中断控制器为 gpio0, 在 gpio0 节点中#interrupt-cells 属性被设置为 2, 所以 ft5x06 节点的 interrupts 属性只有两个值。

1.2 中断实例

1.2.1 硬件原理图

这里我们还是来看我们的imx6ull,alpha有一个按键:

image-20250220091647571

按键 KEY0 是连接到 I.MX6U 的 UART1_CTS 这个 IO 上的, KEY0接了一个 10K 的上拉电阻,因此 KEY0 没有按下的时候 UART1_CTS 应该是高电平,当 KEY0按下以后 UART1_CTS 就是低电平。搜一下参考手册就会发现,这个引脚是GPIO1_IO18

image-20250220092740884

1.2.2 中断号怎么确定?

可以看这个笔记《LV04-07-中断与异常-05-IMX6ULL按键中断实例 | 苏木》这里再简单了解一下。我们前边知道了按键接在了GPIO1_IO18上边,我们可以查看《I.MX6UL参考手册》的3.2 Cortex A7 interrupts一节,找到这个GPIO管脚对应的中断号:

image-20250220093228164

可以看到GPIO1的0 -15管脚使用的是66,16 - 31使用的是67,这里只是IRQ的编号,对应到 GIC 的 SPI中断号需要在此编号基础上加上 32,所以这里的按键中断号实际为99(67+32)。但是其实在linux中开发的时候,会有函数(例如gpio_to_irq())自动帮我们计算,我们只需要知道是哪个引脚就可以了。

1.2.3 触发方式选择

触发方式就可以看这个irq.h - include/dt-bindings/interrupt-controller/irq.h

1
2
3
4
5
6
#define IRQ_TYPE_NONE 0 		// 无中断触发类型
#define IRQ_TYPE_EDGE_RISING 1 // 上升沿触发
#define IRQ_TYPE_EDGE_FALLING 2 // 下降沿触发
#define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)// 双边沿触发
#define IRQ_TYPE_LEVEL_HIGH 4 // 高电平触发
#define IRQ_TYPE_LEVEL_LOW 8 // 低电平触发

1.2.4 按键中断节点

按键 KEY0 使用中断模式,需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
sdev_key {
#address-cells = <1>;
#size-cells = <1>;
compatible = "sdev_key";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_key>;
key-gpio = <&gpio1 18 GPIO_ACTIVE_LOW>; /* KEY0 */
interrupt-parent = <&gpio1>;
interrupts = <18 IRQ_TYPE_EDGE_BOTH>; /* FALLING RISING */
status = "okay";
};

第 8 行,设置 interrupt-parent 属性值为“gpio1”,因为 KEY0 所使用的 GPIO 为GPIO1_IO18,也就是设置 KEY0 的 GPIO 中断控制器为 gpio1。

第 9 行,设置 interrupts 属性,也就是设置中断源,第一个 cells 的 18 表示 GPIO1 组的 18号 IO。

这个gpio1是在 imx6ul.dtsi

1
2
3
4
5
6
7
8
9
10
11
12
13
gpio1: gpio@209c000 {
compatible = "fsl,imx6ul-gpio", "fsl,imx35-gpio";
reg = <0x0209c000 0x4000>;
interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>,
<GIC_SPI 67 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_GPIO1>;
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
gpio-ranges = <&iomuxc 0 23 10>, <&iomuxc 10 17 6>,
<&iomuxc 16 33 16>;
};

2. 时钟

时钟(Clock) 用于描述硬件设备和系统中的时钟源以及时钟相关的配置和连接关系。 时钟在计算机系统中起着至关重要的作用, 用于同步和定时各种硬件设备的操作。 时钟可以分为两个主要角色: 时钟生产者(clock provider) 和时钟消费者(clock consumer) 。

2.1 时钟生产者(clock provider)

时钟生产者是负责生成和提供时钟信号的硬件或软件模块。 它可以是时钟控制器、PLL、 时钟发生器等。时钟节点属性如下:

  • (1) clock-cells

属性用于指定时钟编号的位数。 它是一个整数值, 表示时钟编号的位数。 通常情况下, 当 clock-cells 为 0 时表示一个时钟, 为 1 表示多个时钟。 具体示例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
//示例 1: 单个时钟
osc24m: osc24m {
compatible = "clock";
clock-frequency = <24000000>;
clock-output-names = "osc24m";
#clock-cells = <O>;
};

//示例 2: 多个时钟
clock: clock {
#clock-cells = <1>;
clock-output-names = "clock1", "clock2";
};
  • (2) clock-frequency

clock-frequency 属性是设备树中用于指定时钟频率的属性。 它用于描述时钟节点所提供的时钟信号的频率, 使用 Hertz (Hz) 作为单位。 对于时钟生产者节点, clock-frequency 属性表示该节点生成的时钟信号的频率。 它用于描述时钟控制器、 晶振、 PLL 等产生时钟信号的硬件或软件模块的输出频率, 例如指定时钟频率为 24000000 的具体示例如下所示:

1
2
3
4
5
6
osc24m: osc24m {
compatible = "clock";
clock-frequency = <24000000>;
clock-output-names = "osc24m";
#clock-cells = <O>;
};
  • (3) assigned-clocks 和 assigned-clock-rates

assigned-clocks 和 assigned-clock-rates 是设备树中用于描述多路时钟的属性, 通常一起使用。

assigned-clocks 属性用于标识时钟消费者节点所使用的时钟源。 它是一个整数数组, 每个元素对应一个时钟编号。 时钟编号是指时钟生产者节点(如时钟控制器) 所提供的时钟源的编号。 通过在时钟消费者节点中使用 assigned-clocks 属性, 可以指定该节点所需的时钟源。

assigned-clock-rates 属性用于指定每个时钟源的时钟频率。 它是一个整数数组, 每个元素对应一个时钟源的频率。 时钟频率以 Hz (赫兹) 为单位表示。 assigned-clock-rates 属性的元素数量和顺序应与 assigned-clocks 属性中的时钟编号相对应。

关于 assigned-clocks 和 assigned-clock-rates 属性的一个具体示例如下所示:

1
2
3
4
5
cru: clock-controller@fdd20000 {
#clock-cells = <1>;
assigned-clocks = <&pmucru CLK_RTC_32K>, <&cru ACLK_RKVDEC_PRE>;
assigned-clock-rates = <32768>, <300000000>;
};
  • (4) clock-indices

clock-indices 属性用于指定时钟消费者节点所使用的时钟源的索引值。它是一个整数数组, 每个元素对应一个时钟源的索引。

时钟索引是指时钟生产者节点(如时钟控制器) 所提供的时钟源的编号。 通过在时钟消费者节点中使用 clock-indices 属性, 可以明确指定该节点所需的时钟源, 并按照特定的顺序进行匹配。 一个 clock-indices 示例如下所示:

1
2
3
4
5
6
7
8
9
10
scpi_dvfs: clocks-0 {
#clock-cells = <1>;
clock-indices = <0>, <1>, <2>;
clock-output-names = "atlclk", "aplclk", "gpuclk";
};
scpi_clk: clocks-1 {
#clock-cells = <1>;
clock-indices = <3>;
clock-output-names = "pxlclk";
};

在第一个节点中”atlclk”, “aplclk”, “gpuclk”三个时钟源的索引就分别被设置为了 0、 1、 2, 在第二个节点中”pxlclk”时钟源的索引值被设置为了 3.

  • (5) assigned-clock-parents

assigned-clock-parents 属性用于指定时钟消费者节点所使用的时钟源的父时钟源。它是一个时钟源引用的数组, 每个元素对应一个父时钟源的引用。 在时钟的层次结构中, 某些时钟源可能是其他时钟源的父时钟源, 即它们提供时钟信号给其他时钟源作为输入。 通过在时钟消费者节点中使用 assigned-clock-parents 属性, 可以明确指定该节点所需的父时钟源, 并按照特定的顺序进行匹配。 一个实际的 assigned-clock-parents 属性例子如下所示:

1
2
3
4
5
clock: clock {
assigned-clocks = <&clkcon 0>, <&pll 2>;
assigned-clock-parents = <&pll 2>;
assigned-clock-rates = <115200>, <9600>;
};

上述设备树表示了一个名为 clock 的时钟消费者节点, 具有以下属性:

assigned-clocks 属性指定了该节点使用的时钟源, 引用了两个时钟源节点: clkcon 0 和 pll 2。assigned-clock-parents 属性指定了这些时钟源的父时钟源, 引用了 pll 2 时钟源节点。assigned-clock-rates 属性指定了每个时钟源的时钟频率, 分别是 115200 和 9600。

2.2 时钟消费者(clock consumer)

定义: 时钟消费者是依赖时钟信号的硬件设备或模块。 它们通过引用时钟生产者节点提供的时钟源来获取时钟信号。 时钟消费者属性如下:

  • (1) clocks

该属性用于指定时钟消费者节点所需的时钟源。 它是一个整数数组, 每个元素是一个时钟编号, 表示时钟消费者需要的一个时钟源。

  • (2) clock-names

可选属性, 用于指定时钟消费者节点所需时钟源的名称。 它是一个字符串数组, 与 clocks 数组一一对应, 用于提供时钟源的描述性名称。

一个时钟消费者示例如下所示:

1
2
3
4
clock: clock {
clocks = <&cru CLK_VOP>;
clock-names = "clk_vop";
};

clocks 属性指定了该节点使用的时钟源, 引用了 cru 节点中的 CLK_VOP 时钟源。clock-names 属性指定了时钟源的名称, 这里是 “clk_vop”。

3. CPU

3.1 cpus 节点

设备树的 cpus 节点是用于描述系统中的处理器的一个重要节点。 它是处理器拓扑结构的顶层节点, 包含了所有处理器相关的信息。

  • 节点结构

cpus 节点是一个容器节点, 其下包含了系统中每个处理器的子节点。 每个子节点的名称通常为 cpu@X, 其中 X 是处理器的索引号。 每个子节点都包含了与处理器相关的属性, 例如时钟频率、 缓存大小等。

  • 处理器属性

cpu@X 子节点中的属性可以包括以下信息:

(1) device_type: 指示设备类型为处理器(”cpu”) 。

(2) reg: 指定处理器的地址范围, 通常是物理地址或寄存器地址。

(3) compatible: 指定处理器的兼容性信息, 用于匹配相应的设备驱动程序。

(4) clock-frequency: 指定处理器的时钟频率。

(5) cache-size: 指定处理器的缓存大小。

  • 处理器拓扑关系

除了处理器的基本属性, cpus 节点还可以包含其他用于描述处理器拓扑关系的节点, 以提供更详细的处理器拓扑信息。 这些节点可以帮助操作系统和软件了解处理器之间的连接关系、组织结构和特性。

(1)cpu-map 节点: 描述处理器的映射关系, 通常在多核处理器系统中使用。

(2)socket 节点: 描述多处理器系统中的物理插槽或芯片组。

(3)cluster 节点: 描述处理器集群, 即将多个处理器组织在一起形成的逻辑组。

(4)core 节点: 描述处理器核心, 即一个物理处理器内的独立执行单元。

(5)thread 节点: 描述处理器线程, 即一个物理处理器核心内的线程。

这些节点的嵌套关系可以在 cpus 节点下形成一个层次结构, 反映了处理器的拓扑结构。上述这些节点会在后面的小节进行介绍。 一个单核 CPU 设备树和一个四核 CPU 设备树示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 单核CPU
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
// 其他属性...
};
};

// 多核CPU
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};
cpu1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};
cpu2: cpu@2 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};
cpu3: cpu@3 {
device_type = "cpu";
compatible = "arm,cortex-a9";
};
};

c pus 节点是一个容器节点, 包含了 cpu0 子节点。 该节点使用了 #address-cells 和 #size-cell s 属性来指定地址和大小的单元数量。cpu0 子节点代表第一个处理器,compatible 属性指定了处理器的兼容性信息,device_type 属性指示设备类型为处理器。还可以在此基础上继续添加其他属性来描述处理器的特性, 如时钟频率、 缓存大小等。

3.2 cpu-map、 socket、 cluster 节点

cpu-map 节点是设备树中用于描述大小核架构处理器的映射关系的节点之一。 它的父节点必须是 cpus 节点, 而子节点可以是一个或多个 cluster 和 socket 节点。 通过 cpu-map 节点, 可以定义不同核心和集群之间的连接和组织结构。

socket 节点用于描述处理器插槽(socket) 之间的映射关系。 每个 socket 子节点表示一个处理器插槽, 可以使用 cpu-map-mask 属性来指定该插槽使用的核心。 通过为每个 socket 子节点指定适当的 cpu-map-mask, 可以定义不同插槽中使用的核心。 这样, 操作系统和软件可以了解到不同插槽之间的核心分配情况。

cluster 节点用于描述核心(cluster) 之间的映射关系。 每个 cluster 子节点表示一个核心集群, 可以使用 cpu-map-mask 属性来指定该集群使用的核心。 通过为每个 cluster 子节点指定适当的 cpu-map-mask, 可以定义每个集群中使用的核心。 这样, 操作系统和软件可以了解到不同集群之间的核心分配情况。

通过在 cpu-map 节点中定义 socket 和 cluster 子节点, 并为它们指定适当的 cpu-map-mask,可以提供处理器的拓扑结构信息。 这对于操作系统和软件来说非常有用, 因为它们可以根据这些信息进行任务调度和资源分配的优化, 以充分利用大小核架构处理器的性能和能效特性。

一个大小核架构的具体示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
cpus {
#address-cells = <2>;
#size-cells = <0>;
cpu-map {
cluster0 {
core0 {
cpu = <&cpu_l0>;
};
core1 {
cpu = <&cpu_l1>;
};
core2 {
cpu = <&cpu_l2>;
};
core3 {
cpu = <&cpu_l3>;
};
};
cluster1 {
core0 {
cpu = <&cpu_b0>;
};
core1 {
cpu = <&cpu_b1>;
};
};
};
cpu_l0: cpu@0 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};
cpu_l1: cpu@1 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};
cpu_l2: cpu@2 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};
cpu_l3: cpu@3 {
device_type = "cpu";
compatible = "arm,cortex-a53", "arm,armv8";
};
cpu_b0: cpu@100 {
device_type = "cpu";
compatible = "arm,cortex-a72", "arm,armv8";
};
cpu_b1: cpu@101 {
device_type = "cpu";
compatible = "arm,cortex-a72", "arm,armv8";
};
}

这个设备树描述了一个具有多个 CPU 核心的系统, 包括四个 Cortex-A53 核心和两个Cortex-A72 核心。

(1)#address-cells = <2>; 和 #size-cells = <0>;: 这些属性指定了设备树中地址和大小的编码方式。

(2)cpu-map : 这个节点定义了CPU的映射关系。它包含了两个簇( clusters ) : cluster0 和 cluster1。 cluster0 包含了四个核心: core0、 core1、 core2 和 core3, 分别对应 cpu_l0、cpu_l1、 cpu_l2 和 cpu_l3。 cluster1 包含了两个核心: core0 和 core1, 分别对应 cpu_b0 和 cpu_b1。

(3)cpu_l0、 cpu_l1、 cpu_l2 和 cpu_l3: 这些节点描述了 Cortex-A53 核心。 它们具有相同的设备类型 cpu 和兼容性属性 “arm,cortex-a53”, “arm,armv8”。

(4)cpu_b0 和 cpu_b1: 这些节点描述了 Cortex-A72 核心。 它们具有相同的设备类型 cpu 和兼容性属性 “arm,cortex-a72”, “arm,armv8”。

3.3 core、 thread 节点

“core” 和 “thread” 节点通常用于描述处理器核心和线程的配置。

  • Core 节点用于描述处理器的核心。 一个处理器通常由多个核心组成, 每个核心可以独立执行指令和任务。

  • Thread 节点用于描述处理器的线程。 线程是在处理器核心上执行的基本执行单元, 每个核心可以支持多个线程。

通过使用 Core 和 Thread 节点, 设备树可以准确描述处理器的核心和线程的配置, 例如可以使用设备树来描述一个具有 16 个核心的 CPU, 一个物理插槽, 每个集群中有两个核心,每个核心有两个线程的设备树示例, 具体设备树如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
cpus {
#address-cells = <2>;
cpu-map {
socket0 {
cluster0 {
core0 {
thread0 {
cpu = <&CPU0>;
};
thread1 {
cpu = <&CPU1>;
};
};
core1 {
thread0 {
cpu = <&CPU2>;
};
thread1 {
cpu = <&CPU3>;
};
};
};
cluster1 {
core0 {
thread0 {
cpu = <&CPU4>;
};
thread1 {
cpu = <&CPU5>;
};
};
core1 {
thread0 {
cpu = <&CPU6>;
};
thread1 {
cpu = <&CPU7>;
};
};
};
};
socket1 {
cluster0 {
core0 {
thread0 {
cpu = <&CPU8>;
};
thread1 {
cpu = <&CPU9>;
};
};
core1 {
thread0 {
cpu = <&CPU10>;
};
thread1 {
cpu = <&CPU11>;
};
};
};
cluster1 {
core0 {
thread0 {
cpu = <&CPU12>;
};
thread1 {
cpu = <&CPU13>;
};
};
core1 {
thread0 {
cpu = <&CPU14>;
};
thread1 {
cpu = <&CPU15>;
};
};
};
};
};
};

4. GPIO

4.1 gpio相关属性

下面的是 iTOP-RK3568 开发板 SDK 源码中的 ft5x06 设备树中关于 gpio 相关的描述, 包括了 interrupts、 interrupt-controller、 #interrupt-cells、 interrupt-parent四种常见属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gpio0: gpio@fdd60000 {
compatible = "rockchip,gpio-bank";
reg = <0x0 0xfdd60000 0x0 0x100>;
interrupts = <GIC_SPI 33 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&pmucru PCLK_GPI00>, <&pmucru DBCLK_GPI00>;
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
interrupt-controller;
#interrupt-cells = <2>;
};
ft5x06: ft5x06@38 {
status = "disabled";
compatible = "edt,edt-ft5306";
reg = <0x38>;
touch-gpio = <&gpio0 RK_PB5 IRQ_TYPE_EDGE_RISING>;
interrupt-parent = <&gpio0>;
interrupts = <RK_PB5 IRQ_TYPE_LEVEL_LOW>;
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
touchscreen-size-x = <800>;
touchscreen-size-y = <1280>;
touch_type = <1>;
};

4.1.1 gpio-controller

gpio-controller 属性用于标识一个设备节点作为 GPIO 控制器。 GPIO 控制器是负责管理和控制 GPIO 引脚的硬件模块或驱动程序。通常作为设备节点的一个属性出现, 位于设备节点的属性列表中。

当一个设备节点被标识为 GPIO 控制器时, 它通常会定义一组 GPIO 引脚, 并提供相关的GPIO 控制和配置功能。 其他设备节点可以使用该 GPIO 控制器来控制和管理其 GPIO 引脚。

通过使用 gpio-controller 属性, 设备树可以明确标识出 GPIO 控制器设备节点, 使系统可以正确识别和管理 GPIO 引脚的配置和控制。

4.1.2 #gpio-cells

#gpio-cells 属性用于指定 GPIO 引脚描述符的编码方式。 GPIO 引脚描述符是用于标识和配置 GPIO 引脚的一组值, 例如引脚编号、 引脚属性等。这个属性的属性值是一个整数, 表示用于编码 GPIO 引脚描述符的单元数。 通常,这个值为 2。

在示例中有 1 个 gpio 引脚描述属性,由于#gpio-cells 属性被设置为了 2, 所以每个引脚描述属性中会有两个整数, 具体内容如下所示:

1
2
3
4
5
ft5x06: ft5x06@38 {
//.....
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
//.....
};

通过使用#gpio-cells 属性, 设备树可以指定 GPIO 引脚描述符的编码方式, 使系统能够正确识别和解析 GPIO 引脚的配置和控制。

4.1.3 gpio-ranges

gpio-ranges 属性是设备树中一个用于描述 GPIO 范围映射的属性。 它通常用于描述具有大量 GPIO 引脚的 GPIO 控制器, 以简化 GPIO 引脚的编码和访问。

在设备树中, GPIO 控制器的每个引脚都有一个本地编号, 用于在控制器内部进行引脚寻址。 然而, 这些本地编号并不一定与外部引脚的物理编号或其他系统中使用的编号一致。 为了解决这个问题, 可以使用 gpio-ranges 属性将本地编号映射到实际的引脚编号。

gpio-ranges属性是一个包含一系列整数值的列表, 每个整数值对应于设备树中的一个GPIO控制器。 列表中的每个整数值按照特定的顺序提供以下信息:

(1) 外部引脚编号的起始值。

(2) GPIO 控制器内部本地编号的起始值。

(3) 引脚范围的大小(引脚数量) 。

1
2
3
4
5
6
7
gpio0: gpio@fdd60000 {
//......
gpio-controller;
#gpio-cells = <2>;
gpio-ranges = <&pinctrl 0 0 32>;
//......
};

gpio-ranges 属性的值为<&pinctrl 0 0 32>, 其中<&pinctrl>表示引用了名为 pinctrl 的引脚控制器节点, 0 0 32 表示外部引脚从 0 开始, 控制器本地编号从 0 开始, 共映射了 32 个引脚。

这样, gpio-ranges 属性将 GPIO 控制器的本地编号直接映射到外部引脚编号, 使得 GPIO 引脚的编码和访问更加简洁和直观。

4.1.4 gpio 引脚描述属性

示例中设备树中关于 gpio 引脚描述属性相关内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
gpio0: gpio@fdd60000 {
//......
#gpio-cells = <2>;
//......
};

ft5x06: ft5x06@38 {
//.....
reset-gpios = <&gpio0 RK_PB6 GPIO_ACTIVE_LOW>;
//.....
};

gpio 引脚描述属性个数由#gpio-cells 所决定, 因为 gpio0 节点中的#gpio-cells 属性设置为了2, 所以上面设备树 gpio 引脚描述属性个数也为 2。 RK_PB6是引脚名,GPIO_ACTIVE_LOW 表示设置为低电平。

4.1.5 其他属性

根据下面的设备树示例学习一下 gpio 的其他重要属性, 设备树具体内容如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gpio-controller@00000000 {
compatible = "foo";
reg = <0x00000000 0x1000>;
gpio-controller;
#gpio-cells = <2>;
ngpios = <18>;
gpio-reserved-ranges = <0 4>, <12 2>;
gpio-line-names = "MMC-CD", "MMC-WP",
"voD eth", "RST eth", "LED R",
"LED G", "LED B", "col A",
"col B", "col C", "col D",
"NMI button", "Row A", "Row B",
"Row C", "Row D", "poweroff",
"reset";
};

第 6 行的 ngpios 属性指定了 GPIO 控制器所支持的 GPIO 引脚数量。 它表示该设备上可用的 GPIO 引脚的总数。 在这个例子中, ngpios 的值为 18, 意味着该 GPIO 控制器支持 18个 GPIO 引脚。

第 7 行的 gpio-reserved-ranges 属性定义了保留的 GPIO 范围。每个范围由两个整数值表示,用尖括号括起来。 保留的 GPIO 范围意味着这些 GPIO 引脚不可用或已被其他设备或功能保留。在这个例子中, 有两个保留范围: <0 4>和<12 2>。 <0 4>表示从第 0 个引脚开始的连续 4 个引脚被保留, 而<12 2>表示从第 12 个引脚开始的连续 2 个引脚被保留。

第 8 行的 gpio-line-names 属性定义了 GPIO 引脚的名称, 以逗号分隔。 每个名称对应一个GPIO 引脚。 这些名称用于标识和识别每个 GPIO 引脚的作用或连接的设备。 在这个例子中, gpio-line-names 属性列出了多个 GPIO 引脚的名称, 如 “MMC-CD”、 “MMC-WP”、 “voD eth” 等等。 通过这些名称, 可以清楚地了解每个 GPIO 引脚的功能或用途。

4.2 GPIO实例

4.2.1 硬件原理图

image-20250220163438114

查看原理图会发现LED0 接到了 GPIO_3 上, 这里的GPIO_3 就是 GPIO1_IO03,当 GPIO1_IO03输出低电平(0)的时候发光二极管 LED0 就会导通点亮,当 GPIO1_IO03 输出高电平(1)的时候发光二极管 LED0 不会导通,因此 LED0 也就不会点亮。所以 LED0 的亮灭取决于 GPIO1_IO03的输出电平,输出 0 就亮,输出 1 就灭。

4.2.2 led灯节点

1
2
3
4
5
6
7
8
9
10
11
sdev_led {
#address-cells = <1>;
#size-cells = <1>;
compatible = "sdev-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 */
};

第 2、 3 行,属性#address-cells 和#size-cells 都为 1,表示 reg 属性中起始地址占用一个字长(cell),地址长度也占用一个字长(cell)。

第 4 行,属性 compatbile 设置 alphaled 节点兼容性为“sdev-led”。

第 5 行,属性 status 设置状态为“okay”。

第 6 ~ 10 行, reg 属性设置了驱动里面所要使用的寄存器物理地址,比如第 6 行的“0X020C406C 0X04”表示 I.MX6ULL 的 CCM_CCGR1 寄存器,其中寄存器首地址为 0X020C406C,长度为 4 个字节。

5. pinctrl

5.1 pinmux 简介  

5.1.1 什么是pinmux

Pinmux( 引脚复用) 是指在系统中配置和管理引脚功能的过程。 在许多现代集成电路中,单个引脚可以具有多个功能, 例如作为 GPIO、 UART、 SPI 或 I2C 等。 通过使用引脚复用功能,可以在这些不同的功能之间切换。

引脚复用通过硬件和软件的方式实现。 硬件层面, 芯片设计会为每个引脚提供多个功能的选择。 这些功能通常由芯片厂商在芯片规格文档中定义。 通过编程设置寄存器或开关, 可以选择某个功能来连接引脚。 这种硬件层面的配置通常是由引脚控制器(Pin Controller) 或引脚复用控制器( Pin Mux Controller) 负责管理。

软件层面, 操作系统或设备驱动程序需要了解和配置引脚的功能。 它们使用设备树( Device Tree) 或设备树绑定( Device Tree Bindings) 来描述和配置引脚的功能。 在设备树中, 可以指定引脚的复用功能, 将其连接到特定的硬件接口或功能。 操作系统或设备驱动程序在启动过程中解析设备树, 并根据配置对引脚进行初始化和设置。

5.1.2 imx6ull的引脚复用

那我们要怎样知晓每一个管脚都可以复用成什么功能呢,对于我使用的alpha-imx6ull开发板,我们可以直接查看《i.MX 6ULL Applications Processor Reference Manual.pdf》手册的32.6 IOMUXC Memory Map/Register Definition 。例如前面按键是接在GPIO1_IO18上,这个引脚默认是UART1_CTS_B,这个引脚复用情况如下:

image-20250220092740884

5.1.3 RK3568的引脚复用

有一些厂家给的原理图甚至会在核心板原理图标注出每个管脚的复用功能,例如讯为的RK3568开发板:

image-20250221093009766

从上图可以看到 UART4_RX_M1 对应的引脚可以复用为以下 6 个功能 LCDC_D16、VOP_BT1120_D7、 GMAC1_RXD0_M0、 UART4_RX_M1、 PWM8_M0、 GPIO3_B1_d, 那么对于RK3568来说,对应的 BGA引脚标号为 AG1,那这里的 AG1 是如何定位的呢。

在 BGA(Ball Grid Array, 球栅阵列) 封装中, 引脚标号是用于唯一标识每个引脚的标识符。这些标号通常由芯片制造商定义, 并在芯片的规格文档或数据手册中提供。

BGA 芯片的引脚标号通常由字母和数字的组合构成。 它们用于在芯片的封装底部的焊盘上进行标记。 每个引脚标号都与芯片内部的功能或信号相对应, 以便正确连接到印刷电路板( PCB) 上的目标位置。 RK3568 的引脚标号图如下:

image-20250221093125213

可以看到纵向为 A-AH 的 28 个字母类型标号, 横向为 1-28 的 28 个字母类型标号, 瑞芯微也在对应的 3568 数据手册中加入了根据 BGA 位置制作的复用功能图, 部分内容如下图

image-20250221093144491

其中黑色框代表被保留的引脚, 其他有颜色的框一般为电源和地, 白色的框代表有具体复用功能的引脚。

5.1.4 总结

引脚复用提高了芯片的灵活性和可重用性, 通过允许同一个引脚在不同的功能之间切换, 可以减少硬件设计的复杂性和成本。 此外, 引脚复用还使得在使用相同芯片的不同应用中可以更加灵活地配置和定制引脚功能。

5.2 RK3568中使用 pinctrl 设置复用关系

pinctrl(引脚控制) 用于描述和配置硬件设备上的引脚功能和连接方式。 它是设备树的一部分, 用于在启动过程中传递引脚配置信息给操作系统和设备驱动程序, 以便正确地初始化和控制引脚。

在设备树中, pinctrl(引脚控制) 使用了客户端和服务端的概念来描述引脚控制的关系和配置。

5.2.1 客户端(client)

接下来将使用三个例子对客户端要用到的属性进行说明。

  • 示例1
1
2
3
4
node {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1>;
}

pinctrl-names 属性定义了一个状态名称: default。pinctrl-0 属性指定了第一个状态 default 对应的引脚配置。<&pinctrl_hog_1> 是一个引脚描述符, 它引用了一个名为 pinctrl_hog_1 的引脚控制器节点。 这表示在 default 状态下, 设备的引脚配置将使用 pinctrl_hog_1 节点中定义的配置。

  • 示例2
1
2
3
4
5
node {
pinctrl-names = "default", "wake up";
pinctrl-0 = <&pinctrl_hog_1>;
pinctrl-1 = <&pinctrl_hog_2>;
}

pinctrl-names 属性定义了两个状态名称: default 和 wake up。pinctrl-0 属性指定了第一个状态 default 对应的引脚配置, 引用了 pinctrl_hog_1 节点。pinctrl-1 属性指定了第二个状态 wake up 对应的引脚配置, 引用了 pinctrl_hog_2 节点。

这意味着设备可以处于两个不同的状态之一, 每个状态分别使用不同的引脚配置。

  • 示例3
1
2
3
4
node {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_hog_1 &pinctrl_hog_2>;
}

这个例子中, pinctrl-names 属性仍然定义了一个状态名称: default。pinctrl-0 属性指定了第一个状态 default 对应的引脚配置, 但与之前的例子不同的是, 它引用了两个引脚描述符: pinctrl_hog_1 和 pinctrl_hog_2。

这表示在 default 状态下, 设备的引脚配置将使用 pinctrl_hog_1 和 pinctrl_hog_2 两个节点中定义的配置。 这种方式可以将多个引脚控制器的配置组合在一起, 以满足特定状态下的引脚需求。

关于客户端的内容, 不同厂家的编写格式是相同的,而服务端每个厂家就有区别了。

5.2.2 服务端(server)

服务端是设备树中定义引脚配置的部分。 它包含引脚组和引脚描述符, 为客户端提供引脚配置选择。 服务端在设备树中定义了 pinctrl 节点, 其中包含引脚组和引脚描述符的定义。

这里以瑞芯微的 RK3568 为例进行 pinctrl 服务端的学习, 瑞芯微原厂 BSP 工程师为了方便用户通过 pinctrl 设置管脚的复用关系, 将包含所有复用关系的配置写在了内核(5.19.17版本,主要是因为有些版本还没加入RK系列的芯片)目录下的 rk3568-pinctrl.dtsi - arch/arm64/boot/dts/rockchip/rk3568-pinctrl.dtsi - Linux source code v5.19.17 中。我们来看一下这个 pinctrl 节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
&pinctrl {
acodec {
/omit-if-no-ref/
acodec_pins: acodec-pins {
rockchip,pins =
/* acodec_adc_sync */
<1 RK_PB1 5 &pcfg_pull_none>,
/* acodec_adcclk */
<1 RK_PA1 5 &pcfg_pull_none>,
/* acodec_adcdata */
<1 RK_PA0 5 &pcfg_pull_none>,
/* acodec_dac_datal */
<1 RK_PA7 5 &pcfg_pull_none>,
/* acodec_dac_datar */
<1 RK_PB0 5 &pcfg_pull_none>,
/* acodec_dacclk */
<1 RK_PA3 5 &pcfg_pull_none>,
/* acodec_dacsync */
<1 RK_PA5 5 &pcfg_pull_none>;
};
};
};

在 pinctrl 节点中就是每个节点的复用功能, 然后我们以 uart4 的引脚复用为例进行学习,uart4的 pinctrl 服务端内容如下:

1
2
3
4
5
6
7
8
9
10
11
uart4 {
//......
/omit-if-no-ref/
uart4m1_xfer: uart4m1-xfer {
rockchip,pins =
/* uart4_rxm1 */
<3 RK_PB1 4 &pcfg_pull_up>,
/* uart4_txm1 */
<3 RK_PB2 4 &pcfg_pull_up>;
};
};

其中<3 RK_PB1 4 &pcfg_pull_up>和<3 RK_PB2 4 &pcfg_pull_up>分别表示将 GPIO3 的 PB1 引脚设置为功能 4, 将 GPIO3 的 PB2 也设置为功能 4, 且电气属性都会设置为上拉。

通过查找原理图可以得到两个引脚在 BGA 封装位置分别为 AG1 和 AF2, 如下图 :

image-20250221092855296

然后在 rk3568 的数据手册中找到引脚复用表对应的位置, 具体内容如下图所示

image-20250221093541551 image-20250221093551160

可以看到功能 4 对应串口 4 的发送端和接收端, pinctrl 服务端的配置和数据手册中的引脚复用功能是一一对应, 那如果要将 RK_PB1 和 RK_PB2 设置为 GPIO 功能要如何设置呢,从上图可以看到 GPIO 对应功能 0, 所以可以通过以下 pinctrl 内容将设置 RK_PB1 和 RK_PB2 设置为 GPIO 功能(事实上如果不对该管脚进行功能复用该引脚默认就会设置为 GPIO 功能) :

1
2
<3 RK_PB1 0 &pcfg_pull_up>,
<3 RK_PB2 0 &pcfg_pull_up>;

最后来看客户端对 uart4 服务端的引用,可能会这样写:

1
2
3
4
5
&uart4 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&uart4m1_xfer>;
};

通过在客户端中引用服务端的引脚描述符, 设备树可以将客户端和服务端的引脚配置关联起来。 这样, 在设备树被解析和处理时, 操作系统和设备驱动程序可以根据客户端的需求, 查找并应用适当的引脚配置。

5.3 RK3568开发板的一个实例

说明:我没有这个开发板,这里是学习教程的时候直接复制过来作为参考笔记,后面在imx6ull开发板中是会再学习pinctrl 子系统,这里也能帮助理解。

通过上面学到的 pinctrl 相关知识, 将 led 的控制引脚复用为 GPIO 模式。首先来对 rk3568 的设备树结构进行以下介绍, 根据 sdk 源码目录下的“device/rockchip/rk356x/BoardConfig-rk3568-evb1-ddr4-v10.mk” 默认配置文件可以了解到编译的设备树为 rk3568 -evb1-ddr4-v10-linux.dts,整理好的设备树之间包含关系列表如下所示:

image-20250221095145496

Led 在 rk3568-evb.dtsi 设备树中已经被正常配置了, 如下图:

image-20250221095201552

这里也并没有配置 pinctrl 呀, 那为什么 led 最后能正常使用呢, 这个原因之前其实已经提到了, 当一个引脚没有被复用为任何功能时, 默认就是 GPIO 功能, 所以这里没有 pinctrl led 功能也可以正常使用。

image-20250221100143833

保存退出之后, 然后进入到 rk3568-evb1-ddr4-v10.dtsi 设备树中, 找到 rk_485_ctl 节点, 如下图 :

image-20250221100203516

这是根节点的最后一个节点, 而且也是用来控制一个 GPIO 的, 我们完全可以仿照该节点,在该节点下方编写 led 控制节点, 仿写完成的设备树内容如下所示 :

1
2
3
4
5
6
my_led: led {
compatible = "topeet,led";
gpios = <&gpio0 RK_PB7 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&rk_led_gpio>;
};

第 1 行: 节点名称为 led, 标签名为 my_led。

第 2 行: compatible 属性指定了设备的兼容性标识, 即设备与驱动程序之间的匹配规则。在这里, 设备标识为 “topeet,led”, 表示该 LED 设备与名为 “topeet,led” 的驱动程序兼容。

第 3 行: gpios 属性指定了与 LED 相关的 GPIO(通用输入/输出) 引脚配置。

第 4 行: pinctrl-names 属性指定了与引脚控制相关的命名。 default 表示状态 0

第 5 行: pinctrl-0 属性指定了与 pinctrl-names 属性中命名的引脚控制相关联的实际引脚控制器配置。 <&rk_led_gpio> 表示引用了名为 rk_led_gpio 的引脚控制器配置。

添加完成如下图:

image-20250221100310837

然后继续找到在同一设备树文件的 485 pinctrl 服务端节点, 具体内容如下 :

image-20250221100328129

然后在该节点下方仿写 led 控制引脚 pinctrl 服务端节点, 仿写完成的节点内容如下所示:

1
2
3
4
5
rk_led{
rk_led_gpio:rk-led-gpio {
rockchip,pins = <0 RK_PB7 RK_FUNC_GPIO &pcfg_pull_none>;
};
};

添加完成之后如下图:

image-20250221100415836

至此, led 的控制引脚就通过 pinctrl 被复用为了 GPIO 功能, 保存退出之后, 重新编译内核,没有报错就证明我们的实例完成了。