当前位置: 首页 > news >正文

RK3568 Linux驱动学习——Linux设备树

前言

在前面多次提到“设备树”这个概念,本章就来详细的谈一谈设备树。掌握设备树是 Linux 驱动开发人员必备的技能!因为在新版本的 Linux 中,ARM 相关的驱动全部
采用了设备树(也有支持老式驱动的,比较少),最新出 CPU 在系统启动的时候就支持设备树,比如 RK3568 系列使用的 Linux 版本为 4.19.232,其支持设备树,所以正点原子 ATK-DLRK3568 开发板的所有 Linux 驱动都是基于设备树的。本章就来了解一下设备树的起源、重点学习一下设备树语法。

什么是设备树

设备树(DeviceTree),将这个词分开就是"设备"和"树",描述设备树的文件叫做DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如 CPU 数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等,如下图:
设备树结构示意图

在上图中,树的主干就是系统总线,IIC 控制器、GPIO 控制器、SPI 控制器等都是接到系统主线上的分支。IIC 控制器有分为 IIC1 和 IIC2 两种,其中 IIC1 上接了 FT5206 和 AT24C02 这两个 IIC 设备,IIC2 上只接了 MPU6050 这个设备。DTS 文件的主要功能就是按照上图所示的结构来描述板子上的设备信息,DTS 文件描述设备信息是有相应的语法规则要求的,稍后会详细的讲解 DTS 语法规则。

在 3.x 版本(具体哪个版本无从考证)以前的 Linux 内核中 ARM 架构并没有采用设备
树。在没有设备树的时候 Linux 是如何描述 ARM 架构中的板级信息呢?在 Linux 内核源码中大量的 arch/arm/mach-xxx 和 arch/arm/plat-xxx 文件夹,这些文件夹里面的文件就是对应平台下的板级信息。比如在 arch/arm/mach-s3c24xx/mach-smdk2440.c 中有如下内容(有缩减):

static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {.lcdcon5	= S3C2410_LCDCON5_FRM565 |S3C2410_LCDCON5_INVVLINE |S3C2410_LCDCON5_INVVFRAME |S3C2410_LCDCON5_PWREN |S3C2410_LCDCON5_HWSWP,.type		= S3C2410_LCDCON1_TFT,.width		= 240,.height		= 320,.pixclock	= 166667, /* HCLK 60 MHz, divisor 10 */.xres		= 240,.yres		= 320,.bpp		= 16,.left_margin	= 20,.right_margin	= 8,.hsync_len	= 4,.upper_margin	= 8,.lower_margin	= 7,.vsync_len	= 4,
};static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {.displays	= &smdk2440_lcd_cfg,.num_displays	= 1,.default_display = 0,#if 0/* currently setup by downloader */.gpccon		= 0xaa940659,.gpccon_mask	= 0xffffffff,.gpcup		= 0x0000ffff,.gpcup_mask	= 0xffffffff,.gpdcon		= 0xaa84aaa0,.gpdcon_mask	= 0xffffffff,.gpdup		= 0x0000faff,.gpdup_mask	= 0xffffffff,
#endif.lpcsel		= ((0xCE6) & ~7) | 1<<4,
};static struct platform_device *smdk2440_devices[] __initdata = {&s3c_device_ohci,&s3c_device_lcd,&s3c_device_wdt,&s3c_device_i2c0,&s3c_device_iis,
};

上述代码中第 125 行的结构体变量 smdk2440_fb_info 就是描述 SMDK2440 这个开发板上的 LCD 信息的,结构体指针数组 smdk2440_devices 描述的 SMDK2440 这个开发板上的所有平台相关信息。这个仅仅是使用 2440 这个芯片的 SMDK2440 开发板下的 LCD 信息,SMDK2440 开发板还有很多的其他外设硬件和平台硬件信息。使用 2440 这个芯片的板子有很多,每个板子都有描述相应板级信息的文件,这仅仅只是一个 2440。随着智能手机的发展,每年新出的 ARM 架构芯片少说都在数十、甚至数百款,Linux 内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c 或.h 文件,都会被硬编码进 Linux 内核中,导致 Linux 内核“虚胖”。当 Linux 之父 linus 看到 ARM 社区向 Linux 内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”。从此以后 ARM 社区就引入了 PowerPC 等架构已经采用的设备树(Flattened Device Tree),将这些描述板级硬件信息的内容都从 Linux 内中分离开来,用一个专属的文件格式来描述,这个专属的文件就叫做设备树,文件扩展名为.dts。一个 SOC 可以作出很多不同的板子,这些不同的板子肯定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts 文件直接引用这个通用文件即可,这个通用文件就是.dtsi 文件,类似于 C 语言中的头文件。一般.dts 描述板级信息(也就是开发板上有哪些 IIC 设备、SPI 设备等),.dtsi 描述 SOC 级信息(也就是 SOC 有几个 CPU、主频是多少、各个外设控制器信息等)

这个就是设备树的由来,简而言之就是,Linux 内核中 ARM 架构下有太多的冗余的垃圾板级信息文件,导致 linus 震怒,然后 ARM 社区引入了设备树。

DTS、DTB 和 DTC

上一小节说了,设备树源文件扩展名为.dts,但是在前面移植 Linux 的时候却一直在使用.dtb 文件,那么 DTS 和 DTB 这两个文件是什么关系呢?DTS 是设备树源码文件,DTB 是将 DTS 编译以后得到的二进制文件。将.c 文件编译为.o 需要用到 gcc 编译器,那么将.dts 编译为.dtb 需要用到 DTC 工具!DTC 工具源码在 Linux 内核的 scripts/dtc 目录下,scripts/dtc/Makefile 文件内容如下

hostprogs-y	:= dtc
always		:= $(hostprogs-y)dtc-objs	:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \srcpos.o checks.o util.o
dtc-objs	+= dtc-lexer.lex.o dtc-parser.tab.o

可以看出,DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这个主机文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令:(对于 RK3568,需要指定 ARCH=arm64)

make ARCH=arm64 all

或者:

make ARCH=arm64 dtbs

“make ARCH=arm64 all” 命令是编译 Linux 源码中的所有东西,包括 uImage/zImage,.ko 驱动模块以及设备树,如果只是编译设备树的话建议使用 “make ARCH=arm64 dtbs” 命令,“make ARCH=arm64 dtbs” 会编译选中的所有设备树文件。如果只要编译指定的某个设备树,比如 ATK-DLRK3568 开发板对应的 “rk3568-atk-evb1-ddr4-v10-linux.dts”,可以输入如下命令:

make ARCH=arm64 rockchip/rk3568-atk-evb1-ddr4-v10-linux.dtb

