本帖最后由 正点原子运营 于 2024-2-29 16:47 编辑
第二十章 Linux设备树
1)实验平台:正点原子 DFZU2EG_4EV MPSoC开发板
2) 章节摘自【正点原子】DFZU2EG_4EV MPSoC开发板之嵌入式Linux 驱动开发指南 V1.0
6)Linux技术交流QQ群:887820935
前面章节中我们多次提到“设备树”这个概念,因为时机未到,所以当时并没有详细的讲解什么是“设备树”,本章我们就来详细的谈一谈设备树。掌握设备树是Linux驱动开发人员必备的技能!因为在新版本的Linux内核中,设备驱动基本全部采用了设备树(也有支持老式驱动的,比较少)的方式,最新出的CPU其驱动开发也基本都是基于设备树的,我们所使用的Linux版本为4.19.0,肯定是支持设备树的,所以正点原子DFZU2EG_4EV MPSoC开发板的所有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文件中有如下内容(有缩减): - 示例代码20.1.1 mach-smdk2440.c文件代码片段
- 90 static struct s3c2410fb_displaysmdk2440_lcd_cfg __initdata = {
- 91
- 92 .lcdcon5 = S3C2410_LCDCON5_FRM565 |
- 93 S3C2410_LCDCON5_INVVLINE |
- 94 S3C2410_LCDCON5_INVVFRAME |
- 95 S3C2410_LCDCON5_PWREN |
- 96 S3C2410_LCDCON5_HWSWP,
- ......
- 113 };
- 114
- 115 static struct s3c2410fb_mach_infosmdk2440_fb_info __initdata = {
- 116 .displays = &smdk2440_lcd_cfg,
- 117 .num_displays = 1,
- 118 .default_display = 0,
- ......
- 133 };
- 134
- 135 static struct platform_device *smdk2440_devices[] __initdata = {
- 136 &s3c_device_ohci,
- 137 &s3c_device_lcd,
- 138 &s3c_device_wdt,
- 139 &s3c_device_i2c0,
- 140 &s3c_device_iis,
- 141 };
复制代码上述代码中的结构体变量smdk2440_fb_info就是描述SMDK2440这个开发板上的LCD硬件信息的,结构体指针数组smdk2440_devices描述的是SMDK2440这个开发板上的所有硬件相关信息。这个仅仅是使用2440这个芯片的SMDK2440开发板下的LCD信息,SMDK2440开发板还有很多的其他外设硬件和平台硬件信息。使用2440这个芯片的板子有很多,每个板子都有描述相应板级硬件信息的文件,这仅仅只是一个2440。随着智能手机的发展,每年新出的ARM架构芯片少说都在数十、数百款,Linux内核下板级信息文件将会成指数级增长!这些板级信息文件都是.c或.h文件,都会被硬编码进Linux内核中,导致Linux内核“虚胖”。 这些板级硬件信息代码对linux内核来说只不过是垃圾代码而已,所以当Linux之父linus看到ARM社区向Linux内核添加了大量“无用”、冗余的板级信息文件,不禁的发出了一句“This whole ARM thing isa 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_LED1换成另一个MIO引脚了),驱动代码就要重写。 引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变 化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。使用设备树之后,许多硬件设备信息可以直接通过它传递给Linux,而不需要在内核中堆积大量的冗余代码。 设备树,将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的硬件设备信息,比如CPU数量、内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等,如图 20.1.1所示: 在图 20.1.1中,树的主干就是系统总线,IIC控制器、GPIO控制器、SPI控制器等都是接到系统主线上的分支。IIC控制器有分为IIC1和IIC2两种,其中IIC1上接了FT5206和AT24C02这两个IIC设备,IIC2上只接了MPU6050这个设备。DTS文件的主要功能就是按照图 20.1.1所示的结构来描述板子上的设备信息,DTS文件描述设备信息是有相应的语法规则要求的,稍后我们会详细的讲解DTS语法规则。 设备树文件的扩展名为.dts,一个.dts(devicetree source)文件就对应一个开发板,一般放置在内核的"arch/arm/boot/dts/"目录下,比如exynos4412开发板的板级设备树文件就是"arch/arm/boot/dts/exynos4412-origen.dts",再比如I.MX6ULL-EVK开发板的板级设备树文件就是arch/arm/boot/dts/imx6ull-14x14-evk.dts。本篇驱动开发我们所使用的板级设备树文件放在“arch/arm64/boot/dts/xilinx/”目录下,包括system-top.dts、pl.dtsi、pcw.dtsi、zyqnmp.dtsi、zynqmp-clk-conf.dtsi和用户自定义设备树system-user.dtsi等。注意,在设备树中添加设备时,我们推荐放在system-user.dtsi中,当然也可以直接放在system-top.dts中。 前面也跟大家讲过,除了内核支持设备树之外,新版的u-boot也是支持设备树的,如果有机会也可以跟大家讲一讲U-Boot的设备树。
1.2 设备树的基本知识
1.2.1 dts设备树的源文件的后缀名就是.dts,每一款硬件平台可以单独写一份xxxx.dts,所以在Linux内核源码中存在大量.dts文件,对于arm 64位架构可以在arch/arm64/boot/dts找到相应的dts。 1.2.2 dtsi值得一提的是,对于一些相同的dts配置可以抽象到dtsi文件中,这个dtsi文件其实就类似于C语言当中的.h头文件,可以通过C语言中使用include来包含一个.dtsi文件,例如arch/arm64/boot/dts/xilinx/system-top.dts文件有如下内容: - 示例代码20.2.2.1 system-top.dts内容片段
- 1 /*
- 2 * CAUTION: This file is automaticallygenerated by Xilinx.
- 3 * Version:
- 4 * Today is: Sat May 21 03:48:08 2022
- 5 */
- 6
- 7
- 8 /dts-v1/;
- 9 #include "zynqmp.dtsi"
- 10#include "zynqmp-clk-ccf.dtsi"
- 11#include "pl.dtsi"
- 12#include "pcw.dtsi"
- 13/ {
- 14 chosen {
- 15 bootargs = "earlyconclk_ignore_unused";
- 16 stdout-path ="serial0:115200n8";
- 17 };
- 18 aliases {
- 19 ethernet0 = &gem0;
- 20 ethernet1 = &gem3;
- 21 i2c0 = &hdmi_ddc;
- 22 i2c1 = &i2c0;
- 23 i2c2 = &i2c1;
- 24 i2c3 = &sensor_iic;
- 25 serial0 = &uart0;
- 26 serial1 = &uart1;
- 27 spi0 = &qspi;
- 28 };
- 29 memory {
- 30 device_type = "memory";
- 31 reg = <0x0 0x0 0x0 0x7ff00000>;
- 32 };
- 33};
- 34#include "system-user.dtsi"
复制代码第9~12行中,通过#include包含了同目录下的四个.dtsi文件,分别为:zynqmp.dtsi、pl.dtsi、pcw.dtsi、zynqmp-clk-ccf.dtsi。这里简答地给大家说一下这四个文件的内容有啥不同,首先zynqmp.dtsi文件中的内容是ZYNQ MPSoC系列处理器相同的硬件外设配置信息(PS端的),pl.dtsi的内容是我们在vivado当中添加的pl端外设对应的配置信息,而pcw.dtsi则表示我们在vivado当中已经使能的PS外设,zynqmp-clk-ccf.dtsi文件是ZYNQ MPSoC系列芯片通用的时钟相关的设备树文件。 那么除此之外,使用#include除了可以包含.dtsi文件之外,还可以包含.dts文件以及C语言当中的.h文件,这些都是可以的,可以这么理解.dtsi和.dts文件语法各方面都是一样的,但是不能直接编译一个.dtsi文件。 1.2.3 dtcdtc其实就是device-tree-compiler,那就是设备文件.dts的编译器嘛,将.c文件编译为.o文件需要用到gcc编译器,那么将.dts文件编译为相应的二进制文件则需要dtc编译器,dtc工具在Linux内核的scripts/dtc目录下,当然必须要编译了内核源码之后才会生成,如下所示: 我们来看看scripts/dtc/Makefile文件,如下所示: - 示例代码20.2.3.1 scripts/dtc/Makefile文件代码段
- 1 hostprogs-y := dtc
- 2 always :=$(hostprogs-y)
- 3
- 4 dtc-objs:=dtc.o flattree.o fstree.o data.o livetree.o treesource.o \
- 5 srcpos.o checks.o util.o
- 6 dtc-objs +=dtc-lexer.lex.o dtc-parser.tab.o
- ......
复制代码可以看出,dtc工具依赖于dtc.c、flattree.c、fstree.c等文件,最终编译并链接出dtc这个主机文件。如果要编译dts文件的话只需要进入到Linux源码根目录下,然后执行如下命令: 或者: “make all”命令是编译Linux源码中的所有东西,包括Image,*.ko驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令。 在内核源码arch/arm64/boot/dts/xilinx/目录下有很多的dts文件,那我们编译的时候如何确定编译的是哪个或者说哪些dts文件的呢?大家可以打开arch/arm64/boot/dts/xilinx/Makefile文件,内容如下所示: 示例代码20.2.3.2 arch/arm64/boot/dts/xilinx/Makefile文件部分内容 - 1 # SPDX-License-Identifier: GPL-2.0
- 2 dtb-$(CONFIG_ARCH_ZYNQMP) += avnet-ultra96-rev1.dtb
- 3 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1232-revA.dtb
- 4 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1254-revA.dtb
- 5 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm015-dc1.dtb
- 6 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm016-dc2.dtb
- 7 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm017-dc3.dtb
- 8 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm018-dc4.dtb
- 9 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zc1751-xm019-dc5.dtb
- 10 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu100-revC.dtb
- 11 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu102-revA.dtb
- 12 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu102-revB.dtb
- 13 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu102-rev1.0.dtb
- 14 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu104-revA.dtb
- 15 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu104-revC.dtb
- 16 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu106-revA.dtb
- 17 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu111-revA.dtb
- 18 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu1275-revA.dtb
- 19 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu1275-revB.dtb
- 20 dtb-$(CONFIG_ARCH_ZYNQMP) += zynqmp-zcu1285-revA.dtb
复制代码当运行“make dtbs”命令时,上面包含的设备树都会被编译。“dtb-$(CONFIG_ARCH_ZYNQMP)+=”后面的dtb文件就是设备树dts文件编译后对应的二进制文件。例如,我们要编译设备树“system-top.dts”文件,只要把“system-top.dtb”添加到Makefile中就行,如下图所示: 图 20.2.2 Makefile中添加要编译的设备树 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语法。关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,路径为:开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\ARM\Devicetree SpecificationV0.2.pdf、开发板资料盘(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之间可以是平行关系,也可以嵌套成父子关系,这样就可以很方便的描述设备间的关系; 下面给出一个设备树的简单的结构示意图: - 示例代码20.3.1.1 设备树结构示意
- 1/{ // 根节点
- 2 node1{ // node1节点
- 3 property1=value1; // node1节点的属性property1
- 4 property2=value2; // node1节点的属性property2
- 5 ...
- 6 };
- 7
- 8 node2{ // node2节点
- 9 property3=value3; // node2节点的属性property3
- 10 ...
- 11 node3{ // node2的子节点node3
- 12 property4=value4;// node3节点的属性property4
- 13 ...
- 14 };
- 15 };
- 16};
复制代码第1行当中的’/’就表示设备树的root node(根节点),所以可知node1节点和node2节点的父节点都是root node,而node3节点的父节点则是node2,node2与node3之间形成了父子节点关系。Rootnode下面的子节点node1和node2可以表示为SoC上的两个控制器,而node3则可以表示挂在node2控制器上的某个设备,例如node2表示ZYNQ MPSoC 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”。 每个节点都有若干属性,属性又有相对应的值(值不是必须要有的),而一个节点当中又可以嵌套其它的节点,形成父子节点。例如下面: - 示例代码20.3.2.1 设备树节点示例
- 23 cpus{
- 24 #address-cells= <1>;
- 25 #size-cells= <0>;
- 26
- 27 cpu0:cpu@0 {
- 28 compatible= "arm,cortex-a53", "arm,armv8";
- 29 device_type= "cpu";
- 30 enable-method= "psci";
- 31 operating-points-v2= <&cpu_opp_table>;
- 32 reg= <0x0>;
- 33 cpu-idle-states= <&CPU_SLEEP_0>;
- 34 };
- 35 cpu1:cpu@1 {
- 36 compatible= "arm,cortex-a53", "arm,armv8";
- 37 device_type= "cpu";
- 38 enable-method= "psci";
- 39 reg= <0x1>;
- 40 operating-points-v2= <&cpu_opp_table>;
- 41 cpu-idle-states= <&CPU_SLEEP_0>;
- 42 };
- 43
- 44 cpu2:cpu@2 {
- 45 compatible= "arm,cortex-a53", "arm,armv8";
- 46 device_type= "cpu";
- 47 enable-method= "psci";
- 48 reg= <0x2>;
- 49 operating-points-v2= <&cpu_opp_table>;
- 50 cpu-idle-states= <&CPU_SLEEP_0>;
- 51 };
- 52
- 53 cpu3:cpu@3 {
- 54 compatible= "arm,cortex-a53", "arm,armv8";
- 55 device_type= "cpu";
- 56 enable-method= "psci";
- 57 reg= <0x3>;
- 58 operating-points-v2= <&cpu_opp_table>;
- 59 cpu-idle-states= <&CPU_SLEEP_0>;
- 60 };
- 61
- 62 idle-states{
- 63 entry-method= "psci";
- 64
- 65 CPU_SLEEP_0:cpu-sleep-0 {
- 66 compatible= "arm,idle-state";
- 67 arm,psci-suspend-param= <0x40000000>;
- 68 local-timer-stop;
- 69 entry-latency-us= <300>;
- 70 exit-latency-us= <600>;
- 71 min-residency-us= <10000>;
- 72 };
- 73 };
- 74 };
复制代码每一个节点(包括root node)都会使用一组括号”{ }”将自己的属性以及子节点包含在里边,注意括号外需要加上一个分号” ; ”,包括每一个属性都使用一个分号来结束。有点像C语言中的表达式后面的分号。 第23行当中的cpus节点,它的名字只有”[label:]node-name[@unit-address]”当中的”node-name”部分,没有其它两部分;第27行节点的定义包含了所有的组成部分,包括label以及unit-address;关于label的作用的我们后面专门讲,这里先不说。 cpus节点有两个属性” #address-cells”和” #size-cells”,它们的值分别为” <1>”和” <0>”。例如cpu@0节点中有compatible、device_type、reg、clocks属性等,它们都有对应的值,大家看到这些值可能有点不明白,为啥有的是字符串,有的是尖括号”<>”括起来的东西,下面单独给大家讲解一波。 每个节点都有不同属性,不同的属性又有不同的值,那么设备树当中值有哪些形式呢? l 字符串 - compatible ="arm,idle-state";
复制代码字符串使用双引号括起来,例如上面的这个compatible属性的值是” arm,cortex-a9”字符串。 l 32位无符号整形数据 32位无符号整形数据使用尖括号括起来,例如reg属性; l 二进制数据 - local-mac-address = [00 0a35 00 1e 53];
复制代码二进制数据使用方括号括起来,例如上面这个就是一个二进制数据组成的数组。 l 字符串数组 - compatible = "arm,cortex-a53","arm,armv8";
复制代码属性值也可以使用字符串列表,例如上面的这个属性,它的值是一个字符串列表,字符串之间使用逗号分割; l 混合值 - mixed-property = "astring", [0x01 0x23 0x45 0x67], <0x12345678>;
复制代码除此之外不同的数据类型还可以混合在一起,以逗号分隔。 l 节点引用 除了上面一些数据类型之外,还有一种非常常见的形式,如下所示: 这其实就是我们上面说到的引用节点的一种形式,”&clkc”就表示引用”clkc”这个节点,而clkc就是前面提到的”label”,引用节点也是使用尖括号来表示,关于节点之间的引用,我们后面还会再讲,这里先告一段落。 1.3.3 使用注释和宏定义在设备树文件中也可以使用注释,注释的方法和C语言当中是一毛一样的,可以使用” // ”进行单行注释,也可以使用”/* */ ”进行多行注释,如下所示: - 1 // SPDX-License-Identifier: GPL-2.0+
- 2 /*
- 3 * dts file for Xilinx ZynqMP
- 4 *
- 5 * (C) Copyright 2014 - 2015, Xilinx, Inc.
- 6 *
- 7 * Michal Simek<michal.simek@xilinx.com>
- 8 *
- 9 * This program is free software; you canredistribute it and/or
- 10 * modify it under the terms of the GNUGeneral Public License as
- 11 * published by the Free Software Foundation;either version 2 of
- 12 * the License, or (at your option) any laterversion.
- 13 */
- 14
- 15 #include<dt-bindings/power/xlnx-zynqmp-power.h>
- 16 #include<dt-bindings/reset/xlnx-zynqmp-resets.h>
- 17
- 18 / {
- 19 compatible= "xlnx,zynqmp";
- 20 #address-cells= <2>;
- 21 #size-cells= <2>;
- 22
- 23 cpus{
- 24 #address-cells= <1>;
- 25 #size-cells= <0>;
- 26
- 27 cpu0:cpu@0 {
- 28 compatible= "arm,cortex-a53", "arm,armv8";
- 29 device_type= "cpu";
- 30 enable-method= "psci";
- 31 operating-points-v2= <&cpu_opp_table>;
- 32 reg= <0x0>;
- 33 cpu-idle-states= <&CPU_SLEEP_0>;
- 34 };
- ……
复制代码前面跟大家讲过,设备树中可以使用“#include”包含dtsi、dts以及C语言的头文件,那我们为什么要包含一个.h的头文件呢?因为在设备树中可以使用宏定义,所以你在arch/arm/boot/dts目录下你会看到很多的设备树文件中都包含了.h头文件,例如下面这个: 关于头文件包含以及宏定义的使用这里就不多说了,本身也非常简单。 1.3.4 标准属性节点的内容是由一堆的属性组成,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。 1、compatible属性 compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值可以是一个字符串,也可以是一个字符串列表;一般该字符串使用”<制造商>,<型号>”这样的形式进行命名,当然这不是必须要这样,这是要求大家按照这样的形式进行命名,目的是为了指定一个确切的设备,并且包括制造商的名字,以避免命名空间冲突,如下所示: - compatible = "cdns,uart-r1p12","xlnx,xuartps";
复制代码例子当中的xlnx和cdns就表示制造商,而后面的xuartps和uart-r1p12就表示具体设备的型号。compatible属性用于将设备和驱动绑定起来,例如该设备首先使用第一个兼容值(cdns,uart-r1p12)在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值(xlnx,xuartps)查找,直到找到或者查找完整个Linux内核也没有找到对应的驱动。 一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设备树中的节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在驱动文件drivers/tty/serial/xilinx_uartps.c中有如下内容: - 示例代码20.3.4.1drivers/tty/serial/xilinx_uartps.c内容片段
- 1392 /* Match table for of_platformbinding */
- 1393 static const struct of_device_id cdns_uart_of_match[] = {
- 1394 { .compatible = "xlnx,xuartps", },
- 1395 { .compatible = "cdns,uart-r1p8", },
- 1396 { .compatible = "cdns,uart-r1p12", .data = &zynqmp_uart_def },
- 1397 { .compatible = "xlnx,zynqmp-uart", .data = &zynqmp_uart_def },
- 1398 {}
- 1399 };
- 1400 MODULE_DEVICE_TABLE(of, cdns_uart_of_match);
- ......
- 1760 static struct platform_drivercdns_uart_platform_driver = {
- 1761 .probe = cdns_uart_probe,
- 1762 .remove = cdns_uart_remove,
- 1763 .driver = {
- 1764 .name = CDNS_UART_NAME,
- 1765 .of_match_table = cdns_uart_of_match,
- 1766 .pm = &cdns_uart_dev_pm_ops,
- 1767 },
- 1768 };
复制代码这个驱动文件是ZYNQ MPSoC PS端的UART设备对应的驱动文件。 第1393~1399行定义的数组cdns_uart_of_match就是xilinx_uartps.c这个驱动文件的匹配表,此匹配表有4个匹配值“xlnx,xuartps”、“cdns,uart-r1p8”、“cdns,uart-r1p12”以及“xlnx,zynqmp-uart”。如果在设备树中有哪个节点的compatible属性值与这4个字符串中的某个相同,那么这个节点就会与此驱动文件匹配成功。 第1760行,UART采用了platform_driver驱动模式,关于platform_driver驱动后面会讲解。此行设置.of_match_table为cdns_uart_of_match,也就是设置这个platform_driver所使用的OF匹配表。 2、model属性 model属性值也是一个字符串描述信息,它指定制造商的设备型号,model属性一般定义在根节点下,一般就是板子的描述信息,没啥实质性的作用,内核在解析设备树的时候会把这个属性对应的字符串信息打印出来。 - 示例代码20.3.4.2 arch/arm64/boot/dts/xilinx/system-user.dtsi内容片段
- 1 #include <dt-bindings/gpio/gpio.h>
- 2 #include <dt-bindings/input/input.h>
- 3
- 4 #define GPIO_ACTIVE_HIGH 0
- 5 #define GPIO_ACTIVE_LOW 1
- 6
- 7 / {
- 8 model = "Alientek ZynqMpSoc Development Board";
- 9 compatible = "xlnx,zynqmp-atk", "xlnx,zynqmp";
- 10
- 11 led {
- 12 compatible = "alientek,led";
- 13 status = "okay";
- 14 default-state = "on";
- 15
- 16 led-gpio = <&gpio 38 GPIO_ACTIVE_HIGH>;
- 17 };
- 18
- 19 key {
- 20 compatible = "alientek,key";
- 21 status = "okay";
- 22 key-gpio = <&gpio 40 GPIO_ACTIVE_LOW>;
- 23 };
- 24
- 25 };
复制代码我之前在system-user.dtsi设备树文件加了一个model属性,它的值等于“AlientekZynq MpSoc Development Board”,内核启动过程中就会打印出来,如下所示: 3、status属性 status属性看名字就知道是和设备状态有关的,device tree中的status标识了设备的状态,使用status可以去禁止设备或者启用设备,下表是设备树规范中的status可选值:
表 20.3.1 status属性值
注意如果节点中没有添加status属性,那么它默认就是“status= okay”。 4、#address-cells和#size-cells属性 这两个属性的值都是无符号32位整形,#address-cells和#size-cells这两个属性可以用在任何拥有子节点的设备节点中,用于描述子节点的地址信息。 Ø #address-cells,用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量; Ø #size-cells,用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量。 #address-cells和#size-cells表明了子节点应该如何编写reg属性值,一般reg属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度,有了这两个属性,子节点中的"reg"属性就可以描述一块连续的地址区域了;reg属性的格式一为: - reg = <address1 length1 address2 length2 address3length3……>
复制代码每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长度,#address-cells表明address字段占用的字长,#size-cells表明length这个字段所占用的字长,比如: - 示例代码20.3.4.3 #address-cells和#size-cells属性
- //zynqmp-zcu102-revA.dts中的代码片段
- 777 &qspi {
- 778 status = "okay";
- 779 is-dual = <1>;
- 780 flash@0 {
- 781 compatible = "m25p80", "jedec,spi-nor"; /* 32MB*/
- 782 #address-cells = <1>;
- 783 #size-cells = <1>;
- 784 reg = <0x0>;
- 785 spi-tx-bus-width = <1>;
- 786 spi-rx-bus-width = <4>; /* FIXME also DUAL configuration pos sible */
- 787 spi-max-frequency = <108000000>; /* Basedon DC1 spec */
- 788 partition@qspi-fsbl-uboot { /* for testing purpose */
- 789 label = "qspi-fsbl-uboot";
- 790 reg = <0x00x100000>;
- 791 };
- 792 partition@qspi-linux { /* fortesting purpose */
- 793 label = "qspi-linux";
- 794 reg = <0x1000000x500000>;
- 795 };
- 796 partition@qspi-device-tree { /* for testing purpose */
- 797 label = "qspi-device-tree";
- 798 reg = <0x6000000x20000>;
- 799 };
- 800 partition@qspi-rootfs { /* fortesting purpose */
- 801 label = "qspi-rootfs";
- 802 reg = <0x6200000x5E0000>;
- 803 };
- //zynqmp.dtsi中的代码片段
- 802 qspi: spi@ff0f0000 {
- 803 u-boot,dm-pre-reloc;
- 804 compatible = "xlnx,zynqmp-qspi-1.0";
- 805 status = "disabled";
- 806 clock-names = "ref_clk", "pclk";
- 807 interrupts = <0 15 4>;
- 808 interrupt-parent = <&gic>;
- 809 num-cs = <1>;
- 810 reg = <0x00xff0f0000 0x0 0x1000>,
- 811 <0x00xc0000000 0x0 0x8000000>;
- 812 #address-cells = <1>;
- 813 #size-cells = <0>;
- 814 #stream-id-cells = <1>;
- 815 iommus = <&smmu 0x873>;
- 816 power-domains = <&zynqmp_firmware PD_QSPI>;
- 817 };
复制代码第812~813行,节点qspi的#address-cells= <1>,#size-cells =<0>,说明qspi的子节点reg属性中起始地址使用一个32bit数据来表示,地址长度没有;第780行,qspi的子节点flash0:flash@0的reg属性值为<0>,因为父节点设置了#address-cells = <1>,#size-cells = <0>,因此addres=0,没有length的值,相当于设置了起始地址,而没有设置地址长度。 第782和783行,设置flash@0节点#address-cells =<1>,#size-cells = <1>,说明flash@0的子节点起始地址所占用的字长为1,地址长度所占用的字长也为1。第788行,flash@0的子节点partition@qspi-fsbl-uboot的reg属性值为reg = <0x0 0x100000>,因为父节点设置了#address-cells =<1>,#size-cells = <1>,所以address使用一个32bit数据来表示,也就address=0x100000,而length也使用一个32bit数据来表示,也就是length=0x0,相当于只设置了起始地址为0x100000,没有设置地址长度。 5、reg属性 reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描述设备地址空间资源信息,一般都是描述某个外设的寄存器地址范围信息、flash设备的分区信息等,比如在arch/arm64/boot/dts/xilinx/zynqmp.dtsi文件中有如下内容: - 示例代码20.3.4.4 uart0节点信息
- 976 uart0: serial@ff000000 {
- 977 u-boot,dm-pre-reloc;
- 978 compatible = "cdns,uart-r1p12", "xlnx,xuartps";
- 979 status = "disabled";
- 980 interrupt-parent = <&gic>;
- 981 interrupts = <0 21 4>;
- 982 reg = <0x00xff000000 0x0 0x1000>;
- 983 clock-names = "uart_clk", "pclk";
- 984 power-domains = <&zynqmp_firmware PD_UART_0>;
- 985 };
复制代码上述代码是节点uart0,uart0节点描述了ZYNQ MPSoC PS端的UART0相关信息,重点是第982行的reg属性。其中uart0的父节点amba设置了#address-cells = <2>、#size-cells = <2>,因此reg属性中address= 0x1000ff000000(0x1000为高32位), length= 0x0。查阅ZYNQ MPSoC的数据手册(开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\Xilinx\UserGuide\ ug1085-zynq-ultrascale-trm.pdf)可知,ZYNQ MPSoC的UART0寄存器首地址确实为0xff000000。 6、ranges属性 ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵。映射表中的子地址、父地址占用的字长分别由ranges属性所在节点的#address-cells属性和ranges属性所在节点的父节点的#address-cells属性来确定。而子地址空间长度占用的字长由ranges属性所在节点的#address-cells属性决定。 child-bus-address:子总线地址空间的物理地址,由ranges属性所在节点的#address-cells属性确定此物理地址占用的字长。 parent-bus-address:父总线地址空间的物理地址,由ranges属性所在节点的父节点的#address-cells属性确定此物理地址所占用的字长。 length:子地址空间的长度,由ranges属性所在节点的#address-cells属性确定此地址长度所占用的字长。 如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的ZYNQ MPSoC来说,子地址空间和父地址空间完全相同,因此会在zynqmp.dtsi文件中找到很多值为空的ranges属性,如下所示: - 示例代码20.3.4.5 zynq-7000.dtsi内容片段
- 998 usb0: usb0@ff9d0000 {
- 999 #address-cells = <2>;
- 1000 #size-cells = <2>;
- 1001 status = "disabled";
- 1002 compatible = "xlnx,zynqmp-dwc3";
- 1003 reg = <0x00xff9d0000 0x0 0x100>;
- 1004 clock-names = "bus_clk", "ref_clk";
- 1005 power-domains = <&zynqmp_firmware PD_USB_0>;
- 1006 ranges;
- 1007 nvmem-cells = <&soc_revision>;
- 1008 nvmem-cell-names = "soc_revision";
- ……
复制代码第1006行定义了ranges属性,但是ranges属性值为空。 ranges属性不为空的示例代码如下所示: - 示例代码20.3.4.6 ranges属性不为空
- 1 soc {
- 2 compatible ="simple-bus";
- 3 #address-cells= <1>;
- 4 #size-cells= <1>;
- 5 ranges =<0x0 0xe00000000x00100000>;
- 6
- 7 serial {
- 8 device_type ="serial";
- 9 compatible ="ns16550";
- 10 reg= <0x46000x100>;
- 11 clock-frequency= <0>;
- 12 interrupts= <0xA0x8>;
- 13 interrupt-parent= <&ipic>;
- 14 };
- 15 };
复制代码第5行,节点soc定义的ranges属性,值为<0x0 0xe00000000x00100000>,此属性值指定了一个1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为0x0,父地址空间的物理起始地址为0xe0000000。 第10行,serial是串口设备节点,reg属性定义了serial设备寄存器的起始地址为0x4600,寄存器长度为0x100。经过地址转换,serial设备可以从0xe0004600开始进行读写操作,0xe0004600=0x4600+0xe0000000。 7、device_type属性 device_type属性值为字符串,表示节点的类型;此属性在设备树当中用的比较少,一般用于cpu节点或者memory节点。zynq-7000.dtsi文件中的cpu0和cpu1节点用到了此属性,内容如下所示: - 示例代码20.3.4.7 zynq-7000.dtsi内容片段
- 24cpu0: cpu@0 {
- 25 compatible ="arm,cortex-a9";
- 26 device_type ="cpu";
- 27 reg =<0>;
- 28 clocks =<&clkc 3>;
- 29 clock-latency= <1000>;
- 30 cpu0-supply= <®ulator_vccpint>;
- 31 operating-points= <
- 32 /* kHz uV */
- 33 666667 1000000
- 34 333334 1000000
- 35 >;
- 36 };
- 37
- 38cpu1: cpu@1 {
- 39 compatible ="arm,cortex-a9";
- 40 device_type ="cpu";
- 41 reg =<1>;
- 42 clocks =<&clkc 3>;
- 43};
复制代码关于标准属性就讲解这么多,后面还会跟大家介绍一些常常会使用到的节点,例如设备树中的中断控制器、GPIO、I2C总线等。 1.3.5 根节点compatible属性每个节点都有compatible属性(除了一些特殊用途的节点),根节点“/”也不例外,在zynq-7000.dtsi文件中根节点的compatible属性内容如下所示: - 示例代码20.3.5.1 zynq-7000.dtsi根节点compatible属性
- 15/ {
- 16 #address-cells= <1>;
- 17 #size-cells= <1>;
- 18 compatible ="xlnx,zynq-7000";
- ......
- 431};
复制代码可以看出,compatible有一个值:“xlnx,zynq-7000”。前面我们说了,设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatible属性是为了做什么工作的?同样根节点下的compatible属性的值可以是一个字符串,也可以是一个字符串列表;该字符串也要求以“<制造商>,<型号>”这样的形式进行命名;比如这里使用的是“xlnx”制造的“zynq-7000”系列处理器。 通过根节点的compatible属性可以知道我们所使用的处理器型号,Linux内核会通过根节点的compoatible属性查看是否支持此该处理器,因为内核在启动初期会进行校验,必须要支持才会启动Linux内核。接下来我们就来学习一下Linux内核在使用设备树之前已以及使用设备树之后是如何判断是否支持某款处理器的。 1、使用设备树之前的校验方法 在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值,machine id可以认为就是一个机器ID编码,告诉Linux内核自己是个什么硬件平台,看看Linux内核是否支持。Linux内核是支持很多硬件平台的,但是针对每一个特定的板子,Linux内核都用MACHINE_START和MACHINE_END来定义一个machine_desc结构体来描述这个硬件平台,比如在文件arch/arm/mach-imx/mach-mx35_3ds.c中有如下定义: - 示例代码20.3.5.2 MX35_3DS设备
- 613 MACHINE_START(MX35_3DS,"Freescale MX35PDK")
- 614 /* Maintainer: Freescale Semiconductor, Inc */
- 615 .atag_offset =0x100,
- 616 .map_io =mx35_map_io,
- 617 .init_early =imx35_init_early,
- 618 .init_irq =mx35_init_irq,
- 619 .init_time = mx35pdk_timer_init,
- 620 .init_machine =mx35_3ds_init,
- 621 .reserve =mx35_3ds_reserve,
- 622 .restart = mxc_restart,
- 623 MACHINE_END
复制代码上述代码就是定义了“Freescale MX35PDK”这个硬件平台,其中MACHINE_START和MACHINE_END定义在文件arch/arm/include/asm/mach/arch.h中,内容如下: - 示例代码20.3.5.3 MACHINE_START和MACHINE_END宏定义
- #define MACHINE_START(_type,_name) \
- static const struct machine_desc __mach_desc_##_type \
- __used \
- __attribute__((__section__(".arch.info.init")))= { \
- .nr = MACH_TYPE_##_type, \
- .name = _name,
- #define MACHINE_END \
- };
复制代码根据MACHINE_START和MACHINE_END的宏定义,将示例代码20.3.5.3展开后如下所示: - 示例代码20.3.5.4 展开以后
- 1 staticconst structmachine_desc __mach_desc_MX35_3DS \
- 2 __used \
- 3 __attribute__((__section__(".arch.info.init")))= {
- 4 .nr =MACH_TYPE_MX35_3DS,
- 5 .name ="Freescale MX35PDK",
- 6 /* Maintainer: Freescale Semiconductor, Inc*/
- 7 .atag_offset= 0x100,
- 8 .map_io =mx35_map_io,
- 9 .init_early= imx35_init_early,
- 10 .init_irq= mx35_init_irq,
- 11 .init_time =mx35pdk_timer_init,
- 12 .init_machine= mx35_3ds_init,
- 13 .reserve= mx35_3ds_reserve,
- 14 .restart =mxc_restart,
- 15 };
复制代码从示例代码20.3.5.4中可以看出,这里定义了一个machine_desc类型的结构体变量__mach_desc_MX35_3DS,这个变量存储在“.arch.info.init”段中。第4行的MACH_TYPE_MX35_3DS就是“Freescale MX35PDK”这个板子的machine id。MACH_TYPE_MX35_3DS定义在文件include/generated/mach-types.h中,此文件定义了大量的machine id,内容如下所示: - 示例代码20.3.5.5 mach-types.h文件中的machine id
- 15 #define MACH_TYPE_EBSA110 0
- 16 #define MACH_TYPE_RISCPC 1
- 17 #define MACH_TYPE_EBSA285 4
- 18 #define MACH_TYPE_NETWINDER 5
- 19 #define MACH_TYPE_CATS 6
- 20 #define MACH_TYPE_SHARK 15
- 21 #define MACH_TYPE_BRUTUS 16
- 22 #define MACH_TYPE_PERSONAL_SERVER 17
- ......
- 287 #define MACH_TYPE_MX35_3DS 1645
- ......
- 1000 #define MACH_TYPE_PFLA03 4575
复制代码第287行就是MACH_TYPE_MX35_3DS的值,为1645。 前面说了,uboot会给Linux内核传递machine id这个参数,Linux内核会检查这个machine id,其实就是将machine id与示例代码20.3.5.5中的这些MACH_TYPE_XXX宏进行对比,看看有没有相等的,如果相等的话就表示Linux内核支持这个硬件平台,如果不支持的话就没法启动Linux内核。 2、使用设备树以后的设备匹配方法 当Linux内核引入设备树以后就不再使用MACHINE_START了,而是换为了DT_MACHINE_START。DT_MACHINE_START也定义在文件arch/arm/include/asm/mach/arch.h里面,定义如下: - 示例代码20.3.5.6 DT_MACHINE_START宏
- #define DT_MACHINE_START(_name, _namestr) \
- static const struct machine_desc __mach_desc_##_name \
- __used \
- __attribute__((__section__(".arch.info.init")))= { \
- .nr = ~0, \
- .name =_namestr,
复制代码可以看出,DT_MACHINE_START和MACHINE_START基本相同,只是.nr的设置不同,在DT_MACHINE_START里面直接将.nr设置为~0。说明引入设备树以后不会再根据machine id来检查Linux内核是否支持某个硬件平台了。 打开文件arch/arm/mach-zynq/common.c,有如下所示内容: - 示例代码20.3.5.7arch/arm/mach-zynq/common.c
- 191 staticconst char* constzynq_dt_match[] ={
- 192 "xlnx,zynq-7000",
- 193 NULL
- 194 };
- 195
- 196 DT_MACHINE_START(XILINX_EP107,"Xilinx Zynq Platform")
- 197 /* 64KB way size, 8-way associativity, parity disabled */
- 198 #ifdef CONFIG_XILINX_PREFETCH
- 199 .l2c_aux_val =0x30400000,
- 200 .l2c_aux_mask = 0xcfbfffff,
- 201 #else
- 202 .l2c_aux_val =0x00400000,
- 203 .l2c_aux_mask = 0xffbfffff,
- 204 #endif
- 205 .smp =smp_ops(zynq_smp_ops),
- 206 .map_io =zynq_map_io,
- 207 .init_irq =zynq_irq_init,
- 208 .init_machine =zynq_init_machine,
- 209 .init_late =zynq_init_late,
- 210 .init_time =zynq_timer_init,
- 211 .dt_compat =zynq_dt_match,
- 212 .reserve =zynq_memory_init,
- 213 MACHINE_END
复制代码machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本硬件平台的兼容属性,示例代码20.3.5.7中设置.dt_compat = zynq_dt_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中,函数内容如下(有缩减): - 示例代码20.3.5.8 setup_arch函数内容
- 913 void__init setup_arch(char**cmdline_p)
- 914 {
- 915 const structmachine_desc *mdesc;
- 916
- 917 setup_processor();
- 918 mdesc =setup_machine_fdt(__atags_pointer);
- 919 if (!mdesc)
- 920 mdesc= setup_machine_tags(__atags_pointer, __machine_arch_type);
- 921 machine_desc = mdesc;
- 922 machine_name = mdesc->name;
- ......
- 986 }
复制代码第918行,调用setup_machine_fdt函数来获取匹配的machine_desc,参数就是atags的首地址,也就是uboot传递给Linux内核的dtb文件首地址,setup_machine_fdt函数的返回值就是找到的已经匹配成功的machine_desc。 函数setup_machine_fdt定义在文件arch/arm/kernel/devtree.c中,内容如下(有缩减): - 示例代码20.3.5.9 setup_machine_fdt函数内容
- 204 const struct machine_desc* __init setup_machine_fdt(unsigned int dt_phys)
- 205 {
- 206 const structmachine_desc *mdesc,*mdesc_best =NULL;
- ......
- 214
- 215 if (!dt_phys|| !early_init_dt_verify(phys_to_virt(dt_phys)))
- 216 return NULL;
- 217
- 218 mdesc = of_flat_dt_match_machine(mdesc_best,arch_get_next_mach);
- 219
- ......
- 247 __machine_arch_type = mdesc->nr;
- 248
- 249 return mdesc;
- 250 }
复制代码 第218行,调用函数of_flat_dt_match_machine来获取匹配的machine_desc,参数mdesc_best是默认的machine_desc,参数arch_get_next_mach是个函数,此函数定义在arch/arm/kernel/devtree.c文件中。找到匹配的machine_desc的过程就是用设备树根节点的compatible属性值和Linux内核中保存的所有的machine_desc结构体的.dt_compat中的值比较,看看那个相等,如果相等的话就表示找到匹配的machine_desc,arch_get_next_mach函数的工作就是获取Linux内核中下一个machine_desc结构体。 最后在来看一下of_flat_dt_match_machine函数,此函数定义在文件drivers/of/fdt.c中,内容如下(有缩减): - 示例代码20.3.5.10 of_flat_dt_match_machine函数内容
- 705 constvoid *__init of_flat_dt_match_machine(constvoid *default_match,
- 706 const void* (*get_next_compat)(constchar *const**))
- 707 {
- 708 const void*data =NULL;
- 709 const void*best_data =default_match;
- 710 const char*const *compat;
- 711 unsigned longdt_root;
- 712 unsigned intbest_score = ~1,score = 0;
- 713
- 714 dt_root =of_get_flat_dt_root();
- 715 while ((data= get_next_compat(&compat))){
- 716 score =of_flat_dt_match(dt_root,compat);
- 717 if (score> 0&& score <best_score) {
- 718 best_data =data;
- 719 best_score =score;
- 720 }
- 721 }
- ......
- 739
- 740 pr_info("Machinemodel: %s\n",of_flat_dt_get_machine_name());
- 741
- 742 return best_data;
- 743 }
复制代码第714行,通过函数of_get_flat_dt_root获取设备树根节点。 第715~720行,此循环就是查找匹配的machine_desc过程,第716行的of_flat_dt_match函数会将根节点compatible属性的值和每个machine_desc结构体中.dt_compat的值进行比较,直至找到匹配的那个machine_desc。 总结一下,Linux内核通过根节点compatible属性找到对应的machine_desc结构体的函数调用过程,如下图所示: 图 20.3.4 查找匹配machine_desc的过程 1.3.6 引用节点前面说到节点的命名格式如下所示: - [label:]node-name[@unit-address]
复制代码也多次给大家提到“label”字段,引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,例如下面这个模板: - 示例代码20.3.6.1 设备树模板
- 1 /{
- 2 aliases {
- 3 can0 =&flexcan1;
- 4 };
- 5
- 6 cpus{
- 7 #address-cells= <1>;
- 8 #size-cells= <0>;
- 9
- 10 cpu0:cpu@0 {
- 11 compatible ="arm,cortex-a7";
- 12 device_type ="cpu";
- 13 reg =<0>;
- 14 };
- 15 };
- 16
- 17 intc:interrupt-controller@00a01000{
- 18 compatible ="arm,cortex-a7-gic";
- 19 #interrupt-cells= <3>;
- 20 interrupt-controller;
- 21 reg =<0x00a01000 0x1000>,
- 22 <0x00a020000x100>;
- 23 };
- 24 };
复制代码通过&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 MPSoC的i2c0总线下的。那么现在要把这两个设备添加到i2c0总线下,打开zynq-7000.dtsi文件,可以看到PS的两组i2c控制器节点定义,如下所示: - 示例代码20.3.7.1 zynqmp.dtsi i2c节点
- 661 i2c0:i2c@ff020000 {
- 662 compatible = "cdns,i2c-r1p14","cdns,i2c-r1p10";
- 663 status = "disabled";
- 664 interrupt-parent =<&gic>;
- 665 interrupts = <017 4>;
- 666 reg = < 0x00xff020000 0x0 0x1000>;
- 667 #address-cells =<1>;
- 668 #size-cells =<0>;
- 669 power-domains= <&zynqmp_firmware 37>;
- 670 };
- 671
- 672 i2c1:i2c@ff030000 {
- 673 compatible = "cdns,i2c-r1p14","cdns,i2c-r1p10";
- 674 status = "disabled";
- 675 interrupt-parent =<&gic>;
- 676 interrupts = <018 4>;
- 677 reg = <0x00xff030000 0x0 0x1000>;
- 678 #address-cells =<1>;
- 679 #size-cells =<0>;
- 680 power-domains= <&zynqmp_firmware 38>;
- 681 };
复制代码因为现在要把开发板的两个i2c器件添加到i2c0总线下,直接在i2c0节点下创建两个子节点即可,一个子节点对应的是eeprom,另一个子节点对应的是rtc,那么最简单的方法就是直接在zynqmp.dtsi文件的i2c0节点中添加这两个节点子节点即可,如下所示: - 示例代码20.3.7.2 zynqmp.dtsi 添加i2c器件
- 122 i2c0:i2c@ff020000 {
- 123 compatible = "cdns,i2c-r1p10";
- 124 status = "disabled";
- 125 clocks = <&clkc38>;
- 126 interrupt-parent =<&intc>;
- 127 interrupts = <025 4>;
- 128 reg = <0xe00040000x1000>;
- 129 #address-cells =<1>;
- 130 #size-cells =<0>;
- 131
- 132 24c64@50 {
- 133 compatible = "atmel,24c64";
- 134 reg = <0x50>;
- 135 pagesize = <32>;
- 136 };
- 137
- 138 rtc@51 {
- 139 compatible = "nxp,pcf8563";
- 140 reg = <0x51>;
- 141 };
- 142 };
- 143
- 144 i2c1:i2c@ff030000 {
- 145 compatible = "cdns,i2c-r1p10";
- 146 status = "disabled";
- 147 clocks = <&clkc39>;
- 148 interrupt-parent =<&intc>;
- 149 interrupts = <048 4>;
- 150 reg = <0xe00050000x1000>;
- 151 #address-cells =<1>;
- 152 #size-cells =<0>;
- 153 };
复制代码第132~136行就是在i2c0总线下添加了eeprom设备,138~141行添加了rtc设备(注意:我这里只是给大家做演示,你们不要去改这个文件);但是这样会有个问题,i2c0节点是定义在zynq-7000.dtsi文件中的,而zynq-7000.dtsi是设备树头文件,前面也跟大家说到过,该文件是zynq-7000系列处理器的一个通用设备树头文件,也就是说它是会被其他dts文件所包含的,直接在i2c0节点中添加这两个子节点就相当于在所有的zynq-7000系列处理器开发板上都添加了这两个设备,如果其他的板子并没有这两个设备呢!因此,按照示例代码24.3.12这样写肯定是不行的。 这里就要引入另外一个内容,那就是向节点追加数据,我们现在要解决的就是如何向i2c0节点追加两个子节点,而且不能影响到其它使用zynq-7000系列处理器的开发板。在本篇中我们使用的设备树文件为system-top.dts,因此我们需要在system-top.dts文件中完成数据追加的内容,方式如下: - 示例代码20.3.7.3 节点追加数据方法
- 1 &i2c0{
- 2 /*要追加或修改的内容 */
- 3 };
复制代码第1行,&i2c0表示要引用到i2c0这个label所对应的节点,也就是zynq-7000.dtsi文件中的“i2c0: i2c@e0004000”。 第2行,花括号内就是要向i2c0这个节点添加的内容,包括修改某些属性的值。 打开system-top.dts,这样我们就可以直接在该文件中追加内容了: - 示例代码20.3.7.4 system-top.dts 向i2c0节点追加内容
- 8/dts-v1/;
- 9#include "zynq-7000.dtsi"
- 10#include "pl.dtsi"
- 11#include "pcw.dtsi"
- 12/ {
- 13 model ="Alientek ZYNQ Development Board";
- 14
- 15 chosen {
- 16 bootargs ="console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rwrootwait";
- 17 stdout-path= "serial0:115200n8";
- 18 };
- 19 aliases {
- 20 ethernet0 =&gem0;
- 21 i2c0 =&i2c_2;
- 22 i2c1 =&i2c0;
- 23 i2c2 =&i2c1;
- 24 serial0 =&uart0;
- 25 serial1 =&uart1;
- 26 spi0 =&qspi;
- 27 };
- 28 memory {
- 29 device_type ="memory";
- 30 reg =<0x0 0x20000000>;
- 31 };
- 32};
- 33
- 34&i2c0 {
- 35 clock-frequency= <100000>;
- 36 status ="okay";
- 37
- 38 24c64@50{
- 39 compatible ="atmel,24c64";
- 40 reg =<0x50>;
- 41 pagesize =<32>;
- 42 };
- 43
- 44 rtc@51 {
- 45 compatible ="nxp,pcf8563";
- 46 reg =<0x51>;
- 47 };
- 48};
- 49
- 50&gem0 {
- 51 local-mac-address= [000a 35 001e 53];
- 52};
复制代码第34~48行就是向i2c0节点添加/修改数据,比如35的属性“clock-frequency= <100000>”就表示将i2c0的时钟设置为100KHz,“clock-frequency”就是新添加的属性。 第36行,将status属性的值由原来的disabled改为okay,这是修改节点的属性值。 第38~47行,我们向i2c0子节点追加了两个子节点,“24c64@50”和“rtc@51”。 除此之外,第12~32行,其实就是向zynq-7000.dtsi中定义的根节点中追加了一些节点。 注意,这里只是给大家演示,大家不要去修改这些文件,后面用到的时候我会再说!!! 因为示例代码24.3.14中的内容是system-top.dts这个文件内的,所以不会对使用ZYNQ-7000系列处理器的其它板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。例如在pcw.dtsi文件中,可以看到很多的节点引用、向节点追加内容、修改节点内容的示例,如下所示: 1.3.8 特殊节点在根节点“/”中有那么几个特殊的子节点:aliases、chosen以及memory,我们接下来看一下这三个比较特殊的节点,我们会发现这三个节点都是没有compatible属性,也就是说它们对应的并不是一个真实的设备。 1、aliases节点 打开system-top.dts文件,可以看到aliases节点的内容如下所示: - 示例代码20.3.8.1 system-top.dts aliases节点
- aliases {
- ethernet0 = &gem0;
- ethernet1 = &gem3;
- i2c0 = &hdmi_ddc;
- i2c1 = &i2c0;
- i2c2 = &i2c1;
- i2c3 = &sensor_iic;
- serial0 = &uart0;
- serial1 = &uart1;
- spi0 = &qspi;
- };
- 19 aliases {
- 20 ethernet0 =&gem0;
- 21 i2c0 =&i2c_2;
- 22 i2c1 =&i2c0;
- 23 i2c2 =&i2c1;
- 24 serial0 =&uart0;
- 25 serial1 =&uart1;
- 26 spi0 =&qspi;
- 27};
复制代码单词aliases的意思是“别名”,因此aliases节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。但是需要注意的是,这里说的方便访问节点并不是在设备树中访问节点,例如前面说到的使用“&label”的方式访问设备树中的节点,而是内核当中方便定位节点,例如在内核中通过ethernet0就可以定位到gem0节点(&gem0引用的节点),再例如内核通过serial0就可以找到uart0节点。 2、chosen节点 chosen节点一般会有两个属性,“bootargs”和“stdout-path”。打开system-top.dts文件,找到chosen节点,内容如下所示: - 示例代码20.3.8.2 chosen节点
- 15chosen {
- 16 bootargs ="console=ttyPS0,115200 earlyprintk root=/dev/mmcblk0p2 rwrootwait";
- 17 stdout-path= "serial0:115200n8";
- 18};
复制代码 在chosen节点当中,属性stdout-path = “serial0:115200n8”,表示标准输出设备使用串口serial0,在system-top.dts文件当中,serial0其实是一个别名,指向的就是uart0;“115200”则表示串口的波特率为115200,“n”表示无校验位,“8”则表示有8位数据位,相信大家都明白这些是什么意思。 当你看到chosen节点中的bootargs属性的时候有没有想到U-Boot的bootargs环境变量呢?内核的bootargs参数不是由U-Boot传给它的吗?为什么要在内核设备树根节点下的chosen节点中定义呢?他们俩有什么区别呢?那么关于这些问题稍后再给大家解释,这里大家想思考另一个问题:“stdout-path”属性指定了标准输出设备,而bootargs参数当中也指定了标准输出设备(console=ttyPS0,115200,ttyPS0其实指的就是根文件系统下的/dev/ttyPS0这个设备文件,那么它对应的硬件设备其实就是板子的uart0),那么内核在初始化标准输出设备的时候到底听谁的呢?关于这个问题,笔者开始也想不明白,于是乎去内核源码中找了找,在内核源码drivers/of/base.c文件中看到了下面这段代码: - 示例代码20.3.8.3 of_console_check函数
- 1822/**
- 1823* of_console_check() - Test and setup console for DT setup
- 1824* @dn - Pointer to device node
- 1825* @name - Name to use for preferred console without index. ex."ttyS"
- 1826* @Index - Index to use for preferred console.
- 1827*
- 1828* Check if the given device node matches the stdout-path property in the
- 1829* /chosen node. If it does then register it as the preferred console and return
- 1830* TRUE. Otherwise return FALSE.
- 1831*/
- 1832bool of_console_check(structdevice_node *dn,char *name,int index)
- 1833{
- 1834 if(!dn ||dn != of_stdout ||console_set_on_cmdline)
- 1835 returnfalse;
- 1836
- 1837 /*
- 1838 * XXX:cast `options' to char pointer to suppress complication
- 1839 *warnings: printk, UART and console drivers expect char pointer.
- 1840 */
- 1841 return!add_preferred_console(name,index, (char*)of_stdout_options);
- 1842}
复制代码看这个函数的名字“of_console_check”,意思是控制台校验(控制台大家可以理解为linux的标准输入、输入终端),第1834行当中的of_stdout其实是内核解析stdout-path = “serial0:115200n8”时得到的serial0指向的设备节点,也就是我们的串口0,;而console_set_on_cmdline是一个int类型的变量,如果bootargs字符串当中指定了console=xxxxx,那么内核也会解析到,并且将console_set_on_cmdline变量设置为1;所以根据代码中的第1834行以及函数定义前面的注释信息,我的猜想如下: 在of_console_check函数中会判断设备树stdout-path属性是否定义了,如果定义了则它拥有优先级。 当然这是我的猜测,我并没有去验证,不想花这个时间去研究了,如果大家有时间可以去找找看,这里就不说这个问题了。 现在给大家解释前面说到的那些问题:内核的bootargs参数不是由U-Boot传给它的吗?为什么还要在内核设备树根节点下的chosen节点中定义bootargs呢?他们俩有什么区别呢?下面给大家一一解释一下。 前面讲解uboot的时候说过,uboot在启动Linux内核的时候会将bootargs的值传递给Linux内核,bootargs会作为Linux内核的命令行参数,Linux内核启动的时候会打印出命令行参数(也就是uboot传递进来的bootargs的值),如所示: 但是我们使用的这个U-Boot,它的环境变量当中并没有定义bootargs变量,大家可以进入U-Boot命令行,通过print命令打印出所有的环境变量,你会发现并没有定义bootargs,那这跟我们前面说的不相符了呀,而事实并不如此。 在uboot源码中全局搜索“chosen”这个字符串,看看能不能找到一些蛛丝马迹,果然在U-Boot源码目录的common/fdt_support.c文件中有个fdt_chosen函数,此函数内容如下所示: - 示例代码20.3.8.4 uboot源码中的fdt_chosen函数
- 275 intfdt_chosen(void*fdt)
- 276 {
- 277 int nodeoffset;
- 278 int err;
- 279 char *str; /* used to set string properties */
- 280
- 281 err =fdt_check_header(fdt);
- 282 if (err< 0){
- 283 printf("fdt_chosen:%s\n",fdt_strerror(err));
- 284 return err;
- 285 }
- 286
- 287 /* find or create "/chosen" node. */
- 288 nodeoffset =fdt_find_or_add_subnode(fdt,0, "chosen");
- 289 if (nodeoffset< 0)
- 290 return nodeoffset;
- 291
- 292 str = getenv("bootargs");
- 293 if (str){
- 294 err =fdt_setprop(fdt,nodeoffset, "bootargs",str,
- 295 strlen(str)+ 1);
- 296 if (err< 0){
- 297 printf("WARNING:could not set bootargs %s.\n",
- 298 fdt_strerror(err));
- 299 returnerr;
- 300 }
- 301 }
- 302
- 303 return fdt_fixup_stdout(fdt,nodeoffset);
- 304 }
复制代码 第288行,调用函数fdt_find_or_add_subnode从内核设备树(.dtb,因为此时内核dtb文件已经被拷贝到DDR中了)中找到chosen节点,如果没有找到的话就会自己创建一个chosen节点。 第292行,读取uboot中bootargs环境变量的内容。 第293行,判断如果读取bootargs环境变量成功,则执行if { }中的代码。 第294行,调用函数fdt_setprop向内核设备的chosen节点添加bootargs属性,并且bootargs属性的值就是环境变量bootargs的内容。(因为此时内核dtb文件已经被拷贝到DDR中了,U-Boot可以通过内核设备树dtb的起始地址对dtb数据进行修改)。 所以从上面这段代码可以看出来,如果U-Boot定义了bootargs环境变量,则会通过fdt_setprop函数在内核设备树的chosen节点追加bootargs属性,它的值就是U-Boot环境变量bootargs的值,如果是这样,那么内核设备树chosen节点的bootargs属性就会被修改。但是对于我们使用这个U-Boot来说,它并没有定义bootargs环境变量,所以使用的就是内核设备树chosen节点下的bootargs属性,也就是说U-Boot的环境变量bootargs拥有最高的优先级。 接下来我们顺着fdt_chosen函数一点点的抽丝剥茧,看看都有哪些函数调用了fdt_chosen,一直找到最终的源头。这里我就不卖关子了,直接告诉大家整个流程是怎么样的,见图 20.3.7: 图 20.3.7 fdt_chosen函数调用流程 图 20.3.7中框起来的部分就是函数do_bootm_linux函数的执行流程,也就是说do_bootm_linux函数会通过一系列复杂的调用,最终通过fdt_chosen函数在内核设备树chosen节点中添加bootargs属性。而U-Boot的bootcmd命令最终会执行bootz命令,而bootz命令启动Linux内核的时候会运行do_bootm_linux函数,至此,真相大白! 3、memory节点 memory节点看名字就知道跟内存是有关系的,如下所示: - 示例代码20.3.8.5 memory节点
- 28memory {
- 29 device_type ="memory";
- 30 reg =<0x0 0x20000000>;
- 31};
复制代码memory节点描述了系统内存的基地址以及系统内存大小,“reg = <0x00x20000000>”就表示系统内存的起始地址为0x0,大小为0x20000000,也就是512MB,该节点一般只有这两个属性,device_type属性的值固定为“memory”。 |