《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​

时间:2023-01-19 12:56:14

Linux设备树​

前面章节中我们多次提到“设备树”这个概念,因为时机未到,所以当时并没有详细的讲解什么是“设备树”,本章我们就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux内核中,设备驱动基本全部采用了设备树(也有支持老式驱动的,比较少)的方式,最新出的CPU其驱动开发也基本都是基于设备树的,我们所使用的Linux版本为4.19.0,肯定是支持设备树的,所以正点原子DFZU2EG_4EV MPSoC开发板的所有Linux驱动都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。



什么是设备树?

在旧版本(大概是3.x以前的版本)的linux内核当中,ARM架构的板级硬件设备信息被硬编码在arch/arm/plat-xxxarch/arm/mach-xxx目录下的文件当中,例如板子上的platform设备信息、设备I/O资源resource、板子上的i2c设备的描述信息i2c_board_info、板子上spi设备的描述信息spi_board_info以及各种硬件设备的platform_data等,所以就导致在Linux内核源码中大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里面的文件就描述了对应平台下的板级硬件设备信息。比如在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中有如下内容(有缩减)

示例代码20.1.1 mach-smdk2440.c文件代码片段

90 static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {​
91 ​
92 .lcdcon5 = S3C2410_LCDCON5_FRM565 |​
93 S3C2410_LCDCON5_INVVLINE |​
94 S3C2410_LCDCON5_INVVFRAME |​
95 S3C2410_LCDCON5_PWREN |​
96 S3C2410_LCDCON5_HWSWP,​
......​
113 };​
114 ​
115 static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {​
116 .displays = &smdk2440_lcd_cfg,​
117 .num_displays = 1,​
118 .default_display = 0,​
......​
133 };​
134 ​
135 static struct platform_device *smdk2440_devices[] __initdata = {​
136 &s3c_device_ohci,​
137 &s3c_device_lcd,​
138 &s3c_device_wdt,​
139 &s3c_device_i2c0,​
140 &s3c_device_iis,​
141 };

上述代码中的结构体变量smdk2440_fb_info就是描述SMDK2440这个开发板上的LCD硬件信息的,结构体指针数组smdk2440_devices描述的是SMDK2440这个开发板上的所有硬件相关信息。这个仅仅是使用2440这个芯片的SMDK2440开发板下的LCD信息,SMDK2440开发板还有很多的其他外设硬件和平台硬件信息。使用2440这个芯片的板子有很多,每个板子都有描述相应板级硬件信息的文件,这仅仅只是一个2440。随着智能手机的发展,每年新出的ARM架构芯片少说都在数十、数百款,Linux内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c或.h文件,都会被硬编码进Linux内核中,导致Linux内核“虚胖”。

这些板级硬件信息代码对linux内核来说只不过是垃圾代码而已,所以当Linux之父linus看到ARM社区向Linux内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”。从此以后ARM社区就开始引入设备树DTS了。

DTS即Device Tree Source设备树源码, Device Tree是一种描述硬件的数据结构,它起源于OpenFirmware(OF),用于实现驱动代码与设备信息相分离;在设备树出现以前,所有关于板子上硬件设备的具体都要硬编码在arch/arm/plat-xxxarch/arm/mach-xxx目录下的文件当中,或者直接硬编码在驱动代码当中,例如我们前面编写的LED驱动就是直接将led的信息(用的哪个管脚、GPIO寄存器的基地址等)直接编码在了驱动源码当中,一旦外围设备变化(例如PS_LED1换成另一个MIO引脚了),驱动代码就要重写。

引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。使用设备树之后,许多硬件设备信息可以直接通过它传递给Linux,而不需要在内核中堆积大量的冗余代码。

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

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.1.1 设备树结构示意图

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

设备树文件的扩展名为.dts,一个.dts(device tree source)文件就对应一个开发板,一般放置在内核的"arch/arm/boot/dts/"目录下,比如exynos4412开发板的板级设备树文件就是"arch/arm/boot/dts/exynos4412-origen.dts",再比如I.MX6ULL-EVK开发板的板级设备树文件就是arch/arm/boot/dts/imx6ull-14x14-evk.dts。本篇驱动开发我们所使用的板级设备树文件放在“arch/arm64/boot/dts/xilinx/”目录下,包括system-top.dts、pl.dtsipcw.dtsi、zyqnmp.dtsi、zynqmp-clk-conf.dtsi和用户自定义设备树system-user.dtsi等。注意,在设备树中添加设备时,我们推荐放在system-user.dtsi中,当然也可以直接放在system-top.dts中。

前面也跟大家讲过,除了内核支持设备树之外,新版的u-boot也是支持设备树的,如果有机会也可以跟大家讲一讲U-Boot的设备树。

设备树的基本知识

dts

设备树的源文件的后缀名就是.dts,每一款硬件平台可以单独写一份xxxx.dts,所以在Linux内核源码中存在大量.dts文件,对于arm 64位架构可以在arch/arm64/boot/dts找到相应的dts。

dtsi

值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,这个dtsi文件其实就类似于C语言当中的.h头文件,可以通过C语言中使用include来包含一个.dtsi文件,例如arch/arm64/boot/dts/xilinx/system-top.dts文件有如下内容:

示例代码20.2.2.1 system-top.dts内容片段

1 /*​
2 * CAUTION: This file is automatically generated by Xilinx.​
3 * Version: ​
4 * Today is: Sat May 21 03:48:08 2022​
5 */​
6 ​
7 ​
8 /dts-v1/;​
9 #include "zynqmp.dtsi"​
10 #include "zynqmp-clk-ccf.dtsi"​
11 #include "pl.dtsi"​
12 #include "pcw.dtsi"​
13 / {​
14 chosen {​
15 bootargs = "earlycon clk_ignore_unused";​
16 stdout-path = "serial0:115200n8";​
17 };​
18 aliases {​
19 ethernet0 = &gem0;​
20 ethernet1 = &gem3;​
21 i2c0 = &hdmi_ddc;​
22 i2c1 = &i2c0;​
23 i2c2 = &i2c1;​
24 i2c3 = &sensor_iic;​
25 serial0 = &uart0;​
26 serial1 = &uart1;​
27 spi0 = &qspi;​
28 };​
29 memory {​
30 device_type = "memory";​
31 reg = <0x0 0x0 0x0 0x7ff00000>;​
32 };​
33 };​
34 #include "system-user.dtsi"

第9~12行中,通过#include包含了同目录下的四个.dtsi文件,分别为:zynqmp.dtsipl.dtsipcw.dtsi、zynqmp-clk-ccf.dtsi。这里简答地给大家说一下这四个文件的内容有啥不同,首先zynqmp.dtsi文件中的内容是ZYNQ MPSoC系列处理器相同的硬件外设配置信息(PS端的),pl.dtsi的内容是我们在vivado当中添加的pl端外设对应的配置信息,而pcw.dtsi则表示我们在vivado当中已经使能的PS外设,zynqmp-clk-ccf.dtsi文件是ZYNQ MPSoC系列芯片通用的时钟相关的设备树文件。

那么除此之外,使用#include除了可以包含.dtsi文件之外,还可以包含.dts文件以及C语言当中的.h文件,这些都是可以的,可以这么理解.dtsi和.dts文件语法各方面都是一样的,但是不能直接编译一个.dtsi文件。

dtc

dtc其实就是device-tree-compiler,那就是设备文件.dts的编译器嘛,将.c文件编译为.o文件需要用到gcc编译器,那么将.dts文件编译为相应的二进制文件则需要dtc编译器,dtc工具在Linux内核的scripts/dtc目录下,当然必须要编译了内核源码之后才会生成,如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.2.1 dtc编译器

我们来看看scripts/dtc/Makefile文件,如下所示:

示例代码20.2.3.1 scripts/dtc/Makefile文件代码段

1 hostprogs-y := dtc​
2 always := $(hostprogs-y)​
3 ​
4 dtc-objs:= dtc.o flattree.o fstree.o data.o livetree.o treesource.o \​
5 srcpos.o checks.o util.o​
6 dtc-objs += dtc-lexer.lex.o dtc-parser.tab.o​
......

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

make all

或者:

make dtbs

“make all”命令是编译Linux源码中的所有东西,包括Image,*.ko驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令。

在内核源码arch/arm64/boot/dts/xilinx/目录下有很多的dts文件,那我们编译的时候如何确定编译的是哪个或者说哪些dts文件的呢?大家可以打开arch/arm64/boot/dts/xilinx/Makefile文件,内容如下所示:

