当前位置: 首页 > news >正文

Linux设备树简介

1.什么是设备树

        设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device

Tree Source),这个DTS文件采用树形结构描述板级设备,也就是开发板上的设备信息,比如

CPU数量、 内存基地址、IIC接口上接了哪些设备、SPI接口上接了哪些设备等等。如图:

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

        以前的Linux内核中ARM架构并没有采用设备树。在没有设备树的时候Linux是如何描述ARM架构中的板级信息呢?在Linux内核源码中大量的arch/arm/mach-xxx和arch/arm/plat-xxx文件夹,这些文件夹里面的文件就是对应平台下的板级信息。

static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = {.lcdcon5    = S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_INVVLINE | S3C2410_LCDCON5_INVVFRAME | S3C2410_LCDCON5_PWREN | S3C2410_LCDCON5_HWSWP, 
};static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = {.displays   = &smdk2440_lcd_cfg, .num_displays   = 1, .default_display = 0, 
};static struct platform_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之父linus看到ARM社区向Linux内核添加了大量“无用”、冗余

的板级信息文件,不禁的发出了一句“This whole ARM thing is a f*cking pain in the ass”。从此以

后ARM社区就引入了PowerPC等架构已经采用的设备树(Flattened Device Tree),将这些描述

板级硬件信息的内容都从Linux内中分离开来,用一个专属的文件格式来描述,这个专属的文

件就叫做设备树,文件扩展名为.dts。一个SOC可以作出很多不同的板子,这些不同的板子肯

定是有共同的信息,将这些共同的信息提取出来作为一个通用的文件,其他的.dts文件直接引

用这个通用文件即可,这个通用文件就是.dtsi文件,类似于C语言中的头文件。一般.dts描述

板级信息(也就是开发板上有哪些IIC设备、SPI设备等),.dtsi描述SOC级信息(也就是SOC有

几个CPU、主频是多少、各个外设控制器信息等)。

        这个就是设备树的由来,简而言之就是,Linux内核中ARM架构下有太多的冗余的垃圾板

级信息文件,导致linus震怒,然后ARM社区引入了设备树。

2.DTS、DTB和DTC

        上一小节说了,设备树源文件扩展名为.dts,但是我们在前面移植Linux的时候却一直在使

用.dtb文件,那么DTS和DTB这两个文件是什么关系呢?DTS是设备树源码文件,DTB是将

DTS编译以后得到的二进制文件。将.c文件编译为.o需要用到gcc编译器,那么将.dts编译为.dtb

需要什么工具呢?需要用到DTC 工具!DTC工具源码在Linux内核的 scripts/dtc目录下,

scripts/dtc/Makefile文件内容如下:

 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”命令。

        基于ARM架构的SOC有很多种,一种SOC又可以制作出很多款板子,每个板子都有一

个对应的DTS文件,那么如何确定编译哪一个DTS文件呢?我们就以I.MX6ULL这款芯片对

应的板子为例来看一下,打开arch/arm/boot/dts/Makefile,有如下内容:

dtb-$(CONFIG_SOC_IMX6UL) += \imx6ul-14x14-ddr3-arm2.dtb \imx6ul-14x14-ddr3-arm2-emmc.dtb \......dtb-$(CONFIG_SOC_IMX6ULL) += \imx6ull-14x14-ddr3-arm2.dtb \imx6ull-14x14-ddr3-arm2-adc.dtb \imx6ull-14x14-ddr3-arm2-cs42888.dtb \imx6ull-14x14-ddr3-arm2-ecspi.dtb \imx6ull-14x14-ddr3-arm2-emmc.dtb \imx6ull-14x14-ddr3-arm2-epdc.dtb \imx6ull-14x14-ddr3-arm2-flexcan2.dtb \imx6ull-14x14-ddr3-arm2-gpmi-weim.dtb \imx6ull-14x14-ddr3-arm2-lcdif.dtb \imx6ull-14x14-ddr3-arm2-ldo.dtb \imx6ull-14x14-ddr3-arm2-qspi.dtb \imx6ull-14x14-ddr3-arm2-qspi-all.dtb \imx6ull-14x14-ddr3-arm2-tsc.dtb \imx6ull-14x14-ddr3-arm2-uart2.dtb \imx6ull-14x14-ddr3-arm2-usb.dtb \imx6ull-14x14-ddr3-arm2-wm8958.dtb \imx6ull-14x14-evk.dtb \imx6ull-14x14-evk-btwifi.dtb \imx6ull-14x14-evk-emmc.dtb \imx6ull-14x14-evk-gpmi-weim.dtb \imx6ull-14x14-evk-usb-certi.dtb \imx6ull-alientek-emmc.dtb \imx6ull-alientek-nand.dtb \imx6ull-9x9-evk.dtb \imx6ull-9x9-evk-btwifi.dtb \imx6ull-9x9-evk-ldo.dtbdtb-$(CONFIG_SOC_IMX6SLL) += \imx6sll-lpddr2-arm2.dtb \imx6sll-lpddr3-arm2.dtb \......

        可以看出,当选中I.MX6ULL这个SOC以后(CONFIG_SOC_IMX6ULL=y),所有使用到