基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,那么如何确定编译哪一个 DTS 文件呢?以 RK3568 这款芯片对应的板子为例来看一下,打开 arch/arm64/boot/dts/rockchip/Makefile,有如下内容:

dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10-android9.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10-linux.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb1-ddr4-v10-linux-spi-nor.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb2-lp4x-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb2-lp4x-v10-bt1120-to-hdmi.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb4-lp3-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb5-ddr4-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-linux.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-rk628-bt1120-to-hdmi.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-rk628-rgb2hdmi.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb6-ddr3-v10-rk630-bt656-to-cvbs.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-evb7-ddr4-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-iotest-ddr3-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-iotest-ddr3-v10-linux.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v10.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v10-linux.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v10-linux-spi-nand.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v12-linux.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-nvr-demo-v12-linux-spi-nand.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk3568-atk-evb1-ddr4-v10-linux.dtb
dtb-$(CONFIG_ARCH_ROCKCHIP) += rk630-rk3568-ddr3-v10.dtb

可以看出,比如这个 Makefile 下有许多 RK3326、RK3566、RK3568 等不同的.dtb 文件。如果使用 RK3568 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到这个 Makefile 下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb 文件。

DTS 语法

虽然基本上不会从头到尾重写一个.dts 文件,大多时候是直接在 SOC 厂商提供的.dts 文件上进行修改。但是 DTS 文件语法还是需要详细的学习一遍,因为后续工作中肯定需要修改.dts 文件。不要看到要学习新的语法就觉得会很复杂,DTS 语法非常的人性化,是一种 ASCII 文本文件,不管是阅读还是修改都很方便。

本节就以 rk3568-atk-evb1-ddr4-v10-linux.dts 这个文件为例来讲解一下 DTS 语法。

dtsi 头文件

和 C 语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 rk3568-atk-evb1-ddr4-v10-linux.dtsi 中有如下所示内容:

#include "rk3568-atk-evb1-ddr4-v10.dtsi"
#include "rk3568-linux.dtsi"

上述代码中使用 “#include” 来引用 “rk3568-atk-evb1-ddr4-v10.dtsi” 和 “rk3568-
linux.dtsi”这两个.dtsi 头文件。

设备树里面除了可以通过 “#include” 来引用.dtsi 文件,也可以引用.h 文件头文件,打开 rk3568.dtsi 这个文件,找到如下代码:

#include <dt-bindings/clock/rk3568-cru.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include <dt-bindings/interrupt-controller/irq.h>
#include <dt-bindings/pinctrl/rockchip.h>
#include <dt-bindings/soc/rockchip,boot-mode.h>
#include <dt-bindings/phy/phy.h>
#include <dt-bindings/power/rk3568-power.h>
#include <dt-bindings/soc/rockchip-system-status.h>
#include <dt-bindings/suspend/rockchip-rk3568.h>
#include <dt-bindings/thermal/thermal.h>
#include "rk3568-dram-default-timing.dtsi"

设备树文件不仅可以应用 C 语言里面的.h 头文件,甚至也可以引用.dts 文件。因此在.dts 设备树文件中,可以通过 “#include” 来引用.h、.dtsi 和.dts 文件。只是,在编写设备树头文件的时候最好选择.dtsi 后缀。

一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、IIC 等等。比如 rk3568.dtsi 就是描述 RK3568 芯片本身的外设信息,内容如下:

/ {compatible = "rockchip,rk3568";interrupt-parent = <&gic>;#address-cells = <2>;#size-cells = <2>;aliases {csi2dphy0 = &csi2_dphy0;csi2dphy1 = &csi2_dphy1;csi2dphy2 = &csi2_dphy2;dsi0 = &dsi0;dsi1 = &dsi1;ethernet0 = &gmac0;ethernet1 = &gmac1;gpio0 = &gpio0;gpio1 = &gpio1;gpio2 = &gpio2;gpio3 = &gpio3;gpio4 = &gpio4;i2c0 = &i2c0;i2c1 = &i2c1;i2c2 = &i2c2;i2c3 = &i2c3;i2c4 = &i2c4;i2c5 = &i2c5;mmc0 = &sdhci;mmc1 = &sdmmc0;mmc2 = &sdmmc1;mmc3 = &sdmmc2;serial0 = &uart0;serial1 = &uart1;serial2 = &uart2;serial3 = &uart3;serial4 = &uart4;serial5 = &uart5;serial6 = &uart6;serial7 = &uart7;serial8 = &uart8;serial9 = &uart9;spi0 = &spi0;spi1 = &spi1;spi2 = &spi2;spi3 = &spi3;spi4 = &sfc; // for U-Boot};cpus {#address-cells = <2>;#size-cells = <0>;cpu0: cpu@0 {device_type = "cpu";compatible = "arm,cortex-a55";reg = <0x0 0x0>;enable-method = "psci";clocks = <&scmi_clk 0>;operating-points-v2 = <&cpu0_opp_table>;cpu-idle-states = <&CPU_SLEEP>;#cooling-cells = <2>;dynamic-power-coefficient = <187>;};cpu1: cpu@100 {device_type = "cpu";compatible = "arm,cortex-a55";reg = <0x0 0x100>;enable-method = "psci";clocks = <&scmi_clk 0>;operating-points-v2 = <&cpu0_opp_table>;cpu-idle-states = <&CPU_SLEEP>;};cpu2: cpu@200 {device_type = "cpu";compatible = "arm,cortex-a55";reg = <0x0 0x200>;enable-method = "psci";clocks = <&scmi_clk 0>;operating-points-v2 = <&cpu0_opp_table>;cpu-idle-states = <&CPU_SLEEP>;};cpu3: cpu@300 {device_type = "cpu";compatible = "arm,cortex-a55";reg = <0x0 0x300>;enable-method = "psci";clocks = <&scmi_clk 0>;operating-points-v2 = <&cpu0_opp_table>;cpu-idle-states = <&CPU_SLEEP>;};idle-states {entry-method = "psci";CPU_SLEEP: cpu-sleep {compatible = "arm,idle-state";local-timer-stop;arm,psci-suspend-param = <0x0010000>;entry-latency-us = <100>;exit-latency-us = <120>;min-residency-us = <1000>;};};};

上述代码中第 65-122 行就是 RK3568 的 CPU 信息,这个节点信息描述了 RK3568 这颗 SOC 所有的 CPU 信息,一共有 4 个 CPU,也就是 4 核,架构是 cortex-A55。 rk3568.dtsi 文件中不仅仅描述了CPU信息,RK3568这颗SOC所有的外设都描述的清清楚楚,比如i2c0-i2c5、uart0-uart9 等等,关于这些设备节点信息的具体内容后面具体章节里面再详细的讲解。

设备节点

设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下文件是结合 RK 官方的设备树缩减出来设备树文件内容:

1  / {
2      compatible = "rockchip,rk3568";
3
4      interrupt-parent = <&gic>;
5      #address-cells = <2>;
6      #size-cells = <2>;
7
8      aliases {
9          serial0 = &uart0;
10     };
11
12     cpus {
13         #address-cells = <2>;
14         #size-cells = <0>;
15
16     cpu0: cpu@0 {
17         device_type = "cpu";
18         compatible = "arm,cortex-a55";
19         reg = <0x0 0x0>;
20         enable-method = "psci";
21         clocks = <&scmi_clk 0>;
22         operating-points-v2 = <&cpu0_opp_table>;
23         cpu-idle-states = <&CPU_SLEEP>;
24         #cooling-cells = <2>;
25         dynamic-power-coefficient = <187>;
26     };
27
28     cpu1: cpu@100 {
29         device_type = "cpu";
30         compatible = "arm,cortex-a55";
31         reg = <0x0 0x100>;
32         enable-method = "psci";
33         clocks = <&scmi_clk 0>;
34         operating-points-v2 = <&cpu0_opp_table>;
35         cpu-idle-states = <&CPU_SLEEP>;
36     };
37
38     cpu2: cpu@200 {
39         device_type = "cpu";
40         compatible = "arm,cortex-a55";
41         reg = <0x0 0x200>;
42         enable-method = "psci";
43         clocks = <&scmi_clk 0>;
44         operating-points-v2 = <&cpu0_opp_table>;
45         cpu-idle-states = <&CPU_SLEEP>;
46     };
47
48     cpu3: cpu@300 {
49         device_type = "cpu";
50         compatible = "arm,cortex-a55";
51         reg = <0x0 0x300>;
52         enable-method = "psci";
53         clocks = <&scmi_clk 0>;
54         operating-points-v2 = <&cpu0_opp_table>;
55         cpu-idle-states = <&CPU_SLEEP>;
56     };
57
58     idle-states {
59         entry-method = "psci";
60         CPU_SLEEP: cpu-sleep {
61             compatible = "arm,idle-state";
62             local-timer-stop;
63             arm,psci-suspend-param = <0x0010000>;
64             entry-latency-us = <100>;
65             exit-latency-us = <120>;
66             min-residency-us = <1000>;
67             };
68         };
69     };
70 
71     i2c0: i2c@fdd40000 {
72         compatible = "rockchip,rk3399-i2c";
73         reg = <0x0 0xfdd40000 0x0 0x1000>;
74         clocks = <&pmucru CLK_I2C0>, <&pmucru PCLK_I2C0>;
75         clock-names = "i2c", "pclk";
76         interrupts = <GIC_SPI 46 IRQ_TYPE_LEVEL_HIGH>;
77         pinctrl-names = "default";
78         pinctrl-0 = <&i2c0_xfer>;
79         #address-cells = <1>;
80         #size-cells = <0>;
81         status = "disabled";
82     };
83 };

第 1 行,“/” 是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,在 rk3568.dtsi 和 rk3568-linux.dtsi 这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点的内容会合并成一个根节点。

第 8、12 和 71 行,aliases、cpus 和 i2c0 是根节点 “/” 的三个子节点,在设备树中节点命名格式如下:

node-name@unit-address

其中 “node-name” 是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如 “uart1” 就表示这个节点是 UART1 外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话 “unit-address” 可以不要,比如“cpus”、“cpu@f00”、“i2c@ff3f0000”。

但是在示例代码中看到的节点命名却如下所示:

cpu0:cpu@0

上述命令并不是 “node-name@unit-address” 这样的格式,而是用“:”隔开成了两部分,“:”前面是节点标签(label),“:”后面的才是节点名字,格式如下所示:

label: node-name@unit-address

引入 label 的目的就是为了方便访问节点,可以直接通过 &label 来访问这个节点,比如通过 &cpu0 就可以访问 “cpu@f00” 这个节点,而不需要输入完整的节点名字。再比如节点 “i2c0: i2c@ff3f0000”,节点 label 是 i2c0,而节点名字就很长了,为 “i2c@ff3f0000”。很明显通过 &i2c0 来访问 “i2c@ff3f0000” 这个节点要方便很多!

第 16 行,cpu0 也是一个节点,只是 cpu0 是 cpus 的子节点。

每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:

  1. 字符串
compatible = "rockchip,rk3568";

上述代码设置 compatible 属性的值为字符串 “rockchip,rk3568”。

  1. 32 位无符号整数
reg = <0>;

上述代码设置 reg 属性的值为 0,reg 的值也可以设置为一组值,比如:

reg = <0 0x123456 100>;
  1. 字符串列表

属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:

compatible = "rockchip,rk3568-evb ", "rockchip,rk3568";

上述代码设置属性 compatible 的值为 “rockchip,RK3568-evb” 和 “rockchip,rk3568”。

标准属性

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

  1. compatible 属性

compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible 属性的值是一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible 属性值的格式如下所示:

"manufacturer,model"

其中 manufacturer 表示厂商,model 一般是模块对应的驱动名字。比如 rk3568-atk-evb1-ddr4-v10.dtsi中有一个 MIPI 摄像头节点,这个节点的摄像头芯片采用的SONY 公司出品的 IMX415,compatible 属性值如下:

compatible = "sony,imx415";

属性值为 “sony,imx415”,其中‘sony’表示厂商是 sony,也就是索尼,“imx415” 表示驱动模块名字。

compatible 也可以多个属性值。比如:

compatible = "ilitek,ili9881d", "simple-panel-dsi";

这样设备就有两个属性值,这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查,以此类推,直到查找完 compatible 属性中的所有值。

一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件 imx415.c 中有如下内容:

static const struct of_device_id imx415_of_match[] = {{ .compatible = "sony,imx415" },{},
};

数组 imx415_of_match 就是 imx415.c 这个驱动文件的匹配表,此匹配表只有一个匹配值 “sony,imx415”。如果在设备树中有哪个节点的 compatible 属性值与此相等,那么这个节点就会使用此驱动文件。

  1. model 属性

model 属性值也是一个字符串,一般 model 属性描述开发板的名字或者设备模块信息,比如名字,比如:

model = "Rockchip rk3568 EVB DDR4 V10 Board";
  1. status 属性

status 属性看名字就知道是和设备状态有关的,status 属性值也是字符串,字符串是设备的状态信息,可选的状态如下图所示:
status 属性值表

  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 这个数据所占用的字长,比如:

cpus {#address-cells = <1>;#size-cells = <0>;cpu0: cpu@f00 {device_type = "cpu";compatible = "arm,cortex-a7";reg = <0xf00>;resets = <&cru SRST_CORE0>;operating-points = </* KHz    uV */816000 1000000>;#cooling-cells = <2>; /* min followed by max */clock-latency = <40000>;clocks = <&cru ARMCLK>;};cpu1: cpu@f01 {device_type = "cpu";compatible = "arm,cortex-a7";reg = <0xf01>;resets = <&cru SRST_CORE1>;};cpu2: cpu@f02 {device_type = "cpu";compatible = "arm,cortex-a7";reg = <0xf02>;resets = <&cru SRST_CORE2>;};cpu3: cpu@f03 {device_type = "cpu";compatible = "arm,cortex-a7";reg = <0xf03>;resets = <&cru SRST_CORE3>;};};amba {compatible = "simple-bus";#address-cells = <1>;#size-cells = <1>;ranges;pdma: pdma@110f0000 {compatible = "arm,pl330", "arm,primecell";reg = <0x110f0000 0x4000>;interrupts = <GIC_SPI 0 IRQ_TYPE_LEVEL_HIGH>,<GIC_SPI 1 IRQ_TYPE_LEVEL_HIGH>;#dma-cells = <1>;clocks = <&cru ACLK_DMAC>;clock-names = "apb_pclk";};};

第 2,3 行,节点 cpus 的#address-cells = <1>,#size-cells = <0>,说明 cpus 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。
第 5 行,子节点 cpu0: cpu@0 的 reg 属性值为 <0xf00>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此 addres=0xf00,没有 length 的值,相当于设置了起始地址,而没有设置地址长度。
第 20,21 行,设置 amba 节点#address-cells = <1>,#size-cells = <1>,说明 amba 的所有子节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。
第26行,amba子节点dmac: dma-controller@ff4e0000的reg属性值为< 0xff4e0000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0xff4e0000,length= 0x4000,相当于设置了起始地址为 0xff4e0000,地址长度为 0x4000。

  1. reg 属性

reg 属性前面已经提到过了,reg 属性的值一般是(address,length)对。reg 属性一般用于描述设备地址空间资源信息或者设备地址信息,比如某个外设的寄存器地址范围信息,或者 IIC 器件的设备地址等,比如在 rk3568.dtsi 中有如下内容:

uart5: serial@fe690000 {compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";reg = <0x0 0xfe690000 0x0 0x100>;interrupts = <GIC_SPI 121 IRQ_TYPE_LEVEL_HIGH>;clocks = <&cru SCLK_UART5>, <&cru PCLK_UART5>;clock-names = "baudclk", "apb_pclk";reg-shift = <2>;reg-io-width = <4>;dmas = <&dmac0 10>, <&dmac0 11>;pinctrl-names = "default";pinctrl-0 = <&uart5m0_xfer>;status = "disabled";};

uart5 节点描述了 rk3568 系列芯片的 UART5 相关信息,重点是第 3104 行的 reg 属性。由于 uart5 的父节点“/”设置了#address-cells = <1>、#size-cells = <1>,因此 reg 属性中 address=0xff5a0000,length=0x100。查阅《Rockchip RK3568 TRM Part1》可知,RK3568 芯片的 UART5 寄存器首地址为 0xfe690000,长度为 64KB,但是 UART5 的寄存器远远用不了 64KB,0X100 完全够了,这里重点是获取 UART5 寄存器首地址。

  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 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于所使用的 RK3568 来说,子地址空间和父地址空间完全相同,因此会在 rk3568.dtsi 中找到大量的值为空的 ranges 属性.

  1. name 属性

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

  1. device_type 属性

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

cpu0: cpu@0 {device_type = "cpu";compatible = "arm,cortex-a55";reg = <0x0 0x0>;enable-method = "psci";clocks = <&scmi_clk 0>;operating-points-v2 = <&cpu0_opp_table>;cpu-idle-states = <&CPU_SLEEP>;#cooling-cells = <2>;dynamic-power-coefficient = <187>;};

关于标准属性就讲解这么多,其他的比如中断、IIC、SPI 等使用的标准属性等到具体的例程再讲解。

根节点 compatible 属性

每个节点都有 compatible 属性,根节点“/”也不例外,在 rk3568-atk-evb1-ddr4-v10.dtsi 文件中根节点的 compatible 属性内容如下所示:

/ {model = "Rockchip RK3568 ATK EVB1 DDR4 V10 Board";compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";rk_headset: rk-headset {

可以看出,compatible 有两个值:“rockchip,rk3568-evb1-ddr4-v10”和 “rockchip,rk3568”。前面说了,设备节点的 compatible 属性值是为了匹配 Linux 内核中的驱动程序,那么根节点中的 compatible 属性是为了做什么工作的? 通过根节点的 compatible 属性可以知道所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“rk3568-evb1-ddr4-v10”这个设备,第二个值描述了设备所使用的 SOC,比如这里使用的是“RK3568”这颗 SOC。Linux 内核会通过根节点的 compatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。

打开 include/linux/rockchip/cpu.h 文件,有如下内容:

static inline bool cpu_is_rk3568(void)
{if (rockchip_soc_id)return (rockchip_soc_id & ROCKCHIP_CPU_MASK) == ROCKCHIP_CPU_RK3568;return of_machine_is_compatible("rockchip,rk3568");
}

函数 cpu_is_rk3568 用于判断当前是否为 RK3568,第 168 行使用 of_machine_is_compatible 函数判断根节点 compatible 值里面是否有 “rockchip,rk3568”。根据上述代码可知,根节点的 compatible 中有“rockchip,rk3568”,所以匹配。

向节点追加或修改内容

产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 IIC 接口的六轴芯片 MPU6050,第二版硬件又要把这个 MPU6050 更换为 MPU9250 等。一旦硬件修改了,就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片 fxls8471,fxls8471 要接到 ATK-DLRK3568 开发板的 I2C5 接口上,那么相当于需要在 i2c5 这个节点上添加一个 fxls8471 子节点。先看一下 I2C5 接口对应的节点,打开文件 rk3568.dtsi 文件,找到如下所示内容:

i2c5: i2c@fe5e0000 {compatible = "rockchip,rk3399-i2c";reg = <0x0 0xfe5e0000 0x0 0x1000>;clocks = <&cru CLK_I2C5>, <&cru PCLK_I2C5>;clock-names = "i2c", "pclk";interrupts = <GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH>;pinctrl-names = "default";pinctrl-0 = <&i2c5m0_xfer>;#address-cells = <1>;#size-cells = <0>;status = "disabled";};

上述代码就是 RK3568 的 i2c5 节点,现在要在 i2c5 节点下创建一个子节点,这个
子节点就是 fxls8471,最简单的方法就是在 i2c5 下直接添加一个名为 fxls8471 的子节点,如下所示:

i2c5: i2c@fe5e0000 {compatible = "rockchip,rk3399-i2c";reg = <0x0 0xfe5e0000 0x0 0x1000>;clocks = <&cru CLK_I2C5>, <&cru PCLK_I2C5>;clock-names = "i2c", "pclk";interrupts = <GIC_SPI 51 IRQ_TYPE_LEVEL_HIGH>;pinctrl-names = "default";pinctrl-0 = <&i2c5m0_xfer>;#address-cells = <1>;#size-cells = <0>;status = "disabled";// fxls8471 子节点fxls8471@1e {compatible = "fsl,fxls8471";reg = <0x1e>;};};

但是这样会有个问题!i2c5 节点是定义 rk3568.dtsi 文件中的,而 rk3568.dtsi 是共有的设备树头文件,其他所有使用到 rk3568 这颗 SOC 的板子都会引用 rk3568.dtsi 这个文件。直接在 i2c5 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,如上写法这样写肯定是不行的。

这里就要引入另外一个内容,那就是如何向节点追加数据,现在要解决的就是如何向 i2c5 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 RK3568 的板子。ATK-DLRK3568 开发板使用的设备树文件为 rk3568-atk-evb1-ddr4-v10.dtsi 和 rk3568-linux.dtsi,因此需要在 rk3568-atk-evb1-ddr4-v10.dtsi 文件中完成数据追加的内容,方式如下:

&i2c5 {/* 要追加或者修改的内容 */
}

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

正确的做法就是在rk3568-atk-evb1-ddr4-v10.dtsi中,向 i2c5 节点追加fxls8471相关的信息,如下所示:

&i2c5 {status = "okay";clock-frequency = <400000>;fxls8471@1e {compatible = "fsl,fxls8471";reg = <0x1e>;};
}

上述代码中的内容是 rk3568-atk-evb1-ddr4-v10.dtsi 这个文件内的,所以不会对
使用 RK3568 这颗 SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过 &label 来访问节点,然后直接在里面编写要追加或者修改的内容。

创建小型模板设备树

上一节已经对 DTS 的语法做了比较详细的讲解,本节就根据前面讲解的语法,从头到尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为了掌握设备树的语法。在实际产品开发中是不需要完完全全的重写一个.dts 设备树文件,一般都是使用 SOC 厂商提供好的.dts 文件,只需要在上面根据自己的实际情况做相应的修改即可。在编写设备树之前要先定义一个设备,就以 RK3568 这个 SOC 为例,需要在设备树里面描述的内容如下:

  1. 这个芯片是由四个 Cortex-A55 架构的 64 位 CPU 组成。
  2. RK3568 内部 uart2,起始地址为 0xfe660000,大小为 256B(0x100)。
  3. RK3568 内部 spi0,起始地址为 0xfe610000,大小为 4KB(0x1000)。
  4. RK3568 内部 i2c5,起始地址为 0xfe5e0000,大小为 4KB(0x1000)。

为了简单起见,在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点 “/” 的基础的框架,新建一个名为 myfirst.dts 文件,在里面输入如下所示内容:

1 / {
2     compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
3 }

设备树框架很简单,就一个根节点 “/”,根节点里面只有一个 compatible 属性。就在这个基础框架上面将上面列出的内容一点点添加进来。

  1. 添加 cpus 节点
    首先添加 CPU 节点,RK3568 采用 Cortex-A55 架构,先添加一个 cpus 节点,在 cpus 节点下添加 cpu0~cpu3 子节点,完成以后如下所示:
1  / {
2      compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
3 
4      cpus {
5          #address-cells = <2>;
6          #size-cells = <0>;
7 
8          /* CPU0 节点 */
9          cpu0: cpu@0 {
10             device_type = "cpu";
11             compatible = "arm,cortex-a55";
12             reg = <0x0 0x0>;
13         };
14 
15         /* CPU1 节点 */
16         cpu1: cpu@1 {
17             device_type = "cpu";
18             compatible = "arm,cortex-a55";
19             reg = <0x0 0x100>;
20         };
21
22         /* CPU2 节点 */
23         cpu2: cpu@2 {
24             device_type = "cpu";
25             compatible = "arm,cortex-a55";
26             reg = <0x0 0x200>;
27         };
28
29         /* CPU3 节点 */
30         cpu3: cpu@3 {
31             device_type = "cpu";
32             compatible = "arm,cortex-a55";
33             reg = <0x0 0x300>;
34         };
35     };
36 }

第 4~35 行,cpus 节点,此节点用于描述 SOC 内部的所有 CPU,因为 RK3568 有四个 CPU核,所以在 cpus 下添加四个子节点分别为 cpu0、cpu1、cpu2 和 cpu3。注意,上述代码里面 cpu0 等子节点内容是参考示例代码,并没有写全。

  1. 添加 uart2、spi0 和 i2c5 节点

最后在 myfirst.dts 文件中加入 uart2、spi0 和 i2c1 这三个外设控制器的节点。最终的 myfirst.dts 文件内容如下:

1  / {
2      compatible = "rockchip,rk3568-evb1-ddr4-v10", "rockchip,rk3568";
3 
4      cpus {
5          #address-cells = <2>;
6          #size-cells = <0>;
7 
8          /* CPU0 节点 */
9          cpu0: cpu@0 {
10             device_type = "cpu";
11             compatible = "arm,cortex-a55";
12             reg = <0x0 0x0>;
13         };
14 
15         /* CPU1 节点 */
16         cpu1: cpu@1 {
17             device_type = "cpu";
18             compatible = "arm,cortex-a55";
19             reg = <0x0 0x100>;
20         };
21
22         /* CPU2 节点 */
23         cpu2: cpu@2 {
24             device_type = "cpu";
25             compatible = "arm,cortex-a55";
26             reg = <0x0 0x200>;
27         };
28
29         /* CPU3 节点 */
30         cpu3: cpu@3 {
31             device_type = "cpu";
32             compatible = "arm,cortex-a55";
33             reg = <0x0 0x300>;
34         };
35     };
36 
37     /* uart2 节点 */
38     uart2: serial@fe660000 {
39         compatible = "rockchip,rk3568-uart", "snps,dw-apb-uart";
40         reg = <0x0 0xfe660000 0x0 0x100>;
41     };
42 
43     /* spi0 节点 */
44     spi0: spi@fe610000 {
45         compatible = "rockchip,rk3568-spi", "rockchip,rk3066-spi";
46         reg = <0x0 0xfe610000 0x0 0x1000>;
47     };
48 
49     /* i2c5 节点 */
50     i2c5: i2c@fe5e0000 {
51         compatible = "rockchip,rk3568-i2c", "rockchip,rk3399-i2c";
52         reg = <0x0 0xfe5e0000 0x0 0x1000>;
53     };
54 }

第 38~41 行,uart2 外设控制器节点。
第 44~47 行,spi0 外设控制器节点。
第 50~53 行,i2c5 外设控制器节点。

到这里,myfirst.dts 小型的设备树模板就编辑好了,基本和 rk3568.dtsi 很像,可以看做是 rk3568.dtsi 的缩小版。在 myfirst.dts 里面仅仅是编写了 RK3568 的外设控制器节点,节点下的属性并没有写出来。

设备树在系统中体现

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device/tree 目录下根据节点名字创建不同文件夹,如下图所示:
根节点“/”的属性以及子节点

上图就是目录/proc/device-tree 目录下的内容,/proc/device-tree 目录下是根节点 “/”的所有属性和子节点,依次来看一下这些属性和子节点。

  1. 根节点“/”各个属性

在上图中,根节点属性表现为一个个的文件,比如上图中的 “#address-cells”、“#size-cells”、“compatible”、“model” 和 “name” 这 5 个文件,它们在设备树中就是根节点的 5 个属性。既然是文件那么肯定可以查看其内容,输入 cat 命令来查看 model 和 compatible 这两个文件的内容,结果如下图所示:
model 和 compatible 文件内容

从上图可以看出,文件 model 的内容是 “Rockchip RK3568 ATK EVB1 DDR4 V10 Board”,文件 compatible 的内容为 “rockchip,rk3568-evb1-ddr4-v10rockchip,rk3568”。打开文件 rk3568-atk-evb1-ddr4-v10.dtsi 查看一下,这不正是根节点“/”的 model 和 compatible 属性值吗!

  1. 根节点“/”下各子节点

之前图中各个文件夹就是根节点 “/” 的各个子节点,比如 “aliases”、“chosen” 和 “cpus” 等等。可以查看一下 rk3568-atk-evb1-ddr4-v10.dtsi 和其所引用的所有.dtsi 文件,看看根节点的子节点都有哪些。

/proc/device-tree 目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/cpus 目录中就可以看到 cpus 节点的所有子节点,如下图所示:
cpus 子节点

和根节点“/”一样,上图中的所有文件分别为 soc 节点的属性文件和子节点文件夹。可以自行查看一下这些属性文件的内容是否和 rk3568.dtsi 中 cpus 节点的属性值相同。

特殊节点

在根节点 “/” 中有两个特殊的子节点:aliases 和 chosen,接下来看一下这两个特殊的子节点。

aliases 子节点

打开 rk3568.dtsi 文件,aliases 节点内容如下所示:

aliases {csi2dphy0 = &csi2_dphy0;csi2dphy1 = &csi2_dphy1;csi2dphy2 = &csi2_dphy2;dsi0 = &dsi0;dsi1 = &dsi1;ethernet0 = &gmac0;ethernet1 = &gmac1;gpio0 = &gpio0;gpio1 = &gpio1;gpio2 = &gpio2;gpio3 = &gpio3;gpio4 = &gpio4;i2c0 = &i2c0;i2c1 = &i2c1;i2c2 = &i2c2;i2c3 = &i2c3;i2c4 = &i2c4;i2c5 = &i2c5;mmc0 = &sdhci;mmc1 = &sdmmc0;mmc2 = &sdmmc1;mmc3 = &sdmmc2;serial0 = &uart0;serial1 = &uart1;serial2 = &uart2;serial3 = &uart3;serial4 = &uart4;serial5 = &uart5;serial6 = &uart6;serial7 = &uart7;serial8 = &uart8;serial9 = &uart9;spi0 = &spi0;spi1 = &spi1;spi2 = &spi2;spi3 = &spi3;spi4 = &sfc; // for U-Boot};

单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过一般会在节点命名的时候会加上 label,然后通过 &label 来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。

chosen 子节点

chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少,rk3568-linux.dtsi 中chosen 节点内容如下所示:

chosen: chosen {bootargs = "earlycon=uart8250,mmio32,0xfe660000 console=ttyFIQ0 root=PARTUUID=614e0000-0000 rw rootwait";};

从上述代码中可以看出,chosen 节点设置了属性“bootargs”的值。但是进入到 /proc/device-tree/chosen 目录里面查看 bootargs 的值,结果如下图所示:
bootargs 文件内容

从上图可以看出,bootargs 这个文件的内容为 “storagemedia=emmc
androidboot.storagemedia=emmc androidboot.mode=normal androidboot.verifiedbootstate=orange
androidboot.serialno=fd62623257635519 rw rootwait earlycon=uart8250,mmio32,0xfe660000
console=ttyFIQ0 root=PARTUUID=614e0000-0000”。和代码中的 bootargs 不一样,多了前面的“ storagemedia=emmc androidboot.storagemedia=emmc androidboot.mode=normal rootwait”。

那么问题来了,多出来的 “storagemedia=emmc androidboot.storagemedia=emmc androidboot.mode=normal rootwait” 是怎么来的。

uboot 里面的 bootargs 环境变量会传递给内核,RK3568 里面的 bootargs 值如下图所示:
bootargs 环境变量

从上图可以看出,uboot 里面的 bootargs 环境变量值为:

storagemedia=emmc androidboot.storagemedia=emmc androidboot.mode=normal

这个就是上面 bootargs 多出来的部分。也就是说最终传递给 Linux 内核的 bootargs 是 uboot 下 bootargs 环境变量的值加设备树里面 bootargs 属性的值。这个通过查看 linux 内核的 cmdline(命令行)参数来得到,如下图所示:
cmdline 值

从上图可以看出,cmdline 的值就和之前图中的一样,因此是 uboot 来完成 bootargs 环境变量和设备树中的 bootargs 属性值拼接的,然后将其结合体传递给内核。

绑定信息文档

设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在 Linux 内核源码中有详细的 TXT 文档描述了如何添加节点,这些 TXT 文档叫做绑定文档,路径为:Linux 源码目录/Documentation/devicetree/bindings。

比如现在要想在 RK3568 这颗 SOC 的 I2C 下添加一个节点,那么就可以查看
Documentation/devicetree/bindings/i2c/i2c-rk3x.txt,此文档详细的描述了瑞芯微出品的 SOC 如何在设备树中添加 I2C 设备节点,文档内容如下所示:

* Rockchip RK3xxx I2C controllerThis driver interfaces with the native I2C controller present in Rockchip
RK3xxx SoCs.Required properties :- reg : Offset and length of the register set for the device- compatible: should be one of the following:- "rockchip,rv1108-i2c": for rv1108- "rockchip,rv1126-i2c": for rv1126- "rockchip,rk3066-i2c": for rk3066- "rockchip,rk3188-i2c": for rk3188- "rockchip,rk3228-i2c": for rk3228- "rockchip,rk3288-i2c": for rk3288- "rockchip,rk3328-i2c", "rockchip,rk3399-i2c": for rk3328- "rockchip,rk3399-i2c": for rk3399- interrupts : interrupt number- clocks: See ../clock/clock-bindings.txt- For older hardware (rk3066, rk3188, rk3228, rk3288):- There is one clock that's used both to derive the functional clockfor the device and as the bus clock.- For newer hardware (rk3399): specified by name- "i2c": This is used to derive the functional clock.- "pclk": This is the bus clock.Required on RK3066, RK3188 :- rockchip,grf : the phandle of the syscon node for the general registerfile (GRF)- on those SoCs an alias with the correct I2C bus ID (bit offset in the GRF)is also required.Optional properties :- clock-frequency : SCL frequency to use (in Hz). If omitted, 100kHz is used.- i2c-scl-rising-time-ns : Number of nanoseconds the SCL signal takes to rise(t(r) in I2C specification). If not specified this is assumed to bethe maximum the specification allows(1000 ns for Standard-mode,300 ns for Fast-mode) which might cause slightly slower communication.- i2c-scl-falling-time-ns : Number of nanoseconds the SCL signal takes to fall(t(f) in the I2C specification). If not specified this is assumed tobe the maximum the specification allows (300 ns) which might causeslightly slower communication.- i2c-sda-falling-time-ns : Number of nanoseconds the SDA signal takes to fall(t(f) in the I2C specification). If not specified we'll use the SCLvalue since they are the same in nearly all cases.Example:aliases {i2c0 = &i2c0;
}i2c0: i2c@2002d000 {compatible = "rockchip,rk3188-i2c";reg = <0x2002d000 0x1000>;interrupts = <GIC_SPI 40 IRQ_TYPE_LEVEL_HIGH>;#address-cells = <1>;#size-cells = <0>;rockchip,grf = <&grf>;clock-names = "i2c";clocks = <&cru PCLK_I2C0>;i2c-scl-rising-time-ns = <800>;i2c-scl-falling-time-ns = <100>;
};

有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。

设备树常用 OF 操作函数

设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存器地址为 0X02005482,长度为 0X400,在编写驱动的时候需要获取到 reg 属性的 0X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。

查找节点的 OF 函数

设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中,定义如下:

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
};

与查找节点有关的 OF 函数有 5 个,依次来看一下。

  1. of_find_node_by_name 函数

of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_name(struct device_node *from, 
const char *name);

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值:找到的节点,如果为 NULL 表示查找失败。

  1. of_find_node_by_type 函数
    of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:
struct device_node *of_find_node_by_type(struct device_node *from, const char *type);

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值:找到的节点,如果为 NULL 表示查找失败。

  1. of_find_compatible_node 函数

of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点,函数原型如下:

struct device_node *of_find_compatible_node(struct device_node *from, const char *type, const char *compat);

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示忽略掉 device_type 属性。
compat:要查找的节点所对应的 compatible 属性列表。
返回值:找到的节点,如果为 NULL 表示查找失败

  1. of_find_matching_node_and_match 函数

of_find_matching_node_and_match 函数通过 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);

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
match:找到的匹配的 of_device_id。
返回值:找到的节点,如果为 NULL 表示查找失败

  1. of_find_node_by_path 函数

of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:

inline struct device_node *of_find_node_by_path(const char *path);

函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
返回值:找到的节点,如果为 NULL 表示查找失败。

查找父/子节点的 OF 函数

Linux 内核提供了几个查找节点对应的父节点或子节点的 OF 函数,依次来看一下。

  1. of_get_parent 函数

of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:

struct device_node *of_get_parent(const struct device_node *node);

函数参数和返回值含义如下:
node:要查找的父节点的节点。
返回值:找到的父节点。

  1. of_get_next_child 函数

of_get_next_child 函数用迭代的查找子节点,函数原型如下:

struct device_node *of_get_next_child(const struct device_node *node,struct device_node *prev);

函数参数和返回值含义如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为 NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。

提取属性值的 OF 函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内核中使用结构体 property 表示属性,此结构体同样定义在文件 include/linux/of.h 中,内容如下:

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
};

Linux 内核也提供了提取属性值的 OF 函数,依次来看一下。

  1. of_find_property 函数

of_find_property 函数用于查找指定的属性,函数原型如下:

struct property *of_find_property(const struct device_node *np,const char *name,int *lenp);

函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。

  1. of_property_count_elems_of_size 函数

of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

int of_property_count_elems_of_size(const struct device_node *np,const char *propname, int elem_size);

函数参数和返回值含义如下:
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值:得到的属性元素数量。

  1. of_property_read_u32_index 函数

of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此函数原型如下:

int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

  1. of_property_read_u8_array 函数
    of_property_read_u16_array 函数
    of_property_read_u32_array 函数
    of_property_read_u64_array 函数

这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型如下:

int of_property_read_u8_array(const struct device_node *np,const char *propname, u8 *out_values, size_t sz);
int of_property_read_u16_array(const struct device_node *np,const char *propname, u16 *out_values, size_t sz);
int of_property_read_u32_array(const struct device_node *np,const char *propname, u32 *out_values,size_t sz);
int of_property_read_u64_array(const struct device_node *np,const char *propname, u64 *out_values,size_t sz);

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
sz:要读取的数组元素数量。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

  1. of_property_read_u8 函数
    of_property_read_u16 函数
    of_property_read_u32 函数
    of_property_read_u64 函数

有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8、u16、u32 和 u64 类型属性值,函数原型如下:

int of_property_read_u8(const struct device_node *np, const char *propname,u8 *out_value);
int of_property_read_u16(const struct device_node *np, const char *propname,u16 *out_value);
int of_property_read_u32(const struct device_node *np, const char *propname,u32 *out_value);
int of_property_read_u64(const struct device_node *np, const char *propname,u64 *out_value);

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

  1. of_property_read_string 函数

of_property_read_string 函数用于读取属性中字符串值,函数原型如下:

int of_property_read_string(struct device_node *np, const char *propname,const char **out_string);

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值:0,读取成功,负值,读取失败。

  1. of_n_addr_cells 函数

of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:

int of_n_addr_cells(struct device_node *np);

函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#address-cells 属性值。

  1. of_n_size_cells 函数

of_n_size_cells 函数用于获取#size-cells 属性值,函数原型如下:

int of_n_size_cells(struct device_node *np);

函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells 属性值。

其他常用的 OF 函数

  1. of_device_is_compatible 函数

of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 name 指定的字符串,也就是检查设备节点的兼容性,函数原型如下:

int of_device_is_compatible(const struct device_node *device,const char *name);

函数参数和返回值含义如下:
device:设备节点。
name:要查看的字符串。
返回值:0,节点的 compatible 属性中不包含 name 指定的字符串;正数,节点的 compatible 属性中包含 name 指定的字符串。

  1. of_get_address 函数

of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数属性如下:

const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,unsigned int *flags);

函数参数和返回值含义如下:
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。

  1. of_translate_address 函数

of_translate_address 函数负责将从设备树读取到的物理地址转换为虚拟地址,函数原型如下:

u64 of_translate_address(struct device_node *dev, const __be32 *addr);

函数参数和返回值含义如下:
dev:设备节点。
in_addr:要转换的地址。
返回值:得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。

  1. of_address_to_resource 函数

IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux 内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource 结构体描述的都是设备资源信息,resource 结构体定义在文件 include/linux/ioport.h 中,定义如下:

#define IORESOURCE_BITS		0x000000ff	/* Bus-specific bits */#define IORESOURCE_TYPE_BITS	0x00001f00	/* Resource type */
#define IORESOURCE_IO		0x00000100	/* PCI/ISA I/O ports */
#define IORESOURCE_MEM		0x00000200
#define IORESOURCE_REG		0x00000300	/* Register offsets */
#define IORESOURCE_IRQ		0x00000400
#define IORESOURCE_DMA		0x00000800
#define IORESOURCE_BUS		0x00001000#define IORESOURCE_PREFETCH	0x00002000	/* No side effects */
#define IORESOURCE_READONLY	0x00004000
#define IORESOURCE_CACHEABLE	0x00008000
#define IORESOURCE_RANGELENGTH	0x00010000
#define IORESOURCE_SHADOWABLE	0x00020000#define IORESOURCE_SIZEALIGN	0x00040000	/* size indicates alignment */
#define IORESOURCE_STARTALIGN	0x00080000	/* start field is alignment */#define IORESOURCE_MEM_64	0x00100000
#define IORESOURCE_WINDOW	0x00200000	/* forwarded by bridge */
#define IORESOURCE_MUXED	0x00400000	/* Resource is software muxed */#define IORESOURCE_EXT_TYPE_BITS 0x01000000	/* Resource extended types */
#define IORESOURCE_SYSRAM	0x01000000	/* System RAM (modifier) */#define IORESOURCE_EXCLUSIVE	0x08000000	/* Userland may not map this resource */#define IORESOURCE_DISABLED	0x10000000
#define IORESOURCE_UNSET	0x20000000	/* No address assigned yet */
#define IORESOURCE_AUTO		0x40000000
#define IORESOURCE_BUSY		0x80000000	/* Driver has marked this resource busy *//* I/O resource extended types */
#define IORESOURCE_SYSTEM_RAM		(IORESOURCE_MEM|IORESOURCE_SYSRAM)

一般最常见的资源标志就是 IORESOURCE_MEM、IORESOURCE_REG 和
IORESOURCE_IRQ 等。接下来回到 of_address_to_resource 函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是提取 reg 属性值,然后将其转换为 resource 结构体类型,函数原型如下所示:

int of_address_to_resource(struct device_node *dev, int index,struct resource *r);

函数参数和返回值含义如下:
dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值。
返回值:0,成功;负值,失败。

  1. of_iomap 函数

of_iomap 函数用于直接内存映射,以前会通过 ioremap 函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址,不需要使用 ioremap 函数了。

of_iomap 函数本质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:

void __iomem *of_iomap(struct device_node *np, int index);

函数参数和返回值含义如下:
np:设备节点。
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

关于设备树常用的 OF 函数就先讲解到这里,Linux 内核中关于设备树的 OF 函数不仅仅只有前面讲的这几个,还有很多 OF 函数并没有讲解,这些没有讲解的 OF 函数要结合具体的驱动,比如获取中断号的 OF 函数、获取 GPIO 的 OF 函数等等,这些 OF 函数在后面的驱动实验中再详细的讲解。

关于设备树就讲解到这里,关于设备树重点要了解一下几点内容:

  1. DTS、DTB 和 DTC 之间的区别,如何将.dts 文件编译为.dtb 文件。
  2. 设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。
  3. 设备树的几个特殊子节点。
  4. 关于设备树的 OF 操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO 信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用 Linux 内核提供的众多的 OF 函数。

从下一章开始所以的 Linux 驱动实验都将采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。将会由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发技能。

http://www.lryc.cn/news/626272.html

相关文章:

  • 【SQL优化案例】统计信息缺失
  • 51单片机拼接板(开发板积木)
  • oracle官网下载jdk历史版本,jdk历史版本下载
  • Pandas数据预处理中缺失值处理
  • 【数据结构】堆和二叉树详解(下)
  • JavaScript 性能优化实战:从分析到落地的全指南
  • 【Task01】:简介与环境配置(第一章1、2节)
  • 彻底清理旧版本 Docker 的痕迹
  • 3.Kotlin 集合 Set 所有方法
  • STL——string的使用(快速入门详细)
  • 让AI学会“边做边想“:ReAct的实战指南
  • 第9章 React与TypeScript
  • 46 C++ STL模板库15-容器7-顺序容器-双端队列(deque)
  • 人工智能统一信息结构的挑战与前景
  • 八大排序简介
  • 08.5【C++ 初阶】实现一个相对完整的日期类--附带源码
  • JVM垃圾回收(GC)深度解析:原理、调优与问题排查
  • 算法——快速幂
  • 猫头虎AI分享|字节开源了一款具备长期记忆能力的多模态智能体:M3-Agent 下载、安装、配置、部署教程
  • Python 与 VS Code 结合操作指南
  • 深入理解抽象类
  • css过渡属性
  • 从繁琐到优雅:Java Lambda 表达式全解析与实战指南
  • 05高级语言逻辑结构到汇编语言之逻辑结构转换 while (...) {...} 结构
  • 实现Johnson SU分布的参数计算和优化过程
  • Windows系统维护,核心要点与解决方案
  • 行业分析---领跑汽车2025第二季度财报
  • 基于决策树模型的汽车价格预测分析
  • 中科米堆CASAIM自动化三维测量设备测量汽车壳体直径尺寸
  • 浅看架构理论(二)