示例代码20.2.3.2 arch/arm64/boot/dts/xilinx/Makefile文件部分内容

1 # SPDX-License-Identifier: GPL-2.0​
2 dtb-$(CONFIG_ARCH_ZYNQMP) += avnet-ultra96-rev1.dtb​
3 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1232-revA.dtb​
4 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1254-revA.dtb​
5 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm015-dc1.dtb​
6 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm016-dc2.dtb​
7 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm017-dc3.dtb​
8 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm018-dc4.dtb​
9 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm019-dc5.dtb​
10 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu100-revC.dtb​
11 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu102-revA.dtb​
12 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu102-revB.dtb​
13 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu102-rev1.0.dtb​
14 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu104-revA.dtb​
15 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu104-revC.dtb​
16 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu106-revA.dtb​
17 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu111-revA.dtb​
18 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu1275-revA.dtb​
19 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu1275-revB.dtb​
20 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu1285-revA.dtb

当运行“make dtbs”命令时,上面包含的设备树都会被编译。“dtb-$(CONFIG_ARCH_ZYNQMP) +=”后面的dtb文件就是设备树dts文件编译后对应的二进制文件。例如,我们要编译设备树“system-top.dts”文件,只要把“system-top.dtb”添加到Makefile中就行,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.2.2 Makefile中添加要编译的设备树

dtb

.dtb文件就是将.dts文件编译成二进制数据之后得到的文件,这就跟.c文件编译为.o文件是一样的道理,关于.dtb文件怎么使用这里就不多说了,前面讲解Uboot移植、Linux内核移植的时候已经无数次的提到如何使用.dtb文件了(uboot中使用bootz或bootm命令向Linux内核传递二进制设备树文件(.dtb))

dts语法

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

本节我们就以system-top.dts这个文件为例来讲解一下DTS语法。关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中

设备树的结构

设备树用树状结构描述设备信息,组成设备树的基本单元是node(设备节点),这些node被组织成树状结构,有如下一些特征:

  • 一个device tree文件中只有一个root node(根节点);
  • 除了root node,每个node都只有一个parent node(父节点);
  • 一般来说,开发板上的每一个设备都能够对应到设备树中的一个node;
  • 每个node中包含了若干的property-value(键-值对,当然也可以没有value)来描述该node的一些特性;
  • 每个node都有自己的node name(节点名字);
  • node之间可以是平行关系,也可以嵌套成父子关系,这样就可以很方便的描述设备间的关系;

下面给出一个设备树的简单的结构示意图:

示例代码20.3.1.1 设备树结构示意

1 /{ // 根节点​
2 node1{ // node1节点​
3 property1=value1; // node1节点的属性property1​
4 property2=value2; // node1节点的属性property2​
5 ...​
6 }; ​
7 ​
8 node2{ // node2节点​
9 property3=value3; // node2节点的属性property3​
10 ...​
11 node3{ // node2的子节点node3​
12 property4=value4;// node3节点的属性property4​
13 ...​
14 }; ​
15 }; ​
16 };

第1行当中的’/’就表示设备树的root node(根节点),所以可知node1节点和node2节点的父节点都是root node,而node3节点的父节点则是node2,node2与node3之间形成了父子节点关系。Root node下面的子节点node1和node2可以表示为SoC上的两个控制器,而node3则可以表示挂在node2控制器上的某个设备,例如node2表示ZYNQ MPSoC PS的一个I2C控制器,而node3则表示挂在该I2C总线下的某个设备,例如eeprom、RTC等。

节点与属性

在设备树文件中如何定义一个节点,节点的命名有什么要求呢?在设备树中节点的命名格式如下:

[label:]node-name[@unit-address] {
[properties definitions]
[child nodes]
};

[]”中的内容表示可选的,可有也可以没有;节点名字前加上label”则方便在dts文件中被其他的节点引用,我们后面会说这个;其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存基地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”。

每个节点都有若干属性,属性又有相对应的值(值不是必须要有的),而一个节点当中又可以嵌套其它的节点,形成父子节点。例如下面:

示例代码20.3.2.1 设备树节点示例

23 cpus {​
24 #address-cells = <1>;​
25 #size-cells = <0>;​
26​
27 cpu0: cpu@0 {​
28 compatible = "arm,cortex-a53", "arm,armv8";​
29 device_type = "cpu";​
30 enable-method = "psci";​
31 operating-points-v2 = <&cpu_opp_table>;​
32 reg = <0x0>;​
33 cpu-idle-states = <&CPU_SLEEP_0>;​
34 };​
35 cpu1: cpu@1 {​
36 compatible = "arm,cortex-a53", "arm,armv8";​
37 device_type = "cpu";​
38 enable-method = "psci";​
39 reg = <0x1>;​
40 operating-points-v2 = <&cpu_opp_table>;​
41 cpu-idle-states = <&CPU_SLEEP_0>;​
42 };​
43​
44 cpu2: cpu@2 {​
45 compatible = "arm,cortex-a53", "arm,armv8";​
46 device_type = "cpu";​
47 enable-method = "psci";​
48 reg = <0x2>;​
49 operating-points-v2 = <&cpu_opp_table>;​
50 cpu-idle-states = <&CPU_SLEEP_0>;​
51 };​
52​
53 cpu3: cpu@3 {​
54 compatible = "arm,cortex-a53", "arm,armv8";​
55 device_type = "cpu";​
56 enable-method = "psci";​
57 reg = <0x3>;​
58 operating-points-v2 = <&cpu_opp_table>;​
59 cpu-idle-states = <&CPU_SLEEP_0>;​
60 };​
61​
62 idle-states {​
63 entry-method = "psci";​
64​
65 CPU_SLEEP_0: cpu-sleep-0 {​
66 compatible = "arm,idle-state";​
67 arm,psci-suspend-param = <0x40000000>;​
68 local-timer-stop;​
69 entry-latency-us = <300>;​
70 exit-latency-us = <600>;​
71 min-residency-us = <10000>;​
72 };​
73 };​
74 };

每一个节点(包括root node)都会使用一组括号”{ }”将自己的属性以及子节点包含在里边,注意括号外需要加上一个分号” ; ”,包括每一个属性都使用一个分号来结束。有点像C语言中的表达式后面的分号。

第23行当中的cpus节点,它的名字只有” [label:]node-name[@unit-address]”当中的node-name部分,没有其它两部分;第27行节点的定义包含了所有的组成部分,包括label以及unit-address;关于label的作用的我们后面专门讲,这里先不说。

cpus节点有两个属性” #address-cells”” #size-cells”,它们的值分别为” <1>”” <0>”。例如cpu@0节点中有compatibledevice_type、reg、clocks属性等,它们都有对应的值,大家看到这些值可能有点不明白,为啥有的是字符串,有的是尖括号”<>”括起来的东西,下面单独给大家讲解一波。

每个节点都有不同属性,不同的属性又有不同的值,那么设备树当中值有哪些形式呢?

  • 字符串compatible = "arm,idle-state";
    字符串使用双引号括起来,例如上面的这个compatible属性的值是” arm,cortex-a9”字符串。
  • 32位无符号整形数据reg = <0x0>;
    32位无符号整形数据使用尖括号括起来,例如reg属性;
  • 二进制数据local-mac-address = [00 0a 35 00 1e 53];
    二进制数据使用方括号括起来,例如上面这个就是一个二进制数据组成的数组。
  • 字符串数组compatible = "arm,cortex-a53", "arm,armv8";
    属性值也可以使用字符串列表,例如上面的这个属性,它的值是一个字符串列表,字符串之间使用逗号分割;
  • 混合值mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;
    除此之外不同的数据类型还可以混合在一起,以逗号分隔。
  • 节点引用

除了上面一些数据类型之外,还有一种非常常见的形式,如下所示:

clocks = <&clkc 3>;

这其实就是我们上面说到的引用节点的一种形式,”&clkc”就表示引用”clkc”这个节点,而clkc就是前面提到的”label”,引用节点也是使用尖括号来表示,关于节点之间的引用,我们后面还会再讲,这里先告一段落。

使用注释和宏定义

在设备树文件中也可以使用注释,注释的方法和C语言当中是一毛一样的,可以使用” // ”进行单行注释,也可以使用” /* */ ”进行多行注释,如下所示:

1 // SPDX-License-Identifier: GPL-2.0+​
2 /*​
3 * dts file for Xilinx ZynqMP​
4 *​
5 * (C) Copyright 2014 - 2015, Xilinx, Inc.​
6 *​
7 * Michal Simek <michal.simek@xilinx.com>​
8 *​
9 * This program is free software; you can redistribute it and/or​
10 * modify it under the terms of the GNU General Public License as​
11 * published by the Free Software Foundation; either version 2 of​
12 * the License, or (at your option) any later version.​
13 */​
14 ​
15 #include <dt-bindings/power/xlnx-zynqmp-power.h>​
16 #include <dt-bindings/reset/xlnx-zynqmp-resets.h>​
17 ​
18 / {​
19 compatible = "xlnx,zynqmp";​
20 #address-cells = <2>;​
21 #size-cells = <2>;​
22 ​
23 cpus {​
24 #address-cells = <1>;​
25 #size-cells = <0>;​
26 ​
27 cpu0: cpu@0 {​
28 compatible = "arm,cortex-a53", "arm,armv8";​
29 device_type = "cpu";​
30 enable-method = "psci";​
31 operating-points-v2 = <&cpu_opp_table>;​
32 reg = <0x0>;​
33 cpu-idle-states = <&CPU_SLEEP_0>;​
34 };​
……

前面跟大家讲过,设备树中可以使用“#include”包含dtsi、dts以及C语言的头文件,那我们为什么要包含一个.h的头文件呢?因为在设备树中可以使用宏定义,所以你在arch/arm/boot/dts目录下你会看到很多的设备树文件中都包含了.h头文件,例如下面这个:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.1 头文件包含

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.2 使用宏定义

关于头文件包含以及宏定义的使用这里就不多说了,本身也非常简单。

标准属性

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

1、compatible属性

compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值可以是一个字符串,也可以是一个字符串列表;一般该字符串使用<制造商>,<型号>这样的形式进行命名,当然这不是必须要这样,这是要求大家按照这样的形式进行命名,目的是为了指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突,如下所示:

compatible = "cdns,uart-r1p12", "xlnx,xuartps";

例子当中的xlnxcdns就表示制造商,而后面的xuartps和uart-r1p12就表示具体设备的型号。compatible属性用于将设备和驱动绑定起来,例如该设备首先使用第一个兼容值(cdns,uart-r1p12)在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值(xlnx,xuartps)查找,直到找到或者查找完整个Linux内核也没有找到对应的驱动。

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

示例代码20.3.4.1 drivers/tty/serial/xilinx_uartps.c内容片段

1392 /* Match table for of_platform binding */​
1393 static const struct of_device_id cdns_uart_of_match[] = {​
1394 { .compatible = "xlnx,xuartps", },​
1395 { .compatible = "cdns,uart-r1p8", },​
1396 { .compatible = "cdns,uart-r1p12", .data = &zynqmp_uart_def },​
1397 { .compatible = "xlnx,zynqmp-uart", .data = &zynqmp_uart_def },​
1398 {}​
1399 };​
1400 MODULE_DEVICE_TABLE(of, cdns_uart_of_match);​

......​
1760 static struct platform_driver cdns_uart_platform_driver = {​
1761 .probe = cdns_uart_probe,​
1762 .remove = cdns_uart_remove,​
1763 .driver = {​
1764 .name = CDNS_UART_NAME,​
1765 .of_match_table = cdns_uart_of_match,​
1766 .pm = &cdns_uart_dev_pm_ops,​
1767 },​
1768 };

这个驱动文件是ZYNQ MPSoC PS端的UART设备对应的驱动文件。

第1393~1399行定义的数组cdns_uart_of_match就是xilinx_uartps.c这个驱动文件的匹配表,此匹配表有4个匹配值“xlnx,xuartps”、“cdns,uart-r1p8”、“cdns,uart-r1p12”以及“xlnx,zynqmp-uart”。如果在设备树中有哪个节点的compatible属性值与这4个字符串中的某个相同,那么这个节点就会与此驱动文件匹配成功。

1760行,UART采用了platform_driver驱动模式,关于platform_driver驱动后面会讲解。此行设置.of_match_tablecdns_uart_of_match,也就是设置这个platform_driver所使用的OF匹配表。

2、model属性

model属性值也是一个字符串描述信息,它指定制造商的设备型号,model属性一般定义在根节点下,一般就是板子的描述信息,没啥实质性的作用,内核在解析设备树的时候会把这个属性对应的字符串信息打印出来。

示例代码20.3.4.2 arch/arm64/boot/dts/xilinx/system-user.dtsi内容片段

1 #include <dt-bindings/gpio/gpio.h>​
2 #include <dt-bindings/input/input.h>​
3 ​
4 #define GPIO_ACTIVE_HIGH 0​
5 #define GPIO_ACTIVE_LOW 1​
6 ​
7 / {​
8 model = "Alientek Zynq MpSoc Development Board";​
9 compatible = "xlnx,zynqmp-atk", "xlnx,zynqmp";​
10 ​
11 led {​
12 compatible = "alientek,led";​
13 status = "okay";​
14 default-state = "on";​
15 ​
16 led-gpio = <&gpio 38 GPIO_ACTIVE_HIGH>;​
17 };​
18 ​
19 key {​
20 compatible = "alientek,key";​
21 status = "okay";​
22 key-gpio = <&gpio 40 GPIO_ACTIVE_LOW>;​
23 };​
24 ​
25 };

我之前在system-user.dtsi设备树文件加了一个model属性,它的值等于“Alientek Zynq MpSoc Development Board”,内核启动过程中就会打印出来,如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.3 打印model字符串

3、status属性

status属性看名字就知道是和设备状态有关的,device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,下表是设备树规范中的status可选值:

描述

okay

表明设备是可操作的。启动设备

disabled

表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于disabled的具体含义还要看设备的绑定文档。

fail

表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。

fail-sss

含义和“fail”相同,后面的sss部分是检测到的错误内容。










20.3.1 status属性值

注意如果节点中没有添加status属性,那么它默认就是“status = okay”。

4、#address-cells和#size-cells属性

这两个属性的值都是无符号32位整形,#address-cells和#size-cells这两个属性可以用在任何拥有子节点的设备节点中,用于描述子节点的地址信息。

  • #address-cells,用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量;
  • #size-cells,用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量。

#address-cells和#size-cells表明了子节点应该如何编写reg属性值,一般reg属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,有了这两个属性,子节点中的"reg"属性就可以描述一块连续的地址区域了;reg属性的格式一为:

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

每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address字段占用的字长,#size-cells表明length这个字段所占用的字长,比如:

示例代码20.3.4.3 #address-cells和#size-cells属性

//zynqmp-zcu102-revA.dts中的代码片段​
777 &qspi {​
778 status = "okay";​
779 is-dual = <1>;​
780 flash@0 {​
781 compatible = "m25p80", "jedec,spi-nor"; /* 32MB */​
782 #address-cells = <1>;​
783 #size-cells = <1>;​
784 reg = <0x0>;​
785 spi-tx-bus-width = <1>;​
786 spi-rx-bus-width = <4>; /* FIXME also DUAL configuration pos sible */​
787 spi-max-frequency = <108000000>; /* Based on DC1 spec */​
788 partition@qspi-fsbl-uboot { /* for testing purpose */​
789 label = "qspi-fsbl-uboot";​
790 reg = <0x0 0x100000>;​
791 };​
792 partition@qspi-linux { /* for testing purpose */​
793 label = "qspi-linux";​
794 reg = <0x100000 0x500000>;​
795 };​
796 partition@qspi-device-tree { /* for testing purpose */​
797 label = "qspi-device-tree";​
798 reg = <0x600000 0x20000>;​
799 };​
800 partition@qspi-rootfs { /* for testing purpose */​
801 label = "qspi-rootfs";​
802 reg = <0x620000 0x5E0000>;​
803 };​

//zynqmp.dtsi中的代码片段​
802 qspi: spi@ff0f0000 {​
803 u-boot,dm-pre-reloc;​
804 compatible = "xlnx,zynqmp-qspi-1.0";​
805 status = "disabled";​
806 clock-names = "ref_clk", "pclk";​
807 interrupts = <0 15 4>;​
808 interrupt-parent = <&gic>;​
809 num-cs = <1>;​
810 reg = <0x0 0xff0f0000 0x0 0x1000>,​
811 <0x0 0xc0000000 0x0 0x8000000>;​
812 #address-cells = <1>;​
813 #size-cells = <0>;​
814 #stream-id-cells = <1>;​
815 iommus = <&smmu 0x873>;​
816 power-domains = <&zynqmp_firmware PD_QSPI>;​
817 };

第812~813行,节点qspi#address-cells = <1>#size-cells = <0>,说明qspi的子节点reg属性中起始地址使用一个32bit数据来表示,地址长度没有;第780行,qspi的子节点flash0:flash@0reg属性值为<0>,因为父节点设置了#address-cells = <1>#size-cells = <0>,因此addres=0,没有length的值,相当于设置了起始地址,而没有设置地址长度。

782和783行,设置flash@0节点#address-cells = <1>#size-cells = <1>,说明flash@0的子节点起始地址所占用的字长为1,地址长度所占用的字长也为1。第788行,flash@0的子节点partition@qspi-fsbl-uboot的reg属性值为reg = <0x0 0x100000>,因为父节点设置了#address-cells = <1>#size-cells = <1>,所以address使用一个32bit数据来表示,也就address=0x100000,而length也使用一个32bit数据来表示,也就是length=0x0,相当于只设置了起始地址为0x100000,没有设置地址长度。

5、reg属性

reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息,一般都是描述某个外设的寄存器地址范围信息、flash设备的分区信息等,比如在arch/arm64/boot/dts/xilinx/zynqmp.dtsi文件中有如下内容:

示例代码20.3.4.4 uart0节点信息

976 uart0: serial@ff000000 {​
977 u-boot,dm-pre-reloc;​
978 compatible = "cdns,uart-r1p12", "xlnx,xuartps";​
979 status = "disabled";​
980 interrupt-parent = <&gic>;​
981 interrupts = <0 21 4>;​
982 reg = <0x0 0xff000000 0x0 0x1000>;​
983 clock-names = "uart_clk", "pclk";​
984 power-domains = <&zynqmp_firmware PD_UART_0>;​
985 };

上述代码是节点uart0,uart0节点描述了ZYNQ MPSoC PS端的UART0相关信息,重点是第982行的reg属性。其中uart0的父节点amba设置了#address-cells = <2>#size-cells = <2>,因此reg属性中address= 0x1000ff000000(0x1000为高32位), length= 0x0。查阅ZYNQ MPSoC的数据手册(开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\Xilinx\User Guide\ ug1085-zynq-ultrascale-trm.pdf)可知,ZYNQ MPSoC的UART0寄存器首地址确实为0xff000000。

6、ranges属性

ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵。映射表中的子地址、父地址占用的字长分别由ranges属性所在节点的#address-cells属性和ranges属性所在节点的父节点的#address-cells属性来确定。而子地址空间长度占用的字长由ranges属性所在节点的#address-cells属性决定。

child-bus-address:子总线地址空间的物理地址,由ranges属性所在节点的#address-cells属性确定此物理地址占用的字长。

parent-bus-address父总线地址空间的物理地址,由ranges属性所在节点的父节点的#address-cells属性确定此物理地址所占用的字长。

length:子地址空间的长度,由ranges属性所在节点的#address-cells属性确定此地址长度所占用的字长。

如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的ZYNQ MPSoC来说,子地址空间和父地址空间完全相同,因此会在zynqmp.dtsi文件中找到很多值为空的ranges属性,如下所示:

示例代码20.3.4.5 zynq-7000.dtsi内容片段

998 usb0: usb0@ff9d0000 {​
999 #address-cells = <2>;​
1000 #size-cells = <2>;​
1001 status = "disabled";​
1002 compatible = "xlnx,zynqmp-dwc3";​
1003 reg = <0x0 0xff9d0000 0x0 0x100>;​
1004 clock-names = "bus_clk", "ref_clk";​
1005 power-domains = <&zynqmp_firmware PD_USB_0>;​
1006 ranges;​
1007 nvmem-cells = <&soc_revision>;​
1008 nvmem-cell-names = "soc_revision";​
……

第1006行定义了ranges属性,但是ranges属性值为空。

ranges属性不为空的示例代码如下所示:

示例代码20.3.4.6 ranges属性不为空

1 soc {​
2 compatible = "simple-bus";​
3 #address-cells = <1>;​
4 #size-cells = <1>;​
5 ranges = <0x0 0xe0000000 0x00100000>;​
6 ​
7 serial {​
8 device_type = "serial";​
9 compatible = "ns16550";​
10 reg = <0x4600 0x100>;​
11 clock-frequency = <0>;​
12 interrupts = <0xA 0x8>;​
13 interrupt-parent = <&ipic>;​
14 };​
15 };

第5行,节点soc定义的ranges属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0xe0000000

10行,serial是串口设备节点,reg属性定义了serial设备寄存器的起始地址为0x4600,寄存器长度为0x100。经过地址转换,serial设备可以从0xe0004600开始进行读写操作,0xe0004600=0x4600+0xe0000000

7、device_type属性

device_type属性值为字符串,表示节点的类型;此属性在设备树当中用的比较少,一般用于cpu节点或者memory节点。zynq-7000.dtsi文件中的cpu0和cpu1节点用到了此属性,内容如下所示:

示例代码20.3.4.7 zynq-7000.dtsi内容片段

24 cpu0: cpu@0 {​
25 compatible = "arm,cortex-a9";​
26 device_type = "cpu";​
27 reg = <0>;​
28 clocks = <&clkc 3>;​
29 clock-latency = <1000>;​
30 cpu0-supply = <®ulator_vccpint>;​
31 operating-points = <​
32 /* kHz uV */​
33 666667 1000000​
34 333334 1000000​
35 >;​
36 };​
37 ​
38 cpu1: cpu@1 {​
39 compatible = "arm,cortex-a9";​
40 device_type = "cpu";​
41 reg = <1>;​
42 clocks = <&clkc 3>;​
43 };

关于标准属性就讲解这么多,后面还会跟大家介绍一些常常会使用到的节点,例如设备树中的中断控制器、GPIO、I2C总线等。

根节点compatible属性

每个节点都有compatible属性(除了一些特殊用途的节点),根节点“/”也不例外,在zynq-7000.dtsi文件中根节点的compatible属性内容如下所示:

示例代码20.3.5.1 zynq-7000.dtsi根节点compatible属性

15 / {​
16 #address-cells = <1>;​
17 #size-cells = <1>;​
18 compatible = "xlnx,zynq-7000";​
......​
431 };

可以看出,compatible有一个值:“xlnx,zynq-7000”。前面我们说了,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatible属性是为了做什么工作的?同样根节点下的compatible属性的值可以是一个字符串,也可以是一个字符串列表;该字符串也要求以<制造商>,<型号>这样的形式进行命名;比如这里使用的是“xlnx”制造的“zynq-7000”系列处理器。

通过根节点的compatible属性可以知道我们所使用的处理器型号,Linux内核会通过根节点的compoatible属性查看是否支持此该处理器,因为内核在启动初期会进行校验,必须要支持才会启动Linux内核。接下来我们就来学习一下Linux内核在使用设备树之前已以及使用设备树之后是如何判断是否支持某款处理器的。

1、使用设备树之前的校验方法

在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值,machine id可以认为就是一个机器ID编码,告诉Linux内核自己是个什么硬件平台,看看Linux内核是否支持。Linux内核是支持很多硬件平台的,但是针对每一个特定的板子,Linux内核都用MACHINE_STARTMACHINE_END来定义一个machine_desc结构体来描述这个硬件平台,比如在文件arch/arm/mach-imx/mach-mx35_3ds.c中有如下定义:

示例代码20.3.5.2 MX35_3DS设备

613 MACHINE_START(MX35_3DS, "Freescale MX35PDK")​
614 /* Maintainer: Freescale Semiconductor, Inc */​
615 .atag_offset = 0x100,​
616 .map_io = mx35_map_io,​
617 .init_early = imx35_init_early,​
618 .init_irq = mx35_init_irq,​
619 .init_time = mx35pdk_timer_init,​
620 .init_machine = mx35_3ds_init,​
621 .reserve = mx35_3ds_reserve,​
622 .restart = mxc_restart,​
623 MACHINE_END

上述代码就是定义了“Freescale MX35PDK”这个硬件平台,其中MACHINE_STARTMACHINE_END定义在文件arch/arm/include/asm/mach/arch.h中,内容如下:

示例代码20.3.5.3 MACHINE_START和MACHINE_END宏定义

#define MACHINE_START(_type,_name) \​
static const struct machine_desc __mach_desc_##_type \​
__used \​
__attribute__((__section__(".arch.info.init"))) = { \​
.nr = MACH_TYPE_##_type, \​
.name = _name,​

#define MACHINE_END \​
};

根据MACHINE_STARTMACHINE_END的宏定义,将示例代码20.3.5.3展开后如下所示:

示例代码20.3.5.4展开以后

1 static const struct machine_desc __mach_desc_MX35_3DS  \​
2 __used \​
3 __attribute__((__section__(".arch.info.init"))) = { ​
4 .nr = MACH_TYPE_MX35_3DS, ​
5 .name = "Freescale MX35PDK",​
6 /* Maintainer: Freescale Semiconductor, Inc */​
7 .atag_offset = 0x100,​
8 .map_io = mx35_map_io,​
9 .init_early = imx35_init_early,​
10 .init_irq = mx35_init_irq,​
11 .init_time = mx35pdk_timer_init,​
12 .init_machine = mx35_3ds_init,​
13 .reserve = mx35_3ds_reserve,​
14 .restart = mxc_restart,​
15 };

从示例代码20.3.5.4中可以看出,这里定义了一个machine_desc类型的结构体变量__mach_desc_MX35_3DS,这个变量存储在“.arch.info.init”段中。第4行的MACH_TYPE_MX35_3DS就是“Freescale MX35PDK”这个板子的machine id。MACH_TYPE_MX35_3DS定义在文件include/generated/mach-types.h中,此文件定义了大量的machine id,内容如下所示:

示例代码20.3.5.5 mach-types.h文件中的machine id

15 #define MACH_TYPE_EBSA110  0​
16 #define MACH_TYPE_RISCPC 1​
17 #define MACH_TYPE_EBSA285 4​
18 #define MACH_TYPE_NETWINDER 5​
19 #define MACH_TYPE_CATS 6​
20 #define MACH_TYPE_SHARK 15​
21 #define MACH_TYPE_BRUTUS 16​
22 #define MACH_TYPE_PERSONAL_SERVER 17​
......​
287 #define MACH_TYPE_MX35_3DS 1645​
......​
1000 #define MACH_TYPE_PFLA03 4575

第287行就是MACH_TYPE_MX35_3DS的值,为1645

前面说了,uboot会给Linux内核传递machine id这个参数,Linux内核会检查这个machine id,其实就是将machine id与示例代码20.3.5.5中的这些MACH_TYPE_XXX宏进行对比,看看有没有相等的,如果相等的话就表示Linux内核支持这个硬件平台,如果不支持的话就没法启动Linux内核。

2、使用设备树以后的设备匹配方法

当Linux内核引入设备树以后就不再使用MACHINE_START了,而是换为了DT_MACHINE_STARTDT_MACHINE_START也定义在文件arch/arm/include/asm/mach/arch.h里面,定义如下:

示例代码20.3.5.6 DT_MACHINE_START宏

#define DT_MACHINE_START(_name, _namestr)    \​
static const struct machine_desc __mach_desc_##_name \​
__used \​
__attribute__((__section__(".arch.info.init"))) = { \​
.nr = ~0, \​
.name = _namestr,

可以看出,DT_MACHINE_STARTMACHINE_START基本相同,只是.nr的设置不同,在DT_MACHINE_START里面直接将.nr设置为~0。说明引入设备树以后不会再根据machine id来检查Linux内核是否支持某个硬件平台了。

打开文件arch/arm/mach-zynq/common.c,有如下所示内容:

示例代码20.3.5.7 arch/arm/mach-zynq/common.c

191 static const char * const zynq_dt_match[] = {​
192 "xlnx,zynq-7000",​
193 NULL​
194 };​
195 ​
196 DT_MACHINE_START(XILINX_EP107, "Xilinx Zynq Platform")​
197 /* 64KB way size, 8-way associativity, parity disabled */​
198 #ifdef CONFIG_XILINX_PREFETCH​
199 .l2c_aux_val = 0x30400000,​
200 .l2c_aux_mask = 0xcfbfffff,​
201 #else​
202 .l2c_aux_val = 0x00400000,​
203 .l2c_aux_mask = 0xffbfffff,​
204 #endif​
205 .smp = smp_ops(zynq_smp_ops),​
206 .map_io = zynq_map_io,​
207 .init_irq = zynq_irq_init,​
208 .init_machine = zynq_init_machine,​
209 .init_late = zynq_init_late,​
210 .init_time = zynq_timer_init,​
211 .dt_compat = zynq_dt_match,​
212 .reserve = zynq_memory_init,​
213 MACHINE_END

machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本硬件平台的兼容属性,示例代码20.3.5.7中设置.dt_compat = zynq_dt_matchzynq_dt_match数组的定义在第191~194行中,可以看到它匹配的字符串是“xlnx,zynq-7000”。只要某个板子的设备树根节点“/”的compatible属性值与zynq_dt_match表中的任何一个值相等,那么就表示Linux内核支持这个开发板、支持这个硬件平台。前面也跟大家说过了,我们使用的设备树文件是system-top.dts,该文件中使用include包含了zynq-7000.dtsi,在zynq-7000.dtsi文件中根节点的compatible属性值就是“xlnx,zynq-7000”,所以内核是支持我们开发板的

如果将zynq-7000.dtsi根节点的compatible属性改为其他的值,那么它就启动不了了。

当我们修改了根节点compatible属性内容以后,因为Linux内核找不到对应的硬件平台,因此Linux内核无法启动。

接下来我们简单看一下Linux内核是如何根据设备树根节点的compatible属性来匹配出对应的machine_desc,Linux内核调用start_kernel函数来启动内核,start_kernel函数会调用setup_arch函数来匹配machine_descsetup_arch函数定义在文件arch/arm/kernel/setup.c中,函数内容如下(有缩减)

示例代码20.3.5.8 setup_arch函数内容

913 void __init setup_arch(char **cmdline_p)​
914 {​
915 const struct machine_desc *mdesc;​
916 ​
917 setup_processor();​
918 mdesc = setup_machine_fdt(__atags_pointer);​
919 if (!mdesc)​
920 mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);​
921 machine_desc = mdesc;​
922 machine_name = mdesc->name;​
......​
986 }

第918行,调用setup_machine_fdt函数来获取匹配的machine_desc,参数就是atags的首地址,也就是uboot传递给Linux内核的dtb文件首地址,setup_machine_fdt函数的返回值就是找到的已经匹配成功的machine_desc

函数setup_machine_fdt定义在文件arch/arm/kernel/devtree.c中,内容如下(有缩减)

示例代码20.3.5.9 setup_machine_fdt函数内容

204 const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)​
205 {​
206 const struct machine_desc *mdesc, *mdesc_best = NULL;​
......​
214 ​
215 if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))​
216 return NULL;​
217 ​
218 mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);​
219 ​
......​
247 __machine_arch_type = mdesc->nr;​
248 ​
249 return mdesc;​
250 }

第218行,调用函数of_flat_dt_match_machine来获取匹配的machine_desc,参数mdesc_best是默认的machine_desc,参数arch_get_next_mach是个函数,此函数定义在arch/arm/kernel/devtree.c文件中。找到匹配的machine_desc的过程就是用设备树根节点的compatible属性值和Linux内核中保存的所有的machine_desc结构体的.dt_compat中的值比较,看看那个相等,如果相等的话就表示找到匹配的machine_desc,arch_get_next_mach函数的工作就是获取Linux内核中下一个machine_desc结构体。

最后在来看一下of_flat_dt_match_machine函数,此函数定义在文件drivers/of/fdt.c中,内容如下(有缩减)

示例代码20.3.5.10 of_flat_dt_match_machine函数内容

705 const void * __init of_flat_dt_match_machine(const void *default_match,​
706 const void * (*get_next_compat)(const char * const**))​
707 {​
708 const void *data = NULL;​
709 const void *best_data = default_match;​
710 const char *const *compat;​
711 unsigned long dt_root;​
712 unsigned int best_score = ~1, score = 0;​
713 ​
714 dt_root = of_get_flat_dt_root();​
715 while ((data = get_next_compat(&compat))) {​
716 score = of_flat_dt_match(dt_root, compat);​
717 if (score > 0 && score < best_score) {​
718 best_data = data;​
719 best_score = score;​
720 }​
721 }​
......​
739 ​
740 pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());​
741 ​
742 return best_data;​
743 }