I.MX6ULL这个SOC的板子对应的.dts文件都会被编译为.dtb。如果我们使用I.MX6ULL新做

了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-

$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts编译为二进制的.dtb

文件。

        示例代码就是我们在给正点原子的I.MX6U-ALPHA开发板移植Linux系统的时候添加的设备树。关于.dtb文件怎么使用这里就不多说了,前面讲解Uboot移植、Linux内核移植的时候已经无数次的提到如何使用.dtb文件了(uboot中使用bootz或bootm命令向Linux内核传递二进制设备树文件(.dtb))。

3.DTS语法

        虽然我们基本上不会从头到尾重写一个.dts文件,大多时候是直接在SOC厂商提供的.dts

文件上进行修改。但是DTS文件语法我们还是需要详细的学习一遍,因为我们肯定需要修改.dts

文件。大家不要看到要学习新的语法就觉得会很复杂,DTS语法非常的人性化,是一种ASCII

文本文件,不管是阅读还是修改都很方便。

        本节我们就以imx6ull-alientek-emmc.dts这个文件为例来讲解一下DTS语法。关于设备树

详 细 的 语 法 规 则 请 参 考 《 DevicetreeSpecificationV0.2.pdf 》 和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档,此两份文档已经放到了开发板光盘中,

路 径为: 4 、参考资 料 ->Devicetree SpecificationV0.2.pdf 、 4 、参 考资料 ->

Power_ePAPR_APPROVED_v1.12.pdf

3.1 .dtsi头文件

        和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在imx6ull-alientek-

emmc.dts中有如下所示内容:

 #include <dt-bindings/input/input.h> #include "imx6ull.dtsi"

        在.dts设备树文件中,可以通过“#include”来引用.h、.dtsi和.dts文件。只是,我们在编写设备树头文件的时候最好选择.dtsi后缀。

        一般.dtsi文件用于描述SOC的内部外设信息,比如CPU架构、主频、外设寄存器地址范

围,比如UART、IIC等等。比如imx6ull.dtsi就是描述I.MX6ULL这颗SOC内部外设情况信息

的,内容如下:

#include <dt-bindings/clock/imx6ul-clock.h>
#include <dt-bindings/gpio/gpio.h>
#include <dt-bindings/interrupt-controller/arm-gic.h>
#include "imx6ull-pinfunc.h"
#include "imx6ull-pinfunc-snvs.h"
#include "skeleton.dtsi"/ {aliases {can0 = &flexcan1;......};cpus {#address-cells = <1>;#size-cells = <0>;cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";......};};intc: interrupt-controller@00a01000 {compatible = "arm,cortex-a7-gic";#interrupt-cells = <3>;interrupt-controller;reg = <0x00a01000 0x1000>,<0x00a02000 0x100>;};clocks {#address-cells = <1>;#size-cells = <0>;ckil: clock@0 {compatible = "fixed-clock";reg = <0>;#clock-cells = <0>;clock-frequency = <32768>;clock-output-names = "ckil";};......};soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";interrupt-parent = <&gpc>;ranges;busfreq {compatible = "fsl,imx_busfreq";......};gpmi: gpmi-nand@01806000{compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";#address-cells = <1>;#size-cells = <1>;reg = <0x01806000 0x2000>, <0x01808000 0x4000>;......};......};
};

        示例代码中第 54~89 行就是 cpu0 这个设备节点信息,这个节点信息描述了I.MX6ULL这颗SOC所使用的CPU信息,比如架构是cortex-A7,频率支持996MHz、792MHz、528MHz、396MHz和198MHz等等。在imx6ull.dtsi文件中不仅仅描述了cpu0这一个节点信息,I.MX6ULL这颗SOC所有的外设都描述的清清楚楚,比如ecspi1~4、uart1~8、usbphy1~2、i2c1~4

等等,关于这些设备节点信息的具体内容我们稍后在详细的讲解。

