11.1 Linux 设备树
一、什么是设备树?
设备树(Device Tree),描述设备树的文件叫做 DTS(DeviceTree Source),这个 DTS 文件采用树形结构描述板级设备,也就是开发板上的设备信息:
树的主干就是系统总线, IIC 控制器、 GPIO 控制器、 SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02这两个 IIC 设备, IIC2 上只接了 MPU6050 这个设备。
一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、 SPI 设备等), .dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)。
Linux 内核中 ARM 架构下有太多垃圾板级信息文件,所以才引进设备树。
二、DTS、DTB 和 DTC
DTS 是设备树源码文件, DTB 是将DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb 需要用到 DTC 工具。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:
cd
cd /linux/atk-mpl/linux/my_linux/linux-5.4.31\
make all
#或者
make dtbs
# make all命令是编译 Linux 源码中的所有东西,包括 uImage,.ko 驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令,“make dtbs”会编译选中的所有设备树文件
如果只要编译指定的某个设备树,比如 ST 官方编写的“stm32mp157d-ed1.dts”,可以输入如下命令:
make stm32mp157d-ed1.dtb
每个板子都有一个对应的 DTS 文件,如何确定编译哪一个 DTS 文件呢? 就拿我们手中的 STM32MP1 这个芯片对应的板子为例,打开arch/arm/boot/dts/Makefile :
dtb-$(CONFIG_ARCH_STM32) += \ # 981 行开始
stm32f429-disco.dtb \
stm32f469-disco.dtb \
stm32f746-disco.dtb \
stm32f769-disco.dtb \
stm32429i-eval.dtb \
stm32746g-eval.dtb \
stm32h743i-eval.dtb \
stm32h743i-disco.dtb \
stm32mp157a-avenger96.dtb \
stm32mp157a-dk1.dtb \
stm32mp157d-dk1.dtb \
stm32mp157c-dk2.dtb \
stm32mp157f-dk2.dtb \
stm32mp157c-dk2-a7-examples.dtb \
stm32mp157c-dk2-m4-examples.dtb \
stm32mp157f-dk2-a7-examples.dtb \
stm32mp157f-dk2-m4-examples.dtb \
stm32mp157a-ed1.dtb \
stm32mp157c-ed1.dtb \
stm32mp157d-ed1.dtb \
stm32mp157f-ed1.dtb \
stm32mp157a-ev1.dtb \
stm32mp157c-ev1.dtb \
stm32mp157d-ev1.dtb \
stm32mp157f-ev1.dtb \
stm32mp157c-ev1-a7-examples.dtb \
stm32mp157c-ev1-m4-examples.dtb \
stm32mp157f-ev1-a7-examples.dtb \
stm32mp157f-ev1-m4-examples.dtb \
stm32mp157d-atk.dtb # 这是之前加的
当选中 STM32MP1 这个 SOC 以后(CONFIG_ARCH_STM32=y),所有使用到STM32MP1 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用 STM32MP1 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$( CONFIG_ARCH_STM32)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb 文件。
在哪用到了.dtb 文件呢,其实就在Uboot开启操作系统的时候用到了。
三、DTS 语法
一般情况不会从头到尾写一个 .dts 文件,大多时候都是去修改 SOC 厂商提供的 .dts 文件上修改。学习一遍 DTS 语法可以让我们修改 .dts 文件。
1. dtsi 头文件
在设备树中可以引用.h、.dtsi 和 .dts 文件。只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀。
前 STM32MP1 系列里有 stm32mp151、 stm32mp153和 stm32mp157 这三款 SOC,其中 151 是外设最少的, 153 和 157 的外设是在 151 的基础上逐渐增加的。因此 151 就相当于“基类”, 153 和 157 是在 151 基础上得到的“派生类”。因此 ST就把最基本的外设资源都写在 stm32mp151.dtsi 文件里。 stm32mp151.dtsi 就是描述 151、 153 和 157 共有的外设信息的。
/ {
#address-cells = <1>;
#size-cells = <1>;
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
...
nvmem-cell-names = "part_number";
#cooling-cells = <2>;
};
};
cpu0_opp_table: cpu0-opp-table {
compatible = "operating-points-v2";
opp-shared;
};
...
spi2: spi@4000b000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32h7-spi";
reg = <0x4000b000 0x400>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc SPI2_K>;
resets = <&rcc SPI2_R>;
dmas = <&dmamux1 39 0x400 0x01>,
<&dmamux1 40 0x400 0x01>;
dma-names = "rx", "tx";
power-domains = <&pd_core>;
status = "disabled";
};
}
顶层节点是根节点"/",下面包含了两个子节点:cpus和spi2。每个节点都使用花括号括起来,并包含一系列属性和属性值。在cpus节点中定义了一个子节点cpu0,用于描述第一个CPU的配置,spi2节点中定义了SPI控制器的配置信息。
2. 设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息, 属性就是键—值对,每个属性由一个键和一个值组成。以下是缩减后的设备树模板:
/ { # "/" 是根节点,每个设备树文件都只有一个根节点。
#address-cells = <1>;
#size-cells = <1>;
aliases { # 子节点
serial0 = &uart4;
};
cpus { # 子节点
#address-cells = <1>;
#size-cells = <0>;
# 节点标签:label:node-name@unit-address
cpu0: cpu@0 { # 引入节点标签是为了可以直接通过 &label来访问,比如cpu0,可以直接用&cpu0访问
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
clocks = <&scmi0_clk CK_SCMI0_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
nvmem-cells = <&part_number_otp>;
nvmem-cell-names = "part_number";
#cooling-cells = <2>;
};
};
soc { # 子节点
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges;
sram: sram@10000000 { # sram是soc的子节点
compatible = "mmio-sram";
reg = <0x10000000 0x60000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x10000000 0x60000>;
};
};
}
# 节点格式:node-name@unit-address
# 其中 node-name 是节点名字,比如"uart1"表示UART1外设,"unit-address"表示设备地址或寄存器首地址,没有地址可以不要,比如 cpu@0,soc
① 字符串
compatible = "arm,cortex-a7";
设置compatible 属性为字符串 "arm,cortex-a7,"。
② 32位无符号整数
reg = <0>;
设置 reg 属性的值为 0, reg 的值也可以设置为一组值。reg = <0 0x123456 100>;
③ 字符串列表
compatible = "st,stm32mp157d-atk", "st,stm32mp157";
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开。
3. 标准属性
① compatible 属性
compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性! compatible 属性的值是一个字符串列表, compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序, compatible 属性的值格式如下所示:
"manufacturer,model"
# 其中 manufacturer 表示厂商, model 一般是模块对应的驱动名字。
比如 stm32mp15xx-dkx.dtsi中有一个音频设备节点,这个节点的音频芯片采用的Cirrus Logic公司出品的cs42l51:
compatible = "cirrus,cs42l51";
# 属性值为“cirrus,cs42l51”,其中‘cirrus’表示厂商是 Cirrus Logic,“cs42l51”表示驱动模块名字
compatible 也可以多个属性值比如:
compatible = "cirrus,my_cs42l51","cirrus,cs42l51";
# 这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查,以此类推,直到查找完 compatible 属性中的所有值。
一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件cs42l51.c 中有如下内容:
const struct of_device_id cs42l51_of_match[] = {
{ .compatible = "cirrus,cs42l51", },
{ }
};
# 数组 cs42l51_of_match 就是 cs42l51.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“cirrus,cs42l51”。如果在设备树中有哪个节点的 compatible 属性值与此相等,那么这个节点就会使用此驱动文件。
② model 属性
model 属性值也是一个字符串,一般 model 属性描述开发板的名字或者设备模块信息,比如:
model = "STMicroelectronics STM32MP157C-DK2 Discovery Board";
③ status 属性
status 属性值也是字符串,字符串是设备的状态信息:
“okay” | 表明设备是可操作的。 |
“disabled” | 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于 disabled 的具体含义还要看设备的绑定文档。 |
“fail” | 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。 |
“fail-sss” | 含义和“fail”相同,后面的 sss 部分是检测到的错误内容。 |
④ #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 这个数据所占用的字长,比如:
cpus {
#address-cells = <1>;
#size-cells = <0>;
# 说明 cpus 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>; # 因为父节点设置了 address-cells和size-cells ,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没有设置地址长度
clocks = <&scmi0_clk CK_SCMI0_MPU>;
clock-names = "cpu";
operating-points-v2 = <&cpu0_opp_table>;
nvmem-cells = <&part_number_otp>;
nvmem-cell-names = "part_number";
#cooling-cells = <2>;
};
};
scmi_sram: sram@2ffff000 {
compatible = "mmio-sram";
reg = <0x2ffff000 0x1000>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x2ffff000 0x1000>;
scmi0_shm: scmi_shm@0 {
reg = <0 0x80>; # 设置了起始地址为0x0,地址长度为 0x80
};
};
⑤ reg 属性
reg 属性的值一般是(address, length) 。reg 属性一般用于描述设备地址空间资源信息或者设备地址信息,比如某个外设的寄存器地址范围信息或者IIC期间的设备地址等。
⑥ 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 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的 stm32mp157 来说,子地址空间和父地址空间完全相同,因此会在stm32mp151.dtsi 中找到大量的值为空的 ranges 属性。
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
ranges = <0 0x10000000 0x100000>;
# 节点soc定义了ranges属性,此属性值指定了一个 1024KB(0x100000)的地址范围,子地址空间的物理起始地址为 0,父地址空间的物理起始地址为 0x10000000
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x0 0x60000>; # reg定义了sram设备起始地址为0,寄存器长度为0x60000,经过地址转换,sram设备可以从0x10000000开始读写操作
#address-cells = <1>;
#size-cells = <1>;
ranges = <0 0x10000000 0x60000>;
};
};
4. 根节点下的 compatible 属性
每个节点都有 compatible 属性,根节点“/”也不例外,在我们新建的 stm32mp157d-atk.dts文件中根节点的 compatible 属性内容如下:
/ {
model = "STMicroelectronics STM32MP157C-DK2 Discovery Board";
compatible = "st,stm32mp157d-atk", "st,stm32mp157"; # 匹配Linux内核中的驱动程序
# 描述设备 描述设备使用的SOC
....
};
通过根节点的 compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“stm32mp157d-atk”这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“stm32mp157”这颗 SOC, Linux内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux内核。
当 Linux 内核引入设备树后就是换成 DT_MACHINE_START。
static const char *const stm32_compat[] __initconst = {
"st,stm32f429",
"st,stm32f469",
"st,stm32f746",
"st,stm32f769",
"st,stm32h743",
"st,stm32mp151",
"st,stm32mp153",
"st,stm32mp157",
NULL
};
# st,stm32mp157”与 stm32_compat 中的“ st,stm32mp157”匹配,因此 STM32MP157 开发板可以正常启动 Linux 内核。
只要某个设备(板子)根节点“/”的compatible 属性值与 stm32_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。 stm32mp157d-atk.dts 中根节点的 compatible 属性值如下 :
compatible = "st,stm32mp157d-atk", "st,stm32mp157";
总体来说:
内核启动时加载设备树。设备树描述了硬件设备的结构和特性,包括各个设备节点及其属性。
内核根据设备树中的根节点开始解析。根节点是设备树中的顶层节点,通常以/
表示。
内核读取根节点的compatible
属性。该属性包含了设备的类型和兼容性信息。
内核遍历已加载的驱动程序列表,尝试与根节点的compatible
属性进行匹配。
如果找到匹配的驱动程序,内核通过调用驱动程序中的初始化函数来初始化设备。
驱动程序的初始化函数会读取设备树中与设备相关的属性,进行设备的初始化和配置。
5. 向节点追加或修改内容
什么时候需要追加和修改内容呢?如果硬件被修改了,那么我们需要同步修改设备树文件。假设现在有个六轴芯片fxls8471, fxls8471 要接到 STM32MP157D-ATK 开发板的 I2C1 接口上,那么相当于需要在 i2c1这个节点上添加一个 fxls8471 子节点。先看一下 I2C1 接口对应的节点,打开文件 stm32mp157.dtsi 文件:
i2c1: i2c@40012000 {
compatible = "st,stm32mp15-i2c";
reg = <0x40012000 0x400>;
interrupt-names = "event", "error";
interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
<&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&rcc I2C1_K>;
resets = <&rcc I2C1_R>;
#address-cells = <1>;
#size-cells = <0>;
dmas = <&dmamux1 33 0x400 0x80000001>,
<&dmamux1 34 0x400 0x80000001>;
dma-names = "rx", "tx";
power-domains = <&pd_core>;
st,syscfg-fmp = <&syscfg 0x4 0x1>;
wakeup-source;
status = "disabled";
};
};
打开 stm32mp157d-atk.dts,在根节点后添加以下代码:
/*
追加的方法:
&i2c1{
// 要追加或修改的内容
}
*/
&i2c1 {
pinctrl-names = "default", "sleep"; // 引脚控制名称
pinctrl-0 = <&i2c1_pins_b>; // 默认引脚控制配置
pinctrl-1 = <&i2c1_pins_sleep_b>; // 睡眠模式下的引脚控制配置
status = "okay";
clock-frequency = <100000>; // I2C时钟频率
fxls8471@1e {
compatible = "fsl,fxls8471";
reg = <0x1e>; // 指定设备寄存器的地址
position = <0>;
interrupt-parent = <&gpioh>; // 定义中断信号的父节点
interrupts = <6 IRQ_TYPE_EDGE_FALLING>; // 指定中断引脚为6,并且为下降沿触发
};
};
最重要的其实就是:通过 &label 来访问节点,然后直接在里面编写要追加或者修改的内容。
四、创建小型模板设备树
编写的这个设备树没有意义,只让我们去掌握设备树的语法。
在编写设备树之前要先定义一个设备,我们就以 STM32MP157 这个 SOC 为例,我们需要在设备树里面描述的内容如下:
1、这个芯片是由两个 Cortex-A7 架构的 32 位 CPU 和 Cortex-M4 组成。
2、STM32MP157 内部 sram,起始地址为 0x10000000,大小为 384KB(0x60000)。
3、STM32MP157 内部 timers6,起始地址为 0x40004000,大小为 25.6KB(0x400)。
4、STM32MP157 内部 spi2,起始地址为 0x4000b000,大小为 25.6KB(0x400)。
5、STM32MP157 内部 usart2,起始地址为 0x4000e000,大小为 25.6KB(0x400)。
6、STM32MP157 内部 i2c1,起始地址为 0x40012000,大小为 25.6KB(0x400)。
首先在 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 路径下去修改或者创造.dts/.dtsi 文件,一般情况都是在dts目录下。首先,搭建一个仅含有根节点“/”的基础的框架并创建一个 myfirstdevicetree.dts,输入以下内容:
/ {
compatible = "st,stm32mp157d-atk", "st,stm32mp157";
};
1. 添加 cpus 节点
/* 此节点用于描述 SOC 内部的所有 CPU,因为 STM32MP157 有两个 CPU,所以在 cpus 下添加两个子节点分别为 cpu0 和 cpu1。*/
/* cpu 节点 */
cpus {
#address-cells = <1>;
#size-cells = <0>;
/* CPU0 节点 */
cpu0:cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>;
};
/* CPU1 节点 */
cpu1:cpu@1 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <1>;
};
}
2. 添加 soc 节点
像 uart, iic 控制器等等这些都属于 SOC 内部外设,因此一般会创建一个叫做 soc 的父节点来管理这些 SOC 内部外设的子节点:
/* soc 节点 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>; // soc 子节点的 reg 属性中起始地占用一个字长,地址空间长度也占用一个字长
ranges; // ranges 属性为空,说明子空间和父空间地址范围相同
};
3. 添加 sram 节点
sram 是 STM32MP157 内部 RAM, M4 内核会用到 SRAM4。 sram是soc节点的子节点。sram起始地址为0x10000000,大小为384KB:
/* soc 节点 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* sram 节点 */
sram: sram@10000000 { // 0x10000000 就是 sram 的起始地址
compatible = "mmio-sram";
reg = <0x10000000 0x60000> // sram 内存的起始地址为 0x10000000,大小为 0x60000
ranges = <0 0x10000000 0x60000>;
};
};
4. 添加 timers6、spi2、usart2 和 i2c1 节点
/* soc 节点 */
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
/* sram 节点 */
sram: sram@10000000 {
compatible = "mmio-sram";
reg = <0x10000000 0x60000>
ranges = <0 0x10000000 0x60000>;
};
/* timers6 节点 */
timers6: timer@40004000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32-timers";
reg = <0x40004000 0x400>;
};
/* spi2 节点 */
spi2: spi@4000b000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "st,stm32h7-spi";
reg = <0x4000b000 0x400>;
};
/* usart2 节点 */
usart2: serial@4000e000 {
compatible = "st,stm32h7-uart";
reg = <0x4000e000 0x400>;
};
/* i2c1 节点 */
i2c1: i2c@40012000 {
compatible = "st,stm32mp15-i2c";
reg = <0x40012000 0x400>;
};
};
五、设备树在系统中的体现
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹:
1.根节点 "/" 各个属性
“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设备树中就是根节点的5 个属性,用命令 cat 可以看 model 和 compatible 文件内容:
其实这些都是 "/" 根节点 model 和 compatible 属性值。
2.根节点 "/" 各子节点
进入/proc/device-tree/soc 目录中就可以看到 soc 节点的所有子节点:
总结:无论根节点还是子节点,白色的都是该节点的属性,蓝色的都是该节点的子节点,进入soc节点,也是一样。
六、特殊节点
在根节点“/”中有两个特殊的子节点: aliases 和 chosen。
1. aliases 子节点
打开 stm32mp157d-atk.dts 文件, aliases 节点内容如下:
aliases {
serial0 = &uart4;
};
aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过 &label 来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。
2. chosen 子节点
chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少。进入 chosen 节点发现 bootargs文件,它内容为:
这个跟我们在 Uboot 设置 bootargs 一样的值。 uboot 在启动 Linux 内核的时候会将 bootargs 的值传递给 Linux内核, bootargs 会作为 Linux 内核的命令行参数, Linux 内核启动的时候会打印出命令行参数(也就是 uboot 传递进来的 bootargs 的值) 。其实就是 uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs 属性,并且还设置了 bootargs 属性值。
七、设备树常用 OF 操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存器地址为 0X02005482,长度为 0X400,我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。 Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。
1. 查找节点的 OF 函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。
① of_find_node_by_name 函数
/*
* @description : 通过节点名字查找指定的节点
* @param - from : 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
* @param - name : 查找的节点名字
* @return : 找到的节点,如果为 NULL 表示查找失败
*/
struct device_node *of_find_node_by_name(struct device_node *from, const char *name);
② of_find_node_by_type 函数
/*
* @description : 通过 device_type 属性查找指定的节点
* @param - from : 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
* @param - type : 查找的节点对应的 type 字符串,也就是 device_type 属性值
* @return : 找到的节点,如果为 NULL 表示查找失败
*/
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);
③ of_find_compatible_node 函数
/*
* @description : 根据 device_type 和 compatible 这两个属性查找指定的节点
* @param - from : 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
* @param - type : 查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性
* @param - compatible : 要查找的节点所对应的 compatible 属性列表
* @return : 找到的节点,如果为 NULL 表示查找失败
*/
struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compatible)
④ of_find_matching_node_and_match 函数
/*
* @description : 通过 of_device_id 匹配表来查找指定的节点
* @param - from : 开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树
* @param - matches : of_device_id 匹配表,也就是在此匹配表里面查找节点
* @param - match : 找到的匹配的 of_device_id
* @return : 找到的节点,如果为 NULL 表示查找失败
*/
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);
⑤ of_find_node_by_path 函数
/*
* @description : 通过路径来查找指定的节点
* @param - path : 带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径
* @return : 找到的节点,如果为 NULL 表示查找失败
*/
inline struct device_node *of_find_node_by_path(const char *path);
2. 查找父/子节点的 OF 函数
查找节点对应的父节点或者子节点的 OF 函数。
① of_get_parent 函数
/*
* @description : 获取指定节点的父节点(如果有父节点的话)
* @param - node : 要查找的父节点的节点
* @return : 找到的父节点
*/
struct device_node *of_get_parent(const struct device_node *node);
② of_get_next_child 函数
/*
* @description : 迭代的查找子节点
* @param - node : 父节点
* @param - prev : 前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始
* @return : 找到的下一个子节点
*/
struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev);
3. 提取属性值的 OF 函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要, Linux 内核中使用结构体 property 表示属性。
① of_find_property 函数
/*
* @description : 查找指定的属性
* @param - np : 设备节点
* @param - name : 设备名字
* @param - lenp : 属性值的字节数
* @return : 找到的属性
*/
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp);
② of_property_count_elems_of_size 函数
/*
* @description : 获取属性中元素的数量,比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小
* @param - np : 设备节点
* @param - proname : 需要统计元素数量的属性名字
* @param - elem_size : 元素长度
* @return : 得到的属性元素数量
*/
int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size);
③ of_property_read_u32_index 函数
/*
* @description : 从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值
* @param - np : 设备节点
* @param - proname : 要读取的属性名字
* @param - index : 要读取的值标号
* @param - out_value : 读取到的值
* @return : 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小
*/
int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value);
④ of_property_read_u8_array 函数 (还有u16/u32/u64)
/*
* @description : 读取属性中 u8、 u16、 u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据
* @param - np : 设备节点
* @param - proname : 要读取的属性名字
* @param - out_value : 读取到的数组值,分别为 u8、 u16、 u32 和 u64
* @param - sz : 要读取的数组元素数量
* @return : 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小
*/
int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz);
⑤ of_property_read_u8 函数 (还有u16/u32/u64)
/*
* @description : 有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性
* @param - np : 设备节点
* @param - proname : 要读取的属性名字
* @param - out_value : 读取到的数组值
* @return : 0 读取成功,负值,读取失败, -EINVAL 表示属性不存在, -ENODATA 表示没有要读取的数据, -EOVERFLOW 表示属性值列表太小
*/
int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value);
⑥ of_property_read_string 函数
/*
* @description : 用于读取属性中字符串值
* @param - np : 设备节点
* @param - proname : 要读取的属性名字
* @param - out_string : 读取到的字符串值
* @return : 0,读取成功,负值,读取失败
*/
int of_property_read_string(struct device_node *np,
const char *propname,
const char **out_string);
⑦ of_n_addr_cells 函数
/*
* @description : 用于获取#address-cells 属性值
* @param - np : 设备节点
* @return : 获取到的#address-cells 属性值
*/
int of_n_addr_cells(struct device_node *np);
⑧ of_n_size_cells 函数
/*
* @description : 用于获取#size-cells 属性值
* @param - np : 设备节点
* @return : 获取到的#size-cells 属性值
*/
int of_n_size_cells(struct device_node *np);
4. 其他常用的 OF 函数
① of_device_is_compatible 函数
/*
* @description : 用于查看节点的 compatible 属性是否有包含 compat 指定的字符串,也就是检查设备节点的兼容性
* @param - device : 设备节点
* @param - compat : 要查看的字符串
* @return : 点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible属性中包含 compat 指定的字符串
*/
int of_device_is_compatible(const struct device_node *device,
const char *compat);
② of_get_address 函数
/*
* @description : 用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值
* @param - dev : 设备节点
* @param - index : 要读取的地址标号
* @param - size : 地址长度
* @param - flags : 参数,比如 IORESOURCE_IO、 IORESOURCE_MEM 等
* @return : 读取到的地址数据首地址,为 NULL 的话表示读取失败
*/
const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags);
③ of_translate_address 函数
/*
* @description : 将从设备树读取到的地址转换为物理地址
* @param - dev : 设备节点
* @param - in_addr : 要转换的地址
* @return : 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败
*/
u64 of_translate_address(struct device_node *dev,
const __be32 *addr);
④ of_address_to_resource 函数
/*
* @description : 将 reg 属性值,然后将其转换为 resource 结构体类型
* @param - dev : 设备节点
* @param - index : 地址资源标号
* @param - r : 得到的 resource 类型的资源值
* @return : 0,成功;负值,失败
*/
int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r);
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
unsigned long desc;
struct resource *parent, *sibling, *child;
};
/* 对于 32 位的 SOC 来说, resource_size_t 是 u32 类型的。其中 start 表示开始地址, end 表示
结束地址, name 是这个资源的名字, flags 是资源标志位,一般表示资源类型 */
⑤ of_iomap 函数
of_iomap 函数用于直接内存映射,在采用设备树以后,大部分的驱动都使用 of_iomap 函数。of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段。
/*
* @description : 用于直接内存映射
* @param - np : 设备节点
* @param - index : reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0
* @return : 经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败
*/
void __iomem *of_iomap(struct device_node *np,
int index);
设备树重点:设备树语法、设备树的 OF 操作函数。
总结:设备树就是查询板子信息的,我们驱动开发所需要的信息都会在上面,比如设备名字、设备地址之类的都在里面,简而言之,设备树其实就是类似于查询手册一样。