第714行,通过函数of_get_flat_dt_root获取设备树根节点。

第715~720行,此循环就是查找匹配的machine_desc过程,第716行的of_flat_dt_match函数会将根节点compatible属性的值和每个machine_desc结构体中.dt_compat的值进行比较,直至找到匹配的那个machine_desc

总结一下,Linux内核通过根节点compatible属性找到对应的machine_desc结构体的函数调用过程,如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.4 查找匹配machine_desc的过程

引用节点

前面说到节点的命名格式如下所示:

[label:]node-name[@unit-address]

也多次给大家提到“label”字段,引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,例如下面这个模板:

示例代码20.3.6.1 设备树模板

1 / {​
2 aliases {​
3 can0 = &flexcan1;​
4 };​
5 ​
6 cpus {​
7 #address-cells = <1>;​
8 #size-cells = <0>;​
9 ​
10 cpu0: cpu@0 {​
11 compatible = "arm,cortex-a7";​
12 device_type = "cpu";​
13 reg = <0>;​
14 };​
15 };​
16 ​
17 intc: interrupt-controller@00a01000 {​
18 compatible = "arm,cortex-a7-gic";​
19 #interrupt-cells = <3>;​
20 interrupt-controller;​
21 reg = <0x00a01000 0x1000>,​
22 <0x00a02000 0x100>;​
23 };​
24 };

