Linux如何在设备树中表示和引用设备信息
DTS基本知识
dts
硬件的相应信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts,一般在Linux源码中存在大量的dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个ARM的machie。
dtsi
值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,然后类似于C语言的方式可以include到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置。
dtc
dtc是编译dts的工具,可以在Ubuntu系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具;
scripts/dtc/Makefile 文件内容如下:
示例代码 43.2.1 scripts/dtc/Makefile 文件代码段 1 hostprogs-y := dtc 2 always := $(hostprogs-y) 3 4 dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \ 5 srcpos.o checks.o util.o 6 dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o ......
可以看出,DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:
make all
或者:
make dtbs
“make all”命令是编译 Linux 源码中的所有东西,包括 zImage,.ko 驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令。
dtb
dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过Bootloader引导程序加载到内核。所以Bootloader需要支持设备树才行;Kernel也需要加入设备树的支持;
基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?我们就以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,有如下内容:
示例代码 43.2.2 arch/arm/boot/dts/Makefile 文件代码段 381 dtb-$(CONFIG_SOC_IMX6UL) += \ 382 imx6ul-14x14-ddr3-arm2.dtb \ 383 imx6ul-14x14-ddr3-arm2-emmc.dtb \ ...... 400 dtb-$(CONFIG_SOC_IMX6ULL) += \ 401 imx6ull-14x14-ddr3-arm2.dtb \ 402 imx6ull-14x14-ddr3-arm2-adc.dtb \ 403 imx6ull-14x14-ddr3-arm2-cs42888.dtb \ 404 imx6ull-14x14-ddr3-arm2-ecspi.dtb \ 405 imx6ull-14x14-ddr3-arm2-emmc.dtb \ 406 imx6ull-14x14-ddr3-arm2-epdc.dtb \ 407 imx6ull-14x14-ddr3-arm2-flexcan2.dtb \ 408 imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \ 409 imx6ull-14x14-ddr3-arm2-lcdif.dtb \ 410 imx6ull-14x14-ddr3-arm2-ldo.dtb \ 411 imx6ull-14x14-ddr3-arm2-qspi.dtb \ 412 imx6ull-14x14-ddr3-arm2-qspi-all.dtb \ 413 imx6ull-14x14-ddr3-arm2-tsc.dtb \ 414 imx6ull-14x14-ddr3-arm2-uart2.dtb \ 415 imx6ull-14x14-ddr3-arm2-usb.dtb \ 416 imx6ull-14x14-ddr3-arm2-wm8958.dtb \ 417 imx6ull-14x14-evk.dtb \ 418 imx6ull-14x14-evk-btwifi.dtb \ 419 imx6ull-14x14-evk-emmc.dtb \ 420 imx6ull-14x14-evk-gpmi-weim.dtb \ 421 imx6ull-14x14-evk-usb-certi.dtb \ 422 imx6ull-alientek-emmc.dtb \ 423 imx6ull-alientek-nand.dtb \ 424 imx6ull-9x9-evk.dtb \ 425 imx6ull-9x9-evk-btwifi.dtb \ 426 imx6ull-9x9-evk-ldo.dtb 427 dtb-$(CONFIG_SOC_IMX6SLL) += \ 428 imx6sll-lpddr2-arm2.dtb \ 429 imx6sll-lpddr3-arm2.dtb \ ......
可以看出,当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到I.MX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。示例代码 43.2.2 中第 422 和 423 行就是我们在给正点原子的 I.MX6U-ALPHA 开发板移植Linux 系统的时候添加的设备树。
DTS 语法
虽然我们基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts文件上进行修改。但是 DTS 文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改.dts文件。大家不要看到要学习新的语法就觉得会很复杂,DTS 语法非常的人性化,是一种 ASCII文本文件,不管是阅读还是修改都很方便。本节我们就以 imx6ull-alientek-emmc.dts 这个文件为例来讲解一下 DTS 语法。
dtb文件是由booloader解析还是kernel解析?
DTB文件主要是由内核解析的,但Bootloader也在一定程度上参与处理DTB文件。以下是对这一过程的详细解释:
Bootloader对DTB文件的处理
加载DTB到内存:Bootloader的主要职责之一是在系统启动初期将操作系统内核和相关资源加载到内存中。在这个过程中,如果存在DTB文件(通常是由设备树源文件编译而来的二进制文件),Bootloader会将其加载到内存中的特定位置。例如,在U-Boot这样的Bootloader中,会将DTB文件存储在内存的某个预定义区域,以便后续内核能够找到并使用它。
告知内核DTB位置:Bootloader在将控制权交给内核之前,需要将DTB文件在内存中的地址或其他相关信息传递给内核。这样,内核在启动时就能够知道从哪里找到DTB文件,并开始对其进行解析。
内核对DTB文件的解析
验证DTB文件格式:内核首先会对加载的DTB文件进行格式验证,检查其头部信息是否正确,包括魔数、版本号等字段,以确保这是一个有效的设备树描述文件。
解析节点和属性:如果DTB文件格式正确,内核会进一步解析其中的节点和属性信息。节点代表了硬件设备或组件,而属性则描述了这些节点的各种特性和参数,如设备的型号、寄存器地址、中断号等。内核会将这些信息组织成内部的数据结构,以便后续的设备驱动程序能够方便地访问和使用。
创建设备模型:基于解析得到的设备树信息,内核会在内存中构建出整个系统的设备模型。这个设备模型反映了系统中所有硬件设备的层次结构和连接关系,为内核的设备管理和驱动程序的加载提供了基础。
综上所述,虽然Bootloader参与了DTB文件的加载和传递工作,但真正对DTB文件进行深入解析和利用的是Linux内核。通过解析DTB文件,内核能够获取硬件设备的信息,从而正确地初始化和管理这些设备,确保系统能够正常运行。
设备树dtb被解析后是存在内存里还是文件系统里?
设备树DTB文件在解析后主要存在于内存中,但也可能与文件系统存在一定关联。以下是对这一问题的详细解释:
内存中的存在形式
作为内核数据结构:设备树DTB文件在被内核解析后,其内容会被转化为一系列的内核数据结构,如
device_node
和property
等。这些数据结构存储在内存中,用于描述系统中的设备及其属性和配置信息。内存中的保留区域:在某些情况下,设备树DTB文件可能会被加载到内存中的特定区域,以便内核能够直接访问和解析。例如,在ARM架构的系统中,设备树DTB文件通常会被加载到内存的起始地址处。
文件系统中的存在形式
/sys文件系统:内核会在
/sys
文件系统的/sys/firmware/fdt/
目录下创建设备树的相关信息,包括设备树的字符串表(strings)和结构体(structure)部分。这些信息以文件的形式存在,用户可以通过读取这些文件来查看设备树的内容。/proc文件系统:虽然/proc文件系统主要用于提供内核运行时的信息,但在某些情况下,它也可以用来查看与设备树相关的信息。例如,通过访问
/proc/device-tree
目录,可以获取到设备树的原始内容或经过处理后的信息。综上所述,设备树DTB文件在解析后主要存在于内存中,但也可能与文件系统(如/sys和/proc)存在一定的关联,以便用户和开发者能够查看和管理设备树的内容。这种设计既保证了设备树信息的高效使用,又提供了方便的接口供外部访问和调试。
.dtsi 头文件
和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 imx6ull-alientek
emmc.dts 中有如下所示内容:
示例代码 43.3.1.1 imx6ull-alientek-emmc.dts 文件代码段 12 #include <dt-bindings/input/input.h> 13 #include "imx6ull.dtsi"
第 12 行,使用“#include”来引用“input.h”这个.h 头文件。
第 13 行,使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。
看到这里,大家可能会疑惑,不是说设备树的扩展名是.dtsi 吗?为什么也可以直接引用 C语言中的.h 头文件呢?这里并没有错,.dts 文件引用 C 语言中的.h 文件,甚至也可以引用.dts 文件,打开 imx6ull-14x14-evk-gpmi-weim.dts 这个文件,此文件中有如下内容:
示例代码 43.3.1.2 imx6ull-14x14-evk-gpmi-weim.dts 文件代码段 #include "imx6ull-14x14-evk.dts"
可以看出,示例代码 43.3.1.2 中直接引用了.dts 文件,因此在.dts 设备树文件中,可以通过“#include”来引用.h、.dtsi 和.dts 文件。只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀。
一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。比如 imx6ull.dtsi 就是描述 I.MX6ULL 这颗 SOC 内部外设情况信息的,内容如下:
示例代码 43.3.1.3 中第 54~89 行就是 cpu0 这个设备节点信息,这个节点信息描述了I.MX6ULL 这颗 SOC 所使用的 CPU 信息,比如架构是 cortex-A7,频率支持 996MHz、792MHz、528MHz、396MHz 和 198MHz 等等。在 imx6ull.dtsi 文件中不仅仅描述了 cpu0 这一个节点信息,I.MX6ULL 这颗 SOC 所有的外设都描述的清清楚楚,比如 ecspi1~4、uart1~8、usbphy1~2、i2c1~4等等,关于这些设备节点信息的具体内容我们稍后在详细的讲解。
设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下是从imx6ull.dtsi 文件中缩减出来的设备树文件内容:
第 1 行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi和 imx6ull-alientek-emmc.dts 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点的内容会合并成一个根节点。
dtsi中的根结点会和包含它的dts文件中的根结点冲突吗?
在Linux内核中,设备树(Device Tree)是一种用于描述硬件拓扑结构的数据结构。设备树通常由多个文件组成,包括.dts(Device Tree Source)文件和.dtsi(Device Tree Source Include)文件。这些文件最终会被编译成一个设备树二进制文件(Device Tree Blob,简称DTB),供内核在启动时使用。
关于DTSI中的根结点是否会和包含它的DTS文件中的根结点冲突,实际上并不会冲突。原因如下:
编译时的节点合并:当包含DTSI文件的DTS文件被编译成DTB时,编译器会将DTSI中的节点合并到DTS中的相应位置。这意味着,尽管在源代码层面看起来DTSI和DTS各自有一个根结点,但在最终编译生成的设备树中,这些节点会被合并成一个统一的树状结构。
单一的根节点:设备树本质上是一棵树,因此它只能有一个根节点。在编译过程中,所有的节点都会被整合到这个单一的根节点下,形成一个完整的设备树结构。
综上所述,DTSI中的根结点不会与包含它的DTS文件中的根结点冲突。在设备树的编译和链接过程中,这些节点会被适当地合并和管理,以确保最终生成的设备树具有一致且正确的结构。
dts中的根结点会和包含它的dts文件中的根结点冲突吗?
在Linux中,DTS中的根结点和包含它的DTS文件中的根结点实际上不会冲突。
在一个DTS文件中,使用
#include
语句可以引用其他文件(通常是.dtsi文件),这些被引用的文件可以看作是对当前DTS文件的扩展或补充。当编译器处理这种包含关系时,它会将被包含的内容合并到主DTS文件中,从而形成一个完整的设备树结构。在这个过程中,并不会因为包含操作而产生多个根结点,最终生成的设备树仍然只有一个根结点。其实,只要最终能保证层级关系不乱就行了,这还是很容易做到的。
第 2、6 和 17 行,aliases、cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:
node-name@unit-address
其中“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。
但是我们在示例代码 43.3.2.1 中我们看到的节点命名却如下所示:
cpu0:cpu@0
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:
label: node-name@unit-address
引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “intc:interrupt-controller@00a01000”,节点 label 是 intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方便很多!
第 10 行,cpu0 也是一个节点,只是 cpu0 是 cpus 的子节点。
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
①、字符串
compatible = "arm,cortex-a7";
上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
②、32 位无符号整数
reg = <0>;
上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
③、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。
标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。
compatible 属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible 属性的值格式如下所示:
"manufacturer,model"
其中manufacturer 表示厂商,model一般是模块对应的驱动名字。比如imx6ull-alientek-emmc.dts 中sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点,I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的 WM8960,sound 节点的 compatible 属性值如下:
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 中有如下内容:
示例代码 43.3.3.1 imx-wm8960.c 文件代码段 632 static const struct of_device_id imx_wm8960_dt_ids[] = { 633 { .compatible = "fsl,imx-audio-wm8960", }, 634 { /* sentinel */ } 635 }; 636 MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids); 637 638 static struct platform_driver imx_wm8960_driver = { 639 .driver = { 640 .name = "imx-wm8960", 641 .pm = &snd_soc_pm_ops, 642 .of_match_table = imx_wm8960_dt_ids, 643 }, 644 .probe = imx_wm8960_probe, 645 .remove = imx_wm8960_remove, 646 };
第 632~635 行的数组 imx_wm8960_dt_ids 就是 imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的 compatible 属性值与此相等,那么这个节点就会使用此驱动文件。
第 642 行,wm8960 采用了 platform_driver 驱动模式,关于 platform_driver 驱动后面会讲解。此行设置.of_match_table 为 imx_wm8960_dt_ids,也就是设置这个 platform_driver 所使用的OF 匹配表。
model 属性
model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
status 属性
status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的状态信息,可选的状态如表 43.3.3.1 所示:
#address-cells 和#size-cells 属性
这两个属性的值都是无符号 32 位整形,#address-cells 和#size-cells 这两个属性可以并且只能用在任何拥有子节点的节点中,用于描述子节点的地址信息。#address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位),#size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。#address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,reg 属性的格式为:
reg = <address1 length1 address2 length2 address3 length3……>
每个“address length”组合表示一个地址范围,其中 address 是起始地址,length 是地址长度,#address-cells 表明 address 这个数据所占用的字长,#size-cells 表明 length 这个数据所占用的字长,比如:
第 3,4 行,节点 spi4 的#address-cells = <1>,#size-cells = <0>,说明 spi4 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。
第 8 行,子节点 gpio_spi: gpio_spi@0 的 reg 属性值为 <0>,因为父节点设置了#address cells = <1>,#size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没有设置地址长度。
第 14,15 行,设置 aips3: aips-bus@02200000 节点#address-cells = <1>,#size-cells = <1>,说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。
第 19 行,子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0x02280000,length= 0x4000,相当于设置了起始地址为 0x02280000,地址长度为 0x40000。
不太明白,地址和长度不都是1个字长吗?难道还有超过1个字长的地址或者长度?
参考:终于搞懂Linux 设备树中的#address-cells,#size-cells 和reg 属性-CSDN博客
再看个例子:
alphaled { #address-cells = <1>; #size-cells = <1>; compatible = "atkalpha-led"; status = "okay"; reg = < 0X020C406C 0X04 /* CCM_CCGR1_BAE */ 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
,size-cells = 1
, 表示reg
属性中,地址信息的长度是1
个字长,地址长度信息也是1
个字长。可见,大部分情况下,都是1个字长。
有没有不是1个字长的?
比如:
pci@1,0 { #address-cells = <3>; #size-cells = <2>; compatible = "intel,ce4100-pci", "pci"; device_type = "pci"; bus-range = <1 1>; reg = <0x0800 0x0 0x0 0x0 0x0>; ... }
这里
address-cells = 3
,size-cells = 2
, 表示reg
属性中,地址信息的长度是3
个字长,地址长度信息是2
个字长,即是:address = 0x0800 0x0 0x0, length = 0x00 0x00,具体含义具体对待。此时,地址和长度均无法通过1个字长来描述。
在设备树(Device Tree)中,
#size-cells
前面的 符号具有特定的含义和作用。以下是对其的解释:属性标识作用
区分普通属性名与特殊属性:在设备树的语法规则中,以
#
开头的属性通常具有特殊的意义或用途,与常规的属性名有所区别。这种标识方式能够让设备树的解析器快速识别和正确处理这些特殊属性,避免与普通属性产生混淆。例如,#size-cells
表示该属性是用于描述节点地址空间中尺寸信息的特殊属性,而普通的属性如compatible
、model
等则用于描述设备的兼容性、型号等信息。明确属性语义类别:
#
符号的使用有助于将与地址、大小相关的属性归类在一起,方便管理和理解。像#address-cells
和#size-cells
都属于这类描述节点地址信息的属性,通过#
符号的统一标识,可以清晰地表明它们在语义上的关联性和特殊性。
reg 属性
reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在 imx6ull.dtsi 中有如下内容:
上述代码是节点 uart1,uart1 节点描述了 I.MX6ULL 的 UART1 相关信息,重点是第 326 行的 reg 属性。其中 uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、#sizecells = <1>,因此 reg 属性中 address=0x02020000,length=0x4000。查阅《I.MX6ULL 参考手册》可知,I.MX6ULL 的 UART1 寄存器首地址为 0x02020000,但是 UART1 的地址长度(范围)并没有 0x4000 这么多,这里我们重点是获取 UART1 寄存器首地址。
进一步思考。
假设reg里的数据是这样的
reg = <0x08000000 0x4000 0x08006000 0x5000 0x0800a000 0x4000>
你知道这里面哪些是地址哪些是长度吗?只知道用空格隔开了一些数据。而且,就算我们能从中看出来哪个是长度哪个是地址,但计算机怎判断呢?这就要我们告诉计算机,地址和长度所占的数据个数是怎么样的。
假设
#address-cells = <1>;
#size-cells = <1>;
那么上述6个数的顺序就是:<地址 长度 地址 长度 地址 长度>
假设
#address-cells = <1>;
#size-cells = <0>;
那么上述6个数的顺序就是:<地址 地址 地址 地址 地址 地址>,即没有哪个数据表示长度了
假设
#address-cells = <3>;
#size-cells = <3>;
那么上述6个数的顺序就是:<地址 长度>,前面三个数都表示地址,后面三个数都表示长度。
只是,一般都是
#address-cells = <1>;
#size-cells = <0>;
或者
#address-cells = <1>;
#size-cells = <1>;
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 属性,如下所示:
第 142 行定义了 ranges 属性,但是 ranges 属性值为空。
ranges 属性不为空的示例代码如下所示:
第 5 行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
第 10 行,serial 是串口设备节点,reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,
寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,
0xe0004600=0x4600+0xe0000000。
name 属性
name 属性值为字符串,name 属性用于记录节点名字,name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。
device_type 属性
device_type 属性值为字符串,IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:
在Linux的设备树(Device Tree)中,
device_type
属性是一个用于标识设备节点类型的字符串类型属性。以下是关于device_type
属性的详细解释:
基本概念
device_type
属性用于描述设备树中各个设备节点的具体类型,它为内核或用户空间提供了一种识别和区分不同设备的机制。通过这个属性,系统可以知道某个节点所代表的是CPU、内存、I2C设备、SPI设备还是其他类型的设备。常见取值
CPU相关:对于CPU设备节点,
device_type
通常被设置为“cpu”,以明确表示该节点是一个CPU设备。内存相关:内存设备节点的
device_type
一般被设置为“memory”,用于标识内存设备。存储设备:如MMC(MultiMediaCard)设备节点,其
device_type
可能被设置为“mmc”,以表明这是一个MMC存储设备。通信接口设备:例如I2C设备节点的
device_type
可以是“i2c”,SPI设备节点的device_type
可以是“spi”,USB设备节点的device_type
可以是“usb”等,这些设置有助于驱动程序正确地识别和处理相应的通信接口设备。网络设备:以太网设备节点的
device_type
可能被设置为“ethernet”,从而让系统能够识别以太网相关的设备。其他外设设备:像GPIO设备节点的
device_type
可以被设置为“gpio”,PWM设备节点的device_type
可以是“pwm”等,以便系统对各种外设进行准确的识别和管理。作用与意义
设备识别与驱动绑定:内核在启动或运行时会读取设备树中的信息,根据
device_type
属性来确定每个设备节点的类型,并将相应的设备与对应的驱动程序进行绑定。这样,当系统需要与某个设备进行交互时,就能够找到正确的驱动程序来操作该设备。硬件资源管理:通过明确设备的类型,系统可以更好地管理和分配硬件资源。例如,对于不同类型的设备,系统可以根据其特性和需求分配适当的内存、中断等资源,以确保设备的正常运行。
兼容性与扩展性:使用
device_type
属性可以提高系统的兼容性和扩展性。在添加新的设备类型时,只需要在设备树中为新设备节点设置正确的device_type
属性,并编写相应的驱动程序即可,无需对系统的核心代码进行大规模的修改。综上所述,device_type属性在Linux的设备树中扮演着至关重要的角色,它不仅帮助内核和系统准确地识别和区分各种设备节点,还促进了设备与驱动程序的正确绑定以及硬件资源的有效管理。
注意,名称并不是强制的,是自定义的,只不过一般很多名称都是约定俗成的而已。
关于标准属性就讲解这么多,其他的比如中断、IIC、SPI 等使用的标准属性等到具体的例程再讲解。
特殊节点
/aliases 子节点
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。例如:定义 flexcan1 和 flexcan2 的别名是 can0 和 can1。
aliases { can0 = &flexcan1; can1 = &flexcan2; };
单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
/memory 子节点
所有设备树都需要一个memory设备节点,它描述了系统的物理内存布局。如果系统有多个内存块,可以创建多个memory节点,或者可以在单个memory节点的reg属性中指定这些地址范围和内存空间大小。
例如:一个64位的系统有两块内存空间:RAM1: 起始地址是0x0,地址空间是 0x80000000;RAM2: 起始地址是0x10000000,地址空间也是0x80000000;同时根节点下的 #address-cells = <2>和#size-cells = <2>,这个memory节点描述为:
memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x80000000 0x00000000 0x10000000 0x00000000 0x80000000>; };
或者:
memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x80000000>; }; memory@10000000 { device_type = "memory"; reg = <0x00000000 0x10000000 0x00000000 0x80000000>; };
更多待补充。
/chosen 子节点
chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。例如:
chosen { bootargs = "root=/dev/nfs rw nfsroot=192.168.1.1 console=ttyS0,115200"; };
更多待补充。
在Linux设备树中,可以自定义属性。以下是关于自定义属性的定义和引用方法的详细解释:
自定义属性的定义
自定义属性允许用户根据特定硬件设备的特性和需求来定义属性名和属性值。这些属性在Linux内核和驱动程序中并不具有特殊的含义,而是由用户在设备树文件中定义,并在驱动程序中通过特定的API接口进行读取和处理。
自定义属性的引用
在设备树文件中定义自定义属性:
自定义属性以键值对的形式存在,属性名由用户自定义,属性值可以是整数、字符串、布尔值等各种类型。例如:
&i2c1 { my-device@48 { compatible = "my-company,my-device"; reg = <0x48>; example-config = <1 9600>; /* 工作模式为1,波特率为9600 */ }; };
在这个例子中,
example-config
就是一个自定义属性,用于描述设备的特定配置选项。在驱动程序中读取自定义属性:
在Linux驱动程序中,可以使用如
of_property_read_u32
、of_property_read_string
等函数来读取设备树中的自定义属性。例如:#include <linux> static int my_driver_probe(struct platform_device *pdev) { struct device_node *np = pdev->dev.of_node; u32 example_config; int ret; /* 读取自定义属性example-config */ ret = of_property_read_u32(np, "example-config", &example_config); if (ret) { dev_err(&pdev->dev, "Failed to read 'example-config' property"); return ret; } /* 使用example_config进行设备配置 */ // ... return 0; }
在这个示例中,驱动程序在
probe
函数中通过of_property_read_u32
函数读取了设备树中的自定义属性example-config
,并根据该属性的值进行设备配置。综上所述,Linux设备树允许用户自定义属性,并通过特定的API接口在驱动程序中读取和处理这些属性。这为用户提供了灵活的方式来描述硬件设备的特定特性和配置选项。
如何在设备树中表示和引用设备信息
先分析一个完整的设备树,是怎么表达各种外设信息的。
以imux6ull开发板为例进行说明。
待补充。。。
更多补充
中断结点
是一个整数类型的属性,用于指定在设备树的中断描述中使用多少个单元来表示一个中断。例如,如果 #interrupt-cells
#interrupt-cells
的值为2,那么在描述每个中断时,需要使用2个单元的数据来表示相关信息。注意,不是对应的reg,具体对应着什么,暂时我也不知道,后面再看。在设备树中,
interrupt-controller
属性通常是一个空属性,其作用是声明当前节点是一个中断控制器。通过设置interrupt-controller
属性(即使为空),可以让设备树和系统清晰地知道该节点代表的是一个中断控制器设备。这有助于在复杂的硬件架构中快速识别和定位负责中断处理的设备,方便系统对其进行正确的配置和管理。在设备树中,存在各种不同类型的设备节点,如CPU、内存、I/O设备等。interrupt-controller
属性为空可以作为一种简单的标识方式,将中断控制器与其他普通设备区分开来,避免混淆,确保系统能够准确地识别和处理中断相关的设备。上面的#interrupt-cells应该对应的是子结点的interrupts属性的表示个数。
属性是一个整数数组,用于指定与该设备相关联的中断请求信号的信息。每个元素通常包含两个主要部分信息,一是中断控制器的引用(通过 phandle 或设备树节点路径表示),二是中断编号或中断名称(具体取决于设备和平台的表示方式)。 interrupts
例如,
interrupts = <&intc 23 0x04>;
这个属性值中,&intc
是中断控制器的设备树节点标签,23
是中断编号,0x04
表示中断触发类型(如上升沿触发等)。作用与意义
硬件中断配置:它告诉操作系统或硬件抽象层(HAL)当设备需要产生中断时,应该向哪个中断控制器发送信号,以及使用哪个中断号。这样,当设备发生特定事件(如数据接收、错误发生等)需要通知处理器时,处理器能够正确地响应并执行相应的中断服务程序。
系统资源分配和管理:系统可以根据
interrupts
属性的信息,为设备分配合适的中断处理资源,确保不同设备的中断请求能够得到及时、准确的处理,避免中断冲突和混乱。驱动程序开发:对于设备驱动开发者来说,
interrupts
属性提供了关键的信息,以便编写正确的中断处理代码。驱动程序需要根据这个属性的值来注册相应的中断处理函数,使设备能够正常工作。常见属性值的含义
中断编号:如果
interrupts
属性值中只包含一个数字,那么这个数字通常代表中断编号。例如,在一些简单的系统中,interrupts = <5>;
表示设备使用中断编号为 5 的中断线。中断名称:在某些情况下,可能会使用中断名称来代替中断编号。例如,
interrupts = <"irq_name">;
这里的"irq_name"
是在设备树中定义的一个中断名称标识符。这种方式可以使设备树更加易读和可维护,尤其是当中断编号较多且不易理解时。复合值:有些复杂的设备可能会有更复杂的
interrupts
属性值,包含多个单元的复合值。这些复合值可能包括中断域、触发方式、CPU 接口信息等。例如,interrupts = <gic_ppi>;
这个属性值中,GIC_PPI
表示这是一个与通用中断控制器(GIC)相关的私有外设中断(PPI),37
是中断号,1
表示中断触发类型等信息。总之,设备树中的
interrupts
属性至关重要,它不仅明确了设备的中断请求信号细节,还为系统资源分配、驱动程序开发提供了关键依据,确保了硬件中断的有效管理和设备的正常运行。具体还要看程序里是怎么获取的。
#clock-cells
通过指定
#clock-cells
的值,可以明确子节点的时钟信息在设备树中应该如何被表示和解析。例如,如果#clock-cells = <2>
,则表示该节点下的子节点时钟信息将使用 2 个单元的数据来描述。这有助于系统准确地理解和处理不同节点的时钟配置信息,确保硬件设备的时钟设置能够被正确地识别和应用。
芯片上的外设通常都作为soc结点的子结点。
设备树的语法规定了属性值可以是整数、字符串、数组等多种形式。当属性值为数组时,尖括号用于标识数组的开始和结束。分开写尖括号并不影响设备树编译器对属性值的解析,因为编译器会按照设备树的语法规则正确处理这种写法。例如,对于一个包含多个数值的数组属性
reg = <0x1000 0x2000 0x3000>;
,无论是连续写还是分开写尖括号,如reg = <0x1000>,<0x2000>,<0x3000>;
,中间以逗号或者空格隔开应该都可以,设备树编译器都能识别并将其解析为一个包含三个元素的数组。也可以分行写reg = <0x1000>,
<0x2000>,
<0x3000>;
比如:
gpio-controller
在设备树(Device Tree)中,
gpio-controller
属性通常用于标识某个节点代表一个 GPIO(通用输入输出)控制器。这个属性告诉系统该节点下的硬件资源是与 GPIO 控制相关的,使得操作系统能够正确地识别和使用这些 GPIO 引脚。
使用pinctrl子系统时,我们通常都会将所有的pin引脚信息都放在一起;然后具体被哪个外设使用时,需要再在对应外设节点中引用pin信息。
目前还是没太搞明白,pin,gpio和具体外设之间的设备树的关系,所有的引脚都是pin,需要先设置好pin的复用功能、驱动能力以及上下拉等基本属性;如果是复用为gpio,那么就作为gpio这种外设功能;但是,gpio也需要依附其他外设来使用的。这里要搞清楚,gpio是一种片上外设,一般都定义在soc节点中吧?然后片外的外设,就不放在soc里面?这块还得再研究研究,捋一捋。