本帖最后由 正点原子运营 于 2020-12-21 18:27 编辑  
 
1)实验平台:正点原子领航者ZYNQ开发板 
2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761 
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-301505-1-1.html 
4)对正点原子FPGA感兴趣的同学可以加群讨论:712557122 
5)关注正点原子公众号,获取最新资料更新 
 
 
 
 
 
  
第二十二章 Linux设备树  
前面章节中我们多次提到“设备树”这个概念,因为时机未到,所以当时并没有详细的讲解什么是“设备树”,本章我们就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux内核中,设备驱动基本全部采用了设备树(也有支持老式驱动的,比较少)的方式,最新出的CPU其驱动开发也基本都是基于设备树的,我们所使用的Linux版本为4.14.0,肯定是支持设备树的,所以正点原子领航者开发板的所有Linux驱动都是基于设备树的。本章我们就来了解一下设备树的起源、重点学习一下设备树语法。  
 
1.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文件代码片段 -   static structs3c2410fb_display smdk2440_lcd_cfg __initdata = {
 
 -   
 
 -       .lcdcon5    =S3C2410_LCDCON5_FRM565 |
 
 -                 S3C2410_LCDCON5_INVVLINE |
 
 -                 S3C2410_LCDCON5_INVVFRAME |
 
 -                 S3C2410_LCDCON5_PWREN |
 
 -                 S3C2410_LCDCON5_HWSWP,
 
 - ......
 
 -  };
 
  
-  static structs3c2410fb_mach_info smdk2440_fb_info __initdata = {
 
 -      .displays   = &smdk2440_lcd_cfg,
 
 -      .num_displays   = 1,
 
 -      .default_display= 0,
 
 - ......
 
 -  };
 
  
-  static structplatform_device *smdk2440_devices[]__initdata = {
 
 -     &s3c_device_ohci,
 
 -      &s3c_device_lcd,
 
 -      &s3c_device_wdt,
 
 -      &s3c_device_i2c0,
 
 -      &s3c_device_iis,
 
 -  };
 
  复制代码上述代码中的结构体变量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内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“Thiswhole ARM thing is a f*cking pain in the ass”。从此以后ARM社区就开始引入设备树DTS了。 DTS即Device TreeSource设备树源码, 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所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image002.png 图 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的设备树。 1.2 设备树的基本知识1.2.1 dts设备树的源文件的后缀名就是.dts,每一款硬件平台可以单独写一份xxxx.dts,所以在Linux内核源码中存在大量.dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts。 1.2.2 dtsi值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,这个dtsi文件其实就类似于C语言当中的.h头文件,可以通过C语言中使用include来包含一个.dtsi文件,例如arch/arm/boot/dts/system-top.dts文件有如下内容: 示例代码24.2.2.1 system-top.dts内容片段  
-  /*
 
 -   * CAUTION: This file is automaticallygenerated by Xilinx.
 
 -   * Version: HSI
 
 -   * Today is: Mon Mar 16 02:51:23 2020
 
 -   */
 
  
 
-  /dts-v1/;
 
 - #include "zynq-7000.dtsi"
 
 - #include "pl.dtsi"
 
 - #include "pcw.dtsi"
 
 -  / {
 
 -          model = "AlientekZYNQ Development Board";
 
  
-          chosen {
 
 -                  bootargs = "console=ttyPS0,115200earlyprintk root=/dev/mmcblk0p2 rw rootwait";
 
 -                  stdout-path = "serial0:115200n8";
 
 -          };
 
 -          aliases {
 
 -                  ethernet0 = &gem0;
 
 -                  i2c0 = &i2c_2;
 
 -                  i2c1 = &i2c0;
 
 -                  i2c2 = &i2c1;
 
 -                  serial0 = &uart0;
 
 -                  serial1 = &uart1;
 
 -                  spi0 = &qspi;
 
 -          };
 
 -          memory {
 
 -                  device_type = "memory";
 
 -                  reg = <0x0 0x20000000>;
 
 -          };
 
 -  };
 
  复制代码第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文件。 1.2.3 dtcdtc其实就是device-tree-compiler,那就是设备文件.dts的编译器嘛,将.c文件编译为.o文件需要用到gcc编译器,那么将.dts文件编译为相应的二进制文件则需要dtc编译器,dtc工具在Linux内核的scripts/dtc目录下,当然必须要编译了内核源码之后才会生成,如下所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image004.jpg 图35.2.1 dtc编译器 我们来看看scripts/dtc/Makefile文件,如下所示: 示例代码24.2.3.1 scripts/dtc/Makefile文件代码段 - hostprogs-y :=dtc
 
 - always      := $(hostprogs-y)
 
  
- dtc-objs:=dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
 
 -           srcpos.o checks.o util.o
 
 - dtc-objs  +=dtc-lexer.lex.o dtc-parser.tab.o
 
 - ......
 
  复制代码可以看出,dtc工具依赖于dtc.c、flattree.c、fstree.c等文件,最终编译并链接出dtc这个主机文件。如果要编译dts文件的话只需要进入到Linux源码根目录下,然后执行如下命令: make all 或者: 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文件部分内容 ...... -  dtb-$(CONFIG_ARCH_ZYNQ) += \
 
 -          zynq-cc108.dtb \
 
 -          zynq-microzed.dtb \
 
 -          zynq-parallella.dtb \
 
 -          zynq-zc702.dtb \
 
 -          zynq-zc706.dtb \
 
 -          zynq-zc770-xm010.dtb \
 
 -          zynq-zc770-xm011.dtb \
 
 -          zynq-zc770-xm012.dtb \
 
 -          zynq-zc770-xm013.dtb \
 
 -          zynq-zed.dtb \
 
 -          zynq-zybo.dtb \
 
 -          system-top.dtb
 
  复制代码...... 由上面可以知道,当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,如下: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image006.jpg 图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文件,例如下面: make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-system-top.dtb file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image008.jpg 图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文件的问题就说到这里了,这里讲的有点太多了,如果大家还不明白可以去网上找找资料。 1.2.4 dtb.dtb文件就是将.dts文件编译成二进制数据之后得到的文件,这就跟.c文件编译为.o文件是一样的道理,关于.dtb文件怎么使用这里就不多说了,前面讲解Uboot移植、Linux内核移植的时候已经无数次的提到如何使用.dtb文件了(uboot中使用bootz或bootm命令向Linux内核传递二进制设备树文件(.dtb))。 1.3 dts语法虽然我们基本上不会从头到尾重写一个.dts文件,大多时候是直接在SOC厂商提供的.dts文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改.dts文件。大家不要看到要学习新的语法就觉得会很复杂,DTS语法非常的人性化,是一种ASCII文本文件,不管是阅读还是修改都很方便。 本节我们就以system-top.dts这个文件为例来讲解一下DTS语法。关于设备树详细的语法规则请参考《DevicetreeSpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,路径为:领航者ZYNQ开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\ARM\ DevicetreeSpecificationV0.2.pdf、领航者ZYNQ开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\ARM\ Power_ePAPR_APPROVED_v1.12.pdf。 1.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 设备树结构示意  