通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc: interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多!

所以如果我们要在设备树中引用其它的节点,那么就可以在这个被引用的节点前加上“label:”,这样我们就可以很方便的通过“&label”的方式进行引用了。

向节点追加或修改内容

这里面有两个知识点:向节点追加内容,也就是添加属性;另一个就是修改节点的内容。我相信大家都理解我这里说的意思。在实际的开发当中肯定是有这样的需求存在的,例如在我们的开发板上有一个eeprom器件(24c64)和一个rtc器件(pcf8563),假如它俩都是挂在ZYNQ MPSoC的i2c0总线下的。那么现在要把这两个设备添加到i2c0总线下,打开zynq-7000.dtsi文件,可以看到PS的两组i2c控制器节点定义,如下所示:

示例代码20.3.7.1 zynqmp.dtsi i2c节点

661 i2c0: i2c@ff020000 {​
662 compatible = "cdns,i2c-r1p14", "cdns,i2c-r1p10";​
663 status = "disabled";​
664 interrupt-parent = <&gic>;​
665 interrupts = <0 17 4>;​
666 reg = < 0x0 0xff020000 0x0 0x1000>;​
667 #address-cells = <1>;​
668 #size-cells = <0>;​
669 power-domains = <&zynqmp_firmware 37>;​
670 };​
671 ​
672 i2c1: i2c@ff030000 {​
673 compatible = "cdns,i2c-r1p14", "cdns,i2c-r1p10";​
674 status = "disabled";​
675 interrupt-parent = <&gic>;​
676 interrupts = <0 18 4>;​
677 reg = <0x0 0xff030000 0x0 0x1000>;​
678 #address-cells = <1>;​
679 #size-cells = <0>;​
680 power-domains = <&zynqmp_firmware 38>;​
681 };

