设备树

设备树的定义

设备树是用来以树形结构描述开发板板级逻辑(比如CPU数量、内存基地址、IIC上挂了什么设备…)的文件

image-20240131200735359
  • 设备树的意义:引入一个单独的文件描述板级信息,解耦板级信息和驱动,增加驱动代码的通用型

举个例子,我有2个板子,第一个板子的MPU6050接在i2c1上,第二个板子的6050接在i2c2上,如果我不用设备树的话,我就得在驱动程序里写一堆寄存器的地址;且不同的板子的驱动程序应该不同。但我如果用了设备树,就可以把寄存器相关的信息写在设备树里,然后驱动直接根据去设备树里找MPU6050节点里的地址就行了(相当于实现了动态修改硬件设别的寄存器地址),不同的板子可以共用驱动程序(内核),只需修改设备树文件

设备树语法

正如其名字,设备树是个树形结构,每个硬件都是树中的一个节点,不能在设备树中写孤立的语法。同时,如果.dtsi.dts中都定义了根节点,则该根节点会合并为一个

此外,所有设备都嵌套在根节点/{}下,我们往设备树中添加新设备时,应该找到其最近的控制器,作为其父节点(比如MPU6050节点就应该作为某个i2c控制器节点的子节点),因为可能要复用父节点中的一些属性

但是不是任何情况下新设备都需要作为某个设备的子节点,只要能通过某种方式引用到所需要的父节点中的一些属性,就不要作为子节点了,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/ {
gpio0: gpio@40000000 {
compatible = "vendor,gpio-controller";
#gpio-cells = <2>;
gpio-controller;
};

leds {
compatible = "gpio-leds";
led1 {
label = "user-led";
gpios = <&gpio0 5 GPIO_ACTIVE_HIGH>; // 关键:引用 gpio0
};
};
};

虽然leds是挂载在gpio0上的,但是它可以通过&gpio0访问到父节点中的一些属性,所以就不需要嵌套在gpio0

对其他文件的引用

设备树源码可以像C语言一样#include来引用其他文件,具体可以引用.h.dtsi.dts文件

设备节点

设备树是采用树形结构描述板子上的设备信息的文件,每个设备都是一个节点,叫作设备节点,每个设备节点都通过属性(键值对)来描述节点信息。

节点的命名

设备节点的命名方式如下:

1
2
3
label: node-name@unit-address{

}

label是节点标签,可以通过&label方便的访问到某个设备节点,而不需要再用node-name@unit-address来访问了

节点的属性

节点的属性都为键值对,其中可以为空和任意字节流,常见的几种形式如下:

1.字符串

1
compatible = "arm,cortex-a7";

2.32位无符号整形

1
reg = <0>;

3.一组32位无符号整形

1
reg = <0 0x123456 100>;

4.字符串列表

1
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";

标准属性

每一个节点都是由若干属性组成,不同的设备节点可能有不同的属性,但是有一些属性许多设备节点都会用到,这些属性被称为标准属性

下面列举一些标准属性:

1.compatible属性:用于在Linux内核中匹配此设备所使用的驱动程序,它的值是字符串,格式如下:

1
compatible = "manufacturer,model"