3.2设备节点

        设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设

备节点,每个节点都通过一些属性信息来描述节点信息,属性就是键—值对。以下是从

imx6ull.dtsi文件中缩减出来的设备树文件内容:

/ {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>;};
}

        第1行,“/”是根节点,每个设备树文件只有一个根节点。细心的同学应该会发现,imx6ull.dtsi

和imx6ull-alientek-emmc.dts这两个文件都有一个“/”根节点,这样不会出错吗?不会的,因为

这两个“/”根节点的内容会合并成一个根节点。

        第2、6和17行,aliases、cpus和intc是三个子节点,在设备树中节点命名格式如下:

node-name@unit-address

        其中“node-name”是节点名字,为ASCII字符串,节点名字应该能够清晰的描述出节点的

功能,比如“uart1”就表示这个节点是UART1外设。“unit-address”一般表示设备的地址或寄

存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、

“interrupt-controller@00a01000”。

        但是我们在示例代码中我们看到的节点命名却如下所示:

cpu0:cpu@0 label: node-name@unit-address 

        引入label的目的就是为了方便访问节点,可以直接通过&label来访问这个节点,比如通过

&cpu0就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “intc:

interrupt-controller@00a01000”,节点label 是intc,而节点名字就很长了,为“interrupt-

controller@00a01000”。很明显通过&intc来访问“interrupt-controller@00a01000”这个节点要方

便很多!

        第10行,cpu0也是一个节点,只是cpu0是cpus的子节点。

        每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任

意的字节流。设备树源码中常用的几种数据形式如下所示:

1.字符串
compatible = "arm,cortex-a7"; 2.32位无符号整数
reg = <0>; 
上述代码设置reg属性的值为0,reg的值也可以设置为一组值,比如:
reg = <0 0x123456 100>; 3.字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示: 
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand"; 
上述代码设置属性compatible的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。

3.3 标准属性

        节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以

自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux下的很多外设驱动都会使用

这些标准属性,本节我们就来学习一下几个常用的标准属性。

1、compatible属性

        compatible属性也叫做“兼容性”属性,这是非常重要的一个属性!compatible属性的值是

一个字符串列表,compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要

使用的驱动程序,compatible属性的值格式如下所示:

"manufacturer,model" 

        其中 manufacturer表示厂商,model一般是模块对应的驱动名字。比如 imx6ull-alientek-

emmc.dts中sound节点是I.MX6U-ALPHA开发板的音频设备节点,I.MX6U-ALPHA开发板上

的音频芯片采用的欧胜(WOLFSON)出品的WM8960,sound节点的compatible属性值如下:

compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960"; 

        属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”

表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。sound

这个设备首先使用第一个兼容值在Linux内核里面查找,看看能不能找到与之匹配的驱动文件,

如果没有找到的话就使用第二个兼容值查。

        一般驱动程序文件都会有一个OF匹配表,此OF匹配表保存着一些compatible值,如果设

备节点的compatible属性值和OF匹配表中的任何一个值相等,那么就表示设备可以使用这个

驱动。比如在文件imx-wm8960.c中有如下内容:

static const struct of_device_id imx_wm8960_dt_ids[] = {{ .compatible = "fsl,imx-audio-wm8960", },{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);static struct platform_driver imx_wm8960_driver = {.driver = {.name = "imx-wm8960",.pm = &snd_soc_pm_ops,.of_match_table = imx_wm8960_dt_ids,},.probe = imx_wm8960_probe,.remove = imx_wm8960_remove,
};

        数组imx_wm8960_dt_ids就是imx-wm8960.c这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的compatible属性值与此相等,那么这个节点就会使用此驱动文件。

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

2、model属性

        model属性值也是一个字符串,一般model属性描述设备模块信息,比如名字什么的,比

如:model = "wm8960-audio";

3.status属性

        status属性看名字就知道是和设备状态有关的,status属性值也是字符串,字符串是设备的

状态信息,

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

        这两个属性的值都是无符号32位整形,#address-cells和#size-cells这两个属性可以用在任

何拥有子节点的设备中,用于描述子节点的地址信息。#address-cells属性值决定了子节点reg属

性中地址信息所占用的字长(32位),#size-cells属性值决定了子节点reg属性中长度信息所占的

字长(32位)。#address-cells和#size-cells表明了子节点应该如何编写reg属性值,一般reg属性

都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度。

        每个“address length”组合表示一个地址范围,其中address是起始地址,length是地址长

度,#address-cells表明address这个数据所占用的字长,#size-cells表明length这个数据所占用

的字长,比如:

spi4 {compatible = "spi-gpio";#address-cells = <1>;#size-cells = <0>;gpio_spi: gpio_spi@0 {compatible = "fairchild,74hc595";reg = <0>;};
};aips3: aips-bus@02200000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;dcp: dcp@02280000 {compatible = "fsl,imx6sl-dcp";reg = <0x02280000 0x4000>;};
};

        节点spi4的#address-cells = <1>,#size-cells = <0>,说明spi4的子节点reg属