因为现在要把开发板的两个i2c器件添加到i2c0总线下,直接在i2c0节点下创建两个子节点即可,一个子节点对应的是eeprom,另一个子节点对应的是rtc,那么最简单的方法就是直接在zynqmp.dtsi文件的i2c0节点中添加这两个节点子节点即可,如下所示:

示例代码20.3.7.2 zynqmp.dtsi 添加i2c器件

122 i2c0: i2c@ff020000 {​
123 compatible = "cdns,i2c-r1p10";​
124 status = "disabled";​
125 clocks = <&clkc 38>;​
126 interrupt-parent = <&intc>;​
127 interrupts = <0 25 4>;​
128 reg = <0xe0004000 0x1000>;​
129 #address-cells = <1>;​
130 #size-cells = <0>;​
131 ​
132 24c64@50 {​
133 compatible = "atmel,24c64";​
134 reg = <0x50>;​
135 pagesize = <32>;​
136 };​
137 ​
138 rtc@51 {​
139 compatible = "nxp,pcf8563";​
140 reg = <0x51>;​
141 };​
142 };​
143 ​
144 i2c1: i2c@ff030000 {​
145 compatible = "cdns,i2c-r1p10";​
146 status = "disabled";​
147 clocks = <&clkc 39>;​
148 interrupt-parent = <&intc>;​
149 interrupts = <0 48 4>;​
150 reg = <0xe0005000 0x1000>;​
151 #address-cells = <1>;​
152 #size-cells = <0>;​
153 };

第132~136行就是在i2c0总线下添加了eeprom设备,138~141行添加了rtc设备(注意:我这里只是给大家做演示,你们不要去改这个文件);但是这样会有个问题,i2c0节点是定义在zynq-7000.dtsi文件中的,而zynq-7000.dtsi是设备树头文件,前面也跟大家说到过,该文件是zynq-7000系列处理器的一个通用设备树头文件,也就是说它是会被其他dts文件所包含的,直接在i2c0节点中添加这两个子节点就相当于在所有的zynq-7000系列处理器开发板上都添加了这两个设备,如果其他的板子并没有这两个设备呢!因此,按照示例代码24.3.12这样写肯定是不行的。

这里就要引入另外一个内容,那就是向节点追加数据,我们现在要解决的就是如何向i2c0节点追加两个子节点,而且不能影响到其它使用zynq-7000系列处理器的开发板。在本篇中我们使用的设备树文件为system-top.dts,因此我们需要在system-top.dts文件中完成数据追加的内容,方式如下:

示例代码20.3.7.3 节点追加数据方法

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

第1行,&i2c0表示要引用到i2c0这个label所对应的节点,也就是zynq-7000.dtsi文件中的“i2c0: i2c@e0004000”。

第2行,花括号内就是要向i2c0这个节点添加的内容,包括修改某些属性的值。

打开system-top.dts,这样我们就可以直接在该文件中追加内容了:

示例代码20.3.7.4 system-top.dts 向i2c0节点追加内容

8 /dts-v1/;​
9 #include "zynq-7000.dtsi"​
10 #include "pl.dtsi"​
11 #include "pcw.dtsi"​
12 / {​
13 model = "Alientek ZYNQ Development Board";​
14 ​
15 chosen {​
16 bootargs = "cnotallow=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";​
17 stdout-path = "serial0:115200n8";​
18 };​
19 aliases {​
20 ethernet0 = &gem0;​
21 i2c0 = &i2c_2;​
22 i2c1 = &i2c0;​
23 i2c2 = &i2c1;​
24 serial0 = &uart0;​
25 serial1 = &uart1;​
26 spi0 = &qspi;​
27 };​
28 memory {​
29 device_type = "memory";​
30 reg = <0x0 0x20000000>;​
31 };​
32 };​
33 ​
34 &i2c0 {​
35 clock-frequency = <100000>;​
36 status = "okay";​
37​
38 24c64@50 {​
39 compatible = "atmel,24c64";​
40 reg = <0x50>;​
41 pagesize = <32>;​
42 };​
43 ​
44 rtc@51 {​
45 compatible = "nxp,pcf8563";​
46 reg = <0x51>;​
47 };​
48 };​
49 ​
50 &gem0 {​
51 local-mac-address = [00 0a 35 00 1e 53];​
52 };

第34~48行就是向i2c0节点添加/修改数据,比如35的属性“clock-frequency = <100000>”就表示将i2c0的时钟设置为100KHz,“clock-frequency”就是新添加的属性。

36行,将status属性的值由原来的disabled改为okay,这是修改节点的属性值。

第38~47行,我们向i2c0子节点追加了两个子节点,“24c64@50”和“rtc@51”。

除此之外,第12~32行,其实就是向zynq-7000.dtsi中定义的根节点中追加了一些节点。

注意,这里只是给大家演示,大家不要去修改这些文件,后面用到的时候我会再说!!!

因为示例代码24.3.14中的内容是system-top.dts这个文件内的,所以不会对使用ZYNQ-7000系列处理器的其它板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。例如在pcw.dtsi文件中,可以看到很多的节点引用、向节点追加内容、修改节点内容的示例,如下所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.5 pcw.dtsi示例

特殊节点

