韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南...

1)实验平台:正点原子领航者ZYNQ开发板

2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761

3)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/fpga/zdyz_linhanz.html

4)对正点原子FPGA感兴趣的同学可以加群讨论:876744900

5)关注正点原子公众号,获取最新资料

http://weixin.qq.com/r/hEhUTLbEdesKrfIv9x2W (二维码自动识别)

第二十四章Linux设备树


前面章节中我们多次提到“设备树”这个概念,因为时机未到,所以当时并没有详细的讲解什么是“设备树”,本章我们就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux内核中,设备驱动基本全部采用了设备树(也有支持老式驱动的,比较少)的方式,最新出的CPU其驱动开发也基本都是基于设备树的,我们所使用的Linux版本为4.14.0,肯定是支持设备树的,所以正点原子领航者开发板的所有Linux驱动都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。
24.1什么是设备树?
在旧版本(大概是3.x以前的版本)的linux内核当中,ARM架构的板级硬件设备信息被硬编码在arch/arm/plat-xxx和arch/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文件中有如下内容(有缩减):
示例代码24.1.1 mach-smdk2440.c文件代码片段

  1. 90 static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {
  2. 91
  3. 92 .lcdcon5 = S3C2410_LCDCON5_FRM565 |
  4. 93 S3C2410_LCDCON5_INVVLINE |
  5. 94 S3C2410_LCDCON5_INVVFRAME |
  6. 95 S3C2410_LCDCON5_PWREN |
  7. 96 S3C2410_LCDCON5_HWSWP,
  8. ......
  9. 113 };
  10. 114
  11. 115 static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {
  12. 116 .displays = &smdk2440_lcd_cfg,
  13. 117 .num_displays = 1,
  14. 118 .default_display = 0,
  15. ......
  16. 133 };
  17. 134
  18. 135 static struct platform_device *smdk2440_devices[] __initdata = {
  19. 136 &s3c_device_ohci,
  20. 137 &s3c_device_lcd,
  21. 138 &s3c_device_wdt,
  22. 139 &s3c_device_i2c0,
  23. 140 &s3c_device_iis,
  24. 141 };

复制代码


上述代码中的结构体变量smdk2440_fb_info就是描述SMDK2440这个开发板上的LCD硬件信息的,结构体指针数组smdk2440_devices描述的是SMDK2440这个开发板上的所有硬件相关信息。这个仅仅是使用2440这个芯片的SMDK2440开发板下的LCD信息,SMDK2440开发板还有很多的其他外设硬件和平台硬件信息。使用2440这个芯片的板子有很多,每个板子都有描述相应板级硬件信息的文件,这仅仅只是一个2440。随着智能手机的发展,每年新出的ARM架构芯片少说都在数十、数百款,Linux内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c或.h文件,都会被硬编码进Linux内核中,导致Linux内核“虚胖”。就好比你喜欢吃自助餐,然后花了100多到一家宣传看着很不错的自助餐厅,结果你想吃的牛排、海鲜、烤肉基本没多少,全都是一些凉菜、炒面、西瓜、饮料等小吃,相信你此时肯定会脱口而出一句“F*k!”、“骗子!”。
这些板级硬件信息代码对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-xxx和arch/arm/mach-xxx目录下的文件当中,或者直接硬编码在驱动代码当中,例如我们前面编写的LED驱动就是直接将led的信息(用的哪个管脚、GPIO寄存器的基地址等)直接编码在了驱动源码当中,一旦外围设备变化(例如PS_LED0换成另一个MIO引脚了),驱动代码就要重写。
引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。使用设备树之后,许多硬件设备信息可以直接通过它传递给Linux,而不需要在内核中堆积大量的冗余代码。
设备树,将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的硬件设备信息,比如CPU数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等,如图 35.1.1所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第1张图片

图 35.1.1 设备树结构示意图


在图 35.1.1中,树的主干就是系统总线,IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支。IIC控制器有分为IIC1和IIC2两种,其中IIC1上接了FT5206和AT24C02这两个IIC设备,IIC2上只接了MPU6050这个设备。DTS文件的主要功能就是按照图 35.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/arm/boot/dts/system-top.dts,这个文件是在第三十一章时候使用hsi命令自动生成的,前面已经跟大家讲过了,除了system-top.dts文件之外,还生成了另外三个文件pl.dtsi、pcw.dtsi以及zynq-7000.dtsi(system-top.dts包含它们三个,后面会说到),并且一并把它们放入了linux内核源码arch/arm/boot/dts目录下了。
前面也跟大家讲过,除了内核支持设备树之外,新版的u-boot也是支持设备树的,如果有机会也可以跟大家讲一讲U-Boot的设备树。
24.2设备树的基本知识
24.2.1dts
设备树的源文件的后缀名就是.dts,每一款硬件平台可以单独写一份xxxx.dts,所以在Linux内核源码中存在大量.dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts。
24.2.2dtsi
值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,这个dtsi文件其实就类似于C语言当中的.h头文件,可以通过C语言中使用include来包含一个.dtsi文件,例如arch/arm/boot/dts/system-top.dts文件有如下内容:
示例代码24.2.2.1 system-top.dts内容片段

  1. 1 /*
  2. 2 * CAUTION: This file is automatically generated by Xilinx.
  3. 3 * Version: HSI
  4. 4 * Today is: Mon Mar 16 02:51:23 2020
  5. 5 */
  6. 6
  7. 7
  8. 8 /dts-v1/;
  9. 9 #include "zynq-7000.dtsi"
  10. 10 #include "pl.dtsi"
  11. 11 #include "pcw.dtsi"
  12. 12 / {
  13. 13 model = "Alientek ZYNQ Development Board";
  14. 14
  15. 15 chosen {
  16. 16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
  17. 17 stdout-path = "serial0:115200n8";
  18. 18 };
  19. 19 aliases {
  20. 20 ethernet0 = &gem0;
  21. 21 i2c0 = &i2c_2;
  22. 22 i2c1 = &i2c0;
  23. 23 i2c2 = &i2c1;
  24. 24 serial0 = &uart0;
  25. 25 serial1 = &uart1;
  26. 26 spi0 = &qspi;
  27. 27 };
  28. 28 memory {
  29. 29 device_type = "memory";
  30. 30 reg = <0x0 0x20000000>;
  31. 31 };
  32. 32 };

复制代码


第9~11行中,通过#include包含了同目录下的三个.dtsi文件,分别为:zynq-7000.dtsi、pl.dtsi、pcw.dtsi。这里简答地给大家说一下这三个文件的内容有啥不同,首先zynq-7000.dtsi文件中的内容是zynq-7000系列处理器相同的硬件外设配置信息(PS端的),pl.dtsi的内容是我们在vivado当中添加的pl端外设对应的配置信息,而pcw.dtsi则表示我们在vivado当中已经使能的PS外设。
那么除此之外,使用#include除了可以包含.dtsi文件之外,还可以包含.dts文件以及C语言当中的.h文件,这些都是可以的,可以这么理解.dtsi和.dts文件语法各方面都是一样的,但是不能直接编译一个.dtsi文件。
24.2.3dtc
dtc其实就是device-tree-compiler,那就是设备文件.dts的编译器嘛,将.c文件编译为.o文件需要用到gcc编译器,那么将.dts文件编译为相应的二进制文件则需要dtc编译器,dtc工具在Linux内核的scripts/dtc目录下,当然必须要编译了内核源码之后才会生成,如下所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第2张图片

图 35.2.1 dtc编译器


我们来看看scripts/dtc/Makefile文件,如下所示:
示例代码24.2.3.1 scripts/dtc/Makefile文件代码段

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