- /{                                        // 根节点
 
 -          node1{                            // node1节点
 
 -                  property1=value1;        // node1节点的属性property1
 
 -                  property2=value2;        // node1节点的属性property2
 
 -                  ...
 
 -          };      
 
 -          
 
 -          node2{                             // node2节点
 
 -                  property3=value3;        // node2节点的属性property3
 
 -                  ...
 
 -                  node3{                     // node2的子节点node3
 
 -                          property4=value4;//node3节点的属性property4
 
 -                          ...
 
 -                  };      
 
 -          };      
 
 -  };
 
  复制代码第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等。 1.3.2 节点与属性在设备树文件中如何定义一个节点,节点的命名有什么要求呢?在设备树中节点的命名格式如下: [label:]node-name[@unit-address] { [propertiesdefinitions] [childnodes] }; “[]”中的内容表示可选的,可有也可以没有;节点名字前加上”label”则方便在dts文件中被其他的节点引用,我们后面会说这个;其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄存基地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interrupt-controller@00a01000”。 每个节点都有若干属性,属性又有相对应的值(值不是必须要有的),而一个节点当中又可以嵌套其它的节点,形成父子节点。例如下面: 示例代码24.3.2.1 设备树节点示例 -  cpus {
 
 -      #address-cells= <1>;
 
 -      #size-cells= <0>;
 
  
-      cpu0:cpu@0 {
 
 -          compatible = "arm,cortex-a9";
 
 -          device_type = "cpu";
 
 -          reg = <0>;
 
 -          clocks = <&clkc 3>;
 
 -          clock-latency= <1000>;
 
 -          cpu0-supply= <®ulator_vccpint>;
 
 -          operating-points= <
 
 -              /* kHz  uV */
 
 -              666667 1000000
 
 -              333334 1000000
 
 -              >;
 
 -      };
 
  
-      cpu1:cpu@1 {
 
 -          compatible = "arm,cortex-a9";
 
 -          device_type = "cpu";
 
 -          reg = <1>;
 
 -          clocks = <&clkc 3>;
 
 -          };
 
 -      };
 
  复制代码每一个节点(包括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属性等,它们都有对应的值,大家看到这些值可能有点不明白,为啥有的是字符串,有的是尖括号”<>”括起来的东西,下面单独给大家讲解一波。 每个节点都有不同属性,不同的属性又有不同的值,那么设备树当中值有哪些形式呢? l  字符串 compatible = "arm,cortex-a9"; 字符串使用双引号括起来,例如上面的这个compatible属性的值是” arm,cortex-a9”字符串。 l  32位无符号整形数据 clock-latency = <1000>; reg = <0x00000000 0x00500000>; 32位无符号整形数据使用尖括号括起来,例如属性cock-latency的值是一个32位无符号整形数据1000,而reg属性有两个数据,使用空格隔开,那么这个就可以认为是一个数组,很容易理解! l  二进制数据 local-mac-address = [00 0a 35 00 1e 53]; 二进制数据使用方括号括起来,例如上面这个就是一个二进制数据组成的数组。 l  字符串数组 compatible ="n25q512a","micron,m25p80"; 属性值也可以使用字符串列表,例如上面的这个属性,它的值是一个字符串列表,字符串之间使用逗号分割; l  混合值 mixed-property = "a string", [0x01 0x230x45 0x67], <0x12345678>; 除此之外不同的数据类型还可以混合在一起,以逗号分隔。 l  节点引用 除了上面一些数据类型之外,还有一种非常常见的形式,如下所示: clocks = <&clkc 3>; 这其实就是我们上面说到的引用节点的一种形式,”&clkc”就表示引用”clkc”这个节点,而clkc就是前面提到的”label”,引用节点也是使用尖括号来表示,关于节点之间的引用,我们后面还会再讲,这里先告一段落。 1.3.3 使用注释和宏定义在设备树文件中也可以使用注释,注释的方法和C语言当中是一毛一样的,可以使用” // ”进行单行注释,也可以使用” /*  */ ”进行多行注释,如下所示: -  //SPDX-License-Identifier: GPL-2.0+
 
 -  /*
 
 -   * Copyright (C) 2011 - 2015 Xilinx
 
 -   *
 
 -   * This software is licensed under the termsof the GNU General Public
 
 -   * License version 2, as published by the FreeSoftware Foundation, and
 
 -   * may be copied, distributed, and modifiedunder those terms.
 
 -   *
 
 -   * This program is distributed in the hopethat it will be useful,
 
 -   * but WITHOUT ANY WARRANTY; without even theimplied warranty of
 
 -   * MERCHANTABILITY or FITNESS FOR A PARTICULARPURPOSE.  See the
 
 -   * GNU General Public License for moredetails.
 
 -   */
 
  
-  / {
 
 -        #address-cells= <1>;
 
 -        #size-cells= <1>;
 
 -        compatible = "xlnx,zynq-7000";
 
  
-        cpus {
 
 -            #address-cells= <1>;
 
 -            #size-cells= <0>;
 
  
-            cpu0:cpu@0 {
 
 -                compatible = "arm,cortex-a9";
 
 -                device_type = "cpu";
 
 -                reg = <0>;
 
 -                clocks = <&clkc 3>;
 
 -                clock-latency= <1000>;
 
 -                cpu0-supply= <®ulator_vccpint>;
 
 -                operating-points= <
 
 -                    /* kHz  uV */
 
 -                    666667  1000000
 
 -                    333334  1000000
 
 -                    >;
 
 -            };
 
  
-            cpu1:cpu@1 {
 
 -                compatible = "arm,cortex-a9";
 
 -                device_type = "cpu";
 
 -                reg = <1>;
 
 -                clocks = <&clkc 3>;
 
 -            };
 
 -        };
 
  复制代码前面跟大家讲过,设备树中可以使用”#include”包含dtsi、dts以及C语言的头文件,那我们为什么要包含一个.h的头文件呢?因为在设备树中可以使用宏定义,所以你在arch/arm/boot/dts目录下你会看到很多的设备树文件中都包含了.h头文件,例如下面这个: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image010.jpg 图35.3.1 头文件包含 file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image012.jpg 图35.3.2 使用宏定义 关于头文件包含以及宏定义的使用这里就不多说了,本身也非常简单。 1.3.4 标准属性节点的内容是由一堆的属性组成,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。 1、compatible属性 compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值可以是一个字符串,也可以是一个字符串列表;一般该字符串使用”<制造商>,<型号>”这样的形式进行命名,当然这不是必须要这样,这是要求大家按照这样的形式进行命名,目的是为了指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突,如下所示: 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内容片段  
-  /*Match table for of_platform binding */
 
 -  static const structof_device_id cdns_uart_of_match[] = {
 
 -      { .compatible= "xlnx,xuartps", },
 
 -      { .compatible= "cdns,uart-r1p8", },
 
 -      { .compatible= "cdns,uart-r1p12", .data = &zynqmp_uart_def},
 
 -      { .compatible= "xlnx,zynqmp-uart", .data = &zynqmp_uart_def},
 
 -      {}
 
 -  };
 
 - MODULE_DEVICE_TABLE(of,cdns_uart_of_match);
 
 - ......
 
  
-  static struct platform_drivercdns_uart_platform_driver = {
 
 -      .probe   = cdns_uart_probe,
 
 -      .remove  =cdns_uart_remove,
 
 -      .driver  = {
 
 -          .name =CDNS_UART_NAME,
 
 -          .of_match_table= cdns_uart_of_match,
 
 -          .pm = &cdns_uart_dev_pm_ops,
 
 -      },
 
 -  };
 
  复制代码这个驱动文件是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内容片段 -  /dts-v1/;
 
 -  #include "zynq-7000.dtsi"
 
 -  #include "pl.dtsi"
 
 -  #include "pcw.dtsi"
 
 -  / {
 
 -          model = "AlientekZYNQ Development Board";
 
  
-          chosen {
 
 -                  bootargs = "console=ttyPS0,115200earlyprintk root=/dev/mmcblk0p2 rw rootwait";
 
 -                  stdout-path = "serial0:115200n8";
 
 -          };
 
 -          aliases {
 
 -                  ethernet0 = &gem0;
 
 -                  i2c0 = &i2c_2;
 
 -                  i2c1 = &i2c0;
 
 -                  i2c2 = &i2c1;
 
 -                  serial0 = &uart0;
 
 -                  serial1 = &uart1;
 
 -                  spi0 = &qspi;
 
 -          };
 
 -          memory {
 
 -                  device_type = "memory";
 
 -                  reg = <0x0 0x20000000>;
 
 -          };
 
 -  };
 
  复制代码我之前在system-top.dts设备树文件加了一个model属性,它的值等于“Alientek ZYNQ DevelopmentBoard”,内核启动过程中就会打印出来,如下所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image014.jpg 图35.3.3 打印model字符串 3、status属性 status属性看名字就知道是和设备状态有关的,device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值: |    值    |  |   |  |   | 表明设备当前是不可操作的,但是在未来可以变为可操作的,比如热插拔设备插入以后。至于disabled的具体含义还要看设备的绑定文档。  |   | 表明设备不可操作,设备检测到了一系列的错误,而且设备也不大可能变得可操作。  |   | 含义和“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 = <address1 length1 address2 length2 address3length3……> 每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address字段占用的字长,#size-cells表明length这个字段所占用的字长,比如: 示例代码24.3.4.3 #address-cells和#size-cells属性 -  &qspi {
 
 -      #address-cells= <1>;
 
 -      #size-cells= <0>;
 
 -      flash0:flash@0 {
 
 -          compatible = "n25q512a","micron,m25p80";
 
 -          reg = <0x0>;
 
 -          #address-cells= <1>;
 
 -          #size-cells= <1>;
 
 -          spi-max-frequency= <50000000>;
 
 -          partition@0x00000000 {
 
 -              label = "boot";
 
 -              reg = <0x00000000 0x00500000>;
 
 -          ;
 
 -          partition@0x00500000 {
 
 -              label = "bootenv";
 
 -              reg = <0x00500000 0x00020000>;
 
 -          };
 
 -          partition@0x00520000 {
 
 -              label = "kernel";
 
 -              reg = <0x00520000 0x00a80000>;
 
 -          };
 
 -          partition@0x00fa0000 {
 
 -              label = "spare";
 
 -              reg = <0x00fa0000 0x00000000>;
 
 -          };
 
 -      };
 
 -  };
 
  复制代码第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节点信息 - uart0: serial@e0000000 {
 
 -      compatible = "xlnx,xuartps", "cdns,uart-r1p8";
 
 -      status = "disabled";
 
 -      clocks = <&clkc 23>, <&clkc 40>;
 
 -      clock-names= "uart_clk", "pclk";
 
 -      reg = <0xE0000000 0x1000>;
 
 -      interrupts = <0 27 4>;
 
 -  };
 
  复制代码上述代码是节点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参考资料\Xilinx\User Guide\ug585-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内容片段 -  fpga_full: fpga-full {
 
 -      compatible = "fpga-region";
 
 -      fpga-mgr = <&devcfg>;
 
 -      #address-cells= <1>;
 
 -      #size-cells= <1>;
 
 -      ranges;
 
 -  };
 
  复制代码第51行定义了ranges属性,但是ranges属性值为空。 ranges属性不为空的示例代码如下所示: 示例代码24.3.4.6 ranges属性不为空 -  soc {
 
 -    compatible= "simple-bus";
 
 -    #address-cells= <1>;
 
 -    #size-cells= <1>;
 
 -    ranges=<0x00xe00000000x00100000>;
 
 -   
 
 -       serial{
 
 -           device_type= "serial";
 
 -           compatible= "ns16550";
 
 -        reg=<0x46000x100>;
 
 -        clock-frequency= <0>;
 
 -        interrupts= <0xA 0x8>;
 
 -        interrupt-parent= <&ipic>;
 
 -       };
 
 -  };
 
  复制代码第5行,节点soc定义的ranges属性,值为<0x00xe0000000 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内容片段 -  cpu0:cpu@0 {
 
 -      compatible = "arm,cortex-a9";
 
 -      device_type = "cpu";
 
 -      reg = <0>;
 
 -      clocks = <&clkc 3>;
 
 -      clock-latency= <1000>;
 
 -      cpu0-supply= <®ulator_vccpint>;
 
 -      operating-points= <
 
 -                          /*kHz  uV */
 
 -                          666667  1000000
 
 -                          333334  1000000
 
 -                          >;
 
 -      };
 
  
-  cpu1:cpu@1 {
 
 -      compatible = "arm,cortex-a9";
 
 -      device_type = "cpu";
 
 -      reg = <1>;
 
 -      clocks = <&clkc 3>;
 
 -  };
 
  复制代码关于标准属性就讲解这么多,后面还会跟大家介绍一些常常会使用到的节点,例如设备树中的中断控制器、GPIO、I2C总线等。 1.3.5 根节点compatible属性每个节点都有compatible属性(除了一些特殊用途的节点),根节点“/”也不例外,在zynq-7000.dtsi文件中根节点的compatible属性内容如下所示: 示例代码24.3.5.1 zynq-7000.dtsi根节点compatible属性 -  / {
 
 -          #address-cells= <1>;
 
 -          #size-cells= <1>;
 
 -          compatible = "xlnx,zynq-7000";
 
 - ......
 
 -  };
 
  复制代码可以看出,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设备 - MACHINE_START(MX35_3DS, "FreescaleMX35PDK")
 
 -      /* Maintainer: FreescaleSemiconductor, Inc */
 
 -      .atag_offset= 0x100,
 
 -      .map_io= mx35_map_io,
 
 -      .init_early= imx35_init_early,
 
 -      .init_irq= mx35_init_irq,
 
 -      .init_time  =mx35pdk_timer_init,
 
 -      .init_machine= mx35_3ds_init,
 
 -      .reserve= mx35_3ds_reserve,
 
 -      .restart    = mxc_restart,
 
 - MACHINE_END
 
  复制代码上述代码就是定义了“FreescaleMX35PDK”这个硬件平台,其中MACHINE_START和MACHINE_END定义在文件arch/arm/include/asm/mach/arch.h中,内容如下: 示例代码24.3.5.3 MACHINE_START和MACHINE_END宏定义 - #defineMACHINE_START(_type,_name)          \
 
 - staticconst struct machine_desc __mach_desc_##_type   \
 
 - __used                         \
 
 - __attribute__((__section__(".arch.info.init")))= {    \
 
 -     .nr    = MACH_TYPE_##_type,        \
 
 -     .name      = _name,
 
  
- #defineMACHINE_END             \
 
 - };
 
  复制代码根据MACHINE_START和MACHINE_END的宏定义,将示例代码24.3.2展开后如下所示: 示例代码24.3.5.6 展开以后 -   static const structmachine_desc __mach_desc_MX35_3DS     \
 
 -     __used                                                   \
 
 -     __attribute__((__section__(".arch.info.init"))) = {     
 
 - .nr     =MACH_TYPE_MX35_3DS,
 
 -     .name       = "FreescaleMX35PDK",
 
 -     /*Maintainer: Freescale Semiconductor, Inc */
 
 -     .atag_offset= 0x100,
 
 -     .map_io= mx35_map_io,
 
 -     .init_early= imx35_init_early,
 
 -       .init_irq =mx35_init_irq,
 
 -       .init_time  =mx35pdk_timer_init,
 
 -       .init_machine= mx35_3ds_init,
 
 -       .reserve= mx35_3ds_reserve,
 
 -       .restart    =mxc_restart,
 
 -  };
 
  复制代码从示例代码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中,此文件定义了大量的machineid,内容如下所示: 示例代码24.3.5.7 mach-types.h文件中的machine id -    #define MACH_TYPE_EBSA110               0
 
 -    #define MACH_TYPE_RISCPC                1
 
 -    #define MACH_TYPE_EBSA285               4
 
 -    #define MACH_TYPE_NETWINDER             5
 
 -    #define MACH_TYPE_CATS                   6
 
 -    #define MACH_TYPE_SHARK                 15
 
 -    #define MACH_TYPE_BRUTUS                16
 
 -    #define MACH_TYPE_PERSONAL_SERVER     17
 
 - ......
 
 -   #define MACH_TYPE_MX35_3DS              1645
 
 - ......
 
  复制代码1000#define MACH_TYPE_PFLA03                4575 第287行就是MACH_TYPE_MX35_3DS的值,为1645。 前面说了,uboot会给Linux内核传递machine id这个参数,Linux内核会检查这个machine id,其实就是将machine id与示例代码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宏 - #defineDT_MACHINE_START(_name, _namestr)                \
 
 - staticconst struct machine_desc __mach_desc_##_name    \
 
 - __used                                                      \
 
 - __attribute__((__section__(".arch.info.init")))= {     \
 
 -     .nr    = ~0,                                        \
 
 -     .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 -  static const char * constzynq_dt_match[] = {
 
 -          "xlnx,zynq-7000",
 
 -          NULL
 
 -  };
 
  
- DT_MACHINE_START(XILINX_EP107, "XilinxZynq Platform")
 
 -          /* 64KB waysize, 8-way associativity, parity disabled */
 
 - #ifdef CONFIG_XILINX_PREFETCH
 
 -          .l2c_aux_val    = 0x30400000,
 
 -          .l2c_aux_mask   = 0xcfbfffff,
 
 -  #else
 
 -          .l2c_aux_val    = 0x00400000,
 
 -          .l2c_aux_mask   = 0xffbfffff,
 
 - #endif
 
 -          .smp            =smp_ops(zynq_smp_ops),
 
 -          .map_io         =zynq_map_io,
 
 -          .init_irq       =zynq_irq_init,
 
 -          .init_machine   =zynq_init_machine,
 
 -          .init_late      =zynq_init_late,
 
 -          .init_time      =zynq_timer_init,
 
 -          .dt_compat      =zynq_dt_match,
 
 -          .reserve        = zynq_memory_init,
 
 - 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函数内容 -  void__init setup_arch(char **cmdline_p)
 
 -  {
 
 -      const structmachine_desc *mdesc;
 
  
-      setup_processor();
 
 -      mdesc =setup_machine_fdt(__atags_pointer);
 
 -      if (!mdesc)
 
 -      mdesc =setup_machine_tags(__atags_pointer, __machine_arch_type);
 
 -      machine_desc =mdesc;
 
 -      machine_name =mdesc->name;
 
 - ......
 
 -  }
 
  复制代码第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函数内容 - const struct machine_desc *__init setup_machine_fdt(unsigned int dt_phys)
 
 -  {
 
 -      const structmachine_desc *mdesc, *mdesc_best= NULL;
 
 - ......
 
  
-      if (!dt_phys|| !early_init_dt_verify(phys_to_virt(dt_phys)))
 
 -          return NULL;
 
  
- mdesc =of_flat_dt_match_machine(mdesc_best, arch_get_next_mach);
 
  
- ......
 
 -      __machine_arch_type =mdesc->nr;
 
  
-      returnmdesc;
 
 -  }
 
  复制代码 第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函数内容 -  const void *__init of_flat_dt_match_machine(const void *default_match,
 
 -          const void * (*get_next_compat)(const char * const**))
 
 -  {
 
 -      const void *data = NULL;
 
 -      const void *best_data= default_match;
 
 -      const char *const *compat;
 
 -      unsigned longdt_root;
 
 -      unsigned intbest_score = ~1,score = 0;
 
  
-      dt_root =of_get_flat_dt_root();
 
 - while((data =get_next_compat(&compat))){
 
 -          score =of_flat_dt_match(dt_root,compat);
 
 - if(score>0&&score < best_score){
 
 -              best_data = data;
 
 -              best_score =score;
 
 - }
 
 - }
 
 - ......
 
  
-      pr_info("Machinemodel: %s\n", of_flat_dt_get_machine_name());
 
  
-      returnbest_data;
 
 -  } const void *__init of_flat_dt_match_machine(const void *default_match,
 
 -          const void * (*get_next_compat)(const char * const**))
 
 -  {
 
 -      const void *data = NULL;
 
 -      const void *best_data= default_match;
 
 -      const char *const *compat;
 
 -      unsigned longdt_root;
 
 -      unsigned intbest_score = ~1,score = 0;
 
  
-      dt_root =of_get_flat_dt_root();
 
 - while((data =get_next_compat(&compat))){
 
 -          score =of_flat_dt_match(dt_root,compat);
 
 - if(score>0&&score < best_score){
 
 -              best_data = data;
 
 -              best_score =score;
 
 - }
 
 - }
 
 - ......
 
  
-      pr_info("Machinemodel: %s\n", of_flat_dt_get_machine_name());
 
  
-      returnbest_data;
 
 -  }
 
  复制代码第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结构体的函数调用过程,如下图所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image016.png 图35.3.4 查找匹配machine_desc的过程 1.3.6 引用节点前面说到节点的命名格式如下所示: [label:]node-name[@unit-address] 也多次给大家提到“label”字段,引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,例如下面这个模板: 示例代码24.3.6.1 设备树模板 -  / {
 
 -      aliases{
 
 -          can0 = &flexcan1;
 
 -      };
 
 -  
 
 -       cpus {
 
 -          #address-cells= <1>;
 
 -          #size-cells= <0>;
 
 -  
 
 -          cpu0:cpu@0 {
 
 -              compatible = "arm,cortex-a7";
 
 -              device_type = "cpu";
 
 -              reg = <0>;
 
 -          };
 
 -      };
 
  
-       intc:interrupt-controller@00a01000 {
 
 -           compatible = "arm,cortex-a7-gic";
 
 -            #interrupt-cells= <3>;
 
 -            interrupt-controller;
 
 -            reg = <0x00a01000 0x1000>,
 
 -                  <0x00a02000 0x100>;
 
 -       };
 
 -  };
 
  复制代码通过&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点“intc:interrupt-controller@00a01000”,节点label是intc,而节点名字就很长了,为“interrupt-controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方便很多! 所以如果我们要在设备树中引用其它的节点,那么就可以在这个被引用的节点前加上“label:”,这样我们就可以很方便的通过“&label”的方式进行引用了。 1.3.7 向节点追加或修改内容这里面有两个知识点:向节点追加内容,也就是添加属性;另一个就是修改节点的内容。我相信大家都理解我这里说的意思。在实际的开发当中肯定是有这样的需求存在的,例如在我们的领航者开发板上有一个eeprom器件(24c64)和一个rtc器件(pcf8563),假如它俩都是挂在ZYNQ的i2c0总线下的。那么现在要把这两个设备添加到i2c0总线下,打开zynq-7000.dtsi文件,可以看到PS的两组i2c控制器节点定义,如下所示: 示例代码24.3.7.1 zynq-7000.dtsi i2c节点 -  i2c0:i2c@e0004000 {
 
 -      compatible = "cdns,i2c-r1p10";
 
 -      status = "disabled";
 
 -      clocks = <&clkc 38>;
 
 -      interrupt-parent= <&intc>;
 
 -      interrupts = <0 25 4>;
 
 -      reg = <0xe0004000 0x1000>;
 
 -      #address-cells= <1>;
 
 -      #size-cells= <0>;
 
 -  };
 
  
-  i2c1:i2c@e0005000 {
 
 -      compatible = "cdns,i2c-r1p10";
 
 -      status = "disabled";
 
 -      clocks = <&clkc 39>;
 
 -      interrupt-parent= <&intc>;
 
 -      interrupts = <0 48 4>;
 
 -      reg = <0xe0005000 0x1000>;
 
 -      #address-cells= <1>;
 
 -      #size-cells= <0>;
 
 -  };
 
  复制代码因为现在要把开发板的两个i2c器件添加到i2c0总线下,直接在i2c0节点下创建两个子节点即可,一个子节点对应的是eeprom,另一个子节点对应的是rtc,那么最简单的方法就是直接在zynq-7000.dtsi文件的i2c0节点中添加这两个节点子节点即可,如下所示: 示例代码24.3.7.2 zynq-7000.dtsi 添加i2c器件 -  i2c0:i2c@e0004000 {
 
 -      compatible = "cdns,i2c-r1p10";
 
 -      status = "disabled";
 
 -      clocks = <&clkc 38>;
 
 -      interrupt-parent= <&intc>;
 
 -      interrupts = <0 25 4>;
 
 -      reg = <0xe0004000 0x1000>;
 
 -      #address-cells= <1>;
 
 -      #size-cells= <0>;
 
  
-      24c64@50 {
 
 -          compatible = "atmel,24c64";
 
 -          reg = <0x50>;
 
 -          pagesize = <32>;
 
 -      };
 
  
-      rtc@51 {
 
 -          compatible = "nxp,pcf8563";
 
 -          reg = <0x51>;
 
 -      };
 
 -  };
 
  
-  i2c1:i2c@e0005000 {
 
 -      compatible = "cdns,i2c-r1p10";
 
 -      status = "disabled";
 
 -      clocks = <&clkc 39>;
 
 -      interrupt-parent= <&intc>;
 
 -      interrupts = <0 48 4>;
 
 -      reg = <0xe0005000 0x1000>;
 
 -      #address-cells= <1>;
 
 -      #size-cells= <0>;
 
 -  };
 
  复制代码第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 节点追加数据方法 - &i2c0 {
 
 -    /* 要追加或修改的内容 */
 
 -  };
 
  复制代码第1行,&i2c0表示要引用到i2c0这个label所对应的节点,也就是zynq-7000.dtsi文件中的“i2c0: i2c@e0004000”。 第2行,花括号内就是要向i2c0这个节点添加的内容,包括修改某些属性的值。 打开system-top.dts,这样我们就可以直接在该文件中追加内容了: 示例代码24.3.7.4 system-top.dts 向i2c0节点追加内容  
-  /dts-v1/;
 
 -  #include "zynq-7000.dtsi"
 
 -  #include "pl.dtsi"
 
 -  #include "pcw.dtsi"
 
 -  / {
 
 -      model = "AlientekZYNQ Development Board";
 
  
-      chosen {
 
 -          bootargs = "console=ttyPS0,115200earlyprintk root=/dev/mmcblk0p2 rw rootwait";
 
 -          stdout-path = "serial0:115200n8";
 
 -      };
 
 -      aliases {
 
 -          ethernet0 = &gem0;
 
 -          i2c0 = &i2c_2;
 
 -          i2c1 = &i2c0;
 
 -          i2c2 = &i2c1;
 
 -          serial0 = &uart0;
 
 -          serial1 = &uart1;
 
 -          spi0 = &qspi;
 
 -      };
 
 -      memory {
 
 -          device_type = "memory";
 
 -          reg = <0x0 0x20000000>;
 
 -      };
 
 -  };
 
  
-  &i2c0 {
 
 -      clock-frequency= <100000>;
 
 -      status = "okay";
 
  
-      24c64@50 {
 
 -          compatible = "atmel,24c64";
 
 -          reg = <0x50>;
 
 -          pagesize = <32>;
 
 -      };
 
  
-      rtc@51 {
 
 -          compatible = "nxp,pcf8563";
 
 -          reg = <0x51>;
 
 -      };
 
 -  };
 
  
-  &gem0 {
 
 -      local-mac-address= [00 0a 35 00 1e 53];
 
 -  };
 
  复制代码第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文件中,可以看到很多的节点引用、向节点追加内容、修改节点内容的示例,如下所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image018.jpg 图35.3.5 pcw.dtsi示例 1.3.8 特殊节点在根节点“/”中有那么几个特殊的子节点:aliases、chosen以及memory,我们接下来看一下这三个比较特殊的节点,我们会发现这三个节点都是没有compatible属性,也就是说它们对应的并不是一个真实的设备。 1、aliases节点 打开system-top.dts文件,可以看到aliases节点的内容如下所示: 示例代码24.3.8.1 system-top.dts aliases节点 -  aliases {
 
 -      ethernet0 = &gem0;
 
 -      i2c0 = &i2c_2;
 
 -      i2c1 = &i2c0;
 
 -      i2c2 = &i2c1;
 
 -      serial0 = &uart0;
 
 -      serial1 = &uart1;
 
 -      spi0 = &qspi;
 
 -  };
 
  复制代码单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是需要注意的是,这里说的方便访问节点并不是在设备树中访问节点,例如前面说到的使用“&label”的方式访问设备树中的节点,而是内核当中方便定位节点,例如在内核中通过ethernet0就可以定位到gem0节点(&gem0引用的节点),再例如内核通过serial0就可以找到uart0节点。 2、chosen节点 chosen节点一般会有两个属性,“bootargs”和“stdout-path”。打开system-top.dts文件,找到chosen节点,内容如下所示: 示例代码24.3.8.2 chosen节点 -  chosen {
 
 -      bootargs = "console=ttyPS0,115200earlyprintk root=/dev/mmcblk0p2 rw rootwait";
 
 -      stdout-path = "serial0:115200n8";
 
 - };
 
  复制代码 在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函数 -  /**
 
 -  *of_console_check() - Test and setup console for DT setup
 
 -  * @dn - Pointerto device node
 
 -  * @name -Name to use for preferred console without index. ex. "ttyS"
 
 -  * @Index - Indexto use for preferred console.
 
 -  *
 
 -  * Check if thegiven device node matches the stdout-path property in the
 
 -  * /chosen node.If it does then register it as the preferred console and return
 
 -  * TRUE.Otherwise return FALSE.
 
 -  */
 
 -  boolof_console_check(struct device_node *dn, char *name, intindex)
 
 -  {
 
 -      if (!dn || dn !=of_stdout || console_set_on_cmdline)
 
 -          returnfalse;
 
  
-     /*
 
 -      * XXX: cast `options' to char pointer tosuppress complication
 
 -      * warnings: printk, UART and consoledrivers expect char pointer.
 
 -      */
 
 -      return !add_preferred_console(name,index, (char *)of_stdout_options);
 
 -  }
 
  复制代码看这个函数的名字“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的值),如所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image020.jpg 图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函数 -  intfdt_chosen(void *fdt)
 
 -  {
 
 -      int   nodeoffset;
 
 -      int   err;
 
 -      char  *str;     /* used to set string properties*/
 
  
-      err =fdt_check_header(fdt);
 
 -      if (err < 0) {
 
 -          printf("fdt_chosen:%s\n", fdt_strerror(err));
 
 -          return err;
 
 -      }
 
  
-      /* find or create"/chosen" node. */
 
 -      nodeoffset = fdt_find_or_add_subnode(fdt, 0, "chosen");
 
 -      if (nodeoffset< 0)
 
 -          returnnodeoffset;
 
  
-      str =getenv("bootargs");
 
 -      if (str) {
 
 -          err =fdt_setprop(fdt, nodeoffset, "bootargs", str,
 
 -                    strlen(str) + 1);
 
 -          if (err < 0) {
 
 -              printf("WARNING:could not set bootargs %s.\n",
 
 -                     fdt_strerror(err));
 
 -              return err;
 
 -          }
 
 -      }
 
  
-      return fdt_fixup_stdout(fdt,nodeoffset);
 
 -  }
 
  复制代码 第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: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image022.png 图 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节点 -  memory {
 
 -      device_type = "memory";
 
 -      reg = <0x0 0x20000000>;
 
 -  };
 
  复制代码memory节点描述了系统内存的基地址以及系统内存大小,“reg = <0x0 0x20000000>”就表示系统内存的起始地址为0x0,大小为0x20000000,也就是512MB,该节点一般只有这两个属性,device_type属性的值固定为“memory”。 1.3.9 常用节点本来这小节给大家讲一些常用到的节点,例如中断控制器、GPIO控制器以及在节点当中如何使用中断、如何使用gpio等。当想了想还是放在后面我们用到的时候再给大家介绍。 1.4 驱动与设备节点的匹配这部分内容已经在前面跟大家讲过了,具体请看35.3.4小节中的第一个小点compatible属性介绍。 1.5 内核启动过程中解析设备树Linux内核在启动的时候会解析内核DTB文件,然后在根文件系统的/proc/device-tree(后面给大家演示)目录下生成相应的设备树节点文件。接下来我们简单分析一下Linux内核是如何解析DTB文件的,流程如图43.7.1所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image024.png 图35.5.1 设备树中节点解析流程 从上图中可以看出,在start_kernel函数中完成了设备树节点解析的工作,最终实际工作的函数为unflatten_dt_node。那么具体如何进行设备树解析的这里就不给大家进行一一分析了,如果大家有时间可以自个去研究研究! 1.6 设备树在系统中的体现Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree目录下根据节点名字创建不同文件夹,如下图所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image026.jpg 图 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这两个文件的内容,结果如下图所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image028.jpg 图 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节点的所有子节点,如所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image030.jpg 图 35.6.3 amba节点的所有属性和子节点 和根节点“/”一样,图 35.6.3中的所有文件分别为amba节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和我们使用的设备树中amba节点的属性值相同。 1.7 绑定信息文档设备树是用来描述板子上的硬件设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在Linux内核源码中有详细的.txt文档描述了如何添加节点,这些.txt文档叫做绑定文档,路径为:Linux源码目录/Documentation/devicetree/bindings,如所示: file:///C:/Users/WCY/AppData/Local/Temp/msohtmlclip1/01/clip_image032.png 图35.7.1 绑定文档 比如我们现在要想在ZYNQ7010/7020这颗SOC的I2C下添加一个节点,那么就可以查看Documentation/devicetree/bindings/i2c/i2c-cadence.txt(文件的名字一般都是以i2c-xxx.txt命名的,xxx一般是制造商),此文档详细的描述了ZYNQ-7000系列处理器如何在设备树中添加I2C设备节点,文档内容如下所示: Binding for the Cadence I2Ccontroller  
Requiredproperties:   - reg:Physical base address and size of the controller's register area.   - compatible:Should contain one of:                 * "cdns,i2c-r1p10"                 Note:   Use this when cadence i2c controller version1.0 is used.                 * "cdns,i2c-r1p14"                 Note:   Use this when cadence i2c controller version1.4 is used.   - clocks:Input clock specifier. Refer to commonclock bindings.   - interrupts:Interrupt specifier. Refer tointerrupt bindings.   - #address-cells:Should be 1.   - #size-cells:Should be 0.  
Optionalproperties:   - clock-frequency:Desired operating frequency, in Hz, ofthe bus.   - clock-names:Input clock name, should be 'pclk'.  
Example:         i2c@e0004000 {                 compatible = "cdns,i2c-r1p10";                 clocks = <&clkc 38>;                 interrupts = <GIC_SPI25 IRQ_TYPE_LEVEL_HIGH>;                 reg = <0xe0004000 0x1000>;                 clock-frequency= <400000>;                 #address-cells= <1>;                 #size-cells =<0>;         }; 有时候使用的一些芯片在Documentation/devicetree/bindings目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。 1.8 设备树常用of操作函数设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用reg属性描述了某个外设的寄存器地址为0X02005482,长度为0X400,我们在编写驱动的时候需要获取到reg属性的0X02005482和0X400这两个值,然后初始化外设。Linux内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资料里面也被叫做OF函数。这些OF函数原型都定义在include/linux/of.h文件中。 1.8.1 查找节点的OF函数设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的属性信息,必须先获取到这个设备的节点。Linux内核使用device_node结构体来描述一个节点,此结构体定义在文件include/linux/of.h中,定义如下: 示例代码24.8.1.1 device_node节点 - <div style="text-align: left;"> structdevice_node {</div><div style="text-align: left;">const char *name;                 /* 节点名字 */</div><div style="text-align: left;">const char *type;                 /* 设备类型 */</div><div style="text-align: left;">phandlephandle;</div><div style="text-align: left;">const char *full_name;           /* 节点全名 */</div><div style="text-align: left;">structfwnode_handle fwnode;</div><div style="text-align: left;">
 
 - </div><div style="text-align: left;">struct  property *properties;   /* 属性 */</div><div style="text-align: left;">struct  property *deadprops;     /*removed属性 */</div><div style="text-align: left;">struct  device_node *parent;     /* 父节点 */</div><div style="text-align: left;">struct  device_node *child;      /* 子节点 */</div><div style="text-align: left;">struct  device_node *sibling;</div><div style="text-align: left;">struct  kobject kobj;</div><div style="text-align: left;">unsigned long_flags;</div><div style="text-align: left;">void    *data;</div><div style="text-align: left;">#ifdefined(CONFIG_SPARC)</div><div style="text-align: left;">const char *path_component_name;</div><div style="text-align: left;">unsigned intunique_id;</div><div style="text-align: left;">structof_irq_controller *irq_trans;</div><div style="text-align: left;">#endif</div><div style="text-align: left;">};</div>
 
  复制代码与查找节点有关的OF函数有5个,我们依次来看一下。 1、of_find_node_by_name函数 of_find_node_by_name函数通过节点名字查找指定的节点,函数原型如下: structdevice_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属性查找指定的节点,函数原型如下: structdevice_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这两个属性查找指定的节点,函数原型如下: structdevice_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匹配表来查找指定的节点,函数原型如下: structdevice_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函数通过节点路径来查找指定的节点,函数原型如下: inlinestruct device_node *of_find_node_by_path(const char *path) 函数参数和返回值含义如下: path:带有全路径的节点名,可以使用节点的别名(用aliens节点中定义的别名)。 返回值:找到的节点,如果为NULL表示查找失败 1.8.2 查找父/子节点的OF函数Linux内核提供了几个查找节点对应的父节点或子节点的OF函数,我们依次来看一下。 1、of_get_parent函数 of_get_parent函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下: structdevice_node *of_get_parent(const struct device_node *node) 函数参数和返回值含义如下: node:要查找的父节点的节点。  返回值:找到的父节点。 2、of_get_next_child函数 of_get_next_child函数用迭代的查找子节点,函数原型如下: structdevice_node *of_get_next_child(const struct device_node       *node,                                                     struct device_node                 *prev) 函数参数和返回值含义如下: node:父节点。 prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。 返回值:找到的下一个子节点。 1.8.3 提取属性值的OF函数设备树节点的属性保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux内核中使用结构体property表示属性,此结构体同样定义在文件include/linux/of.h中,内容如下: 示例代码24.8.3.1 property结构体 -  structproperty {
 
 -       char    *name;             /* 属性名字 */
 
 -       intlength;                /* 属性长度 */
 
 -       void    *value;           /* 属性值 */
 
 -       structproperty *next;   /* 下一个属性 */
 
 -       unsigned long_flags;
 
 -       unsigned int unique_id;
 
 -       structbin_attribute attr;
 
 -  };
 
  复制代码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属性值是一个数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下: intof_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数据组成的数组)的u32类型数据值(无符号32位),比如某个属性有多个u32类型的值,那么就可以使用此函数来获取指定下标的数据值,此函数原型如下: - intof_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属性中的所有数据。这四个函数的原型如下: - intof_property_read_u8_array(const struct device_node    *np,
 
 -                                            const char                         *propname,
 
 -                                            u8                                    *out_values,
 
 -                                            size_t                               sz)
 
 - intof_property_read_u16_array(const struct device_node   *np,
 
 -                                             const char                       *propname,
 
 -                                             u16                                *out_values,
 
 -                                             size_t                              sz)
 
 - intof_property_read_u32_array(const struct device_node   *np,
 
 -                                     const char                       *propname,
 
 -                                             u32                                *out_values,
 
 -                                     size_t                              sz)
 
 - intof_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类型属性值,函数原型如下: - intof_property_read_u8(const struct device_node      *np,
 
 -                                     const char                         *propname,
 
 -                                    u8                                    *out_value)
 
 - intof_property_read_u16(const struct device_node     *np,
 
 -                                     const char                         *propname,
 
 -                                    u16                                  *out_value)
 
 - intof_property_read_u32(const struct device_node     *np,
 
 -                                     const char                         *propname,
 
 -                                    u32                                  *out_value)
 
 - intof_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函数用于读取属性中字符串值,函数原型如下: - intof_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属性值,函数原型如下: intof_n_addr_cells(struct device_node *np) 函数参数和返回值含义如下: np:设备节点。 返回值:获取到的#address-cells属性值。 8、of_n_size_cells函数 of_size_cells函数用于获取#size-cells属性值,函数原型如下: intof_n_size_cells(struct device_node *np) 函数参数和返回值含义如下: np:设备节点。 返回值:获取到的#size-cells属性值。 1.8.4 其他常用的OF函数1、of_device_is_compatible函数 of_device_is_compatible函数用于查看节点的compatible属性是否有包含compat指定的字符串,也就是检查设备节点的兼容性,函数原型如下: - intof_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函数负责将从设备树读取到的地址转换为物理地址,函数原型如下: u64of_translate_address(struct device_node       *dev, 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结构体 -  structresource {
 
 -       resource_size_tstart;
 
 -       resource_size_tend;
 
 -       const char *name;
 
 -       unsigned longflags;
 
 -       structresource *parent, *sibling, *child;
 
 -  };
 
  复制代码对于32位的SOC来说,resource_size_t是u32类型的。其中start表示开始地址,end表示结束地址,name是这个资源的名字,flags是资源标志位,一般表示资源类型,可选的资源标志定义在文件include/linux/ioport.h中,如下所示: 示例代码24.8.4.2 资源标志 -  #define IORESOURCE_BITS           0x000000ff  
 
 -  #define IORESOURCE_TYPE_BITS     0x00001f00  
 
 -  #define IORESOURCE_IO             0x00000100  
 
 -  #define IORESOURCE_MEM            0x00000200
 
 -  #define IORESOURCE_REG            0x00000300  
 
 -  #define IORESOURCE_IRQ            0x00000400
 
 -  #define IORESOURCE_DMA            0x00000800
 
 -  #define IORESOURCE_BUS            0x00001000
 
 -  #define IORESOURCE_PREFETCH      0x00002000  
 
 - #define IORESOURCE_READONLY     0x00004000
 
 - #define IORESOURCE_CACHEABLE    0x00008000
 
 - #define IORESOURCE_RANGELENGTH  0x00010000
 
 - #define IORESOURCE_SHADOWABLE    0x00020000
 
 - #define IORESOURCE_SIZEALIGN    0x00040000  
 
 - #define IORESOURCE_STARTALIGN    0x00080000  
 
 - #define IORESOURCE_MEM_64        0x00100000
 
 - #define IORESOURCE_WINDOW        0x00200000  
 
 - #define IORESOURCE_MUXED        0x00400000  
 
 - #define IORESOURCE_EXCLUSIVE    0x08000000  
 
 - #define IORESOURCE_DISABLED      0x10000000
 
 - #define IORESOURCE_UNSET        0x20000000
 
 - #define IORESOURCE_AUTO          0x40000000
 
 - #define IORESOURCE_BUSY          0x80000000
 
  复制代码大家一般最常见的资源标志就是IORESOURCE_MEM、IORESOURCE_REG和IORESOURCE_IRQ等。接下来我们回到of_address_to_resource函数,此函数看名字像是从设备树里面提取资源值,但是本质上就是将reg属性值,然后将其转换为resource结构体类型,函数原型如下所示 intof_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驱动实验都将采用设备树,从最基本的点灯,到复杂的音频、网络或块设备等驱动。将会带领大家由简入深,深度剖析设备树,最终掌握基于设备树的驱动开发技能。  
 |