性中起始地址所占用的字长为1,地址长度所占用的字长为0。

        子节点gpio_spi: gpio_spi@0的reg 属性值为 <0>,因为父节点设置了#address-

cells = <1>,#size-cells = <0>,因此addres=0,没有length的值,相当于设置了起始地址,而没

有设置地址长度。

        设置aips3: aips-bus@02200000节点#address-cells = <1>,#size-cells = <1>,说明aips3: aips-bus@02200000节点起始地址长度所占用的字长为1,地址长度所占用的字长也为1。

        子节点dcp: dcp@02280000的reg属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>,#size-cells = <1>,address= 0x02280000,length= 0x4000,相当于设置了起始地址为0x02280000,地址长度为0x40000。

5.reg属性

        reg属性前面已经提到过了,reg属性的值一般是(address,length)对。reg属性一般用于描

述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在imx6ull.dtsi中有

如下内容:

uart1: serial@02020000 {compatible = "fsl,imx6ul-uart", "fsl,imx6q-uart", "fsl,imx21-uart";reg = <0x02020000 0x4000>;interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_UART1_IPG>, <&clks IMX6UL_CLK_UART1_SERIAL>;clock-names = "ipg", "per";status = "disabled";
};

        上述代码是节点uart1,uart1节点描述了I.MX6ULL的UART1相关信息,重点是第326行

的reg属性。其中uart1的父节点aips1: aips-bus@02000000设置了#address-cells = <1>、#size-

cells = <1>,因此reg属性中address=0x02020000,length=0x4000。查阅《I.MX6ULL参考手册》

可知,I.MX6ULL的UART1寄存器首地址为0x02020000,但是UART1的地址长度(范围)并没

有0x4000这么多,这里我们重点是获取UART1寄存器首地址。

6.ranges属性

        ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字

矩阵,ranges是一个地址映射/转换表,ranges属性每个项目由子地址、父地址和地址空间长度

这三部分组成:

child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址

所占用的字长。

parent-bus-address:父总线地址空间的物理地址,同样由父节点的#address-cells确定此物

理地址所占用的字长。

length:子地址空间的长度,由父节点的#size-cells确定此地址长度所占用的字长。

        如果ranges属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,

对于我们所使用的I.MX6ULL来说,子地址空间和父地址空间完全相同,因此会在imx6ull.dtsi

中找到大量的值为空的ranges属性,如下所示:

soc { #address-cells = <1>; #size-cells = <1>; compatible = "simple-bus"; interrupt-parent = <&gpc>; ranges; ...... } 

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

soc {compatible = "simple-bus";#address-cells = <1>;#size-cells = <1>;ranges = <0x0 0xe0000000 0x00100000>;serial {device_type = "serial";compatible = "ns16550";reg = <0x4600 0x100>;clock-frequency = <0>;interrupts = <0xA 0x8>;interrupt-parent = <&ipic>;};
};

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

        serial是串口设备节点,reg属性定义了serial设备寄存器的起始地址为0x4600,

寄存器长度为 0x100。经过地址转换,serial 设备可以从 0xe0004600 开始进行读写操作,

0xe0004600=0x4600+0xe0000000。

7、name属性

        name属性值为字符串,name属性用于记录节点名字,name属性已经被弃用,不推荐使用

name属性,一些老的设备树文件可能会使用此属性。

8、device_type属性

        device_type属性值为字符串,IEEE 1275会用到此属性,用于描述设备的FCode,但是设

备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。

imx6ull.dtsi的cpu0节点用到了此属性,内容如下所示:

cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;......
};

        关于标准属性就讲解这么多,其他的比如中断、IIC、SPI等使用的标准属性等到具体的例

程再讲解。

3.4 根节点compatible属性

        每个节点都有compatible属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts文件中根

节点的compatible属性内容如下所示:

/ { model = "Freescale i.MX6 ULL 14x14 EVK Board"; compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull"; ...... 
} 

        可以看出,compatible有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。前面我们说了,