复制代码


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

  1. make all

复制代码


或者:

  1. make dtbs

复制代码


“make all”命令是编译Linux源码中的所有东西,包括zImage,.ko驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令。
在内核源码arch/arm/boot/dts目录下有很多的dts文件,那我们编译的时候如何确定编译的是哪个或者说哪些dts文件的呢?大家可以打开arch/arm/boot/dts/Makefile文件,找到CONFIG_ATCH_ZYNQ宏所在的位置,内容如下所示:
示例代码43.2.3.2 arch/arm/boot/dts/Makefile文件部分内容

  1. ......
  2. 1003 dtb-$(CONFIG_ARCH_ZYNQ) +=
  3. 1004 zynq-cc108.dtb
  4. 1005 zynq-microzed.dtb
  5. 1006 zynq-parallella.dtb
  6. 1007 zynq-zc702.dtb
  7. 1008 zynq-zc706.dtb
  8. 1009 zynq-zc770-xm010.dtb
  9. 1010 zynq-zc770-xm011.dtb
  10. 1011 zynq-zc770-xm012.dtb
  11. 1012 zynq-zc770-xm013.dtb
  12. 1013 zynq-zed.dtb
  13. 1014 zynq-zybo.dtb
  14. 1015 system-top.dtb
  15. ......

复制代码


由上面可以知道,当CONFIG_ATCH_ZYNQ=y时,对应下面列举的所有.dts文件会被编译进去,也包括我们之前添加上去的system-top.dtb文件;但是CONFIG_ATCH_ZYNQ=y又怎么确定呢?在31.5小节编译内核的时候我们执行了” make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xilinx_zynq_defconfig”这样一条命令,后面的xilinx_zynq_defconfig(arch/arm/configs/xilinx_zynq_defconfig)文件就是我们zynq平台对应的defconfig配置文件,那么打开它,找到CONFIG_ATCH_ZYNQ,如下:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第3张图片

图 35.2.2 CONFIG_ARCH_ZYNQ


在这个文件当中定义了CONFIG_ATCH_ZYNQ=y。所以对于ZYNQ平台来说,当我们在内核源码目录下执行”make all”或者是”make dtbs”命令的时候,arch/arm/boot/dts/Makefile文件中对应的那些dts文件就会被编译成dtb(二进制文件)文件。但是需要注意并不是说不在这个Makefile文件中列举出来的dts文件就不能被编译,我们在使用make命令的时候也可以指定需要编译的dts文件,例如下面:

  1. make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- system-top.dtb

复制代码

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第4张图片

图 35.2.3 指定dts文件


system-top.dtb表示需要编译出这个dtb文件,那么系统就会去arch/arm/boot/dts目录下找到相对应的.dts文件,也就是system-top.dts,而system-top.dts文件就可以不用记录在arch/arm/boot/dts/Makefile文件中,我们编译的时候已经指定了。
关于编译dts文件的问题就说到这里了,这里讲的有点太多了,如果大家还不明白可以去网上找找资料。
24.2.4dtb
.dtb文件就是将.dts文件编译成二进制数据之后得到的文件,这就跟.c文件编译为.o文件是一样的道理,关于.dtb文件怎么使用这里就不多说了,前面讲解Uboot移植、Linux内核移植的时候已经无数次的提到如何使用.dtb文件了(uboot中使用bootz或bootm命令向Linux内核传递二进制设备树文件(.dtb))。
24.3dts语法
虽然我们基本上不会从头到尾重写一个.dts文件,大多时候是直接在SOC厂商提供的.dts文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改.dts文件。大家不要看到要学习新的语法就觉得会很复杂,DTS语法非常的人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便。
本节我们就以system-top.dts这个文件为例来讲解一下DTS语法。关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,路径为:领航者ZYNQ开发板资料盘(A盘)8_ZYNQ&FPGA参考资料ARM Devicetree SpecificationV0.2.pdf、领航者ZYNQ开发板资料盘(A盘)8_ZYNQ&FPGA参考资料ARM Power_ePAPR_APPROVED_v1.12.pdf。
24.3.1设备树的结构
设备树用树状结构描述设备信息,组成设备树的基本单元是node(设备节点),这些node被组织成树状结构,有如下一些特征:
一个device tree文件中只有一个root node(根节点);
除了root node,每个node都只有一个parent node(父节点);
一般来说,开发板上的每一个设备都能够对应到设备树中的一个node;
每个node中包含了若干的property-value(键-值对,当然也可以没有value)来描述该node的一些特性;
每个node都有自己的node name(节点名字);
node之间可以是平行关系,也可以嵌套成父子关系,这样就可以很方便的描述设备间的关系;
下面给出一个设备树的简单的结构示意图:
示例代码24.3.1.1 设备树结构示意

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

复制代码


第1行当中的’/’就表示设备树的root node(根节点),所以可知node1节点和node2节点的父节点都是root node,而node3节点的父节点则是node2,node2与node3之间形成了父子节点关系。Root node下面的子节点node1和node2可以表示为SoC上的两个控制器,而node3则可以表示挂在node2控制器上的某个设备,例如node2表示ZYNQ PS的一个I2C控制器,而node3则表示挂在该I2C总线下的某个设备,例如eeprom、RTC等。
24.3.2节点与属性
在设备树文件中如何定义一个节点,节点的命名有什么要求呢?在设备树中节点的命名格式如下:

  1. [label:]node-name[@unit-address] {
  2. [properties definitions]
  3. [child nodes]
  4. };

复制代码


“[]”中的内容表示可选的,可有也可以没有;节点名字前加上”label”则方便在dts文件中被其他的节点引用,我们后面会说这个;其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存基地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。
每个节点都有若干属性,属性又有相对应的值(值不是必须要有的),而一个节点当中又可以嵌套其它的节点,形成父子节点。例如下面:
示例代码24.3.2.1 设备树节点示例

  1. 20 cpus {
  2. 21 #address-cells = <1>;
  3. 22 #size-cells = <0>;
  4. 23
  5. 24 cpu0: cpu@0 {
  6. 25 compatible = "arm,cortex-a9";
  7. 26 device_type = "cpu";
  8. 27 reg = <0>;
  9. 28 clocks = <&clkc 3>;
  10. 29 clock-latency = <1000>;
  11. 30 cpu0-supply = <®ulator_vccpint>;
  12. 31 operating-points = <
  13. 32 /* kHz uV */
  14. 33 666667 1000000
  15. 34 333334 1000000
  16. 35 >;
  17. 36 };
  18. 37
  19. 38 cpu1: cpu@1 {
  20. 39 compatible = "arm,cortex-a9";
  21. 40 device_type = "cpu";
  22. 41 reg = <1>;
  23. 42 clocks = <&clkc 3>;
  24. 43 };
  25. 44 };

复制代码


每一个节点(包括root node)都会使用一组括号”{ }”将自己的属性以及子节点包含在里边,注意括号外需要加上一个分号” ; ”,包括每一个属性都使用一个分号来结束。有点像C语言中的表达式后面的分号。
第20行当中的cpus节点,它的名字只有” [label:]node-name[@unit-address]”当中的”node-name”部分,并有其它两部分;第24行节点的定义包含了所有的组成部分,包括label以及unit-address;关于label的作用的我们后面专门讲,这里先不说。
cpus节点有两个属性” #address-cells”和” #size-cells”,它们的值分别为” <1>”和” <0>”。例如cpu@0节点中有compatible、device_type、reg、clocks属性等,它们都有对应的值,大家看到这些值可能有点不明白,为啥有的是字符串,有的是尖括号”<>”括起来的东西,下面单独给大家讲解一波。
每个节点都有不同属性,不同的属性又有不同的值,那么设备树当中值有哪些形式呢?
字符串

  1. compatible = "arm,cortex-a9";

复制代码


字符串使用双引号括起来,例如上面的这个compatible属性的值是” arm,cortex-a9”字符串。
32位无符号整形数据

  1. clock-latency = <1000>;
  2. reg = <0x00000000 0x00500000>;

复制代码


32位无符号整形数据使用尖括号括起来,例如属性cock-latency的值是一个32位无符号整形数据1000,而reg属性有两个数据,使用空格隔开,那么这个就可以认为是一个数组,很容易理解!
二进制数据

  1. local-mac-address = [00 0a 35 00 1e 53];

复制代码


二进制数据使用方括号括起来,例如上面这个就是一个二进制数据组成的数组。
字符串数组

  1. compatible = "n25q512a","micron,m25p80";

复制代码


属性值也可以使用字符串列表,例如上面的这个属性,它的值是一个字符串列表,字符串之间使用逗号分割;
混合值

  1. mixed-property = "a string", [0x01 0x23 0x45 0x67], <0x12345678>;

复制代码


除此之外不同的数据类型还可以混合在一起,以逗号分隔。
节点引用
除了上面一些数据类型之外,还有一种非常常见的形式,如下所示:

  1. clocks = <&clkc 3>;

复制代码


这其实就是我们上面说到的引用节点的一种形式,”&clkc”就表示引用”clkc”这个节点,而clkc就是前面提到的”label”,引用节点也是使用尖括号来表示,关于节点之间的引用,我们后面还会再讲,这里先告一段落。
24.3.3使用注释和宏定义
在设备树文件中也可以使用注释,注释的方法和C语言当中是一毛一样的,可以使用” // ”进行单行注释,也可以使用” /* */ ”进行多行注释,如下所示:

  1. 1 // SPDX-License-Identifier: GPL-2.0+
  2. 2 /*
  3. 3 * Copyright (C) 2011 - 2015 Xilinx
  4. 4 *
  5. 5 * This software is licensed under the terms of the GNU General Public
  6. 6 * License version 2, as published by the Free Software Foundation, and
  7. 7 * may be copied, distributed, and modified under those terms.
  8. 8 *
  9. 9 * This program is distributed in the hope that it will be useful,
  10. 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. 12 * GNU General Public License for more details.
  13. 13 */
  14. 14
  15. 15 / {
  16. 16 #address-cells = <1>;
  17. 17 #size-cells = <1>;
  18. 18 compatible = "xlnx,zynq-7000";
  19. 19
  20. 20 cpus {
  21. 21 #address-cells = <1>;
  22. 22 #size-cells = <0>;
  23. 23
  24. 24 cpu0: cpu@0 {
  25. 25 compatible = "arm,cortex-a9";
  26. 26 device_type = "cpu";
  27. 27 reg = <0>;
  28. 28 clocks = <&clkc 3>;
  29. 29 clock-latency = <1000>;
  30. 30 cpu0-supply = <®ulator_vccpint>;
  31. 31 operating-points = <
  32. 32 /* kHz uV */
  33. 33 666667 1000000
  34. 34 333334 1000000
  35. 35 >;
  36. 36 };
  37. 37
  38. 38 cpu1: cpu@1 {
  39. 39 compatible = "arm,cortex-a9";
  40. 40 device_type = "cpu";
  41. 41 reg = <1>;
  42. 42 clocks = <&clkc 3>;
  43. 43 };
  44. 44 };

复制代码


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

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第5张图片

图 35.3.1 头文件包含

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第6张图片

图 35.3.2 使用宏定义


关于头文件包含以及宏定义的使用这里就不多说了,本身也非常简单。
24.3.4标准属性
节点的内容是由一堆的属性组成,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。
1、compatible属性
compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值可以是一个字符串,也可以是一个字符串列表;一般该字符串使用”<制造商>,<型号>”这样的形式进行命名,当然这不是必须要这样,这是要求大家按照这样的形式进行命名,目的是为了指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突,如下所示:

  1. compatible = "xlnx,xuartps", "cdns,uart-r1p8";

复制代码


例子当中的xlnx和cdns就表示制造商,而后面的xuartps和uart-r1p8就表示具体设备的型号。compatible属性用于将设备和驱动绑定起来,例如该设备首先使用第一个兼容值(xlnx,xuartps)在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值(cdns,uart-r1p8)查找,直到找到或者查找完整个Linux内核也没有找到对应的驱动。
一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备树中的节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在驱动文件drivers/tty/serial/xilinx_uartps.c中有如下内容:
示例代码24.3.4.1 drivers/tty/serial/xilinx_uartps.c内容片段

  1. 1342
  2. 1343 /* Match table for of_platform binding */
  3. 1344 static const struct of_device_id cdns_uart_of_match[] = {
  4. 1345 { .compatible = "xlnx,xuartps", },
  5. 1346 { .compatible = "cdns,uart-r1p8", },
  6. 1347 { .compatible = "cdns,uart-r1p12", .data = &zynqmp_uart_def },
  7. 1348 { .compatible = "xlnx,zynqmp-uart", .data = &zynqmp_uart_def },
  8. 1349 {}
  9. 1350 };
  10. 1351 MODULE_DEVICE_TABLE(of, cdns_uart_of_match);
  11. ......
  12. 1703
  13. 1704 static struct platform_driver cdns_uart_platform_driver = {
  14. 1705 .probe = cdns_uart_probe,
  15. 1706 .remove = cdns_uart_remove,
  16. 1707 .driver = {
  17. 1708 .name = CDNS_UART_NAME,
  18. 1709 .of_match_table = cdns_uart_of_match,
  19. 1710 .pm = &cdns_uart_dev_pm_ops,
  20. 1711 },
  21. 1712 };

复制代码


这个驱动文件是ZYNQ PS端的UART设备对应的驱动文件。
第1344~1350行定义的数组cdns_uart_of_match就是xilinx_uartps.c这个驱动文件的匹配表,此匹配表有4个匹配值“xlnx,xuartps”、“cdns,uart-r1p8”、“cdns,uart-r1p12”以及“xlnx,zynqmp-uart”。如果在设备树中有哪个节点的compatible属性值与这4个字符串中的某个相同,那么这个节点就会与此驱动文件匹配成功。
第1704行,UART采用了platform_driver驱动模式,关于platform_driver驱动后面会讲解。此行设置.of_match_table为cdns_uart_of_match,也就是设置这个platform_driver所使用的OF匹配表。
2、model属性
model属性值也是一个字符串描述信息,它指定制造商的设备型号,model属性一般定义在根节点下,一般就是对板子的描述信息,没啥实质性的作用,内核在解析设备树的时候会把这个属性对应的字符串信息打印出来。
示例代码24.3.4.2 arch/arm/boot/dts/system-top.dts内容片段

  1. 8 /dts-v1/;
  2. 9 #include "zynq-7000.dtsi"
  3. 10 #include "pl.dtsi"
  4. 11 #include "pcw.dtsi"
  5. 12 / {
  6. 13 model = "Alientek ZYNQ Development Board";
  7. 14
  8. 15 chosen {
  9. 16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
  10. 17 stdout-path = "serial0:115200n8";
  11. 18 };
  12. 19 aliases {
  13. 20 ethernet0 = &gem0;
  14. 21 i2c0 = &i2c_2;
  15. 22 i2c1 = &i2c0;
  16. 23 i2c2 = &i2c1;
  17. 24 serial0 = &uart0;
  18. 25 serial1 = &uart1;
  19. 26 spi0 = &qspi;
  20. 27 };
  21. 28 memory {
  22. 29 device_type = "memory";
  23. 30 reg = <0x0 0x20000000>;
  24. 31 };
  25. 32 };

复制代码


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

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第7张图片

图 35.3.3 打印model字符串


3、status属性
status属性看名字就知道是和设备状态有关的,device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值:
值 描述
okay 表明设备是可操作的。启动设备
disabled 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于disabled的具体含义还要看设备的绑定文档。
fail 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。
fail-sss 含义和“fail”相同,后面的sss部分是检测到的错误内容。
表 35.3.4.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 =
每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address字段占用的字长,#size-cells表明length这个字段所占用的字长,比如:
示例代码24.3.4.3 #address-cells和#size-cells属性

  1. 38 &qspi {
  2. 39 #address-cells = <1>;
  3. 40 #size-cells = <0>;
  4. 41 flash0: flash@0 {
  5. 42 compatible = "n25q512a","micron,m25p80";
  6. 43 reg = <0x0>;
  7. 44 #address-cells = <1>;
  8. 45 #size-cells = <1>;
  9. 46 spi-max-frequency = <50000000>;
  10. 47 partition@0x00000000 {
  11. 48 label = "boot";
  12. 49 reg = <0x00000000 0x00500000>;
  13. 50 ;
  14. 51 partition@0x00500000 {
  15. 52 label = "bootenv";
  16. 53 reg = <0x00500000 0x00020000>;
  17. 54 };
  18. 55 partition@0x00520000 {
  19. 56 label = "kernel";
  20. 57 reg = <0x00520000 0x00a80000>;
  21. 58 };
  22. 59 partition@0x00fa0000 {
  23. 60 label = "spare";
  24. 61 reg = <0x00fa0000 0x00000000>;
  25. 62 };
  26. 63 };
  27. 64 };

复制代码


第39~40行,节点qspi的#address-cells = <1>,#size-cells = <0>,说明qspi的子节点reg属性中起始地址使用一个32bit数据来表示,地址长度没有;第43行,qspi的子节点flash0:flash@0的reg属性值为<0>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此addres=0,没有length的值,相当于设置了起始地址,而没有设置地址长度。
第44~45行,设置flash0:flash@0节点#address-cells = <1>,#size-cells = <1>,说明flash0:flash@0的子节点起始地址长度所占用的字长为1,地址长度所占用的字长也为1。第49行,flash0:flash@0的子节点partition@0x00000000的reg属性值为reg = <0x00000000 0x00500000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,所以address使用一个32bit数据来表示,也就address=0x00000000,而length也使用一个32bit数据来表示,也就是length=0x00500000,相当于设置了起始地址为0x00000000,地址长度为0x00500000。
5、reg属性
reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息,一般都是描述某个外设的寄存器地址范围信息、flash设备的分区信息等,比如在arch/arm/boot/dts/zynq-7000.dts文件中有如下内容:
示例代码24.3.4.4 uart0节点信息

  1. 174 uart0: serial@e0000000 {
  2. 175 compatible = "xlnx,xuartps", "cdns,uart-r1p8";
  3. 176 status = "disabled";
  4. 177 clocks = <&clkc 23>, <&clkc 40>;
  5. 178 clock-names = "uart_clk", "pclk";
  6. 179 reg = <0xE0000000 0x1000>;
  7. 180 interrupts = <0 27 4>;
  8. 181 };

复制代码


上述代码是节点uart0,uart0节点描述了ZYNQ PS端的UART0相关信息,重点是第179行的reg属性。其中uart0的父节点amba设置了#address-cells = <1>、#size-cells = <1>,因此reg属性中address= 0xE0000000,length= 0x1000。查阅ZYNQ的数据手册(领航者ZYNQ开发板资料盘(A盘)8_ZYNQ&FPGA参考资料XilinxUser Guideug585-Zynq-7000-TRM.pdf)可知,ZYNQ的UART0寄存器首地址确实为0xE0000000,但是UART0的地址长度(范围)并没有0x1000这么多,这里我们重点是获取UART0寄存器首地址,只要地址空间没有跨越到其它外设的地址空间也没什么影响。
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来说,子地址空间和父地址空间完全相同,因此会在zynq-7000.dtsi文件中找到大量的值为空的ranges属性,如下所示:
示例代码24.3.4.5 zynq-7000.dtsi内容片段

  1. 46 fpga_full: fpga-full {
  2. 47 compatible = "fpga-region";
  3. 48 fpga-mgr = <&devcfg>;
  4. 49 #address-cells = <1>;
  5. 50 #size-cells = <1>;
  6. 51 ranges;
  7. 52 };

复制代码


第51行定义了ranges属性,但是ranges属性值为空。
ranges属性不为空的示例代码如下所示:
示例代码24.3.4.6 ranges属性不为空

  1. 1 soc {
  2. 2 compatible = "simple-bus";
  3. 3 #address-cells = <1>;
  4. 4 #size-cells = <1>;
  5. 5 ranges = <0x0 0xe0000000 0x00100000>;
  6. 6
  7. 7 serial {
  8. 8 device_type = "serial";
  9. 9 compatible = "ns16550";
  10. 10 reg = <0x4600 0x100>;
  11. 11 clock-frequency = <0>;
  12. 12 interrupts = <0xA 0x8>;
  13. 13 interrupt-parent = <&ipic>;
  14. 14 };
  15. 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节点用到了此属性,内容如下所示:
示例代码24.3.4.7 zynq-7000.dtsi内容片段

  1. 24 cpu0: cpu@0 {
  2. 25 compatible = "arm,cortex-a9";
  3. 26 device_type = "cpu";
  4. 27 reg = <0>;
  5. 28 clocks = <&clkc 3>;
  6. 29 clock-latency = <1000>;
  7. 30 cpu0-supply = <®ulator_vccpint>;
  8. 31 operating-points = <
  9. 32 /* kHz uV */
  10. 33 666667 1000000
  11. 34 333334 1000000
  12. 35 >;
  13. 36 };
  14. 37
  15. 38 cpu1: cpu@1 {
  16. 39 compatible = "arm,cortex-a9";
  17. 40 device_type = "cpu";
  18. 41 reg = <1>;
  19. 42 clocks = <&clkc 3>;
  20. 43 };

复制代码


关于标准属性就讲解这么多,后面还会跟大家介绍一些常常会使用到的节点,例如设备树中的中断控制器、GPIO、I2C总线等。
24.3.5根节点compatible属性
每个节点都有compatible属性(除了一些特殊用途的节点),根节点“/”也不例外,在zynq-7000.dtsi文件中根节点的compatible属性内容如下所示:
示例代码24.3.5.1 zynq-7000.dtsi根节点compatible属性

  1. 15 / {
  2. 16 #address-cells = <1>;
  3. 17 #size-cells = <1>;
  4. 18 compatible = "xlnx,zynq-7000";
  5. ......
  6. 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_START和MACHINE_END来定义一个machine_desc结构体来描述这个硬件平台,比如在文件arch/arm/mach-imx/mach-mx35_3ds.c中有如下定义:
示例代码24.3.5.2 MX35_3DS设备

  1. 613 MACHINE_START(MX35_3DS, "Freescale MX35PDK")
  2. 614 /* Maintainer: Freescale Semiconductor, Inc */
  3. 615 .atag_offset = 0x100,
  4. 616 .map_io = mx35_map_io,
  5. 617 .init_early = imx35_init_early,
  6. 618 .init_irq = mx35_init_irq,
  7. 619 .init_time = mx35pdk_timer_init,
  8. 620 .init_machine = mx35_3ds_init,
  9. 621 .reserve = mx35_3ds_reserve,
  10. 622 .restart = mxc_restart,
  11. 623 MACHINE_END

复制代码


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

  1. #define MACHINE_START(_type,_name)
  2. static const struct machine_desc __mach_desc_##_type
  3. __used
  4. __attribute__((__section__(".arch.info.init"))) = {
  5. .nr = MACH_TYPE_##_type,
  6. .name = _name,
  7. #define MACHINE_END
  8. };

复制代码


根据MACHINE_START和MACHINE_END的宏定义,将示例代码24.3.2展开后如下所示:
示例代码24.3.5.6 展开以后

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

复制代码


从示例代码24.3.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,内容如下所示:
示例代码24.3.5.7 mach-types.h文件中的machine id

  1. 15 #define MACH_TYPE_EBSA110 0
  2. 16 #define MACH_TYPE_RISCPC 1
  3. 17 #define MACH_TYPE_EBSA285 4
  4. 18 #define MACH_TYPE_NETWINDER 5
  5. 19 #define MACH_TYPE_CATS 6
  6. 20 #define MACH_TYPE_SHARK 15
  7. 21 #define MACH_TYPE_BRUTUS 16
  8. 22 #define MACH_TYPE_PERSONAL_SERVER 17
  9. ......
  10. 287 #define MACH_TYPE_MX35_3DS 1645
  11. ......
  12. 1000 #define MACH_TYPE_PFLA03 4575

复制代码


第287行就是MACH_TYPE_MX35_3DS的值,为1645。
前面说了,uboot会给Linux内核传递machine id这个参数,Linux内核会检查这个machine id,其实就是将machine id与示例代码24.3.4中的这些MACH_TYPE_XXX宏进行对比,看看有没有相等的,如果相等的话就表示Linux内核支持这个硬件平台,如果不支持的话就没法启动Linux内核。
2、使用设备树以后的设备匹配方法
当Linux内核引入设备树以后就不再使用MACHINE_START了,而是换为了DT_MACHINE_START。DT_MACHINE_START也定义在文件arch/arm/include/asm/mach/arch.h 里面,定义如下:
示例代码24.3.5.8 DT_MACHINE_START宏

  1. #define DT_MACHINE_START(_name, _namestr)
  2. static const struct machine_desc __mach_desc_##_name
  3. __used
  4. __attribute__((__section__(".arch.info.init"))) = {
  5. .nr = ~0,
  6. .name = _namestr,

复制代码


可以看出,DT_MACHINE_START和MACHINE_START基本相同,只是.nr的设置不同,在DT_MACHINE_START里面直接将.nr设置为~0。说明引入设备树以后不会再根据machine id来检查Linux内核是否支持某个硬件平台了。
打开文件arch/arm/mach-zynq/common.c,有如下所示内容:
示例代码24.3.5.9 arch/arm/mach-zynq/common.c

  1. 191 static const char * const zynq_dt_match[] = {
  2. 192 "xlnx,zynq-7000",
  3. 193 NULL
  4. 194 };
  5. 195
  6. 196 DT_MACHINE_START(XILINX_EP107, "Xilinx Zynq Platform")
  7. 197 /* 64KB way size, 8-way associativity, parity disabled */
  8. 198 #ifdef CONFIG_XILINX_PREFETCH
  9. 199 .l2c_aux_val = 0x30400000,
  10. 200 .l2c_aux_mask = 0xcfbfffff,
  11. 201 #else
  12. 202 .l2c_aux_val = 0x00400000,
  13. 203 .l2c_aux_mask = 0xffbfffff,
  14. 204 #endif
  15. 205 .smp = smp_ops(zynq_smp_ops),
  16. 206 .map_io = zynq_map_io,
  17. 207 .init_irq = zynq_irq_init,
  18. 208 .init_machine = zynq_init_machine,
  19. 209 .init_late = zynq_init_late,
  20. 210 .init_time = zynq_timer_init,
  21. 211 .dt_compat = zynq_dt_match,
  22. 212 .reserve = zynq_memory_init,
  23. 213 MACHINE_END

复制代码


machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本硬件平台的兼容属性,示例代码24.3.6中设置.dt_compat = zynq_dt_match,zynq_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_desc,setup_arch函数定义在文件arch/arm/kernel/setup.c中,函数内容如下(有缩减):
示例代码24.3.5.10 setup_arch函数内容

  1. 913 void __init setup_arch(char **cmdline_p)
  2. 914 {
  3. 915 const struct machine_desc *mdesc;
  4. 916
  5. 917 setup_processor();
  6. 918 mdesc = setup_machine_fdt(__atags_pointer);
  7. 919 if (!mdesc)
  8. 920 mdesc = setup_machine_tags(__atags_pointer, __machine_arch_type);
  9. 921 machine_desc = mdesc;
  10. 922 machine_name = mdesc->name;
  11. ......
  12. 986 }

复制代码


第918行,调用setup_machine_fdt函数来获取匹配的machine_desc,参数就是atags的首地址,也就是uboot传递给Linux内核的dtb文件首地址,setup_machine_fdt函数的返回值就是找到的已经匹配成功的machine_desc。
函数setup_machine_fdt定义在文件arch/arm/kernel/devtree.c中,内容如下(有缩减):
示例代码24.3.5.11 setup_machine_fdt函数内容

  1. 204 const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
  2. 205 {
  3. 206 const struct machine_desc *mdesc, *mdesc_best = NULL;
  4. ......
  5. 214
  6. 215 if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))
  7. 216 return NULL;
  8. 217
  9. 218 mdesc = of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
  10. 219
  11. ......
  12. 247 __machine_arch_type = mdesc->nr;
  13. 248
  14. 249 return mdesc;
  15. 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中,内容如下(有缩减):
示例代码24.3.5.12 of_flat_dt_match_machine函数内容

  1. 705 const void * __init of_flat_dt_match_machine(const void *default_match,
  2. 706 const void * (*get_next_compat)(const char * const**))
  3. 707 {
  4. 708 const void *data = NULL;
  5. 709 const void *best_data = default_match;
  6. 710 const char *const *compat;
  7. 711 unsigned long dt_root;
  8. 712 unsigned int best_score = ~1, score = 0;
  9. 713
  10. 714 dt_root = of_get_flat_dt_root();
  11. 715 while ((data = get_next_compat(&compat))) {
  12. 716 score = of_flat_dt_match(dt_root, compat);
  13. 717 if (score > 0 && score < best_score) {
  14. 718 best_data = data;
  15. 719 best_score = score;
  16. 720 }
  17. 721 }
  18. ......
  19. 739
  20. 740 pr_info("Machine model: %sn", of_flat_dt_get_machine_name());
  21. 741
  22. 742 return best_data;
  23. 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结构体的函数调用过程,如下图所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第8张图片

图 35.3.4 查找匹配machine_desc的过程


24.3.6引用节点
前面说到节点的命名格式如下所示:
[label:]node-name[@unit-address]
也多次给大家提到“label”字段,引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,例如下面这个模板:
示例代码24.3.6.1 设备树模板

  1. 1 / {
  2. 2 aliases {
  3. 3 can0 = &flexcan1;
  4. 4 };
  5. 5
  6. 6 cpus {
  7. 7 #address-cells = <1>;
  8. 8 #size-cells = <0>;
  9. 9
  10. 10 cpu0: cpu@0 {
  11. 11 compatible = "arm,cortex-a7";
  12. 12 device_type = "cpu";
  13. 13 reg = <0>;
  14. 14 };
  15. 15 };
  16. 16
  17. 17 intc: interrupt-controller@00a01000 {
  18. 18 compatible = "arm,cortex-a7-gic";
  19. 19 #interrupt-cells = <3>;
  20. 20 interrupt-controller;
  21. 21 reg = <0x00a01000 0x1000>,
  22. 22 <0x00a02000 0x100>;
  23. 23 };
  24. 24 };通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc: interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多!
    所以如果我们要在设备树中引用其它的节点,那么就可以在这个被引用的节点前加上“label:”,这样我们就可以很方便的通过“&label”的方式进行引用了。
    24.3.7向节点追加或修改内容
    这里面有两个知识点:向节点追加内容,也就是添加属性;另一个就是修改节点的内容。我相信大家都理解我这里说的意思。在实际的开发当中肯定是有这样的需求存在的,例如在我们的领航者开发板上有一个eeprom器件(24c64)和一个rtc器件(pcf8563),假如它俩都是挂在ZYNQ的i2c0总线下的。那么现在要把这两个设备添加到i2c0总线下,打开zynq-7000.dtsi文件,可以看到PS的两组i2c控制器节点定义,如下所示:
    示例代码24.3.7.1 zynq-7000.dtsi i2c节点
  25. 122 i2c0: i2c@e0004000 {
  26. 123 compatible = "cdns,i2c-r1p10";
  27. 124 status = "disabled";
  28. 125 clocks = <&clkc 38>;
  29. 126 interrupt-parent = <&intc>;
  30. 127 interrupts = <0 25 4>;
  31. 128 reg = <0xe0004000 0x1000>;
  32. 129 #address-cells = <1>;
  33. 130 #size-cells = <0>;
  34. 131 };
  35. 132
  36. 133 i2c1: i2c@e0005000 {
  37. 134 compatible = "cdns,i2c-r1p10";
  38. 135 status = "disabled";
  39. 136 clocks = <&clkc 39>;
  40. 137 interrupt-parent = <&intc>;
  41. 138 interrupts = <0 48 4>;
  42. 139 reg = <0xe0005000 0x1000>;
  43. 140 #address-cells = <1>;
  44. 141 #size-cells = <0>;
  45. 142 };

复制代码


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

  1. 122 i2c0: i2c@e0004000 {
  2. 123 compatible = "cdns,i2c-r1p10";
  3. 124 status = "disabled";
  4. 125 clocks = <&clkc 38>;
  5. 126 interrupt-parent = <&intc>;
  6. 127 interrupts = <0 25 4>;
  7. 128 reg = <0xe0004000 0x1000>;
  8. 129 #address-cells = <1>;
  9. 130 #size-cells = <0>;
  10. 131
  11. 132 24c64@50 {
  12. 133 compatible = "atmel,24c64";
  13. 134 reg = <0x50>;
  14. 135 pagesize = <32>;
  15. 136 };
  16. 137
  17. 138 rtc@51 {
  18. 139 compatible = "nxp,pcf8563";
  19. 140 reg = <0x51>;
  20. 141 };
  21. 142 };
  22. 143
  23. 144 i2c1: i2c@e0005000 {
  24. 145 compatible = "cdns,i2c-r1p10";
  25. 146 status = "disabled";
  26. 147 clocks = <&clkc 39>;
  27. 148 interrupt-parent = <&intc>;
  28. 149 interrupts = <0 48 4>;
  29. 150 reg = <0xe0005000 0x1000>;
  30. 151 #address-cells = <1>;
  31. 152 #size-cells = <0>;
  32. 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文件中完成数据追加的内容,方式如下:
示例代码24.3.7.3 节点追加数据方法

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

复制代码


第1行,&i2c0表示要引用到i2c0这个label所对应的节点,也就是zynq-7000.dtsi文件中的“i2c0: i2c@e0004000”。
第2行,花括号内就是要向i2c0这个节点添加的内容,包括修改某些属性的值。
打开system-top.dts,这样我们就可以直接在该文件中追加内容了:
示例代码24.3.7.4 system-top.dts 向i2c0节点追加内容

  1. 8 /dts-v1/;
  2. 9 #include "zynq-7000.dtsi"
  3. 10 #include "pl.dtsi"
  4. 11 #include "pcw.dtsi"
  5. 12 / {
  6. 13 model = "Alientek ZYNQ Development Board";
  7. 14
  8. 15 chosen {
  9. 16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
  10. 17 stdout-path = "serial0:115200n8";
  11. 18 };
  12. 19 aliases {
  13. 20 ethernet0 = &gem0;
  14. 21 i2c0 = &i2c_2;
  15. 22 i2c1 = &i2c0;
  16. 23 i2c2 = &i2c1;
  17. 24 serial0 = &uart0;
  18. 25 serial1 = &uart1;
  19. 26 spi0 = &qspi;
  20. 27 };
  21. 28 memory {
  22. 29 device_type = "memory";
  23. 30 reg = <0x0 0x20000000>;
  24. 31 };
  25. 32 };
  26. 33
  27. 34 &i2c0 {
  28. 35 clock-frequency = <100000>;
  29. 36 status = "okay";
  30. 37
  31. 38 24c64@50 {
  32. 39 compatible = "atmel,24c64";
  33. 40 reg = <0x50>;
  34. 41 pagesize = <32>;
  35. 42 };
  36. 43
  37. 44 rtc@51 {
  38. 45 compatible = "nxp,pcf8563";
  39. 46 reg = <0x51>;
  40. 47 };
  41. 48 };
  42. 49
  43. 50 &gem0 {
  44. 51 local-mac-address = [00 0a 35 00 1e 53];
  45. 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文件中,可以看到很多的节点引用、向节点追加内容、修改节点内容的示例,如下所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第9张图片

图 35.3.5 pcw.dtsi示例


24.3.8特殊节点
在根节点“/”中有那么几个特殊的子节点:aliases、chosen以及memory,我们接下来看一下这三个比较特殊的节点,我们会发现这三个节点都是没有compatible属性,也就是说它们对应的并不是一个真实的设备。
1、aliases节点
打开system-top.dts文件,可以看到aliases节点的内容如下所示:
示例代码24.3.8.1 system-top.dts aliases节点

  1. 19 aliases {
  2. 20 ethernet0 = &gem0;
  3. 21 i2c0 = &i2c_2;
  4. 22 i2c1 = &i2c0;
  5. 23 i2c2 = &i2c1;
  6. 24 serial0 = &uart0;
  7. 25 serial1 = &uart1;
  8. 26 spi0 = &qspi;
  9. 27 };

复制代码


单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是需要注意的是,这里说的方便访问节点并不是在设备树中访问节点,例如前面说到的使用“&label”的方式访问设备树中的节点,而是内核当中方便定位节点,例如在内核中通过ethernet0就可以定位到gem0节点(&gem0引用的节点),再例如内核通过serial0就可以找到uart0节点。
2、chosen节点
chosen节点一般会有两个属性,“bootargs”和“stdout-path”。打开system-top.dts文件,找到chosen节点,内容如下所示:
示例代码24.3.8.2 chosen节点

  1. 15 chosen {
  2. 16 bootargs = "console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rw rootwait";
  3. 17 stdout-path = "serial0:115200n8";
  4. 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参数当中也指定了标准输出设备(console=ttyPS0,115200,ttyPS0其实指的就是根文件系统下的/dev/ttyPS0这个设备文件,那么它对应的硬件设备其实就是板子的uart0),那么内核在初始化标准输出设备的时候到底听谁的呢?关于这个问题,笔者开始也想不明白,于是呼去内核源码中找了找,在内核源码drivers/of/base.c文件中看到了下面这段代码:
示例代码24.3.8.3 of_console_check函数

  1. 1822 /**
  2. 1823 * of_console_check() - Test and setup console for DT setup
  3. 1824 * @dn - Pointer to device node
  4. 1825 * @name - Name to use for preferred console without index. ex. "ttyS"
  5. 1826 * @index - Index to use for preferred console.
  6. 1827 *
  7. 1828 * Check if the given device node matches the stdout-path property in the
  8. 1829 * /chosen node. If it does then register it as the preferred console and return
  9. 1830 * TRUE. Otherwise return FALSE.
  10. 1831 */
  11. 1832 bool of_console_check(struct device_node *dn, char *name, int index)
  12. 1833 {
  13. 1834 if (!dn || dn != of_stdout || console_set_on_cmdline)
  14. 1835 return false;
  15. 1836
  16. 1837 /*
  17. 1838 * XXX: cast `options' to char pointer to suppress complication
  18. 1839 * warnings: printk, UART and console drivers expect char pointer.
  19. 1840 */
  20. 1841 return !add_preferred_console(name, index, (char *)of_stdout_options);
  21. 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的值),如所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第10张图片

图 35.3.6 内核启动打印命令行参数


但是我们使用的这个U-Boot,它的环境变量当中并没有定义bootargs变量,大家可以进入U-Boot命令行,通过print命令打印出所有的环境变量,你会发现并没有定义bootargs,那这跟我们前面说的不相符了呀,而事实并不如此。
在uboot源码中全局搜索“chosen”这个字符串,看看能不能找到一些蛛丝马迹,果然在U-Boot源码目录的common/fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示:
示例代码24.3.8.4 uboot源码中的fdt_chosen函数

  1. 275 int fdt_chosen(void *fdt)
  2. 276 {
  3. 277 int nodeoffset;
  4. 278 int err;
  5. 279 char *str; /* used to set string properties */
  6. 280
  7. 281 err = fdt_check_header(fdt);
  8. 282 if (err < 0) {
  9. 283 printf("fdt_chosen: %sn", fdt_strerror(err));
  10. 284 return err;
  11. 285 }
  12. 286
  13. 287 /* find or create "/chosen" node. */
  14. 288 nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
  15. 289 if (nodeoffset < 0)
  16. 290 return nodeoffset;
  17. 291
  18. 292 str = getenv("bootargs");
  19. 293 if (str) {
  20. 294 err = fdt_setprop(fdt, nodeoffset, "bootargs", str,
  21. 295 strlen(str) + 1);
  22. 296 if (err < 0) {
  23. 297 printf("WARNING: could not set bootargs %s.n",
  24. 298 fdt_strerror(err));
  25. 299 return err;
  26. 300 }
  27. 301 }
  28. 302
  29. 303 return fdt_fixup_stdout(fdt, nodeoffset);
  30. 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,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见图 35.3.7:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第11张图片

图 35.3.7 fdt_chosen函数调用流程


图 35.3.7中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在内核设备树chosen节点中添加bootargs属性。而U-Boot的bootcmd命令最终会执行bootz命令,而bootz命令启动Linux内核的时候会运行do_bootm_linux函数,至此,真相大白!
3、memory节点
memory节点看名字就知道跟内存是有关系的,如下所示:
示例代码24.3.8.5 memory节点

  1. 28 memory {
  2. 29 device_type = "memory";
  3. 30 reg = <0x0 0x20000000>;
  4. 31 };

复制代码


memory节点描述了系统内存的基地址以及系统内存大小,“reg = <0x0 0x20000000>”就表示系统内存的起始地址为0x0,大小为0x20000000,也就是512MB,该节点一般只有这两个属性,device_type属性的值固定为“memory”。
24.3.9常用节点
本来这小节给大家讲一些常用到的节点,例如中断控制器、GPIO控制器以及在节点当中如何使用中断、如何使用gpio等。当想了想还是放在后面我们用到的时候再给大家介绍。
24.4驱动与设备节点的匹配
这部分内容已经在前面跟大家讲过了,具体请看35.3.4小节中的第一个小点compatible属性介绍。
24.5内核启动过程中解析设备树
Linux内核在启动的时候会解析内核DTB文件,然后在根文件系统的/proc/device-tree(后面给大家演示)目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的,流程如图43.7.1所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第12张图片

图 35.5.1 设备树中节点解析流程


从上图中可以看出,在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。那么具体如何进行设备树解析的这里就不给大家进行一一分析了,如果大家有时间可以自个去研究研究!
24.6设备树在系统中的体现
Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹,如下图所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第13张图片

图 35.6.1 根节点的属性以及子节点


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

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第14张图片

图 35.6.2 model和compatible文件内容


从图 35.6.2可以看出,文件model的内容是“Alientek ZYNQ Development Board”,文件compatible的内容为“xlnx,zynq-7000”。这跟system-top.dts文件根节点的model属性值、以及zynq-7000.dtsi文件根节点的compatible属性值是完全一样的。
2、根节点“/”各子节点
图 35.6.1中列出的各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“cpus”、“chosen”和“amba”等等。大家可以查看我们用到的设备树文件,看看根节点的子节点都有哪些,看看是否和图 35.6.1中的一致。
/proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/amba目录中就可以看到amba节点的所有子节点,如所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第15张图片

图 35.6.3 amba节点的所有属性和子节点


和根节点“/”一样,图 35.6.3中的所有文件分别为amba节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和我们使用的设备树中amba节点的属性值相同。
24.7绑定信息文档
设备树是用来描述板子上的硬件设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux内核源码中有详细的.txt文档描述了如何添加节点,这些.txt文档叫做绑定文档,路径为:Linux源码目录/Documentation/devicetree/bindings,如所示:

韦东山 IMX6ULL和正点原子_【正点原子FPGA连载】第二十四章Linux设备树-领航者ZYNQ之linux开发指南..._第16张图片

图 35.7.1 绑定文档


比如我们现在要想在ZYNQ 7010/7020这颗SOC的I2C下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-cadence.txt(文件的名字一般都是以i2c-xxx.txt命名的,xxx一般是制造商),此文档详细的描述了ZYNQ-7000系列处理器如何在设备树中添加I2C设备节点,文档内容如下所示:

  1. Binding for the Cadence I2C controller
  2. Required properties:
  3. - reg: Physical base address and size of the controller's register area.
  4. - compatible: Should contain one of:
  5. * "cdns,i2c-r1p10"
  6. Note: Use this when cadence i2c controller version 1.0 is used.
  7. * "cdns,i2c-r1p14"
  8. Note: Use this when cadence i2c controller version 1.4 is used.
  9. - clocks: Input clock specifier. Refer to common clock bindings.
  10. - interrupts: Interrupt specifier. Refer to interrupt bindings.
  11. - #address-cells: Should be 1.
  12. - #size-cells: Should be 0.
  13. Optional properties:
  14. - clock-frequency: Desired operating frequency, in Hz, of the bus.
  15. - clock-names: Input clock name, should be 'pclk'.
  16. Example:
  17. i2c@e0004000 {
  18. compatible = "cdns,i2c-r1p10";
  19. clocks = <&clkc 38>;
  20. interrupts = ;
  21. reg = <0xe0004000 0x1000>;
  22. clock-frequency = <400000>;
  23. #address-cells = <1>;
  24. #size-cells = <0>;
  25. };

复制代码


有时候使用的一些芯片在Documentation/devicetree/bindings目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。
24.8设备树常用of操作函数
设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的0X02005482和0X400这两个值,然后初始化外设。Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做OF函数。这些OF函数原型都定义在include/linux/of.h文件中。
24.8.1查找节点的OF函数
设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,定义如下:
示例代码24.8.1.1 device_node节点

  1. 49 struct device_node {
  2. 50 const char *name; /* 节点名字 */
  3. 51 const char *type; /* 设备类型 */
  4. 52 phandle phandle;
  5. 53 const char *full_name; /* 节点全名 */
  6. 54 struct fwnode_handle fwnode;
  7. 55
  8. 56 struct property *properties; /* 属性 */
  9. 57 struct property *deadprops; /* removed属性 */
  10. 58 struct device_node *parent; /* 父节点 */
  11. 59 struct device_node *child; /* 子节点 */
  12. 60 struct device_node *sibling;
  13. 61 struct kobject kobj;
  14. 62 unsigned long _flags;
  15. 63 void *data;
  16. 64 #if defined(CONFIG_SPARC)
  17. 65 const char *path_component_name;
  18. 66 unsigned int unique_id;
  19. 67 struct of_irq_controller *irq_trans;
  20. 68 #endif
  21. 69 };

复制代码


与查找节点有关的OF函数有5个,我们依次来看一下。
1、of_find_node_by_name函数
of_find_node_by_name函数通过节点名字查找指定的节点,函数原型如下:

  1. struct device_node *of_find_node_by_name(struct device_node *from,
  2. 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这两个属性查找指定的节点,函数原型如下:

  1. struct device_node *of_find_compatible_node(struct device_node *from,
  2. const char *type,
  3. 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匹配表来查找指定的节点,函数原型如下:

  1. struct device_node *of_find_matching_node_and_match(struct device_node *from,
  2. const struct of_device_id *matches,
  3. 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函数通过节点路径来查找指定的节点,函数原型如下:

  1. inline struct device_node *of_find_node_by_path(const char *path)

复制代码


函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名(用aliens节点中定义的别名)。
返回值:找到的节点,如果为NULL表示查找失败
24.8.2查找父/子节点的OF函数
Linux内核提供了几个查找节点对应的父节点或子节点的OF函数,我们依次来看一下。
1、of_get_parent函数
of_get_parent函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:

  1. struct device_node *of_get_parent(const struct device_node *node)

复制代码


函数参数和返回值含义如下:
node:要查找的父节点的节点。
返回值:找到的父节点。
2、of_get_next_child函数
of_get_next_child函数用迭代的查找子节点,函数原型如下:

  1. struct device_node *of_get_next_child(const struct device_node *node,
  2. struct device_node *prev)

复制代码


函数参数和返回值含义如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。
24.8.3提取属性值的OF函数
设备树节点的属性保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下:
示例代码24.8.3.1 property结构体

  1. 35 struct property {
  2. 36 char *name; /* 属性名字 */
  3. 37 int length; /* 属性长度 */
  4. 38 void *value; /* 属性值 */
  5. 39 struct property *next; /* 下一个属性 */
  6. 40 unsigned long _flags;
  7. 41 unsigned int unique_id;
  8. 42 struct bin_attribute attr;
  9. 43 };

复制代码


Linux内核也提供了提取属性值的OF函数,我们依次来看一下。
1、of_find_property函数
of_find_property函数用于查找指定的属性,函数原型如下:

  1. property *of_find_property(const struct device_node *np,
  2. const char *name,
  3. int *lenp)

复制代码


函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。
2、of_property_count_elems_of_size函数
of_property_count_elems_of_size函数用于获取属性中元素的数量,比如reg属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

  1. int of_property_count_elems_of_size(const struct device_node *np,
  2. const char *propname,
  3. int elem_size)

复制代码


函数参数和返回值含义如下:
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值:得到的属性元素数量。
3、of_property_read_u32_index函数
of_property_read_u32_index函数用于从属性中获取指定下标(属性值是一个u32数据组成的数组)的u32类型数据值(无符号32位),比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定下标的数据值,此函数原型如下:

  1. int of_property_read_u32_index(const struct device_node *np,
  2. const char *propname,
  3. u32 index,
  4. 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属性中的所有数据。这四个函数的原型如下:

  1. int of_property_read_u8_array(const struct device_node *np,
  2. const char *propname,
  3. u8 *out_values,
  4. size_t sz)
  5. int of_property_read_u16_array(const struct device_node *np,
  6. const char *propname,
  7. u16 *out_values,
  8. size_t sz)
  9. int of_property_read_u32_array(const struct device_node *np,
  10. const char *propname,
  11. u32 *out_values,
  12. size_t sz)
  13. int of_property_read_u64_array(const struct device_node *np,
  14. const char *propname,
  15. u64 *out_values,
  16. 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类型属性值,函数原型如下:

  1. int of_property_read_u8(const struct device_node *np,
  2. const char *propname,
  3. u8 *out_value)
  4. int of_property_read_u16(const struct device_node *np,
  5. const char *propname,
  6. u16 *out_value)
  7. int of_property_read_u32(const struct device_node *np,
  8. const char *propname,
  9. u32 *out_value)
  10. int of_property_read_u64(const struct device_node *np,
  11. const char *propname,
  12. u64 *out_value)

复制代码


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

  1. int of_property_read_string(struct device_node *np,
  2. const char *propname,
  3. 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_size_cells函数用于获取#size-cells属性值,函数原型如下:
int of_n_size_cells(struct device_node *np)
函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells属性值。
24.8.4其他常用的OF函数
1、of_device_is_compatible函数
of_device_is_compatible函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型如下:

  1. int of_device_is_compatible(const struct device_node *device,
  2. const char *compat)

复制代码


函数参数和返回值含义如下:
device:设备节点。
compat:要查看的字符串。
返回值:0,节点的compatible属性中不包含compat指定的字符串;正数,节点的compatible属性中包含compat指定的字符串。
2、of_get_address函数
of_get_address函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性值,函数属性如下:

  1. const __be32 *of_get_address(struct device_node *dev,
  2. int index,
  3. u64 *size,
  4. unsigned int *flags)

复制代码


函数参数和返回值含义如下:
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如IORESOURCE_IO、IORESOURCE_MEM等
返回值:读取到的地址数据首地址,为NULL的话表示读取失败。
3、of_translate_address函数
of_translate_address函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:

  1. u64 of_translate_address(struct device_node *dev,
  2. const __be32 *in_addr)

复制代码


函数参数和返回值含义如下:
dev:设备节点。
in_addr:要转换的地址。
返回值:得到的物理地址,如果为OF_BAD_ADDR的话表示转换失败。
4、of_address_to_resource函数
IIC、SPI、GPIO等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux内核使用resource结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用resource结构体描述的都是设备资源信息,resource结构体定义在文件include/linux/ioport.h中,定义如下:
示例代码24.8.4.1 resource结构体

  1. 18 struct resource {
  2. 19 resource_size_t start;
  3. 20 resource_size_t end;
  4. 21 const char *name;
  5. 22 unsigned long flags;
  6. 23 struct resource *parent, *sibling, *child;
  7. 24 };

复制代码


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

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

复制代码


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

  1. int of_address_to_resource(struct device_node *dev,
  2. int index,
  3. 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函数原型如下:

  1. void __iomem *of_iomap(struct device_node *np,
  2. 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驱动实验都将采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。将会带领大家由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发技能。

你可能感兴趣的:(韦东山,IMX6ULL和正点原子)