在根节点“/”中有那么几个特殊的子节点:aliaseschosen以及memory,我们接下来看一下这三个比较特殊的节点,我们会发现这三个节点都是没有compatible属性,也就是说它们对应的并不是一个真实的设备。

1、aliases节点

打开system-top.dts文件,可以看到aliases节点的内容如下所示:

示例代码20.3.8.1 system-top.dts aliases节点

aliases {​
ethernet0 = &gem0;​
ethernet1 = &gem3;​
i2c0 = &hdmi_ddc;​
i2c1 = &i2c0;​
i2c2 = &i2c1;​
i2c3 = &sensor_iic;​
serial0 = &uart0;​
serial1 = &uart1;​
spi0 = &qspi;​
};​

19 aliases {​
20 ethernet0 = &gem0;​
21 i2c0 = &i2c_2;​
22 i2c1 = &i2c0;​
23 i2c2 = &i2c1;​
24 serial0 = &uart0;​
25 serial1 = &uart1;​
26 spi0 = &qspi;​
27 };

单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是需要注意的是,这里说的方便访问节点并不是在设备树中访问节点,例如前面说到的使用“&label”的方式访问设备树中的节点,而是内核当中方便定位节点,例如在内核中通过ethernet0就可以定位到gem0节点(&gem0引用的节点),再例如内核通过serial0就可以找到uart0节点。

2、chosen节点

chosen节点一般会有两个属性,“bootargs”和“stdout-path”。打开system-top.dts文件,找到chosen节点,内容如下所示:

示例代码20.3.8.2 chosen节点

15 chosen {​
16 bootargs = "cnotallow=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";​
17 stdout-path = "serial0:115200n8";​
18 };

在chosen节点当中,属性stdout-path = “serial0:115200n8”,表示标准输出设备使用串口serial0,在system-top.dts文件当中,serial0其实是一个别名,指向的就是uart0;“115200”则表示串口的波特率为115200,“n”表示无校验位,“8”则表示有8位数据位,相信大家都明白这些是什么意思。

当你看到chosen节点中的bootargs属性的时候有没有想到U-Boot的bootargs环境变量呢?内核的bootargs参数不是由U-Boot传给它的吗?为什么要在内核设备树根节点下的chosen节点中定义呢?他们俩有什么区别呢?那么关于这些问题稍后再给大家解释,这里大家想思考另一个问题:“stdout-path”属性指定了标准输出设备,而bootargs参数当中也指定了标准输出设备(cnotallow=ttyPS0,115200,ttyPS0其实指的就是根文件系统下的/dev/ttyPS0这个设备文件,那么它对应的硬件设备其实就是板子的uart0),那么内核在初始化标准输出设备的时候到底听谁的呢?关于这个问题,笔者开始也想不明白,于是乎去内核源码中找了找,在内核源码drivers/of/base.c文件中看到了下面这段代码:

示例代码20.3.8.3 of_console_check函数

1822 /**​
1823 * of_console_check() - Test and setup console for DT setup​
1824 * @dn - Pointer to device node​
1825 * @name - Name to use for preferred console without index. ex. "ttyS"​
1826 * @index - Index to use for preferred console.​
1827 *​
1828 * Check if the given device node matches the stdout-path property in the​
1829 * /chosen node. If it does then register it as the preferred console and return​
1830 * TRUE. Otherwise return FALSE.​
1831 */​
1832 bool of_console_check(struct device_node *dn, char *name, int index)​
1833 {​
1834 if (!dn || dn != of_stdout || console_set_on_cmdline)​
1835 return false;​
1836​
1837 /*​
1838 * XXX: cast `options' to char pointer to suppress complication​
1839 * warnings: printk, UART and console drivers expect char pointer.​
1840 */​
1841 return !add_preferred_console(name, index, (char *)of_stdout_options);​
1842 }

看这个函数的名字“of_console_check”,意思是控制台校验(控制台大家可以理解为linux的标准输入、输入终端),第1834行当中的of_stdout其实是内核解析stdout-path = “serial0:115200n8”时得到的serial0指向的设备节点,也就是我们的串口0,;而console_set_on_cmdline是一个int类型的变量,如果bootargs字符串当中指定了console=xxxxx,那么内核也会解析到,并且将console_set_on_cmdline变量设置为1;所以根据代码中的第1834行以及函数定义前面的注释信息,我的猜想如下:

在of_console_check函数中会判断设备树stdout-path属性是否定义了,如果定义了则它拥有优先级。

当然这是我的猜测,我并没有去验证,不想花这个时间去研究了,如果大家有时间可以去找找看,这里就不说这个问题了。

现在给大家解释前面说到的那些问题:内核的bootargs参数不是由U-Boot传给它的吗?为什么还要在内核设备树根节点下的chosen节点中定义bootargs呢?他们俩有什么区别呢?下面给大家一一解释一下。

前面讲解uboot的时候说过,uboot在启动Linux内核的时候会将bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的bootargs的值),如所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.6 内核启动打印命令行参数

但是我们使用的这个U-Boot,它的环境变量当中并没有定义bootargs变量,大家可以进入U-Boot命令行,通过print命令打印出所有的环境变量,你会发现并没有定义bootargs,那这跟我们前面说的不相符了呀,而事实并不如此。

在uboot源码中全局搜索“chosen”这个字符串,看看能不能找到一些蛛丝马迹,果然在U-Boot源码目录的common/fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示:

示例代码20.3.8.4 uboot源码中的fdt_chosen函数

275 int fdt_chosen(void *fdt)​
276 {​
277 int nodeoffset;​
278 int err;​
279 char *str; /* used to set string properties */​
280 ​
281 err = fdt_check_header(fdt);​
282 if (err < 0) {​
283 printf("fdt_chosen: %s\n", fdt_strerror(err));​
284 return err;​
285 }​
286 ​
287 /* find or create "/chosen" node. */​
288 nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");​
289 if (nodeoffset < 0)​
290 return nodeoffset;​
291 ​
292 str = getenv("bootargs");​
293 if (str) {​
294 err = fdt_setprop(fdt, nodeoffset, "bootargs", str,​
295 strlen(str) + 1);​
296 if (err < 0) {​
297 printf("WARNING: could not set bootargs %s.\n",​
298 fdt_strerror(err));​
299 return err;​
300 }​
301 }​
302 ​
303 return fdt_fixup_stdout(fdt, nodeoffset);​
304 }

第288行,调用函数fdt_find_or_add_subnode从内核设备树(.dtb,因为此时内核dtb文件已经被拷贝到DDR中了)中找到chosen节点,如果没有找到的话就会自己创建一个chosen节点。

第292行,读取uboot中bootargs环境变量的内容。

第293行,判断如果读取bootargs环境变量成功,则执行if { }中的代码。

第294行,调用函数fdt_setprop向内核设备的chosen节点添加bootargs属性,并且bootargs属性的值就是环境变量bootargs的内容。(因为此时内核dtb文件已经被拷贝到DDR中了,U-Boot可以通过内核设备树dtb的起始地址对dtb数据进行修改)。

所以从上面这段代码可以看出来,如果U-Boot定义了bootargs环境变量,则会通过fdt_setprop函数在内核设备树的chosen节点追加bootargs属性,它的值就是U-Boot环境变量bootargs的值,如果是这样,那么内核设备树chosen节点的bootargs属性就会被修改。但是对于我们使用这个U-Boot来说,它并没有定义bootargs环境变量,所以使用的就是内核设备树chosen节点下的bootargs属性,也就是说U-Boot的环境变量bootargs拥有最高的优先级。

接下来我们顺着fdt_chosen函数一点点的抽丝剥茧,看看都有哪些函数调用了fdt_chosen,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见20.3.7

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.3.7 fdt_chosen函数调用流程

20.3.7中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在内核设备树chosen节点中添加bootargs属性。而U-Boot的bootcmd命令最终会执行bootz命令,而bootz命令启动Linux内核的时候会运行do_bootm_linux函数,至此,真相大白!

3、memory节点

memory节点看名字就知道跟内存是有关系的,如下所示:

示例代码20.3.8.5 memory节点

28 memory {​
29 device_type = "memory";​
30 reg = <0x0 0x20000000>;​
31 };

memory节点描述了系统内存的基地址以及系统内存大小,“reg = <0x0 0x20000000>”就表示系统内存的起始地址为0x0,大小为0x20000000,也就是512MB,该节点一般只有这两个属性,device_type属性的值固定为“memory”。