其中 manufacturer 表示厂商,``model` 一般是模块对应的驱动名字 ,比如

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

imx6ul-evk-wm8960和imx-audio-wm8960为驱动的名字,在使用该设备时,会到Linux内核中查找与之匹配的驱动文件。

驱动的基类struct device_driver有个成员变量of_match_table,保存着一些 compatible 值,如果设备节点的 compatible 属性值和 其中的任何一个值相等,那么就表示设备可以使用这个驱动

除了普通的设备节点,根节点也有compatible属性,且子节点的compatible属性在设备树中具有不同的作用和含义:

  • 根节点的compatible属性用于设备树文件的兼容性,用于与引导加载程序(U-Boot)匹配确定加载哪个设备树文件。
  • 子节点的compatible属性用于设备的兼容性,用于与驱动程序匹配确定加载哪个驱动程序。

2.model属性:描述设备的名称,属性值为字符串

3.status属性:描述设备的状态,属性值为字符串

4.reg属性:描述某外设寄存器的信息,属性值为(address,length)对,用于描述某外设寄存器的起始地址 + 大小

1
reg = <address1 length1 address2 length2 address3 length3……>

每个“address,length”组合表示一个地址范围,其中 address 是起始地址, length 是地址长度

5.#address-cells属性:描述子节点reg属性中每个地址信息所占的字长(32位)

6.#size-cells属性:描述子节点reg属性中每个长度信息所占的字长(32位)

7.ranges属性:是一个地址映射/转换表,由于MMU的存在,寄存器的物理地址和内存地址可能不同,因此需要转换。如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换

8.name属性:描述节点的名字,属性值为字符串

在知道了设备树有这么多属性之后,难免会有一个疑问:我怎么知道绑定某设备时创建的节点需要哪些属性?比如我要新加个MPU6050,我该在i2c节点下新增什么信息呢?

SoC厂商会给一个文档,告诉我们在新增一个设备节点时,应该写哪些属性。此文档在内核的Documentation/devicetree/bindings下,里面有不同的SoC对于某个外设应该如何写设备树文件

向节点追加内容

在实际使用设备树时,可能出现以下场景:我们自己的板子的i2c中新挂载了一个MPU6050,所以需要修改这个节点,但是由于i2c节点定义在.dtsi文件中,直接修改该文件的话会影响所有引用此文件的设备。因此需要新建一个.dts文件,并在该文件中像iic节点追加内容。

追加内容的语法如下:

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

即使用节点命名提到的&label来访问就可以了,并且如果有些属性之前被定义了,此处可以进行修改

设备树的解析流程

从源代码文件 dts 文件开始,设备树的处理过程为:

img

① dts 在 PC 机上被编译为 dtb 文件
② u-boot 把 dtb 文件传给内核
③ 内核在启动早期(setup_arch()阶段)解析DTB,将其转换为内核内部的device_node结构体

1
2
3
4
5
6
7
struct device_node {
const char *name; // 节点名(如 "i2c")
const char *full_name; // 全路径名(如 "/soc/i2c@400000")
struct property *properties; // 属性链表(如 "compatible", "reg")
struct device_node *parent; // 父节点
struct device_node *child; // 子节点
};

④ 对于符合条件device_node 实例,会被转换为 platform_device 结构体,转换条件:

  • 根节点下含有 compatile 属性的子节点

  • 含有特定 compatile 属性的节点的子节点

    • 如果一个节点的 compatile 属性,它的值是这 4 者之一: “simple-bus”,”simple-mfd”,”isa”,”arm,amba-bus”, 那么它的子结点(需含 compatile 属性)也可以转换为 platform_device。
  • 总线 I2C、 SPI 节点下的子节点: 不能转换platform_device

    • 某个总线下到子节点, 应该交给对应的总线驱动程序来处理。 它们不应该被转换为platform_device, 而应该转换为i2c_clientspi_deivce等数据结构

OF函数

设备树存在的意义就是供驱动程序使用。在编写驱动程序时,我们必须要知道设备树中某个设备节点的某个属性的值,比如某寄存器的地址+大小。Linux内核提供了一组函数可以实现该功能,且此系列API全以of_开头,因此被称为OF函数。OF的全称为:Open Firmware

查找节点

要获得一个节点的属性,首先得找到该节点。Linux内核中,使用device_node这个结构体来描述设备树中的各个节点,Linux提供了以下API来获得设备树中的某个设备节点:

1
2
3
4
5
6
7
8
9
10
11
12
//1.通过名字查找
struct device_node *of_find_node_by_name(struct device_node *from,const char *name);
//2.通过device_type属性查找
struct device_node *of_find_node_by_type(struct device_node *from, const char *type)
//3.根据 device_type 和 compatible 这两个属性查找
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type,
const char *compatible)
//4.通过 of_device_id 匹配表来查找指定的节点
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)
//5.通过路径查找
inline struct device_node *of_find_node_by_path(const char *path)

查找父/子节点

提取属性值

我们其实可以在设备树创建自定义的属性和值,属性名可以自定义,而值只能是设备树所支持的那几个类型,比如:u32/u64,bool,字符串,数组…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 读取 32 位整数
int of_property_read_u32(const struct device_node *np, const char *propname, u32 *out_value);
// 读取 64 位整数
int of_property_read_u64(const struct device_node *np, const char *propname, u64 *out_value);

// 直接读取字符串
int of_property_read_string(const struct device_node *np, const char *propname, const char ​**​out_string);
// 读取字符串数组中的某一个(index 指定)
int of_property_read_string_index(const struct device_node *np, const char *propname, int index, const char ​**​output);

//读取布尔值
bool of_property_read_bool(const struct device_node *np, const char *propname);

// 读取 u32 数组
int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz);

// 读取 u64 数组
int of_property_read_u64_array(const struct device_node *np, const char *propname, u64 *out_values, size_t sz);

其他常用的OF函数

相关文件格式

设备树文件可能有一下后缀格式:

  • .dts:设备树的源代码,一般描述板级信息(也就是开发板上有哪些 IIC 设备、 SPI 设备等)
  • .dtsi:也是设备树源代码,但更加通用,像.h文件一样可以被引用,用来描述SOC级信息(SOC 有几个 CPU、主频是多少、各个外设控制器信息等)
  • .dtb.dts源代码编译后得到的二进制文件,编译设备树源码需要使用DTC工具

设备树的编译

设备树文件每次修改时都得重新编译,然后放到nfs中,uboot启动时会把设备树文件从nfs加载内存,然后再boot

要编译设备树文件(.dts),有以下几种方式:

法1:

  • 1.修改Linux内核源码的arch/arm/boot/dts/Makefile ,在其中加入自己新写的.dts设备树文件
  • 2.在Linux内核源码的根目录下make dtbs,即可在arch/arm/boot/dts/得到对应的.dtb文件

法2:

  • 在Linux内核源码的根目录下make all,会编译所有东西,包括内核zImage,各个驱动.ko文件,以及设备树文件。最好别用这个方法,编译内核很慢的

法3:

  • 在Linux内核源码的根目录下make imx6ull-alientek-emmc.dtb,编译器会自动到arch/arm/boot/dts/imx6ull-alientek-emmc.dts并进行编译

设备树的使用

1.将编译后的.dtb文件放到nfs的tftpboot文件夹中

2.更改uboot的bootcmd环境变量,主要是把使用的设备树文件改成新编译得到的.dtb文件

具体写法:

1
setenv bootcmd 'tftp 80800000 zImage; tftp 83000000 imx6ull-alientek-emmc.dtb;bootz 80800000 - 83000000'

3.保存uboot的环境变量:saveenv

4.uboot中使用boot命令,将执行bootcmd中包含的操作,加载设备树文件到内存中,并启动内核

调试

1.查看当前系统的设备树拓扑关系:

/proc/device-tree这个目录其实就是内核所使用的设备树的根节点

  • 每个子节点都是目录中的一个文件夹,各个属性是目录中的文件

  • 属性值是字符串时,用 cat 命令可以打印出来;属性值是数值时,用 hexdump 命令可以打印出来

参考链接: