第八节 Linux 设备树
Linux3.x 以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节。在早些的linux内核,这些“硬件平台的板级细节”保存在linux 内核目录“/arch”,以ARM 平台为例“硬件平台的板级细节”保存在“/arch/arm/plat-xxx”和“/arch/arm/mach-xxx”目录下。随着处理器数量的增多用于描述“硬件平台板级细节”的文件越来越多导致Linux 内核非常臃肿,Linux 之父发现这个问题之后决定使用设备树解决这个问题。设备树简单、易用、可重用性强,linux3.x 之后大多采用设备树编写驱动。
关于设备树的详细请参考:https://www.devicetree.org/
设备树简介
设备树的作用就是描述一个硬件平台的硬件资源。这个“设备树”可以被bootloader(uboot) 传递到内核,内核可以从设备树中获取硬件信息。

设备树描述硬件资源时有两个特点。
- 第一,以“树状”结构描述硬件资源。例如本地总线为树的“主干”在设备树里面称为“根节点”,挂载到本地总线的IIC 总线、SPI 总线、UART 总线为树的“枝干”在设备树里称为“根节点的子节点”,IIC 总线下的IIC 设备不止一个,这些“枝干”又可以再分。
- 第二,设备树可以像头文件(.h 文件)那样,一个设备树文件引用另外一个设备树文件,这样可以实现“代码”的重用。例如多个硬件平台都使用STM32MP1 作为主控芯片,那么我们可以将STM32MP1 芯片的硬件资源写到一个单独的设备树文件里面一般使用“.dtsi”后缀,其他设备树文件直接使用“# includexxx”引用即可。
DTS、DTC 和DTB 它们是文档中常见的几个缩写。
- DTS 是指.dts 格式的文件,是一种ASII 文本格式的设备树描述,也是我们要编写的设备树源码,一般一个.dts 文件对应一个硬件平台,位于Linux 源码的“/arch/arm/boot/dts”目录下。
- DTC 是指编译设备树源码的工具,一般情况下我们需要手动安装这个编译工具。
- DTB 是设备树源码编译生成的文件,类似于我们C 语言中“.C”文件编译生成“.bin”文件。
设备树框架
上一小节简单了解了设备树的作用,到现在为止我们还不知道“设备树”是究竟是个什么样子。
下面的内容将围绕着设备树源码,来讲解设备树框架和基本语法。
列表1: 设备树(内核源码/arch/arm/boot/dts/stm32mp157a-basic.dts)
#include "stm32mp157c.dtsi"
#include "stm32mp157cac-pinctrl.dtsi"
#include "stm32mp157c-m4-srm.dtsi"
#include <dt-bindings/input/input.h>
#include <dt-bindings/mfd/st,stpmic1.h>/ {model = "Embedfire STM32MP157 Star LubanCat Robot S1 Board";compatible = "st,stm32mp157a-dk1", "st,stm32mp157";aliases {ethernet0 = ðernet0;serial0 = &uart4;serial1 = &usart1;serial2 = &usart2;serial3 = &usart3;};chosen {stdout-path = "serial0:115200n8";};memory@c0000000 {reg = <0xc0000000 0x40000000>;};/*-------------以下内容省略--------------*/&fmc {pinctrl-names = "default", "sleep";pinctrl-0 = <&fmc_pins_a>;pinctrl-1 = <&fmc_sleep_pins_a>;status = "okay";#address-cells = <1>;#size-cells = <0>;nand: nand@0 {reg = <0>;nand-on-flash-bbt;#address-cells = <1>;#size-cells = <1>;};
};
/ {#address-cells = <1>;#size-cells = <1>;cpus {#address-cells = <1>;#size-cells = <0>;cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;clocks = <&rcc CK_MPU>;clock-names = "cpu";operating-points-v2 = <&cpu0_opp_table>;nvmem-cells = <&part_number_otp>;nvmem-cell-names = "part_number";};
/*-------------以下内容省略--------------*/
设备树源码分为三部分,介绍如下:
-
第1-5 行:头文件。设备树是可以像C 语言那样使用“#include”引用“.h”后缀的头文件,也可以引用设备树“.dtsi”后缀的头文件。stm32mp157c.dtsi 由ST 官方提供,是一个stm32mp157 平台“共用”的设备树文件。
-
第11-23 行:设备树节点。设备树给我们最直观的感受是它由一些嵌套的大括号“{}”组成,每一个“{}”都是一个“节点”。“/ {⋯};”表示“根节点”,每一个设备树只有一个根节点。如果打开“stm32mp157c.dtsi”文件可以发现它也有一个根节点,虽然“stm32mp157a-basic.dts”引用了“stm32mp157c.dtsi”文件,但这并不代表“stm32mp157a-basic.dts”设备树有两个根节点,因为不同文件的根节点最终会合并为一个。在根节点内部的“aliases {⋯}”、“chosen{⋯}”、“memory {⋯}”等字符,都是根节点的子节点。
-
第29-43 行:设备树节点追加内容。第三部分的子节点比根节点下的子节点多了一个“&”,这表示该节点在向已经存在的子节点追加数据。这些“已经存在的节点”可能定义在“stm32mp157a-basic.dts”文件,也可能定义在“stm32mp157a-basic.dts”文件所包含的设备树文件里。本代码中的“&cpu0 {⋯}”、“&clks {⋯}”、“&fec1 {⋯}”等等追加的目标节点,就是定义在“stm32mp157c.dtsi”中。
到目前为止我们知道设备树由一个根节点和众多子节点组成,子节点也可以继续包含其他节点,也就是子节点的子节点。设备树的组成很简单,下面我们一起来看看节点的基本格式和节点属性。
节点基本格式
设备树中的每个节点都按照以下约定命名:
列表3: 节点基本格式
node-name@unit-address{属性1 = ⋯属性2 = ⋯属性3 = ⋯子节点⋯
};
node-name 节点名称
节点格式中的node-name 用于指定节点的名称。它的长度为1 至31 个字符,只能由如下字符组成
表节点名称
| 字符 | 描述 |
|---|---|
| 0-9 | 数字 |
| a-z | 小写字母 |
| A-Z | 大写字母 |
| , | 英文逗号 |
| . | 英文句号 |
| _ | 下划线 |
| • | 加号 |
| • | 减号 |
另外,节点名应当使用大写或小写字母开头,并且能够描述设备类别。
注意,根节点没有节点名,它直接使用“/”指代这是一个根节点。
@unit-address
@unit-address ,其中的符号“@”可以理解为是一个分割符,“unit-address”用于指定“单元地址”,它的值要和节点“reg”属性的第一个地址一致。如果节点没有“reg”属性值,可以直接省略“@unit-address”,不过要注意这时要求同级别的设备树下(相同级别的子节点)节点名唯一, 从这个侧面也可以了解到,同级别的子节点的节点名可以相同,但是要求“单元地址”不同,node-name@unit-address 的整体要求同级唯一。
节点标签
在stm32mp157c.dtsi 头文件中,节点名“cpu”前面多了个“cpu0”, 这个“cpu0”就是我们所说的节点标签。通常节点标签是节点名的简写,所以它的作用是当其它位置需要引用时可以使用节点标签来向该节点中追加内容。
节点路径
通过指定从根节点到所需节点的完整路径,可以唯一地标识设备树中的节点,不同层次的设备树节点名字可以相同,同层次的设备树节点要唯一。这有点类似于我们Windows 上的文件,一个路径唯一标识一个文件或文件夹,不同目录下的文件文件名可以相同。
节点属性
在节点的“{}”中包含的内容是节点属性,通常情况下一个节点包含多个属性信息,这些属性信息就是要传递到内核的“板级硬件描述信息”,驱动中会通过一些API 函数获取这些信息。
例如根节点“/”就有属性compatible = “st,stm32mp157a-dk1”, “st,stm32mp157”。我们可以通过该属性了解到硬件设备相关的名字叫“stm32mp157a-dk1”,设备所使用的的是“stm32mp157”这颗SOC。
我们编写设备树最主要的内容是编写节点的节点属性,通常情况下一个节点代表一个设备,设备有哪些属性、怎么编写这些属性、在驱动中怎么引用这些属性是我们后面讲解的重点,这一小节只讲解设备节点有哪些可设置属性。有一些节点属性是所有节点共有的,一些作用于特定的节点,我们这里介绍那些共有的节点属性,其他节点属性使用到时再详细介绍。
节点属性分为标准属性和自定义属性,也就是说我们在设备树中可以根据自己的实际需要定义、添加设备属性。标准属性的属性名是固定的,自定义属性名可按照要求自行定义。
compatible 属性
属性值类型:字符串
列表4: compatible 属性
model = "Embedfire STM32MP157 Star LubanCat Robot S1 Board";compatible = "st,stm32mp157a-dk1", "st,stm32mp157";aliases {ethernet0 = ðernet0;serial0 = &uart4;serial1 = &usart1;serial2 = &usart2;serial3 = &usart3;
};
compatible 属性值由一个或多个字符串组成,有多个字符串时使用“,”分隔开。
设备树中的每一个代表了一个设备的节点都要有一个compatible 属性。compatible 是系统用来决定绑定到设备的设备驱动的关键。compatible 属性是用来查找节点的方法之一,另外还可以通过节点名或节点路径查找指定节点。
例如系统初始化时会初始化platform 总线上的设备时,根据设备节点”compatible”属性和驱动中of_match_table 对应的值,匹配了就加载对应的驱动。
model 属性
属性值类型:字符串
示例:
列表5: model 属性
model = "Embedfire STM32MP157 Star LubanCat Robot S1 Board";
model 属性用于指定设备的制造商和型号,推荐使用“制造商, 型号”的格式,当然也可以自定义,本例子就没有使用这种格式。
status 属性
属性值类型:字符串
示例:
列表6: status 属性
/* External sound card */
sound: sound {status = "disabled";
};
状态属性用于指示设备的“操作状态”,通过status 可以去禁止设备或者启用设备,可用的操作状态如下表。默认情况下不设置status 属性设备是使能的。
#address-cells 和#size-cells
属性值类型:u32
示例:
列表7: #address-cells 和#size-cells
soc {
#address-cells = <1>;
#size-cells = <1>;compatible = "simple-bus";interrupt-parent = <&gpc>;ranges;ocrams: sram@900000 {compatible = "fsl,lpm-sram";reg = <0x900000 0x4000>;};
};
#address-cells 和#size-cells 属性同时存在,在设备树ocrams 结构中,它们用在有子节点的设备节点(节点),用于设置子节点的“reg”属性的“书写格式”。
补充:reg 属性值由一串数字组成,如上图中的reg = <0x900000 0x4000>,ret 属性的书写格式为reg = < cells cells cells cells cells cells⋯>,长度根据实际情况而定,这些数据分为地址数据(地址字段),长度数据(大小字段)。
#address-cells,用于指定子节点reg 属性“地址字段”所占的长度(单元格cells 的个数)。#size-cells,用于指定子节点reg 属性“大小字段”所占的长度(单元格cells 的个数)。
例如#address-cells=2,#address-cells=1,则reg 内的数据含义为reg = < address address size address address size>,因为每个cells 是一个32 位宽的数字,例如需要表示一个64 位宽的地址时,就要使用两个address 单元来表示。而假如#address-cells=1,#address-cells=1,则reg 内的数据含义为reg = < address size address size address size>。
总之#size-cells 和#address-cells 决定了子节点的reg 属性中哪些数据是“地址”,哪些数据是“长度”信息。
reg 属性
属性值类型:地址、长度数据对
reg 属性描述设备资源在其父总线定义的地址空间内的地址。通常情况下用于表示一块寄存器的起始地址(偏移地址)和长度,在特定情况下也有不同的含义。例如上例中#address-cells = <1>,#address-cells = <1>,reg = <0x9000000 x4000>,其中0x9000000 表示的是地址,0x4000 表示的是地址长度,这里的reg 属性指定了起始地址为0x9000000,长度为0x4000 的一地址空间。
ranges
属性值类型:任意数量的< 子地址、父地址、地址长度> 编码
示例:
列表8: ranges 属性
soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";interrupt-parent = <&gpc>;ranges;busfreq {/*-------------以下内容省略--------------*/};
}
该属性提供了子节点地址空间和父地址空间的映射(转换)方法,常见格式是ranges = < 子地址, 父地址, 转换长度>。如果父地址空间和子地址空间相同则无需转换,如示例中所示,只写了renges, 内容为空,我们也可以直接省略renges 属性。
比如对于#address-cells 和#size-cells 都为1 的话,以ranges=<0x0 0x10 0x20> 为例,表示将子地址的从0x0~(0x0 + 0x20) 的地址空间映射到父地址的0x10~(0x10 + 0x20)。
name 和device_type
属性值类型:字符串。
示例:
列表9: name 属性
example{name = "name"
}
列表10: device_type 属性
cpus {
#address-cells = <1>;
#size-cells = <0>;cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;}
}
这两个属性很少用(已经被废弃),不推荐使用。name 用于指定节点名,在旧的设备树中它用于确定节点名,现在我们使用的设备树已经弃用。device_type 属性也是一个很少用的属性,只用在CPU 和内存的节点上。如上例中所示,device_type 用在了CPU 节点。
追加/修改节点内容
列表11: 追加/修改节点内容
&cpu0{//cpu-supply = <&vddcore>;clock-frequency = <650000000>;
};
这些源码并不包含在根节点“/{⋯}”内,它们不是一个新的节点,而是向原有节点追加内容。以上方源码为例,“&cpu0”表示向“节点标签”为“cpu0”的节点追加数据,这个节点可能定义在本文件也可能定义在本文件所包含的设备树文件中,本例子中源码的“cpu0”定义在“stm32mp157c.dtsi”文件中。
特殊节点
aliases 子节点
aliases 子节点的作用就是为其他节点起一个别名,如下所示。
列表12: 别名子节点
aliases {ethernet0 = ðernet0;serial0 = &uart4;serial1 = &usart1;serial2 = &usart2;serial3 = &usart3;/*----------- 以下省略------------*/
}
以“serial0 = &uart4;”为例。“serial0”是一个节点的名字,设置别名后我们可以使用“serial0”来指代uart4 节点,与节点标签类似。在设备树中更多的是为节点添加标签,没有使用节点别名,别名的作用是“快速找到设备树节点”。在驱动中如果要查找一个节点,通常情况下我们可以使用“节点路径”一步步找到节点。也可以使用别名“一步到位”找到节点。
chosen 子节点
chosen 子节点位于根节点下,如下所示
列表13: chosen 子节点
chosen {stdout-path = "serial0:115200n8";
};
chosen 子节点不代表实际硬件,它主要用于给内核传递参数。这里只设置了“stdout-path =”serial0:115200n8”;”一条属性,表示系统标准输出stdout 使用串口serial0。此外这个节点还用作uboot 向linux 内核传递配置参数的“通道”,我们在Uboot 中设置的参数就是通过这个节点传递到内核的,这部分内容是uboot 和内核自动完成的,作为初学者我们不必深究。
在中断、时钟部分也有自己的节点标准属性,随着深入的学习我们会详细介绍这些节点标准属性。
如何获取设备树节点信息
在设备树中“节点”对应实际硬件中的设备,我们在设备树中添加了一个“led”节点,正常情况下我们可以从这个节点获取编写led 驱动所用到的所有信息,例如led 相关控制寄存器地址、led时钟控制寄存器地址等等。
这一小节我们就开始学习如何从设备树的设备节点获取我们想要的数据。内核提供了一组函数用于从设备节点获取资源(设备节点中定义的属性)的函数,这些函数以of_ 开头,称为OF 操作函数。常用的OF 函数介绍如下:
查找节点函数
根据节点路径寻找节点函数
列表14: of_find_node_by_path 函数(内核源码/include/linux/of.h)
struct device_node *of_find_node_by_path(const char *path)
参数:
- path:指定节点在设备树中的路径。
返回值:
- **device_node:**结构体指针,如果查找失败则返回NULL,否则返回device_node 类型的结构体指针,它保存着设备节点的信息。
device_node 结构体如下所示。
列表15: device_node 结构体
struct device_node {const char *name;const char *type;phandle phandle;const char *full_name;struct fwnode_handle fwnode;struct property *properties;struct property *deadprops; /* removed properties */struct device_node *parent;struct device_node *child;struct device_node *sibling;
#if defined(CONFIG_OF_KOBJ)struct kobject kobj;
#endifunsigned long _flags;void *data;
#if defined(CONFIG_SPARC)const char *path_component_name;unsigned int unique_id;struct of_irq_controller *irq_trans;
#endif
};
- name:节点中属性为name 的值
- type:节点中属性为device_type 的值
- full_name:节点的名字,在device_node 结构体后面放一个字符串,full_name 指向它
- properties:链表,连接该节点的所有属性
- parent:指向父节点
- child:指向子节点
- sibling:指向兄弟节点
得到device_node 结构体之后我们就可以使用其他of 函数获取节点的详细信息。
根据节点名字寻找节点函数
列表16: of_find_node_by_name 函数(内核源码/include/linux/of.h)
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
参数:
-
from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL 表示从根节点开始查找。
-
name:要寻找的节点名。
返回值:
- device_node:结构体指针,如果查找失败则返回NULL,否则返回device_node 类型的结构体指针,它保存着设备节点的信息。
根据节点类型寻找节点函数
列表17: of_find_node_by_type 函数(内核源码/include/linux/of.h)
struct device_node *of_find_node_by_type(struct device_node *from,const char *type)
参数:
-
from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL 表示从根节点开始查找。
-
type:要查找节点的类型,这个类型就是device_node-> type。
返回值:
- device_node: device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
根据节点类型和compatible 属性寻找节点函数
列表18: of_find_compatible_node 函数(内核源码/include/linux/of.h)
struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
相比of_find_node_by_name 函数增加了一个compatible 属性作为筛选条件。
参数:
- from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL 表示从根节点开始查找。
- type:要查找节点的类型,这个类型就是device_node-> type。
- compatible:要查找节点的compatible 属性。
返回值:
- device_node: device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
根据匹配表寻找节点函数
列表19: of_find_matching_node_and_match 函数(内核源码/include/linux/of.h)
static inline struct device_node *of_find_matching_node_and_match(struct device_node *from, const struct of_device_id *matches, const struct of_device_id **match)
可以看到,该结构体包含了更多的匹配参数,也就是说相比前三个寻找节点函数,这个函数匹配的参数更多,对节点的筛选更细。参数match,查找得到的结果。
参数:
- from:指定从哪个节点开始查找,它本身并不在查找行列中,只查找它后面的节点,如果设置为NULL 表示从根节点开始查找。
- matches:源匹配表,查找与该匹配表想匹配的设备节点。
- of_device_id:结构体如下。
返回值:
- device_node: device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
列表20: of_device_id 结构体
/** Struct used for matching a device*/struct of_device_id {char name[32];char type[32];char compatible[128];const void *data;
};
- name:节点中属性为name 的值
- type:节点中属性为device_type 的值
- compatible:节点的名字,在device_node 结构体后面放一个字符串,full_name 指向它
- data:链表,连接该节点的所有属性
寻找父节点函数
列表21: of_get_parent 函数(内核源码/include/linux/of.h)
struct device_node *of_get_parent(const struct device_node *node)
参数:
- node:指定谁(节点)要查找父节点。
返回值:
- device_node: device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
寻找子节点函数
列表22: of_get_next_child 函数(内核源码/include/linux/of.h)
struct device_node *of_get_next_child(const struct device_node *node, struct device_node *prev)
参数:
- node:指定谁(节点)要查找它的子节点。
- prev:前一个子节点,寻找的是prev 节点之后的节点。这是一个迭代寻找过程,例如寻找第二个子节点,这里就要填第一个子节点。参数为NULL 表示寻找第一个子节点。返回值:
- device_node: device_node 类型的结构体指针,保存获取得到的节点。同样,如果失败返回NULL。
这里介绍了7 个寻找节点函数,这7 个函数有一个共同特点——返回值类型相同。只要找到了节点就会返回节点对应的device_node 结构体,在驱动程序中我们就是通过这个device_node 获取设备节点的属性信息、顺藤摸瓜查找它的父、子节点等等。第一函数of_find_node_by_path 与后面六个不同,它是通过节点路径寻找节点的,“节点路径”是从设备树源文件(.dts) 中的到的。而中间四个函数是根据节点属性在某一个节点之后查找符合要求的设备节点,这个“某一个节点”是设备节点结构体(device_node),也就是说这个节点是已经找到的。最后两个函数与中间四个类似,只不过最后两个没有使用节点属性而是根据父、子关系查找。
提取属性值的of 函数
上一小节我们讲解了7 个查找节点的函数,它们有一个共同特点,找到一个设备节点就会返回这个设备节点对应的结构体指针(device_node*)。这个过程可以理解为把设备树中的设备节点“获取”到驱动中。“获取”成功后我们再通过一组of 函数从设备节点结构体(device_node)中获取我们想要的设备节点属性信息。
查找节点属性函数
列表23: of_find_property 函数(内核源码/include/linux/of.h)
struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
参数:
- np:指定要获取那个设备节点的属性信息。
- name:属性名。
- lenp:获取得到的属性值的大小,这个指针作为输出参数,这个参数“带回”的值是实际获取得到的属性大小。
返回值:
- property:获取得到的属性。property 结构体,我们把它称为节点属性结构体,如下所示。失败返回NULL。从这个结构体中我们就可以得到想要的属性值了。
列表24: property 属性结构体
struct property {char *name;int length;void *value;struct property *next;
#if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)unsigned long _flags;
#endif
#if defined(CONFIG_OF_PROMTREE)unsigned int unique_id;
#endif
#if defined(CONFIG_OF_KOBJ)struct bin_attribute attr;
#endif
};
- name:属性名
- length:属性长度
- value:属性值
- next:下一个属性
读取整型属性函数
读取属性函数是一组函数,分别为读取8、16、32、64 位数据。
列表25: of_property_read_uX_array 函数组(内核源码/include/linux/of.h)
//8 位整数读取函数
int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz)//16 位整数读取函数
int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz)//32 位整数读取函数
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz)//64 位整数读取函数
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz)
参数:
- np:指定要读取那个设备节点结构体,也就是说读取那个设备节点的数据。
- propname:指定要获取设备节点的哪个属性。
- out_values:这是一个输出参数,是函数的“返回值”,保存读取得到的数据。
- sz:这是一个输入参数,它用于设置读取的长度。
返回值:
- 返回值,成功返回0,错误返回错误状态码(非零值),-EINVAL(属性不存在),-ENODATA(没有要读取的数据),-EOVERFLOW(属性值列表太小)。
简化后的读取整型属性函数
这里的函数是对读取整型属性函数的简单封装,将读取长度设置为1。用法与读取属性函数完全一致,这里不再赘述。
列表26: of_property_read_uX 函数组(内核源码/include/linux/of.h)
//8 位整数读取函数
int of_property_read_u8 (const struct device_node *np, const char *propname,u8 *out_values)//16 位整数读取函数
int of_property_read_u16 (const struct device_node *np, const char *propname,u16 *out_values)//32 位整数读取函数
int of_property_read_u32 (const struct device_node *np, const char *propname,u32 *out_values)//64 位整数读取函数
int of_property_read_u64 (const struct device_node *np, const char *propname,u64 *out_values)
读取字符串属性函数
在设备节点中存在很多字符串属性,例如compatible、status、type 等等,这些属性可以使用查找节点属性函数of_find_property 来获取,但是这样比较繁琐。内核提供了一组用于读取字符串属性的函数,介绍如下:
列表27: of_property_read_string 函数(内核源码/include/linux/of.h)
int of_property_read_string(const struct device_node *np,const char *propname,const char **out_string)
参数:
- np:指定要获取那个设备节点的属性信息。
- propname:属性名。
- out_string:获取得到字符串指针,这是一个“输出”参数,带回一个字符串指针。也就是字符串属性值的首地址。这个地址是“属性值”在内存中的真实位置,也就是说我们可以通过对地址操作获取整个字符串属性(一个字符串属性可能包含多个字符串,这些字符串在内存中连续存储,使用’0’分隔)。
返回值:
- 返回值:成功返回0,失败返回错误状态码。
这个函数使用相对繁琐,推荐使用下面这个函数。
列表28: of_property_read_string_index 函数(内核源码/include/linux/of.h)
int of_property_read_string_index(const struct device_node *np,const char *propname, int index,const char **out_string)
相比前面的函数增加了参数index,它用于指定读取属性值中第几个字符串,index 从零开始计数。第一个函数只能得到属性值所在地址,也就是第一个字符串的地址,其他字符串需要我们手动修改移动地址,非常麻烦,推荐使用第二个函数。
读取布尔型属性函数
在设备节点中一些属性是BOOL 型,当然内核会提供读取BOOL 型属性的函数,介绍如下:
列表29: of_property_read_string_index 函数(内核源码/include/linux/of.h)
static inline bool of_property_read_bool(const struct device_node *np, const char *propname):
参数:
- np:指定要获取那个设备节点的属性信息。
- propname:属性名。
返回值:
这个函数不按套路出牌,它不是读取某个布尔型属性的值,仅仅是读取这个属性存在或者不存在。如果想要或取值,可以使用之前讲解的“全能”函数查找节点属性函数of_find_property。
内存映射相关of 函数
在设备树的设备节点中大多会包含一些内存相关的属性,比如常用的reg 属性。通常情况下,得到寄存器地址之后我们还要通过ioremap 函数将物理地址转化为虚拟地址。现在内核提供了of 函数,自动完成物理地址到虚拟地址的转换。介绍如下:
列表30: of_iomap 函数(内核源码/drivers/of/address.c)
void \__iomem *of_iomap(struct device_node *np, int index)
参数:
- np:指定要获取那个设备节点的属性信息。
- index:通常情况下reg 属性包含多段,index 用于指定映射那一段,标号从0 开始。
返回值:
- 成功,得到转换得到的地址。失败返回NULL。
内核也提供了常规获取地址的of 函数,这些函数得到的值就是我们在设备树中设置的地址值。介绍如下:
列表31: of_address_to_resource 函数(内核源码/drivers/of/address.c
int of_address_to_resource(struct device_node *dev, int index, struct resource *r)
参数:
- np:指定要获取那个设备节点的属性信息。
- index:通常情况下reg 属性包含多段,index 用于指定映射那一段,标号从0 开始。
- r:这是一个resource 结构体,是“输出参数”用于返回得到的地址信息。
返回值:
- 成功返回0,失败返回错误状态码。
resource 结构体如下所示:
struct resource {resource_size_t start;resource_size_t end;const char *name;unsigned long flags;unsigned long desc;struct resource *parent, *sibling, *child;
};
- start:起始地址
- end:结束地址
- name:属性名字
从这个结构体比较简单,很容从中得到获取得到的具体信息。这里不再赘述。
这里介绍了三类常用的of 函数,这些基本满足我们的需求,其他of 函数后续如果使用到我们在详细介绍。
向设备树中添加设备节点实验
实验说明
通常情况下我们几乎不会从零开始写一个设备树,因为一个功能完善的设备树通常比较庞大,例如本教程引用的ST 官方编写的设备树“stm32mp157c.dtsi”就多达2000 行,另外官方已经写好了主干的部分,我们只需要引用官方写好的设备树,然后根据自己的实际情况修改即可。
本节实验使用野火STM32MP157 S1 Pro 开发板,开发板上的系统保持不变。
实验准备
在板卡上的部分GPIO 可能会被系统占用,在使用前请根据需要修改/boot/uEnv.txt 文件,可注释掉某些设备树插件的加载,重启系统,释放相应的GPIO 引脚。
如本节实验中,可能在鲁班猫系统中默认使能了LED 的设备功能,用在了LED 子系统。引脚被占用后,设备树可能无法再加载或驱动中无法再申请对应的资源。
方法参考如下:

取消LED 设备树插件,以释放系统对应LED 资源,操作如下:

如若运行代码时出现“Device or resource busy”或者运行代码卡死等等现象,请按上述情况检查并按上述步骤操作。
如出现Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root 用户权限,简单的解决方案是在执行语句前加入sudo 或以root 用户运行程序。
代码讲解
本章的示例代码目录为:linux_driver/device_tree
在实际应用中我们最常见的操作是向设备节点中增加一个节点、向现有设备节点追加数据、和编写设备树插件。
根据之前讲解, 我们的系统默认使用的是“ebf_linux_kernel/arch/arm/boot/dts/stm32mp157a-basic.dts”设备树,我们就在这个设备树里尝试增加一个设备节点,如下所示。
列表33: 添加子节点
/ {model = "Embedfire STM32MP157 Star LubanCat Robot S1 Board";compatible = "st,stm32mp157a-dk1", "st,stm32mp157";aliases {thernet0 = ðernet0;serial0 = &uart4;serial1 = &usart1;serial2 = &usart2;serial3 = &usart3;};/* 添加led 节点*/led_test{#address-cells = <1>;#size-cells = <1>;rgb_led_red@0x50002000{compatible = "fire,rgb_led_red";reg = <0x50002000 0x00000020>;status = "okay";};};
};
在我们在stm32mp157a-basic.dts 设备树文件的根节点末尾新增了一个节点名为“led_test”的节点,里面只添加了几个基本属性,我们这里只是学习添加一个设备节点。
在以上代码中,led_test 节点的#address-cells = <1>,#size-cells = <1>,意味着它的子节点的reg 属性里的数据是“地址”、“长度”交替的。
第二部分是led 节点的子节点,它定义了三个属性分别为compatible、reg、status,这三个属性在“节点属性”章节已经介绍。需要注意的是rgb 属性,在父节点设置了#address-cells = <1>,#sizecells= <1>,所以这里0x50002000 表示的是地址(这里填写的是GPIO 控制寄存器的首地址),0x00000020 表示的是地址长度。“rgb_led_red@0x50002000”中的单元的地址0x50002000 要和reg属性的第一个地址一致。
内核编译设备树:
编译内核时会自动编译设备树,但是编译内核很耗时,所以我们推荐使用如下命令只编译设备树。
命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- stm32mp157_ebf_defconfig
make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs
编译成功后生成的设备树文件(.dtb) 位于源码目录下的/arch/arm/boot/dts/, 文件名为“stm32mp157a-basic.dtb”
程序结果
下载设备树
同SCP 或NFS 将编译的设备树拷贝到开发板上。替换/boot/dtbs/stm32mp157a-basic.dtb。
uboot 在启动的时候负责该目录的设备文件加载到内存,供内核解析使用。
重启开发板。
实验结果
设备树中的设备树节点在文件系统中有与之对应的文件,位于“/proc/device-tree”目录。进入“/proc/device-tree”目录如下所示。

接着进入led 文件夹,可以发现led 节点中定义的属性以及它的子节点,如下所示。

在节点属性中多了一个name,我们在led 节点中并没有定义name 属性,这是自从生成的,保存节点名。
这里的属性是一个文件,而子节点是一个文件夹,我们再次进入“rgb_led_red@0x50002000”文件夹。里面有compatible、name、reg、status 四个属性文件。我们可以使用“cat”命令查看这些属性文件,如下所示。

至此,我们已经成功的在设备树中添加了一个名为“led_test”的节点。
在驱动中获取节点属性实验
本实验目的是演示如何使用上一小节讲解的of 函数,进行本实验之前要先完成“在设备树中添加设备节点实验”,因为本实验就是从我们添加的节点中获取设备节点属性。
实验说明
本实验是一个简化的字符设备驱动,在驱动中没有实际操作硬件,仅在open 函数中调用of 函数获取设备树节点中的属性,获取成功后打印获取得到的内容。
代码讲解
列表34: 获取节点属性实验
/*.open 函数*/
static int led_chr_dev_open(struct inode *inode, struct file *filp)
{int error_status = -1;printk("\n open form device \n");/* 获取DTS 属性信息*/led_device_node = of_find_node_by_path("/led_test");if(led_device_node == NULL){printk(KERN_ALERT "\n get led_device_node failed ! \n");return -1;}/* 根据led_device_node 设备节点结构体输出节点的基本信息*/printk(KERN_ALERT "name: %s",led_device_node->name); //输出节点名printk(KERN_ALERT "child name: %s",led_device_node->child->name); //输出子节点的节点名/* 获取rgb_led_red_device_node 的子节点*/rgb_led_red_device_node = of_get_next_child(led_device_node,NULL);if(rgb_led_red_device_node == NULL){printk(KERN_ALERT "\n get rgb_led_red_device_node failed ! \n");return -1;}printk(KERN_ALERT "name: %s",rgb_led_red_device_node->name); //输出节点名printk(KERN_ALERT "parent name: %s",rgb_led_red_device_node->parent->name); //输出父节点的节点名/* 获取rgb_led_red_device_node 节点的"compatible" 属性*/rgb_led_red_property = of_find_property(rgb_led_red_device_node,"compatible",&size);if(rgb_led_red_property == NULL){printk(KERN_ALERT "\n get rgb_led_red_property failed ! \n");return -1;}printk(KERN_ALERT "size = : %d",size); //实际读取得到的长度printk(KERN_ALERT "name: %s",rgb_led_red_property->name); //输出属性名printk(KERN_ALERT "length: %d",rgb_led_red_property->length); //输出属性长度printk(KERN_ALERT "value : %s",(char*)rgb_led_red_property->value); //属性值/* 获取reg 地址属性*/error_status = of_property_read_u32_array(rgb_led_red_device_node,"reg",out_values, 2);if(error_status != 0){printk(KERN_ALERT "\n get out_values failed ! \n");return -1;}printk(KERN_ALERT"0x%08X ", out_values[0]);printk(KERN_ALERT"0x%08X ", out_values[1]);return 0;
}
- 第9-14 行:使用“of_find_node_by_path”函数寻找“test_led”设备节点。参数是“led_test”的设备节点路径。
- 第16-17 行:获取成功后得到的是一个device_node 类型的结构体指针,然后我们就可以从这个结构体中获得我们想要的数据。获取完整的属性信息可能还需要使用其他of 函数。
- 第20-27 行:获取rgb_led_red_device_node 的子节点,在第二部分我们得到了“led”节点的“设备节点结构体”这里就可以使用“of_get_next_child”函数获取它的子节点。当然我们也可以从“led”节点的“设备节点结构体”中直接读取得到它的第一个子节点。
- 第30-39 行:使用“of_find_property”函数获取“rgb_led_red”节点的“compatible”属性。
- 第42-49 行:使用“of_property_read_u32_array”函数获取reg 属性。
进入到驱动模块文件夹中,编译驱动模块:
make
该文件夹会产生get_dts_info.ko 驱动模块
程序结果
编译成功后将驱动.ko 拷贝到开发板,使用insmod 安装驱动模块然后可以在/dev/目录下找到get_dts_info。

向驱动模块随便输入一个字符
sudo sh -c "echo '1' >> /dev/get_dts_info"

从上图中可以看到,驱动程序中得到了设备树中设置的属性值。
参考资料:嵌入式Linux 驱动开发实战指南-基于STM32MP1 系列
相关文章:
第八节 Linux 设备树
Linux3.x 以后的版本才引入了设备树,设备树用于描述一个硬件平台的板级细节。在早些的linux内核,这些“硬件平台的板级细节”保存在linux 内核目录“/arch”,以ARM 平台为例“硬件平台的板级细节”保存在“/arch/arm/plat-xxx”和“/arch/arm…...
一文了解kafka消息队列,实现kafka的生产者(Producer)和消费者(Consumer)的代码,消息的持久化和消息的同步发送和异步发送
文章目录1. kafka的介绍1.2 Kafka适合的应用场景1.2 Kafka的四个核心API2. 代码实现kafka的生产者和消费者2.1 引入加入jar包2.2 生产者代码2.3 消费者代码2.4 介绍kafka生产者和消费者模式3. 消息持久化4. 消息的同步和异步发送5. 参考文档1. kafka的介绍 最近在学习kafka相关…...
数学建模学习笔记(20)典型相关分析
典型相关分析概述:研究两组变量(每组变量都可能有多个指标)之间的相关关系的一种多元统计方法,能够揭示两组变量之间的内在联系。 典型相关分析的思想:把多个变量和多个变量之间的相关化为两个具有代表性的变量之间的…...
EL表达式
EL的概念JSP表达式语言(EL)使得访问存储在JavaBean中的数据变得非常简单。EL的作用用于替换作用域对象.getAttribute("name");3. EL的应用(获取基本类型、字符串)既可以用来创建算术表达式也可以用来创建逻辑表达式。在…...
优先级队列(PriorityQueue 和 Top-K问题)
一、PriorityQueue java中提供了两种优先级队列:PriorityQueue 和 PriorityBlockingQueue。其中 PriorityQueue 是线程不安全的,PriorityBolckingQueue 是线程安全的。 PriorityQueue 使用的是堆,且默认情况下是小堆——每次获取到的元素都是…...
计算机组成与设计04——处理器
系列文章目录 本系列博客重点在深圳大学计算机系统(3)课程的核心内容梳理,参考书目《计算机组成与设计》(有问题欢迎在评论区讨论指出,或直接私信联系我)。 第一章 计算机组成与设计01——计算机概要与技…...
IT行业那么辛苦,我们为什么还要选择它?
疫情三年,我们学会了什么?工作诚可贵,技能价更高。 搞IT辛苦?有啥辛苦的?说什么辛苦?能有工作,工资又高,还要什么自行车,有啥搞啥吧!每次看到网络上有人问有…...
PyTorch学习笔记:nn.CrossEntropyLoss——交叉熵损失
PyTorch学习笔记:nn.CrossEntropyLoss——交叉熵损失 torch.nn.CrossEntropyLoss(weightNone, size_averageNone, ignore_index-100, reduceNone, reductionmean, label_smoothing0.0)功能:创建一个交叉熵损失函数: l(x,y)L{l1,…,lN}T&…...
【VictoriaMetrics】什么是VictoriaMetrics
VictoriaMetrics是一个快速、经济、可扩展的监控解决方案和时间序列数据库,有单机版和集群版本,基础功能及集群版本基本功能不收费,VictoriaMetrics有二进制安装版本、Docker安装版本等多种安装方式,其源码及部署包更新迭代很快,VictoriaMetrics具有以下突出特点: 它可以作…...
(第五章)OpenGL超级宝典学习:统一变量(uniform variable)
统一变量 前言 本篇在讲什么 本篇记录对glsl中的变量uniform的认知和学习 本篇适合什么 适合初学Open的小白 适合想要学习OpenGL中uniform的人 本篇需要什么 对C语法有简单认知 对OpenGL有简单认知 最好是有OpenGL超级宝典蓝宝书 依赖Visual Studio编辑器 本篇的特色 …...
数据存储技术复习(四)未完
1.什么是NAS。一般用途服务器与NAS设备之间有何不同。NAS是一个基于IP的专用高性能文件共享和存储设备。—般用途服务器可用于托管任何应用程序,因为它运行的是一般用途操作系统NAS设备专用于文件服务。它具有专门的操作系统,专用于通过使用行业标准协议…...
Rust编码的信息窃取恶意软件源代码公布,专家警告已被利用
黑客论坛上发布了一个 用Rust编码的信息窃取恶意软件源代码 ,安全分析师警告,该恶意软件已被积极用于攻击。 该恶意软件的开发者称,仅用6个小时就开发完成,相当隐蔽, VirusTotal的检测率约为22% 。 恶意软件开发者在…...
diffusers编写自己的推理管道
英文文献:Stable Diffusion with 🧨 Diffusers 编写自己的推理管道 最后,我们展示了如何使用diffusers. 编写自定义推理管道是对diffusers库的高级使用,可用于切换某些组件,例如上面解释的 VAE 或调度程序。 例如&a…...
计算机操作系统 左万利 第二章课后习题答案
计算机操作系统 左万利 第二章课后习题答案 1、为何引进多道程序设计,在多道程序设计中,内存中作业的道数是否越多越好?说明原因。 引入多道程序设计技术是为了提高计算机系统资源的利用率。在多道程序系统中,内存中作业的道数并…...
CODESYS开发教程10-文件读写(SysFile库)
今天继续我们的小白教程,老鸟就不要在这浪费时间了😊。 前面一期我们介绍了CODESYS的文件操作库CAA File。这一期主要介绍CODESYS的SysFile库所包含的文件读写功能块,主要包括文件路径、名称、大小的获取以及文件的创建、打开、读、写、拷贝…...
Linux安装redis
Linux安装redis一.下载二.解压配置1.创建文件夹2.上传文件3.解压4.编译配置三.启动测试1.启动2.防火墙配置3.测试四.设置开机自启1.配置脚本2.添加服务3.测试一.下载 redis官网:https://redis.io/ redis官方下载地址:http://download.redis.io/releases…...
计算机组成与体系结构 性能设计 William Stallings 第2章 性能问题
2.1 优化性能设计例如,当前需要微处理器强大功能的桌面应用程序包括:图像处理、三维渲染、语音识别、视频会议、多媒体创作、文件的声音和视频注释、仿真建模从计算机组成与体系结构的角度来看,一方面,现代计算机的基本组成与50多…...
anaconda详细介绍、安装及使用(python)
anaconda详细介绍、安装及使用1 介绍1.1 简介1.2 特点1.3 版本下载2 Anaconda管理Python包命令3 安装3.1 windows安装4 操作4.1 Conda 操作4.2 Anaconda Navigator 操作4.3 Spyder 操作4.4 Jupyter Notebook 操作5 示例参考1 介绍 1.1 简介 Anaconda是用于科学计算(…...
雅思经验(6)
反正我是希望遇到的雅思听力section 4.里面填空的地方多一些,之后单选的部分少一些。练了一下剑9 test3 的section 4,感觉还是不难的,都是在复现,而且绕的弯子也不是很多。本次考试的目标就是先弄一个六分,也就是说&am…...
CentOS9源码编译libvirtd工具
卸载原有版本libvirt [rootcentos9 ~]# yum remove libvirt Centos9配置网络源 [rootcentos9 ~]# dnf config-manager --set-enabled crb [rootcentos9 ~]# dnf install epel-release epel-next-release 安装依赖包 [rootcentos9 ~]# yum install -y libtirpc-devel libxml2-de…...
Linux应用开发之网络套接字编程(实例篇)
服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …...
大数据零基础学习day1之环境准备和大数据初步理解
学习大数据会使用到多台Linux服务器。 一、环境准备 1、VMware 基于VMware构建Linux虚拟机 是大数据从业者或者IT从业者的必备技能之一也是成本低廉的方案 所以VMware虚拟机方案是必须要学习的。 (1)设置网关 打开VMware虚拟机,点击编辑…...
基础测试工具使用经验
背景 vtune,perf, nsight system等基础测试工具,都是用过的,但是没有记录,都逐渐忘了。所以写这篇博客总结记录一下,只要以后发现新的用法,就记得来编辑补充一下 perf 比较基础的用法: 先改这…...
OkHttp 中实现断点续传 demo
在 OkHttp 中实现断点续传主要通过以下步骤完成,核心是利用 HTTP 协议的 Range 请求头指定下载范围: 实现原理 Range 请求头:向服务器请求文件的特定字节范围(如 Range: bytes1024-) 本地文件记录:保存已…...
页面渲染流程与性能优化
页面渲染流程与性能优化详解(完整版) 一、现代浏览器渲染流程(详细说明) 1. 构建DOM树 浏览器接收到HTML文档后,会逐步解析并构建DOM(Document Object Model)树。具体过程如下: (…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Python ROS2【机器人中间件框架】 简介
销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...
[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...
Proxmox Mail Gateway安装指南:从零开始配置高效邮件过滤系统
💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「storms…...