设备节点的compatible属性值是为了匹配Linux内核中的驱动程序,那么根节点中的compatible

属性是为了做什么工作的? 通过根节点的compatible属性可以知道我们所使用的设备,一般第

一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二

个值描述了设备所使用的SOC,比如这里使用的是“imx6ull”这颗SOC。Linux内核会通过根

节点的compoatible属性查看是否支持此设备,如果支持的话设备就会启动Linux内核。接下来

我们就来学习一下Linux内核在使用设备树前后是如何判断是否支持某款设备的。

1.使用设备树之前设备匹配方法

        在没有使用设备树以前,uboot会向Linux内核传递一个叫做machine id的值,machine id

也就是设备ID,告诉Linux内核自己是个什么设备,看看Linux内核是否支持。Linux内核是

支持很多设备的,针对每一个设备(板子),Linux内核都用MACHINE_START和MACHINE_END

来定义一个 machine_desc 结构体来描述这个设备,比如在文件 arch/arm/mach-imx/mach-

mx35_3ds.c中有如下定义:

MACHINE_START(MX35_3DS, "Freescale MX35PDK")/* 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,
MACHINE_END

        上述代码就是定义了“Freescale MX35PDK”这个设备,其中 MACHINE_START 和

MACHINE_END定义在文件arch/arm/include/asm/mach/arch.h中,内容如下:

#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的宏定义,将示例代码43.3.4.2展开后如下所

示:

static const struct machine_desc __mach_desc_MX35_3DS \
__used \
__attribute__((__section__(".arch.info.init"))) = {.nr         = MACH_TYPE_MX35_3DS,.name       = "Freescale MX35PDK",/* 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,
};

        从示例代码 中可以看出,这里定义了一个 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,内容如下所示:

#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
......
#define MACH_TYPE_PFLA03                4575

        MACH_TYPE_MX35_3DS的值,为1645。

        前面说了,uboot会给Linux内核传递machine id这个参数,Linux内核会检查这个machine

id,其实就是将machine id与示例代码43.3.4.3中的这些MACH_TYPE_XXX宏进行对比,看

看有没有相等的,如果相等的话就表示Linux内核支持这个设备,如果不支持的话那么这个设

备就没法启动Linux内核。

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

        当 Linux 内核引入设备树以后就不再使用 MACHINE_START 了,而是换为了

DT_MACHINE_START。DT_MACHINE_START也定义在文件arch/arm/include/asm/mach/arch.h

里面,定义如下:

#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-imx/mach-imx6ul.c,有如下所示内容:

static const char *imx6ul_dt_compat[] __initconst = {"fsl,imx6ul","fsl,imx6ull",NULL,
};DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)").map_io     = imx6ul_map_io,.init_irq   = imx6ul_init_irq,.init_machine   = imx6ul_init_machine,.init_late  = imx6ul_init_late,.dt_compat  = imx6ul_dt_compat,
MACHINE_END

        machine_desc结构体中有个.dt_compat成员变量,此成员变量保存着本设备兼容属性,示

例代码中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat表里面有"fsl,imx6ul"

和"fsl,imx6ull"这两个兼容值。只要某个设备(板子)根节点“/”的 compatible 属性值与

imx6ul_dt_compat表中的任何一个值相等,那么就表示Linux内核支持此设备。imx6ull-alientek-

emmc.dts中根节点的compatible属性值如下:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull"; 

        其中“fsl,imx6ull”与imx6ul_dt_compat中的“fsl,imx6ull”匹配,因此I.MX6U-ALPHA开

发板可以正常启动Linux内核。如果将imx6ull-alientek-emmc.dts根节点的compatible属性改为

其他的值,比如:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ullll" 

        重新编译DTS,并用新的DTS启动Linux内核,结果如图所示的错误提示:

        当我们修改了根节点compatible属性内容以后,因为Linux内核找不到对应的设备,因此

Linux内核无法启动。在uboot输出Starting kernel...以后就再也没有其他信息输出了。

        接下来我们简单看一下Linux内核是如何根据设备树根节点的compatible属性来匹配出对

应的 machine_desc,Linux 内核调用 start_kernel 函数来启动内核,start_kernel 函数会调用

setup_arch函数来匹配machine_desc,setup_arch函数定义在文件arch/arm/kernel/setup.c中,函

数内容如下(有缩减):

void __init setup_arch(char **cmdline_p)
{const struct machine_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;......
}

        调用setup_machine_fdt函数来获取匹配的machine_desc,参数就是atags的首

地址,也就是uboot传递给Linux内核的dtb文件首地址,setup_machine_fdt函数的返回值就是

找到的最匹配的machine_desc。

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

const struct machine_desc * __init setup_machine_fdt(unsigned int dt_phys)
{const struct machine_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;return mdesc;
}

        调用函数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中,内

容如下(有缩减):

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 long dt_root;unsigned int best_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("Machine model: %s\n", of_flat_dt_get_machine_name());return best_data;
}

        通过函数of_get_flat_dt_root获取设备树根节点。

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

        总结一下,Linux内核通过根节点compatible属性找到对应的设备的函数调用过程,如图

3.5 向节点追加或修改内容

        产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个IIC接口的六轴芯

片MPU6050,第二版硬件又要把这个MPU6050更换为MPU9250等。一旦硬件修改了,我们

就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片

fxls8471,fxls8471要接到I.MX6U-ALPHA开发板的I2C1接口上,那么相当于需要在i2c1这

个节点上添加一个fxls8471子节点。先看一下I2C1接口对应的节点,打开文件imx6ull.dtsi文

件,找到如下所示内容:

i2c1: i2c@021a0000 {#address-cells = <1>;#size-cells = <0>;compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";reg = <0x021a0000 0x4000>;interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_I2C1>;status = "disabled";
};

        示例代码就是I.MX6ULL的I2C1节点,现在要在i2c1节点下创建一个子节点,这个子节点就是fxls8471,最简单的方法就是在i2c1下直接添加一个名为fxls8471的子节点,如下所示:

i2c1: i2c@021a0000 {#address-cells = <1>;#size-cells = <0>;compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";reg = <0x021a0000 0x4000>;interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;clocks = <&clks IMX6UL_CLK_I2C1>;status = "disabled";//fxls8471子节点fxls8471@1e {compatible = "fsl,fxls8471";reg = <0x1e>;};
};

        添加的fxls8471这个芯片对应的子节点。但是这样会有个问题!i2c1节点是定义在imx6ull.dtsi文件中的,而imx6ull.dtsi是设备树头文件,其他所有使用到I.MX6ULL这颗SOC的板子都会引用imx6ull.dtsi这个文件。直接在i2c1节点中添加fxls8471就相当于在其他的所有板子上都添加了fxls8471这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码这样写肯定是不行的。

        这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向

i2c1节点追加一个名为fxls8471的子节点,而且不能影响到其他使用到I.MX6ULL的板子。

I.MX6U-ALPHA 开发板使用的设备树文件为imx6ull-alientek-emmc.dts,因此我们需要在

imx6ull-alientek-emmc.dts文件中完成数据追加的内容,方式如下:

 &i2c1 { /* 要追加或修改的内容 */ }; 

        第1行,&i2c1表示要访问i2c1这个label所对应的节点,也就是imx6ull.dtsi中的“i2c1:

i2c@021a0000”。

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

打开imx6ull-alientek-emmc.dts,找到如下所示内容:

&i2c1 {clock-frequency = <100000>;pinctrl-names = "default";pinctrl-0 = <&pinctrl_i2c1>;status = "okay";mag3110@0e {compatible = "fsl,mag3110";reg = <0x0e>;position = <2>;};fxls8471@1e {compatible = "fsl,fxls8471";reg = <0x1e>;position = <0>;interrupt-parent = <&gpio5>;interrupts = <0 8>;};
};

        示例代码就是向i2c1节点添加/修改数据,比如第225行的属性“clock-frequency”就表示i2c1时钟为100KHz。“clock-frequency”就是新添加的属性。

        将status属性的值由原来的disabled改为okay。

        i2c1子节点mag3110,因为NXP官方开发板在I2C1上接了一个磁力计芯片mag3110,正点原子的I.MX6U-ALPHA开发板并没有使用mag3110。

        i2c1子节点fxls8471,同样是因为NXP官方开发板在I2C1上接了fxls8471

这颗六轴芯片。

        因为示例代码中的内容是imx6ull-alientek-emmc.dts这个文件内的,所以不会对使用I.MX6ULL这颗SOC的其他板子造成任何影响。这个就是向节点追加或修改内容,重点

就是通过&label来访问节点,然后直接在里面编写要追加或者修改的内容。

4 创建小型模板设备树

        上一节已经对DTS的语法做了比较详细的讲解,本节我们就根据前面讲解的语法,从头到

尾编写一个小型的设备树文件。当然了,这个小型设备树没有实际的意义,做这个的目的是为