常用节点

本来这小节给大家讲一些常用到的节点,例如中断控制器、GPIO控制器以及在节点当中如何使用中断、如何使用gpio等。当想了想还是放在后面我们用到的时候再给大家介绍。

驱动与设备节点的匹配

这部分内容已经在前面跟大家讲过了,具体请看20.3.4小节中的第一个小点compatible属性介绍。

内核启动过程中解析设备树

Linux内核在启动的时候会解析内核DTB文件,然后在根文件系统的/proc/device-tree(后面给大家演示)目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的,流程如下图所示:

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.5.1 设备树中节点解析流程

从上图中可以看出,在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。那么具体如何进行设备树解析的这里就不给大家进行一一分析了,如果大家有时间可以自个去研究研究!

设备树在系统中的体现

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

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.6.1 根节点的属性以及子节点

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

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

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

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.6.2 model和compatible文件内容

20.6.2可以看出,文件model的内容是“Alientek Zynq MpSoc Development Board”,文件compatible的内容为“xlnx,zynqmp”。这跟system-user.dtsi文件根节点的model属性值、以及zynqmp.dtsi文件根节点的compatible属性值是完全一样的。

2、根节点“/”各子节点

20.6.1中列出的各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“cpus”、“chosen”和“amba”等等。大家可以查看我们用到的设备树文件,看看根节点的子节点都有哪些,看看是否和20.6.1中的一致。

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

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.6.3 amba节点的所有属性和子节点

和根节点“/”一样,20.6.3中的所有文件分别为amba节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和我们使用的设备树中amba节点的属性值相同。

绑定信息文档

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

《DFZU2EG_4EV MPSoC之嵌入式Linux开发指南》第二十章 Linux设备树​


20.7.1 绑定文档

这些文档详细的描述了如何在设备树中添加设备节点,有时候使用的一些芯片在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中,定义如下:

示例代码20.8.1.1 device_node节点

49 struct device_node {​
50 const char *name; /* 节点名字 */​
51 const char *type; /* 设备类型 */​
52 phandle phandle;​
53 const char *full_name; /* 节点全名 */​
54 struct fwnode_handle fwnode;​
55 ​
56 struct property *properties; /* 属性 */​
57 struct property *deadprops; /* removed属性 */​
58 struct device_node *parent; /* 父节点 */​
59 struct device_node *child; /* 子节点 */​
60 struct device_node *sibling;​
61 struct kobject kobj;​
62 unsigned long _flags;​
63 void *data;​
64 #if defined(CONFIG_SPARC)​
65 const char *path_component_name;​
66 unsigned int unique_id;​
67 struct of_irq_controller *irq_trans;​
68 #endif​
69 };

与查找节点有关的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表示查找失败。

2、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表示查找失败。

3、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 *compatible)

函数参数和返回值含义如下:

from:开始查找的节点,如果为NULL表示从根节点开始查找整个设备树。

type:要查找的节点对应的type字符串,也就是device_type属性值,可以为NULL,表示忽略掉device_type属性。

compatible:要查找的节点所对应的compatible属性列表。

返回值:找到的节点,如果为NULL表示查找失败

4of_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表示查找失败

5of_find_node_by_path函数

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

inline struct device_node *of_find_node_by_path(const char *path)

函数参数和返回值含义如下:

path:带有全路径的节点名,可以使用节点的别名(用aliens节点中定义的别名)。

返回值:找到的节点,如果为NULL表示查找失败

查找父/子节点的OF函数

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

1of_get_parent函数

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

struct device_node *of_get_parent(const struct device_node *node)

函数参数和返回值含义如下:

node:要查找的父节点的节点。

返回值:找到的父节点。

2of_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中,内容如下:

示例代码20.8.3.1 property结构体

35 struct property {​
36 char *name; /* 属性名字 */​
37 int length; /* 属性长度 */​
38 void *value; /* 属性值 */​
39 struct property *next; /* 下一个属性 */​
40 unsigned long _flags;​
41 unsigned int unique_id;​
42 struct bin_attribute attr;​
43 };

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

1of_find_property函数

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

property *of_find_property(const struct device_node *np,

const char *name,

int *lenp)

函数参数和返回值含义如下:

np:设备节点。

name:属性名字。

lenp:属性值的字节数

返回值:找到的属性。

2of_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:元素长度。

返回值:得到的属性元素数量。

3of_property_read_u32_index函数

of_property_read_u32_index函数用于从属性中获取指定下标(属性值是一个u32数据组成的数组)的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表示属性值列表太小。

4of_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表示属性值列表太小。

5、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表示属性值列表太小。

6of_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,读取成功,负值,读取失败。

7of_n_addr_cells函数

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

int of_n_addr_cells(struct device_node *np)

函数参数和返回值含义如下:

np:设备节点。

返回值:获取到的#address-cells属性值。

8of_n_size_cells函数

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

int of_n_size_cells(struct device_node *np)

函数参数和返回值含义如下:

np:设备节点。

返回值:获取到的#size-cells属性值。

其他常用的OF函数

1of_device_is_compatible函数

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

int of_device_is_compatible(const struct device_node *device,

const char *compat)

函数参数和返回值含义如下:

device:设备节点。

compat:要查看的字符串。

返回值:0,节点的compatible属性中不包含compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。

2of_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的话表示读取失败。

3of_translate_address函数

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

u64 of_translate_address(struct device_node *dev,

const __be32 *in_addr)

函数参数和返回值含义如下:

dev:设备节点。

in_addr:要转换的地址。

返回值:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。

4of_address_to_resource函数

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

示例代码20.8.4.1 resource结构体

18 struct resource {​
19 resource_size_t start;​
20 resource_size_t end;​
21 const char *name;​
22 unsigned long flags;​
23 unsigned long desc;​
24 struct resource *parent, *sibling, *child;​
25 };

对于32位的SOC来说,resource_size_t是u32类型的。其中start表示开始地址,end表示结束地址,name是这个资源的名字,flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h中,如下所示:

示例代码20.8.4.2 资源标志

1 #define IORESOURCE_BITS   0x000000ff ​
2 #define IORESOURCE_TYPE_BITS 0x00001f00 ​
3 #define IORESOURCE_IO 0x00000100 ​
4 #define IORESOURCE_MEM 0x00000200​
5 #define IORESOURCE_REG 0x00000300 ​
6 #define IORESOURCE_IRQ 0x00000400​
7 #define IORESOURCE_DMA 0x00000800​
8 #define IORESOURCE_BUS 0x00001000​
9 #define IORESOURCE_PREFETCH 0x00002000 ​
10 #define IORESOURCE_READONLY 0x00004000​
11 #define IORESOURCE_CACHEABLE 0x00008000​
12 #define IORESOURCE_RANGELENGTH 0x00010000​
13 #define IORESOURCE_SHADOWABLE 0x00020000​
14 #define IORESOURCE_SIZEALIGN 0x00040000 ​
15 #define IORESOURCE_STARTALIGN 0x00080000 ​
16 #define IORESOURCE_MEM_64 0x00100000​
17 #define IORESOURCE_WINDOW 0x00200000 ​
18 #define IORESOURCE_MUXED 0x00400000 ​
19 #define IORESOURCE_EXCLUSIVE 0x08000000 ​
20 #define IORESOURCE_DISABLED 0x10000000​
21 #define IORESOURCE_UNSET 0x20000000​
22 #define IORESOURCE_AUTO 0x40000000​
23 #define IORESOURCE_BUSY 0x80000000

大家一般最常见的资源标志就是IORESOURCE_MEMIORESOURCE_REGIORESOURCE_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,成功;负值,失败。

5of_iomap函数

of_iomap函数用于直接内存映射,以前我们会通过ioremap函数来完成物理地址到虚拟地址的映射,采用设备树以后就可以直接通过of_iomap函数来获取内存地址所对应的虚拟地址,不需要使用ioremap函数了。当然了,你也可以使用ioremap函数来完成物理地址到虚拟地址的内存映射,只是在采用设备树以后,大部分的驱动都使用of_iomap函数了。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函数我们在后面的驱动实验中再详细的讲解。

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

①、DTS、DTB和DTC之间的区别,如何将.dts文件编译为.dtb文件。

②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。

③、设备树的几个特殊子节点。

④、关于设备树的OF操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO信息、中断信息等等。要想在驱动中读取设备树的属性值,那么就必须使用Linux内核提供的众多的OF函数。

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