1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614
在前面章节中我们多次提到“设备树”这个概念和创建自己的设备树。但是并没有在TF-A和uboot里说设备树的原理,主要是一开始就讲解设备树不利于嵌入式Linux入门,对于大多数嵌入式开发人员来说设备树是个全新的概念。
本章我们就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux中,ARM相关的驱动全部采用了设备树(也有支持老式驱动的,比较少),最新出CPU在系统启动的时候就支持设备树,比如我们的MP1系列、NXP的I.MX8系列等。我们所使用的Linux版本为5.4.31,其支持设备树,所以正点原子MP1开发板的所有Linux驱动都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。
23.1 什么是设备树?
设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如CPU数量、 内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等,如图23.1.1所示:
图23.1.1 设备树结构示意图
在图23.1.1中,树的主干就是系统总线,IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支。IIC控制器有分为IIC1和IIC2两种,其中IIC1上接了FT5206和AT24C02这两个IIC设备,IIC2上只接了MPU6050这个设备。DTS文件的主要功能就是按照图23.1.1所示的结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的,稍后我们会详细的讲解DTS语法规则。
在3.x版本(具体哪个版本笔者也无从考证)以前的Linux内核中ARM架构并没有采用设备树。在没有设备树的时候Linux是如何描述ARM架构中的板级信息呢?在Linux内核源码中大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里面的文件就是对应平台下的板级信息。比如在arch/arm/mach-s3c24xx/mach-smdk2440.c中有如下内容(有缩减):
示例代码23.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内核“虚胖”。就好比你喜欢吃自助餐,然后花了100多块钱到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句“Fk!”、“骗子!”。同样的,当Linux之父linus看到ARM社区向Linux内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“This whole ARM thing is a fcking 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社区引入了设备树。
23.2 DTS、DTB和DTC
上一小节说了,设备树源文件扩展名为.dts,但是我们在前面移植Linux的时候却一直在使用.dtb文件,那么DTS和DTB这两个文件是什么关系呢?DTS是设备树源码文件,DTB是将DTS编译以后得到的二进制文件。将.c文件编译为.o需要用到gcc编译器,那么将.dts编译为.dtb需要用到DTC工具!DTC工具源码在Linux内核的scripts/dtc目录下,scripts/dtc/Makefile文件内容如下:
示例代码23.2.1 scripts/dtc/Makefile文件代码段
4 hostprogs-$(CONFIG_DTC) := dtc
5 always := $(hostprogs-y)
6
7 dtc-objs := dtc.o flattree.o fstree.o data.o livetree.o treesource.o
8 srcpos.o checks.o util.o
9 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源码中的所有东西,包括uImage,.ko驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令,“make dtbs”会编译选中的所有设备树文件。如果只要编译指定的某个设备树,比如ST官方编写的“stm32mp157d-ed1.dts”,可以输入如下命令:
make stm32mp157d-ed1.dtb
结果如图23.2.1所示:
图23.2.1 编译单独的设备树
基于ARM架构的SOC有很多种,一种SOC又可以制作出很多款板子,每个板子都有一个对应的DTS文件,那么如何确定编译哪一个DTS文件呢?我们就以STM32MP1这款芯片对应的板子为例来看一下,打开arch/arm/boot/dts/Makefile,有如下内容:
示例代码23.2.2 arch/arm/boot/dts/Makefile文件代码段
981 dtb-$(CONFIG_ARCH_STM32) += \
982 stm32f429-disco.dtb \
983 stm32f469-disco.dtb \
984 stm32f746-disco.dtb \
985 stm32f769-disco.dtb \
986 stm32429i-eval.dtb \
987 stm32746g-eval.dtb \
988 stm32h743i-eval.dtb \
989 stm32h743i-disco.dtb \
990 stm32mp157a-avenger96.dtb \
991 stm32mp157a-dk1.dtb \
992 stm32mp157d-dk1.dtb \
993 stm32mp157c-dk2.dtb \
994 stm32mp157f-dk2.dtb \
995 stm32mp157c-dk2-a7-examples.dtb \
996 stm32mp157c-dk2-m4-examples.dtb \
997 stm32mp157f-dk2-a7-examples.dtb \
998 stm32mp157f-dk2-m4-examples.dtb \
999 stm32mp157a-ed1.dtb \
1000 stm32mp157c-ed1.dtb \
1001 stm32mp157d-ed1.dtb \
1002 stm32mp157f-ed1.dtb \
1003 stm32mp157a-ev1.dtb \
1004 stm32mp157c-ev1.dtb \
1005 stm32mp157d-ev1.dtb \
1006 stm32mp157f-ev1.dtb \
1007 stm32mp157d-atk.dtb \
1008 stm32mp157c-ev1-a7-examples.dtb \
1009 stm32mp157c-ev1-m4-examples.dtb \
1010 stm32mp157f-ev1-a7-examples.dtb \
1011 stm32mp157f-ev1-m4-examples.dtb
可以看出,当选中STM32MP1这个SOC以后(CONFIG_ARCH_STM32=y),所有使用到STM32MP1这个SOC的板子对应的.dts文件都会被编译为.dtb。如果我们使用STM32MP1新做了一个板子,只需要新建一个此板子对应的.dts文件,然后将对应的.dtb文件名添加到dtb-$( CONFIG_ARCH_STM32)下,这样在编译设备树的时候就会将对应的.dts编译为二进制的.dtb文件。
示例代码23.2.2中1007行就是我们在给正点原子的开发板移植Linux系统的时候添加的设备树。关于.dtb文件怎么使用这里就不多说了,前面讲解TF-A移植、Uboot移植、Linux内核移植的时候已经无数次的提到如何使用.dtb文件了(uboot中使用bootz或bootm命令向Linux内核传递二进制设备树文件(.dtb))。
23.3 DTS语法
虽然我们基本上不会从头到尾重写一个.dts文件,大多时候是直接在SOC厂商提供的.dts文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为后续工作中肯定需要修改.dts文件。大家不要看到要学习新的语法就觉得会很复杂,DTS语法非常的人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便。
本节我们就以stm32mp157d-atk.dts这个文件为例来讲解一下DTS语法。关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,路径为:4、参考资料->Devicetree SpecificationV0.2.pdf,4、参考资料-> Power_ePAPR_APPROVED_v1.12.pdf
23.3.1 .dtsi头文件
和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在stm32mp157d-atk.dts中有如下所示内容:
示例代码23.3.1.1 stm32mp157d-atk.dts文件代码段
8 #include "stm32mp157.dtsi"
9 #include "stm32mp15xd.dtsi"
10 #include "stm32mp15-pinctrl.dtsi"
11 #include "stm32mp15xxaa-pinctrl.dtsi"
12 #include "stm32mp157-m4-srm.dtsi"
13 #include "stm32mp157-m4-srm-pinctrl.dtsi"
14 #include "stm32mp157d-atk.dtsi"
第8~17行,使用“#include”来引用“stm32mp15*.dtsi”这些.dtsi头文件。
设备树里面除了可以通过“#include”来引用.dtsi文件,也可以引用.h文件头文件,大家打开stm32mp157f-dk2.dts这个文件,找到如下代码:
示例代码23.3.1.2 stm32mp157f-dk2.dts文件代码段
14 #include
.dts文件不仅可以应用C语言里面的.h头文件,甚至也可以引用.dts文件,打开stm32mp157c-ev1.dts这个文件,此文件中有如下内容:
示例代码23.3.1.3 stm32mp157c-ev1.dts文件代码段
8 #include “stm32mp157c-ed1.dts”
可以看出,示例代码23.3.1.3中直接引用了.dts文件,因此在.dts设备树文件中,可以通过“#include”来引用.h、.dtsi和.dts文件。只是,我们在编写设备树头文件的时候最好选择.dtsi后缀。
一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等等。如果一个系列里有多个SOC就会把相同内部外设信息提炼到一个.dtsi文件里,这样为了减少代码的冗余。目前STM32MP1系列里有stm32mp151、stm32mp153和stm32mp157这三款SOC,其中151是外设最少的,153和157的外设是在151的基础上逐渐增加的。因此151就相当于“基类”,153和157是在151基础上得到的“派生类”。因此ST就把最基本的外设资源都写在stm32mp151.dtsi文件里。stm32mp151.dtsi就是描述151、153和157共有的外设信息的,内容如下:
示例代码23.3.1.4 stm32mp151.dtsi文件代码段
6 #include <dt-bindings/interrupt-controller/arm-gic.h>
7 #include <dt-bindings/clock/stm32mp1-clks.h>
8 #include <dt-bindings/gpio/gpio.h>
9 #include <dt-bindings/reset/stm32mp1-resets.h>
10 #include <dt-bindings/thermal/thermal.h>
11
12
13 / {
14 #address-cells = <1>;
15 #size-cells = <1>;
16
17 cpus {
18 #address-cells = <1>;
19 #size-cells = <0>;
20
21 cpu0: cpu@0 {
22 compatible = "arm,cortex-a7";
......
29 nvmem-cell-names = "part_number";
30 #cooling-cells = <2>;
31 };
32 };
33
34 cpu0_opp_table: cpu0-opp-table {
35 compatible = "operating-points-v2";
36 opp-shared;
37 };
38 };
......
469 spi2: spi@4000b000 {
470 #address-cells = <1>;
471 #size-cells = <0>;
472 compatible = "st,stm32h7-spi";
473 reg = <0x4000b000 0x400>;
474 interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
475 clocks = <&rcc SPI2_K>;
476 resets = <&rcc SPI2_R>;
477 dmas = <&dmamux1 39 0x400 0x01>,
478 <&dmamux1 40 0x400 0x01>;
479 dma-names = "rx", "tx";
480 power-domains = <&pd_core>;
481 status = "disabled";
482 };
示例代码23.3.1.4中第1733行就是CPU0这个设备节点信息,这个节点信息描述了STM32MP151这颗SOC所使用的CPU信息,比如架构是cortex-A7,CPU的时钟等等,stm32mp151.dtsi文件中不仅仅描述了CPU0这个节点信息,STM32MP151这颗SOC所有的外设都描述的清清楚楚,比如SAI14, U(S)ART18,SDIO13等等,关于这些设备节点信息的具体内容我们后面具体章节里面再详细的讲解。
23.3.2 设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下文件是结合ST官方的设备树缩减出来设备树文件内容:
示例代码23.3.2.1 设备树模板
1 / {
2 #address-cells = <1>;
3 #size-cells = <1>;
4
5 aliases {
6 serial0 = &uart4;
7 };
8
9 cpus {
10 #address-cells = <1>;
11 #size-cells = <0>;
12
13 cpu0: cpu@0 {
14 compatible = "arm,cortex-a7";
15 device_type = "cpu";
16 reg = <0>;
17 clocks = <&scmi0_clk CK_SCMI0_MPU>;
18 clock-names = "cpu";
19 operating-points-v2 = <&cpu0_opp_table>;
20 nvmem-cells = <&part_number_otp>;
21 nvmem-cell-names = "part_number";
22 #cooling-cells = <2>;
23 };
24 };
25
26 soc {
27 compatible = "simple-bus";
28 #address-cells = <1>;
29 #size-cells = <1>;
30 interrupt-parent = <&intc>;
31 ranges;
32
33 sram: sram@10000000 {
34 compatible = "mmio-sram";
35 reg = <0x10000000 0x60000>;
36 #address-cells = <1>;
37 #size-cells = <1>;
38 ranges = <0 0x10000000 0x60000>;
39 };
40 };
41 };
第1行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,在内核移植设备树的时候,我们新建的stm32mp157d-atk.dts和stm32mp157d-atk.dtsi这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为这两个“/”根节点的内容会合并成一个根节点。
第5、9和26行,aliases、cpus和soc是三个子节点,33行sram是soc的子节点。在设备树中节点命名格式如下:
node-name@unit-address
其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“sram@10000000”、“soc”。
但是我们在示例代码23.3.2.1中我们看到的节点命名却如下所示:
cpu0:cpu@0
上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面是节点标签(label),“:”后面的才是节点名字,格式如下所示:
label: node-name@unit-address
引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,比如通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “sram: sram@10000000”,节点label是sram,而节点名字就很长了,为“sram@10000000”。很明显通过&sram来访问“sram@10000000”这个节点要方便很多!
第13行,cpu0也是一个节点,只是cpu0是cpus的子节点。
每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
1、字符串
compatible = “arm,cortex-a7”;
上述代码设置compatible属性的值为字符串“arm,cortex-a7”。
2、32位无符号整数
reg = <0>;
上述代码设置reg属性的值为0,reg的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
3、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;
上述代码设置属性compatible的值为“st,stm32mp157d-atk”和“st,stm32mp157”。
23.3.3 标准属性
节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。
1、compatible属性
compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序,compatible属性的值格式如下所示:
“manufacturer,model”
其中manufacturer表示厂商,model一般是模块对应的驱动名字。比如stm32mp15xx-dkx.dtsi中有一个音频设备节点,这个节点的音频芯片采用的Cirrus Logic公司出品的cs42l51,compatible属性值如下:
compatible = “cirrus,cs42l51”;
属性值为“cirrus,cs42l51”,其中‘cirrus’表示厂商是Cirrus Logic,“cs42l51”表示驱动模块名字。compatible也可以多个属性值。比如:
compatible = “cirrus,my_cs42l51”,“cirrus,cs42l51”;
这样我们的设备就有两个属性值,这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查,以此类推,直到查找完compatible属性中的所有值。
一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在文件cs42l51.c中有如下内容:
示例代码23.3.3.1 cs42l51.c 文件代码段
799 const struct of_device_id cs42l51_of_match[] = {
800 { .compatible = "cirrus,cs42l51", },
801 { }
802 };
数组cs42l51_of_match就是cs42l51.c这个驱动文件的匹配表,此匹配表只有一个匹配值“cirrus,cs42l51”。如果在设备树中有哪个节点的compatible属性值与此相等,那么这个节点就会使用此驱动文件。
2、model属性
model属性值也是一个字符串,一般model属性描述开发板的名字或者设备模块信息,比如名字什么的,比如:
model = “STMicroelectronics STM32MP157C-DK2 Discovery Board”;
3、status属性
status属性看名字就知道是和设备状态有关的,status属性值也是字符串,字符串是设备的状态信息,可选的状态如表23.3.3.1所示:
值 描述
“okay” 表明设备是可操作的。
“disabled” 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于disabled的具体含义还要看设备的绑定文档。
“fail” 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
“fail-sss” 含义和“fail”相同,后面的sss部分是检测到的错误内容。
表23.3.3.1 status属性值表
4、#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 =
每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address这个数据所占用的字长,#size-cells表明length这个数据所占用的字长,比如:
示例代码23.3.3.2 #address-cells和#size-cells属性
1 cpus {
2 #address-cells = <1>;
3 #size-cells = <0>;
4
5 cpu0: cpu@0 {
6 compatible = "arm,cortex-a7";
7 device_type = "cpu";
8 reg = <0>;
9 clocks = <&scmi0_clk CK_SCMI0_MPU>;
10 clock-names = "cpu";
11 operating-points-v2 = <&cpu0_opp_table>;
12 nvmem-cells = <&part_number_otp>;
13 nvmem-cell-names = "part_number";
14 #cooling-cells = <2>;
15 };
16 };
17
18 scmi_sram: sram@2ffff000 {
19 compatible = "mmio-sram";
20 reg = <0x2ffff000 0x1000>;
21 #address-cells = <1>;
22 #size-cells = <1>;
23 ranges = <0 0x2ffff000 0x1000>;
24
25 scmi0_shm: scmi_shm@0 {
26 reg = <0 0x80>;
27 };
28 };
第2,3行,节点cpus的#address-cells = <1>,#size-cells = <0>,说明cpus的子节点reg属性中起始地址所占用的字长为1,地址长度所占用的字长为0。
第8行,子节点cpu0: cpu@0的reg 属性值为 <0>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此addres=0,没有length的值,相当于设置了起始地址,而没有设置地址长度。
第21,22行,设置scmi_sram: sram@02ffff000节点#address-cells = <1>,#size-cells = <1>,说明scmi_sram: sram@02ffff000节点起始地址长度所占用的字长为1,地址长度所占用的字长也为1。
第26行,子节点scmi0_shm: scmi_shm@0的reg属性值为<0x0 0x80>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0x0,length= 0x80,相当于设置了起始地址为0x0,地址长度为0x80。
5、reg属性
reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息或者设备地址信息,比如某个外设的寄存器地址范围信息,或者IIC器件的设备地址等,比如在stm32mp151.dtsi中有如下内容:
示例代码23.3.3.3 uart5节点信息
576 uart5: serial@40011000 {
577 compatible = "st,stm32h7-uart";
578 reg = <0x40011000 0x400>;
579 interrupts-extended = <&exti 31 IRQ_TYPE_LEVEL_HIGH>;
580 clocks = <&rcc UART5_K>;
581 resets = <&rcc UART5_R>;
582 wakeup-source;
583 power-domains = <&pd_core>;
584 dmas = <&dmamux1 65 0x400 0x5>,
585 <&dmamux1 66 0x400 0x1>;
586 dma-names = "rx", "tx";
587 status = "disabled";
588 };
uart5节点描述了stm32mp1系列芯片的UART5相关信息,重点是第578行的reg属性。由于uart5的父节点“soc”设置了#address-cells = <1>、#size-cells = <1>,因此reg属性中address=0x40011000,length=0x400。查阅《STM32MP157参考手册》可知,stm32mp157芯片的UART5寄存器首地址为0x40011000,但是UART5的地址长度(范围)并没有0x400这么多,这里我们重点是获取UART5寄存器首地址。
6、ranges属性
ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵,ranges是一个地址映射/转换表,ranges属性每个项目由子地址、父地址和地址空间长度这三部分组成:
child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长。
parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长。
length:子地址空间的长度,由父节点的#size-cells确定此地址长度所占用的字长。
如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的stm32mp157来说,子地址空间和父地址空间完全相同,因此会在stm32mp151.dtsi中找到大量的值为空的ranges属性,如下所示:
示例代码23.3.3.4 stm32mp151.dtsi文件代码段
192 soc {
193 compatible = "simple-bus";
194 #address-cells = <1>;
195 #size-cells = <1>;
196 interrupt-parent = <&intc>;
197 ranges;
......
1968 };
第197行定义了ranges属性,但是ranges属性值为空。
ranges属性不为空的示例代码如下所示:
示例代码23.3.3.5 ranges属性不为空
1 soc {
2 compatible = "simple-bus";
3 #address-cells = <1>;
4 #size-cells = <1>;
5 interrupt-parent = <&intc>;
6 ranges = <0 0x10000000 0x100000>;
7
8 sram: sram@10000000 {
9 compatible = "mmio-sram";
10 reg = <0x0 0x60000>;
11 #address-cells = <1>;
12 #size-cells = <1>;
13 ranges = <0 0x10000000 0x60000>;
14 };
15 };
第6行,节点soc定义的ranges属性,值为<0 0x10000000 0x100000>,此属性值指定了一个1024KB(0x100000)的地址范围,子地址空间的物理起始地址为0,父地址空间的物理起始地址为0x10000000。
第8行,sram是STM32MP1内部RAM节点,reg属性定义了sram设备寄存器的起始地址为0,寄存器长度为0x60000。经过地址转换,sram设备可以从0x10000000开始进行读写操作,0x10000000=0x0 + 0x10000000。
7、name属性
name属性值为字符串,name属性用于记录节点名字,name属性已经被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。
8、device_type属性
device_type属性值为字符串,IEEE 1275会用到此属性,用于描述设备的FCode,但是设备树没有FCode,所以此属性也被抛弃了。此属性只能用于cpu节点或者memory节点。Stm32mp151.dtsi的cpu0节点用到了此属性,内容如下所示:
示例代码23.3.3.6 stm32mp151.dtsi文件代码段
21 cpu0: cpu@0 {
22 compatible = "arm,cortex-a7";
23 device_type = "cpu";
......
32 };
关于标准属性就讲解这么多,其他的比如中断、IIC、SPI等使用的标准属性等到具体的例程再讲解。
23.3.4 根节点compatible属性
每个节点都有compatible属性,根节点“/”也不例外,在我们新建的stm32mp157d-atk.dts文件中根节点的compatible属性内容如下所示:
示例代码23.3.4.1 stm32mp157d-atk.dts根节点compatible属性
16 / {
17 model = "STMicroelectronics STM32MP157C-DK2 Discovery Board";
18 compatible = "st,stm32mp157d-atk", "st,stm32mp157";
......
41 };
可以看出,compatible有两个值:“st,stm32mp157d-atk”和“st,stm32mp157”。前面我们说了,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatible属性是为了做什么工作的? 通过根节点的compatible属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“stm32mp157d-atk”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是“stm32mp157”这颗SOC。Linux内核会通过根节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。接下来我们就来学习一下Linux内核在使用设备树前后是如何判断是否支持某款设备的。
1、使用设备树之前设备匹配方法
在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值,machine id也就是设备ID,告诉Linux内核自己是个什么设备,看看Linux内核是否支持。Linux内核是支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START和MACHINE_END来定义一个machine_desc结构体来描述这个设备,比如在文件arch/arm/mach-imx/mach-mx35_3ds.c中有如下定义:
示例代码23.3.4.2 MX35_3DS设备
506 MACHINE_START(MX35_3DS, "Freescale MX35PDK")
507 /* Maintainer: Freescale Semiconductor, Inc */
508 .atag_offset = 0x100,
509 .map_io = mx35_map_io,
510 .init_early = imx35_init_early,
511 .init_irq = mx35_init_irq,
512 .init_time = mx35pdk_timer_init,
513 .init_machine = mx35_3ds_init,
514 .reserve = mx35_3ds_reserve,
515 .restart = mxc_restart,
516 MACHINE_END
上述代码就是定义了“Freescale MX35PDK”这个设备,其中MACHINE_START和MACHINE_END定义在文件arch/arm/include/asm/mach/arch.h中,内容如下:
示例代码23.3.4.3 MACHINE_START和MACHINE_END宏定义
81 #define MACHINE_START(_type,_name) \
82 static const struct machine_desc __mach_desc_##_type \
83 __used \
84 __attribute__((__section__(".arch.info.init"))) = { \
85 .nr = MACH_TYPE_##_type, \
86 .name = _name,
87
88 #define MACHINE_END \
89
根据MACHINE_START和MACHINE_END的宏定义,将示例代码23.3.4.2展开后如下所示:
示例代码23.3.4.3 展开以后
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 };
从示例代码23.3.4.3中可以看出,这里定义了一个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,内容如下所示:
示例代码23.3.4.3 mach-types.h文件中的machine id
10 #define MACH_TYPE_EBSA110 0
11 #define MACH_TYPE_RISCPC 1
12 #define MACH_TYPE_NEXUSPCI 3
......
1629 #define MACH_TYPE_MX35_3DS 1645
......
5053 #define MACH_TYPE_OMAP3_MRC3D 5114
第1629行就是MACH_TYPE_MX35_3DS的值,为1645。
前面说了,uboot会给Linux内核传递machine id这个参数,Linux内核会检查这个machine id,其实就是将machine id与示例代码23.3.4.3中的这些MACH_TYPE_XXX宏进行对比,看看有没有相等的,如果相等的话就表示Linux内核支持这个设备,如果不支持的话那么这个设备就没法启动Linux内核。
2、使用设备树以后的设备匹配方法
当Linux内核引入设备树以后就不再使用MACHINE_START了,而是换为了DT_MACHINE_START。DT_MACHINE_START也定义在文件arch/arm/include/asm/mach/arch.h 里面,定义如下:
示例代码23.3.4.4 DT_MACHINE_START宏
91 #define DT_MACHINE_START(_name, _namestr) \
92 static const struct machine_desc __mach_desc_##_name \
93 __used \
94 __attribute__((__section__(".arch.info.init"))) = { \
95 .nr = ~0, \
96 .name = _namestr,
97
98 #endif
可以看出,DT_MACHINE_START和MACHINE_START基本相同,只是.nr的设置不同,在DT_MACHINE_START里面直接将.nr设置为~0。说明引入设备树以后不会再根据machine id来检查Linux内核是否支持某个设备了。
打开文件arch/arm/mach-stm32/board-dt.c,有如下所示内容:
示例代码23.3.4.5 stm32mp157设备
14 static const char *const stm32_compat[] __initconst = {
15 "st,stm32f429",
16 "st,stm32f469",
17 "st,stm32f746",
18 "st,stm32f769",
19 "st,stm32h743",
20 "st,stm32mp151",
21 "st,stm32mp153",
22 "st,stm32mp157",
23 NULL
24 };
25
26 DT_MACHINE_START(STM32DT, "STM32 (Device Tree Support)")
27 .dt_compat = stm32_compat,
28 #ifdef CONFIG_ARM_SINGLE_ARMV7M
29 .restart = armv7m_restart,
30 #endif
31 MACHINE_END
machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本设备兼容属性,示例代码23.3.4.5中设置.dt_compat为stm32_compat,此表里面含有Linux内核所支持的soc兼容值。只要某个设备(板子)根节点“/”的compatible属性值与stm32_compat表中的任何一个值相等,那么就表示Linux内核支持此设备。stm32mp157d-atk.dts中根节点的compatible属性值如下:
compatible = “st,stm32mp157d-atk”, “st,stm32mp157”;
其中“st,stm32mp157”与stm32_compat中的“st,stm32mp157”匹配,因此正点原子STM32MP157开发板可以正常启动Linux内核。
接下来我们简单看一下Linux内核是如何根据设备树根节点的compatible属性来匹配出对应的machine_desc,Linux内核调用start_kernel函数来启动内核,start_kernel函数会调用setup_arch函数来匹配machine_desc,setup_arch函数定义在文件arch/arm/kernel/setup.c中,函数内容如下(有缩减):
示例代码23.3.4.6 setup_arch函数内容
1076 void __init setup_arch(char **cmdline_p)
1077 {
1078 const struct machine_desc *mdesc;
1079
1080 setup_processor();
1081 mdesc = setup_machine_fdt(__atags_pointer);
1082 if (!mdesc)
1083 mdesc = setup_machine_tags(__atags_pointer,
__machine_arch_type);
......
1094 machine_desc = mdesc;
1095 machine_name = mdesc->name;
......
1174 }
第1081行,调用setup_machine_fdt函数来获取匹配的machine_desc,参数就是atags的首地址,也就是uboot传递给Linux内核的dtb文件首地址,setup_machine_fdt函数的返回值就是找到的最匹配的machine_desc。
函数setup_machine_fdt定义在文件arch/arm/kernel/devtree.c中,内容如下(有缩减):
示例代码23.3.4.7 setup_machine_fdt函数内容
211 const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
212 {
213 const struct machine_desc *mdesc, *mdesc_best = NULL;
......
224 if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
225 return NULL;
226
227 mdesc = of_flat_dt_match_machine(mdesc_best,
arch_get_next_mach);
......
256 __machine_arch_type = mdesc->nr;
257
258 return mdesc;
259 }
第227行,调用函数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中,内容如下(有缩减):
示例代码23.3.4.8 of_flat_dt_match_machine函数内容
815 const void * __init of_flat_dt_match_machine(const void *default_match,
816 const void * (*get_next_compat)(const char * const**))
817 {
818 const void *data = NULL;
819 const void *best_data = default_match;
820 const char *const *compat;
821 unsigned long dt_root;
822 unsigned int best_score = ~1, score = 0;
823
824 dt_root = of_get_flat_dt_root();
825 while ((data = get_next_compat(&compat))) {
826 score = of_flat_dt_match(dt_root, compat);
827 if (score > 0 && score < best_score) {
828 best_data = data;
829 best_score = score;
830 }
831 }
.....
850 pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());
851
852 return best_data;
853 }
第824行,通过函数of_get_flat_dt_root获取设备树根节点。
第825~831行,此循环就是查找匹配的machine_desc过程,第826行的of_flat_dt_match函数会将根节点compatible属性的值和每个machine_desc结构体中. dt_compat的值进行比较,直至找到匹配的那个machine_desc。
总结一下,Linux内核通过根节点compatible属性找到对应的设备的函数调用过程,如图23.3.4.2所示:
图23.3.4.2 查找匹配设备的过程
23.3.5 向节点追加或修改内容
产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个IIC接口的六轴芯片MPU6050,第二版硬件又要把这个MPU6050更换为MPU9250等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471要接到STM32MP157D-ATK开发板的I2C1接口上,那么相当于需要在i2c1这个节点上添加一个fxls8471子节点。先看一下I2C1接口对应的节点,打开文件stm32mp151.dtsi文件,找到如下所示内容:
示例代码23.3.5.1 i2c1节点
590 i2c1: i2c@40012000 {
591 compatible = "st,stm32mp15-i2c";
592 reg = <0x40012000 0x400>;
593 interrupt-names = "event", "error";
594 interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
595 <&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
596 clocks = <&rcc I2C1_K>;
597 resets = <&rcc I2C1_R>;
598 #address-cells = <1>;
599 #size-cells = <0>;
600 dmas = <&dmamux1 33 0x400 0x80000001>,
601 <&dmamux1 34 0x400 0x80000001>;
602 dma-names = "rx", "tx";
603 power-domains = <&pd_core>;
604 st,syscfg-fmp = <&syscfg 0x4 0x1>;
605 wakeup-source;
606 status = "disabled";
607 };
示例代码23.3.5.1就是STM32MP157的I2C1节点,现在要在i2c1节点下创建一个子节点,这个子节点就是fxls8471,最简单的方法就是在i2c1下直接添加一个名为fxls8471的子节点,如下所示:
示例代码23.3.5.2 添加 fxls8471子节点
590 i2c1: i2c@40012000 {
591 compatible = "st,stm32mp15-i2c";
592 reg = <0x40012000 0x400>;
593 interrupt-names = "event", "error";
594 interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
595 <&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
596 clocks = <&rcc I2C1_K>;
597 resets = <&rcc I2C1_R>;
598 #address-cells = <1>;
599 #size-cells = <0>;
600 dmas = <&dmamux1 33 0x400 0x80000001>,
601 <&dmamux1 34 0x400 0x80000001>;
602 dma-names = "rx", "tx";
603 power-domains = <&pd_core>;
604 st,syscfg-fmp = <&syscfg 0x4 0x1>;
605 wakeup-source;
606 status = "disabled";
607 //fxls8471子节点
608 fxls8471@1e {
609 compatible = "fsl,fxls8471";
610 reg = <0x1e>;
611 };
612 };
第608~611行就是添加的fxls8471这个芯片对应的子节点。但是这样会有个问题!i2c1节点是定义在stm32mp151.dtsi文件中的,而stm32mp151.dtsi是共有的设备树头文件,其他所有使用到stm32mp1这颗SOC的板子都会引用stm32mp151.dtsi这个文件。直接在i2c1节点中添加fxls8471就相当于在其他的所有板子上都添加了fxls8471这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码23.3.5.2这样写肯定是不行的。
这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向i2c1节点追加一个名为fxls8471的子节点,而且不能影响到其他使用到stm32mp1的板子。STM32MP157D-ATK开发板使用的设备树文件为stm32mp157d-atk.dts和stm32mp157d-atk.dtsi,因此我们需要在stm32mp157d-atk.dts文件中完成数据追加的内容,方式如下:
示例代码23.3.5.3 节点追加数据方法
1 &i2c1 {
2 /* 要追加或修改的内容 */
3 };
第1行,&i2c1表示要访问i2c1这个label所对应的节点,也就是stm32mp151.dtsi中的“i2c1: i2c@40012000”。
第2行,花括号内就是要向i2c1这个节点添加的内容,包括修改某些属性的值。
打开stm32mp157d-atk.dts,在根节点后添加以下代码:
示例代码23.3.5.4 向i2c1节点追加数据
1 &i2c1 {
2 pinctrl-names = "default", "sleep";
3 pinctrl-0 = <&i2c1_pins_b>;
4 pinctrl-1 = <&i2c1_pins_sleep_b>;
5 status = "okay";
6 clock-frequency = <100000>;
7
8 fxls8471@1e {
9 compatible = "fsl,fxls8471";
10 reg = <0x1e>;
11 position = <0>;
12 interrupt-parent = <&gpioh>;
13 interrupts = <6 IRQ_TYPE_EDGE_FALLING>;
14 };
15 };
示例代码23.3.5.4就是向i2c1节点添加/修改数据,比如第5行将status属性的值由原来的disabled改为okay。第6行的属性“clock-frequency”表示i2c1时钟为100KHz。“clock-frequency”就是新添加的属性。
第8~14行,i2c1子节点fxls8471,表示I2C1上连接的fxls8471,“fxls8471”子节点里面描述了fxls8471这颗芯片的相关信息。
因为示例代码23.3.5.4中的内容是stm32mp157d-atk.dts这个文件内的,所以不会对使用STM32MP157这颗SOC的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。
23.4 创建小型模板设备树
上一节已经对DTS的语法做了比较详细的讲解,本节我们就根据前面讲解的语法,从头到尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为了掌握设备树的语法。在实际产品开发中,我们是不需要完完全全的重写一个.dts设备树文件,一般都是使用SOC厂商提供好的.dts文件,我们只需要在上面根据自己的实际情况做相应的修改即可。在编写设备树之前要先定义一个设备,我们就以STM32MP157这个SOC为例,我们需要在设备树里面描述的内容如下:
、这个芯片是由两个Cortex-A7架构的32位CPU和Cortex-M4组成。
、STM32MP157内部sram,起始地址为0x10000000,大小为384KB(0x60000)。
、STM32MP157内部timers6,起始地址为0x40004000,大小为25.6KB(0x400)。
、STM32MP157内部spi2,起始地址为0x4000b000,大小为25.6KB(0x400)。
、STM32MP157内部usart2,起始地址为0x4000e000,大小为25.6KB(0x400)。
、STM32MP157内部i2c1,起始地址为0x40012000,大小为25.6KB(0x400)。
为了简单起见,我们在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点“/”的基础的框架,新建一个名为myfirst.dts文件,在里面输入如下所示内容:
示例代码23.4.1 设备树基础框架
1 / {
2 compatible = "st,stm32mp157d-atk", "st,stm32mp157";
3 };
设备树框架很简单,就一个根节点“/”,根节点里面只有一个compatible属性。我们就在这个基础框架上面将上面列出的内容一点点添加进来。
1、添加cpus节点
首先添加CPU节点,STM32MP157采用Cortex-A7架构,先添加一个cpus节点,在cpus节点下添加cpu0子节点和cpu1子节点,完成以后如下所示:
示例代码23.4.2 添加cpus节点
1 / {
2 compatible = "st,stm32mp157d-atk", "st,stm32mp157";
3 /* cpus节点 */
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 /* CPU0节点 */
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 /* CPU1节点 */
15 cpu1: cpu@1 {
16 compatible = "arm,cortex-a7";
17 device_type = "cpu";
18 reg = <1>;
19 };
20 };
21 };
第4~20行,cpus节点,此节点用于描述SOC内部的所有CPU,因为STM32MP157有两个CPU,所以在cpus下添加两个子节点分别为cpu0和cpu1。
2、添加soc节点
像uart,iic控制器等等这些都属于SOC内部外设,因此一般会创建一个叫做soc的父节点来管理这些SOC内部外设的子节点,添加soc节点以后的myfirst.dts文件内容如下所示:
示例代码23.4.3 添加soc节点
1 / {
2 compatible = "st,stm32mp157d-atk", "st,stm32mp157";
3 /* cpus节点 */
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 /* CPU0节点 */
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 /* CPU1节点 */
15 cpu1: cpu@1 {
16 compatible = "arm,cortex-a7";
17 device_type = "cpu";
18 reg = <1>;
19 };
20 };
21 /* soc节点 */
22 soc {
23 compatible = "simple-bus";
24 #address-cells = <1>;
25 #size-cells = <1>;
26 ranges;
27 };
28 };
第22~27行,soc节点,soc节点设置#address-cells = <1>,#size-cells = <1>,这样soc子节点的reg属性中起始地占用一个字长,地址空间长度也占用一个字长。
第26行,ranges属性,ranges属性为空,说明子空间和父空间地址范围相同。
3、添加sram节点
我们继续添加sram节点,sram是STM32MP157内部RAM,M4内核会用到SRAM4。sram是soc节点的子节点。sram起始地址为0x10000000,大小为384KB,添加sram节点以后myfirst.dts文件内容如下所示:
示例代码23.4.4 添加sram节点
1 / {
2 compatible = "st,stm32mp157d-atk", "st,stm32mp157";
3 /* cpus节点 */
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 /* CPU0节点 */
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 /* CPU1节点 */
15 cpu1: cpu@1 {
16 compatible = "arm,cortex-a7";
17 device_type = "cpu";
18 reg = <1>;
19 };
20 };
21 /* soc节点 */
22 soc {
23 compatible = "simple-bus";
24 #address-cells = <1>;
25 #size-cells = <1>;
26 ranges;
27 /* sram节点 */
28 sram: sram@10000000 {
29 compatible = "mmio-sram";
30 reg = <0x10000000 0x60000>;
31 ranges = <0 0x10000000 0x60000>;
32 };
33 };
34 };
第28~32行,sram节点,第28行节点名字@后面的0x10000000就是sram的起始地址。第30行的reg属性也指明了sram内存的起始地址为0x10000000,大小为0x60000。
4、添加timers6、spi2、usart2和i2c1这四个子节点
最后我们在myfirst.dts文件中加入timers6、spi2、usart2和i2c1这四个外设控制器的节点。最终的myfirst.dts文件内容如下:
示例代码23.4.5 最终的myfirst.dts文件
1 / {
2 compatible = "st,stm32mp157d-atk", "st,stm32mp157";
3 /* cpus节点 */
4 cpus {
5 #address-cells = <1>;
6 #size-cells = <0>;
7
8 /* CPU0节点 */
9 cpu0: cpu@0 {
10 compatible = "arm,cortex-a7";
11 device_type = "cpu";
12 reg = <0>;
13 };
14 /* CPU1节点 */
15 cpu1: cpu@1 {
16 compatible = "arm,cortex-a7";
17 device_type = "cpu";
18 reg = <1>;
19 };
20 };
21 /* soc节点 */
22 soc {
23 compatible = "simple-bus";
24 #address-cells = <1>;
25 #size-cells = <1>;
26 ranges;
27 /* sram节点 */
28 sram: sram@10000000 {
29 compatible = "mmio-sram";
30 reg = <0x10000000 0x60000>;
31 #address-cells = <1>;
32 #size-cells = <1>;
33 ranges = <0 0x10000000 0x60000>;
34 };
35 /* timers6节点 */
36 timers6: timer@40004000 {
37 #address-cells = <1>;
38 #size-cells = <0>;
39 compatible = "st,stm32-timers";
40 reg = <0x40004000 0x400>;
41 };
42 /* spi2节点 */
43 spi2: spi@4000b000 {
44 #address-cells = <1>;
45 #size-cells = <0>;
46 compatible = "st,stm32h7-spi";
47 reg = <0x4000b000 0x400>;
48 };
49 /* usart2节点 */
50 usart2: serial@4000e000 {
51 compatible = "st,stm32h7-uart";
52 reg = <0x4000e000 0x400>;
53 };
54 /* i2c1节点 */
55 i2c1: i2c@40012000 {
56 compatible = "st,stm32mp15-i2c";
57 reg = <0x40012000 0x400>;
58 };
59
60 };
61 };
第36~41行,timers6外设控制器节点。
第43~48行,spi2外设控制器节点。
第50~53行,usart2外设控制器节点。
第55~58行,i2c1外设控制器节点。
到这里,我们的myfirst.dts小型的设备树模板就编辑好了,基本和stm32mp151.dtsi很像,可以看做是stm32mp151.dtsi的缩小版。在myfirst.dts里面我们仅仅是编写了STM32MP1的外设控制器节点,像SAI接口,LTDC接口下所连接具体设备我们并没有写,因为具体的设备其设备树属性内容不同,这个等到具体的实验在详细讲解。
23.5 设备树在系统中的体现
Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹,如图23.5.1所示:
图23.5.1 根节点“/”的属性以及子节点
图23.5.1就是目录/proc/device-tree目录下的内容,/proc/device-tree目录下是根节点“/”的所有属性和子节点,我们依次来看一下这些属性和子节点。
1、根节点“/”各个属性
在图23.5.1中,根节点属性属性表现为一个个的文件,比如图23.5.1中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这5个文件,它们在设备树中就是根节点的5个属性。既然是文件那么肯定可以查看其内容,输入cat命令来查看model和compatible这两个文件的内容,结果如图23.5.2所示:
图23.5.2 model和compatible文件内容
从图23.5.2可以看出,文件model的内容是“STMicroelectronics STM32MP157D eval daughter”,文件compatible的内容为“st,stm32mp157d-ed1st,stm32mp157”。打开文件stm32mp157d-atk.dts查看一下,这不正是根节点“/”的model和compatible属性值吗!
2、根节点“/”各子节点
图23.5.1中各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“reboot”、“chosen”和“cpus”等等。大家可以查看一下stm32mp157d-atk.dts和stm32mp157d-atk.dts所引用的.dtsi文件,看看根节点的子节点都有哪些,是否和图23.5.1中的一致。
/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc目录中就可以看到soc节点的所有子节点,如图23.5.3所示:
图23.5.3 soc部分节点
和根节点“/”一样,图23.5.3中的所有文件分别为soc节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和stm32mp151.dtsi中soc节点的属性值相同。
23.6 特殊节点
在根节点“/”中有两个特殊的子节点:aliases和chosen,我们接下来看一下这两个特殊的子节点。
23.6.1 aliases子节点
打开stm32mp157d-atk.dts文件,aliases节点内容如下所示:
示例代码23.6.1.1 aliases子节点
24 aliases {
25 serial0 = &uart4;
26 };
单词alias
es的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label的形式来访问节点。
23.6.2 chosen子节点
chosen并不是一个真实的设备,chosen节点主要是为了uboot向Linux内核传递数据,重点是bootargs参数。一般.dts文件中chosen节点通常为空或者内容很少,stm32mp157d-atk.dts中chosen节点内容如下所示:
示例代码23.6.2.1 chosen子节点
20 chosen {
21 stdout-path = "serial0:115200n8";
22 };
从示例代码23.6.2.1中可以看出,chosen节点仅仅设置了属性“stdout-path”,表示标准输出使用serial0,而aliases已经设置了serial0为uart4,所以开发板启动以后使用UART4作为默认终端。但是当我们进入到/proc/device-tree/chosen目录里面,会发现多了bootargs这个属性,如图23.6.2.1所示:
图23.6.2.1 chosen节点目录
输入cat命令查看bootargs这个文件的内容,结果如图23.6.2.2所示:
图23.6.2.2 bootargs文件内容
从图23.6.2.2可以看出,bootargs这个文件的内容为“console=ttySTM0,115200……”,这个就是我们在uboot中设置的bootargs环境变量,现在有两个疑点:
①、我们并没有在设备树中设置chosen节点的bootargs属性,那么图23.6.2.1中bootargs这个属性是怎么产生的?
②、为何图23.6.2.1中bootargs文件的内容和uboot中bootargs环境变量的值一样?它们之间有什么关系?
前面讲解uboot的时候说过,uboot在启动Linux内核的时候会将bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的bootargs的值),如图23.6.2.3所示:
图23.6.2.3 命令行参数
既然chosen节点的bootargs属性不是我们在设备树里面设置的,那么只有一种可能,那就是uboot自己在chosen节点里面添加了bootargs属性!并且设置bootargs属性的值为bootargs环境变量的值。因为在启动Linux内核之前,只有uboot知道bootargs环境变量的值,并且uboot也知道.dtb设备树文件在DRAM中的位置,因此uboot的“作案”嫌疑最大。在uboot源码中全局搜索“chosen”这个字符串,看看能不能找到一些蛛丝马迹。果然不出所料,在common/fdt_support.c文件中发现了“chosen”的身影,fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示:
示例代码23.6.2.2 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)中找到chosen节点,如果没有找到的话就会自己创建一个chosen节点。
第292行,读取uboot中bootargs环境变量的内容。
第294行,调用函数fdt_setprop向chosen节点添加bootargs属性,并且bootargs属性的值就是环境变量bootargs的内容。
证据“实锤”了,就是uboot中的fdt_chosen函数在设备树的chosen节点中加入了bootargs属性,并且还设置了bootargs属性值。接下来我们顺着fdt_chosen函数一点点的抽丝剥茧,看看都有哪些函数调用了fdt_chosen,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见图23.6.2.4:
图23.6.2.4 fdt_chosen函数调用流程
图23.6.2.4中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在chosen节点中加入了bootargs属性。而我们通过bootz命令启动Linux内核的时候会运行do_bootm_linux函数,至此,真相大白,一切事情的源头都源于如下命令:
bootm c2000000 – c4000000
当我们输入上述命令并执行以后,do_bootm函数就会执行,然后一切就按照图23.6.2.4中所示的流程开始运行。
23.7 Linux内核解析DTB文件
Linux内核在启动的时候会解析DTB文件,然后在/proc/device-tree目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的,流程如图23.7.1所示:
图23.7.1 设备树节点解析流程。
从图23.7.1中可以看出,在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。
23.8 绑定信息文档
设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux内核源码中有详细的TXT文档描述了如何添加节点,这些TXT文档叫做绑定文档,路径为:Linux源码目录/Documentation/devicetree/bindings,如图23.8.1所示:
图23.8.1 绑定文档
比如我们现在要想在STM32MP157这颗SOC的I2C下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-stm32.txt,此文档详细的描述了STM32MP1系列的SOC如何在设备树中添加I2C设备节点,文档内容如下所示:
Required properties:
Optional properties:
Example:
i2c@40005400 {
compatible = "st,stm32f4-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x40005400 0x400>;
interrupts = <31>,
<32>;
resets = <&rcc 277>;
clocks = <&rcc 0 149>;
pinctrl-0 = <&i2c1_sda_pin>, <&i2c1_scl_pin>;
pinctrl-names = "default";
};
i2c@40005400 {
compatible = "st,stm32f7-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x40005400 0x400>;
interrupts = <31>,
<32>;
resets = <&rcc STM32F7_APB1_RESET(I2C1)>;
clocks = <&rcc 1 CLK_I2C1>;
pinctrl-0 = <&i2c1_sda_pin>, <&i2c1_scl_pin>;
pinctrl-1 = <&i2c1_sda_pin_sleep>, <&i2c1_scl_pin_sleep>;
pinctrl-names = "default", "sleep";
st,syscfg-fmp = <&syscfg 0x4 0x1>;
st,syscfg-fmp-clr = <&syscfg 0x44 0x1>;
};
有时候使用的一些芯片在Documentation/devicetree/bindings目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。
23.9 设备树常用OF操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的0X02005482和0X400这两个值,然后初始化外设。Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做OF函数。这些OF函数原型都定义在include/linux/of.h文件中。
23.9.1 查找节点的OF函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,定义如下:
示例代码23.3.9.1 device_node节点
51 struct device_node {
52 const char *name; /*节点名字 */
53 phandle phandle;
54 const char *full_name; /*节点全名 */
55 struct fwnode_handle fwnode;
56
57 struct property *properties; /*属性 */
58 struct property *deadprops; /*removed 属性 */
59 struct device_node *parent; /*父节点 */
60 struct device_node *child; /*子节点 */
61 struct device_node *sibling;
62 #if defined(CONFIG_OF_KOBJ)
63 struct kobject kobj;
64 #endif
65 unsigned long _flags;
66 void *data;
67 #if defined(CONFIG_SPARC)
68 unsigned int unique_id;
69 struct of_irq_controller *irq_trans;
70 #endif
71 };
与查找节点有关的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表示查找失败
4、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表示查找失败
5、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表示查找失败
23.9.2 查找父/子节点的OF函数
Linux内核提供了几个查找节点对应的父节点或子节点的OF函数,我们依次来看一下。
1、of_get_parent函数
of_get_parent函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:
struct device_node *of_get_parent(const struct device_node *node)
函数参数和返回值含义如下:
node:要查找的父节点的节点。
返回值:找到的父节点。
2、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,表示从第一个子节点开始。
返回值:找到的下一个子节点。
23.9.3 提取属性值的OF函数
节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下:
示例代码23.9.3.1 property结构体
31 struct property {
32 char *name; /* 属性名字 */
33 int length; /* 属性长度 */
34 void *value; /* 属性值 */
35 struct property *next; /* 下一个属性 */
36 #if defined(CONFIG_OF_DYNAMIC) || defined(CONFIG_SPARC)
37 unsigned long _flags;
38 #endif
39 #if defined(CONFIG_OF_PROMTREE)
40 unsigned int unique_id;
41 #endif
42 #if defined(CONFIG_OF_KOBJ)
43 struct bin_attribute attr;
44 #endif
45 };
Linux内核也提供了提取属性值的OF函数,我们依次来看一下。
1、of_find_property函数
of_find_property函数用于查找指定的属性,函数原型如下:
property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)
函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。
2、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:元素长度。
返回值:得到的属性元素数量。
3、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表示属性值列表太小。
4、 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表示属性值列表太小。
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表示属性值列表太小。
6、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,读取成功,负值,读取失败。
7、of_n_addr_cells函数
of_n_addr_cells函数用于获取#address-cells属性值,函数原型如下:
int of_n_addr_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#address-cells属性值。
8、of_n_size_cells函数
of_n_size_cells函数用于获取#size-cells属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells属性值。
23.9.4 其他常用的OF函数
1、of_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指定的字符串。
2、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的话表示读取失败。
3、of_translate_address函数
of_translate_address函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:
u64 of_translate_address(struct device_node *dev,
const __be32 *addr)
函数参数和返回值含义如下:
dev:设备节点。
in_addr:要转换的地址。
返回值:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。
4、of_address_to_resource函数
IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用resource结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用resource结构体描述的都是设备资源信息,resource结构体定义在文件include/linux/ioport.h中,定义如下:
示例代码23.9.4.1 resource结构体
20 struct resource {
21 resource_size_t start;
22 resource_size_t end;
23 const char *name;
24 unsigned long flags;
25 unsigned long desc;
26 struct resource *parent, *sibling, *child;
27 };
对于32位的SOC来说,resource_size_t是u32类型的。其中start表示开始地址,end表示结束地址,name是这个资源的名字,flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h中,如下所示:
示例代码23.9.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_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,成功;负值,失败。
5、of_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驱动实验都将采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。将会带领大家由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发技能。