了掌握设备树的语法。在实际产品开发中,我们是不需要完完全全的重写一个.dts设备树文件,

一般都是使用SOC厂商提供好的.dts文件,我们只需要在上面根据自己的实际情况做相应的修

改即可。在编写设备树之前要先定义一个设备,我们就以I.MX6ULL这个SOC为例,我们需要

在设备树里面描述的内容如下:

①、I.MX6ULL这个Cortex-A7架构的32位CPU。

②、I.MX6ULL内部ocram,起始地址0x00900000,大小为128KB(0x20000)。

③、I.MX6ULL内部aips1域下的ecspi1外设控制器,寄存器起始地址为0x02008000,大

小为0x4000。

④、I.MX6ULL内部aips2域下的usbotg1外设控制器,寄存器起始地址为0x02184000,大

小为0x4000。

⑤、I.MX6ULL内部aips3域下的rngb外设控制器,寄存器起始地址为0x02284000,大小

为0x4000。

为了简单起见,我们就在设备树里面就实现这些内容即可,首先,搭建一个仅含有根节点

“/”的基础的框架,新建一个名为myfirst.dts文件,在里面输入如下所示内容:

 / { compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull"; } 

        设备树框架很简单,就一个根节点“/”,根节点里面只有一个compatible属性。我们就在这

个基础框架上面将上面列出的内容一点点添加进来。

1、添加cpus节点

/ {compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";cpus {#address-cells = <1>;#size-cells = <0>;//CPU0节点cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;};};
}

        cpus节点,此节点用于描述SOC内部的所有CPU,因为I.MX6ULL只有一个

CPU,因此只有一个cpu0子节点。

2、添加soc节点

        像uart,iic控制器等等这些都属于SOC内部外设,因此一般会创建一个叫做soc的父节点

来管理这些SOC内部外设的子节点,添加soc节点以后的myfirst.dts文件内容如下所示:

/ {compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";cpus {#address-cells = <1>;#size-cells = <0>;cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;};};soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";ranges;};
}

        soc节点设置#address-cells = <1>,#size-cells = <1>,这样soc子节点的reg属性中起始地占用一个字长,地址空间长度也占用一个字长。

        ranges属性,ranges属性为空,说明子空间和父空间地址范围相同。

3、添加ocram节点

        根据第②点的要求,添加ocram节点,ocram是I.MX6ULL内部RAM,因此ocram节点应

该是soc节点的子节点。ocram起始地址为0x00900000,大小为128KB(0x20000),添加ocram

节点以后myfirst.dts文件内容如下所示:

/ {compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";cpus {#address-cells = <1>;#size-cells = <0>;cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;};};soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";ranges;ocram: sram@00900000 {compatible = "fsl,lpm-sram";reg = <0x00900000 0x20000>;};}    
}

        ocram节点,第24行节点名字@后面的0x00900000就是ocram的起始地址。第26行的reg属性也指明了ocram内存的起始地址为0x00900000,大小为0x20000。

4、添加aips1、aips2和aips3这三个子节点

        I.MX6ULL内部分为三个域:aips1~3,这三个域分管不同的外设控制器.

我们先在设备树中添加这三个域对应的子节点。aips1~3这三个域都属于soc节点的子节点,
完成以后的myfirst.dts文件内容如下所示:
/ {compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";cpus {#address-cells = <1>;#size-cells = <0>;//CPU0节点cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;};};//soc节点soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";ranges;//ocram节点ocram: sram@00900000 {compatible = "fsl,lpm-sram";reg = <0x00900000 0x20000>;};//aips1节点aips1: aips-bus@02000000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;reg = <0x02000000 0x100000>;ranges;  }//aips2节点aips2: aips-bus@02100000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;reg = <0x02100000 0x100000>;ranges;}//aips3节点    aips3: aips-bus@02200000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;reg = <0x02200000 0x100000>;ranges;}}
}

5、添加ecspi1、usbotg1和rngb这三个外设控制器节点

        最后我们在myfirst.dts文件中加入ecspi1,usbotg1和rngb这三个外设控制器对应的节点,

其中ecspi1属于aips1的子节点,usbotg1属于aips2的子节点,rngb属于aips3的子节点。最终

的myfirst.dts文件内容如下:

/ {compatible = "fsl,imx6ull-alientek-evk", "fsl,imx6ull";cpus {#address-cells = <1>;#size-cells = <0>;//CPU0节点cpu0: cpu@0 {compatible = "arm,cortex-a7";device_type = "cpu";reg = <0>;};};//soc节点soc {#address-cells = <1>;#size-cells = <1>;compatible = "simple-bus";ranges;//ocram节点ocram: sram@00900000 {compatible = "fsl,lpm-sram";reg = <0x00900000 0x20000>;};//aips1节点aips1: aips-bus@02000000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;reg = <0x02000000 0x100000>;ranges;  //ecspi1节点ecspi1: ecspi@02008000 {#address-cells = <1>;#size-cells = <0>;compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";reg = <0x02008000 0x4000>;status = "disabled";};}//aips2节点aips2: aips-bus@02100000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;reg = <0x02100000 0x100000>;ranges;//usbotg1节点usbotg1: usb@02184000 {compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";reg = <0x02184000 0x200>;status = "disabled";};}//aips3节点    aips3: aips-bus@02200000 {compatible = "fsl,aips-bus", "simple-bus";#address-cells = <1>;#size-cells = <1>;reg = <0x02200000 0x100000>;ranges;//rngb节点rngb: rngb@02284000 {compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imx-rng";reg = <0x02284000 0x4000>;};}}
}

        至此,myfirst.dts这个小型的模板设备树就编写好了,基本和imx6ull.dtsi很像,可以看做

是imx6ull.dtsi的缩小版。在myfirst.dts里面我们仅仅是编写了I.MX6ULL的外设控制器节点,

像IIC接口,SPI接口下所连接的具体设备我们并没有写,因为具体的设备其设备树属性内容不

同,这个等到具体的实验在详细讲解。

5 设备树在系统中的体现

        Linux内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-

tree目录下根据节点名字创建不同文件夹,如图

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

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

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

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

        图中各个文件夹(途中粗字体文件夹)就是根节点“/”的各个子节点,比如“aliases”、

“backlight”、“chosen”和“clocks”等等。大家可以查看一下 imx6ull-alientek-emmc.dts 和

imx6ull.dtsi这两个文件,看看根节点的子节点都有哪些,看看是否和图中的一致。

        /proc/device-tree目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进

入/proc/device-tree/soc目录中就可以看到soc节点的所有子节点,如图所示:

        和根节点“/”一样,图中的所有文件分别为soc节点的属性文件和子节点文件夹。大家可以自行查看一下这些属性文件的内容是否和imx6ull.dtsi中soc节点的属性值相同,也可以进入“busfreq”这样的文件夹里面查看soc节点的子节点信息。

http://www.dtcms.com/a/337051.html

相关文章:

  • LINUX 818 shell:random;for for
  • 电子元器件-电阻终篇:基本原理,电阻分类及特点,参数/手册详解,电阻作用及应用场景,电阻选型及实战案例
  • Docker常见指令速查
  • 矿物分类案例(二)数据填充后使用6种模型训练
  • Docker学习--认识Docker
  • 遥感机器学习入门实战教程 | Sklearn 案例②:PCA + k-NN 分类与评估
  • AWS Neptune:图数据库的强大潜力
  • 【LLM1】大型语言模型的基本生成机制
  • 将 iPhone 连接到 Windows 11 的完整指南
  • Chromium base 库中的 Observer 模式实现:ObserverList 与 ObserverListThreadSafe 深度解析
  • AI 在金融领域的落地案例
  • 强化学习-CH2 状态价值和贝尔曼等式
  • 算法详细讲解:数据结构 - 单链表与双链表
  • Nacos-6--Naco的QUIC协议实现高可用的工作原理
  • cesium中实时获取鼠标精确坐标和高度
  • IB数学课程知识点有哪些?IB数学课程辅导机构怎么选?
  • GitLab 安全漏洞 CVE-2025-7739 解决方案
  • GitLab 安全漏洞 CVE-2025-6186 解决方案
  • AI全链路赋能:smardaten2.0实现软件开发全流程智能化突破
  • Leetcode 3651. Minimum Cost Path with Teleportations
  • 嵌入式 C++ 语言编程规范文档个人学习版(参考《Google C++ 编码规范中文版》)
  • USB基础 -- 字符串描述符 (String Descriptor) 系统整理文档
  • 2025年8月更新!Windows 7 旗舰版 (32位+64位 轻度优化+离线驱动)
  • hla mHAg
  • cortex-m中断技巧
  • 数组学习2
  • 十年回望:Vue 与 React 的设计哲学、演进轨迹与生态博弈
  • idea部署到docker
  • 静配中心配药智能化:基于高并发架构的Go语言实现
  • MySQL 函数大赏:聚合、日期、字符串等函数剖析