一、介绍
1.1 为什么引入设备树
我们首先回顾一下我们之前学习过的驱动程序。比如:
linux驱动移植-lcd驱动基础;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_lcd,在arch/arm/mach-s3c24xx/mach-smdk2440.c文件定义了platform数据smdk2440_fb_info。
linux驱动移植-linux块设备驱动Nand Flash;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_nand ,在 arch/arm/mach-s3c24xx/common-smdk.c文件定义了platform数据smdk_nand_info。
linux驱动移植-linux块设备驱动Nand Flash;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_nand ,在 arch/arm/mach-s3c24xx/common-smdk.c文件定义了platform数据smdk_nand_info。
linux驱动移植-linux块设备驱动Nor Flash;在drivers/mtd/maps/physmap-core.c文件中定义了platform设备physmap_flash,同时在文件内定义了platform数据physmap_flash_data。
linux驱动移植-DM9000网卡驱动;在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了platform设备smdk2440_device_eth,同时在文件内定义了platform数据smdk2440_dm9k_pdata 。
linux驱动移植-I2C适配器驱动移植;在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了platform设备s3c_device_i2c0,同时在文件内定义了platform数据default_i2c_data 。
linux驱动移植-I2C设备驱动移植(AT24C08);在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了I2C从设备信息i2c_board_info 。
linux驱动移植-SPI控制器驱动;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_spi1,同时在arch/arm/mach-s3c24xx/mach-smdk2440.c文件内定义了platform数据s3c2440_spi1_data 。
linux驱动移植-SPI驱动移植(OLED SSD1306);在arch/arm/mach-s3c24xx/mach-smdk2440.c文件中定义了SPI从设备信息spi_board_info 。
linux驱动移植-UART设备驱动;在arch/arm/plat-samsung/init.c文件中定义了platform设备s3c24xx_uart_device0,同时在该文件内定义了platform数据uart_cfgs。
linux驱动移植-RTC驱动;在arch/arm/plat-samsung/devs.c文件中定义了platform设备s3c_device_rtc ,同时在drivers/rtc/rtc-s3c.c文件内定义了设置platform数据类型为s3c_rtc 。
在之前我们介绍的驱动程序中,Mini2440开发板板极硬件资源都是被硬编码在arch/arm/plat-samsung/devs.c和arch/arm/mach-s3c24xx/mach-smdk2440.c文件,比如板上的platform设备、resource、i2c_board_info、spi_board_info以及各种硬件的platform_data,这些板级细节代码基本都是和开发外围设备相关的,开发板不同这些信息也会因此不同,与之而来就带来了诸多问题。
比如ARM的merge工作量较大,随着着芯片的发展,linux内核中就包含着越来越多这些描述设备的代码,导致Linux内核代码会很臃肿。为此linux还对ARM平台相关代码做出了相关的规范调整:
- ARM的核心代码保存在arch/arm目录下;
- ARM SoC core architecture code保存在arch/arm目录下;
- ARM SoC的周边外设模块的驱动保存在drivers目录下;
- ARM SoC的特定machine代码在arch/arm/mach-xxx目录下;
- ARM SoC board specific的代码(arch/arm/plat-xxx)被移除,由Device Tree机制来负责传递硬件拓扑和硬件资源信息;硬件的细节可以直接通过Device Tree传递给linux驱动程序,而不再需要在kernel中进行大量的冗余编码。
1.2 设备树(Device Tree)
linux内核从3.x开始引入设备树的概念,用于将设备信息与驱动代码分离开来。在设备树出现之前,所有关于设备的硬件信息都要编写在驱动程序里,一旦外围设备变化,驱动代码就要重写。
引入了设备树之后,驱动代码只负责处理驱动的逻辑,而关于设备的具体信息存放到设备树文件中,这样,如果只是硬件接口信息的变化而没有驱动逻辑的变化,驱动开发者只需要修改设备树文件信息,不需要改写驱动代码。
比如在ARM Linux内,一个.dts(device tree source)文件对应一个ARM的machine,一般放置在内核的"arch/arm/boot/dts/"目录内,比如firefly rk3288参考板的板级设备树文件就是"arch/arm/boot/dts/rk3288-firefly.dts"。这个文件可以通过$make dtbs命令编译成二进制的.dtb文件供内核驱动使用。
基于同样的软件分层设计的思想,由于一个SoC可能对应多个machine,如果每个machine的设备树都写成一个完全独立的.dts文件,那么势必相当一些.dts文件有重复的部分,为了解决这个问题,Linux设备树目录把一个SoC公用的部分或者多个machine共同的部分提炼为相应的.dtsi文件。这样每个.dts就只有自己差异的部分,公有的部分只需要"include"相应的.dtsi文件, 这样就是整个设备树的管理更加有序。
一般.dtsi文件一般是芯片外设的一些信息,比如CPU架构、主频、外设寄存器地址范围,比如UART、IIC等等。这些信息一般是不能修改的,而我们的板载外设信息一般放在dts文件中,使我们根据我们板子的外设自己添加的,是可以修改的。
1.3 设备树文件
设备树源文件格式支持dts、dtsi:
- dts:硬件的相关信息都会写在.dts为后缀的文件中,每一款硬件可以单独写一份xxxx.dts,一般在linux源码中存在大量的dts文件,对于arm架构可以在arch/arm/boot/dts找到相应的dts,一个dts文件对应一个ARM的machie。
- dtsi:对于一些相同的dts配置可以抽象到dtsi文件中,然后类似于C语言的方式可以include到dts文件中,对于同一个节点的设置情况,dts中的配置会覆盖dtsi中的配置;
这些源文件同我们的C代码一样,并不能直接使用的,而是得经过一个编译过程生成机器可运行的二进制文件:
- dtb:dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过bootloader引导程序加载到内核。所以bootloader需要支持设备树才行;linux kernel也需要加入设备树的支持;
dts文件使用工具dtc进行编译,可以在ubuntu系统上通过指令apt-get install device-tree-compiler安装dtc工具,不过在内核源码scripts/dtc路径下已经包含了dtc工具。
uboot从v1.1.3开始支持设备树,其对ARM的支持则是和ARM内核支持设备树同期完成。
为了使能设备树,需要在编译uboot的时候在.config文件中加入:
CONfiG_OF_LIBFDT=y·
在uboot中,可以从NAND、SD或者TFTP等任意介质中将.dtb读入内存,假设.dtb放入的内存地址为0x71000000,之后可在uboot中运行fdt addr命令设置.dtb的地址,如:
fdt addr 0x71000000·
fdt的其他命令就变得可以使用,如fdt resize、fdt print等。
然后通过以下命令来启动内核:
bootm uImage_addr initrd_address dtb_address
其中:
- 第一个参数为内核映像在内存的地址;
- 第二个参数为initrd的地址,若不存在initrd,可以用“-”符号代替;
- 第三个参数dtb_address为.dtb文件在内存的地址
二、设备树结构
设备树源文件也是需要根据一定规则来编写的,同C语言一样,也要遵循一些语法规则。
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点;下面是一个典型的设备树结构:
/dts-v1/; /* 版本 */
#include <dt-bindings/input/input.h> /* 包含c头文件 */
#include "xxx.dtsi" /* 包含设备树头文件 */ / { /* 根节点 */ node1 { a-string-property = "A string"; a-string-list-property = "first string", "second string"; // hex is implied in byte arrays. no '0x' prefix is required a-byte-data-property = [01 23 34 56]; child-node1 { first-child-property; second-child-property = <1>; a-string-property = "Hello, world"; }; child-node2 { }; }; node2 { an-empty-property; a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */ child-node1 { }; }; };
设备树文件具有以下几种特性:
- 每个设备树文件都有一个根节点,除了根节点,每个节点都只有一个父节点;
- 每个节点用节点名字标识,节点名字的格式是node-name@unit-address;如果该节点没有reg属性,那么该节点名字中必须不能包括@和unit-address;
- 根节点的名字是确定的,必须是"/";
- 每个节点都包含了若干个key-value对(属性)来描述该节点的一些特性,每个属性的描述用
;
结束;
在dts文件中,一个节点被定义成:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
}
上图中:
- []:表示该项可以省略;
- label:标签,方便在dts文件中引用;
- node-name:节点名;
- unit-address:地址;
- properties:属性定义;
- child nodes:子节点;
2.1 标签
在设备树中,如果我们想引用一个节点,必须要使用全路径,比如/node-name-1/node-name-2/node-name-N,这样当一个节点嵌套比较深的时候就不是很方便。
所以,设备树允许我们为一个节点起一个别名,也就是标签,借以省去冗长的路径。这样就可以实现类似函数调用的效果。
编译设备树的时候,相同的节点的不同属性信息都会被合,相同节点的相同的属性会被重写,使用引用可以避免移植者四处找节点,直接在板级.dts增改即可。
下面的例子中就是直接引用了dtsi中的节点cpu0,并向其中添加/修改新的属性信息:
&cpu0 { cpu0-supply = <&vdd_cpu>; };
2.2 node-name[@unit-address]
linux内核约定节点名应写成形如node-name[@unit_address]的形式,其中:
- node-name是节点名字,为ASCII字符串,最长可以是31个字符长度;节点名字应该能够清晰的描述出节点的功能,比如uart0就表示这个节点是UART0外设;
- unit_address一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话unit-address可以不要,比如cpu@0、interrupt-controller@00a01000;
下面就是典型节点的写法,该节点用于描述rtc设备硬件信息:
rtc@57000000 { compatible = "samsung,s3c2410-rtc"; reg = <0x57000000 0x100>; interrupts = <0 0 30 3>, <0 0 8 3>; status = "disabled"; };
linux中的设备树还包括一些特殊的节点,比如chosen,aliases等。
2.2.1 choosen节点
chosen 并不是一个真实的设备,chosen节点主要目的就是将uboot里面bootargs环境变量值传递给linux内核作为命令行参数,比如:
chosen { bootargs = "noinitrd root=/dev/mtdblock4 rw init=/linuxrc console=ttySAC0,115200"; };
2.2.2 aliases节点
aliases节点为了解决节点路径名过长的问题,引入了节点别名的概念,可以引用到一个全路径的节点。
aliases { pinctrl0 = &pinctrl_0; serial0 = &uart0; serial1 = &uart1; serial2 = &uart2; };
2.2.3 CPU节点
cpus节点下有1个或多个cpu子节点,,cpu子节点中用reg属性用来标明自己是哪一个cpu:
cpus { #address-cells = <1>; #size-cells = <0>; enable-method = "rockchip,rk3066-smp"; rockchip,pmu = <&pmu>; cpu0: cpu@500 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x500>; resets = <&cru SRST_CORE0>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; cpu1: cpu@501 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x501>; resets = <&cru SRST_CORE1>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; cpu2: cpu@502 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x502>; resets = <&cru SRST_CORE2>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; cpu3: cpu@503 { device_type = "cpu"; compatible = "arm,cortex-a12"; reg = <0x503>; resets = <&cru SRST_CORE3>; operating-points-v2 = <&cpu_opp_table>; #cooling-cells = <2>; /* min followed by max */ clock-latency = <40000>; clocks = <&cru ARMCLK>; dynamic-power-coefficient = <370>; }; };
2.2.4 memory节点
所有设备树都需要一个memory设备节点,它描述了系统的物理内存布局。如果系统有多个内存块,可以创建多个memory节点,或者可以在单个memory节点的reg属性中指定这些地址范围和内存空间大小。
例如:一个64位的系统有两块内存空间:
- RAM1:起始地址是0x0,地址空间是大小0x08000000;
- RAM2:起始地址是0x10000000,地址空间大小也是0x08000000;
同时根节点下的 #address-cells = <2>和#size-cells = <2>,这个memory节点描述为:
memory@0 { device_type = "memory"; reg = <0x00000000 0x00000000 0x00000000 0x08000000>; }; memory@10000000 { device_type = "memory"; reg = <0x00000000 0x10000000 0x00000000 0x08000000>; };
2.3 key
在设备树中,键值对是描述属性的方式,比如linux驱动中可以通过设备节点中的"compatible"这个属性查找设备节点。
linux设备树语法中定义了一些具有规范意义的属性,包括:compatible, address, interrupts等,这些信息能够在内核初始化找到节点的时候,自动解析生成相应的设备信息。
此外,还有一些Linux内核定义好的,一类设备通用的有默认意义的属性,这些属性一般不能被内核自动解析生成相应的设备信息,但是内核已经编写的相应的解析提取函数,常见的有 "mac_addr","gpio","clock","power"。"regulator" 等等。
2.3.1 compatible
compatible属性也叫做兼容性属性,这是非常重要的一个属性。compatible属性的值是一个字符串列表,compatible属性用于将设备和驱动绑定起来。compatible属性的值格式如下所示:
"manufacturer,model"
其中:
- manufacturer:表示厂商;
- model:一般是模块对应的驱动名字。
比如S3C2440 RTC驱动和设备节点就是通过"compatible"进行匹配,arch/arm/boot/dts/s3c24xx.dtsi中定义有设备节点:
rtc@57000000 { compatible = "samsung,s3c2410-rtc"; /* 支持字符串数组 */ reg = <0x57000000 0x100>; interrupts = <0 0 30 3>, <0 0 8 3>; status = "disabled"; };
然后我们在内核源码中找到RTC驱动定义,在drivers/rtc/rtc-s3c.c文件,可以发现这个驱动使用了设备树描述的设备信息,我们可以找到它用来描述设备信息的结构体。
可以看出,驱动中用于匹配的结构使用的compatible和设备树中一模一样,否则就可能无法匹配,这里另外的一点是struct of_device_id数组的最后一个成员一定是空,因为相关的操作API会读取这个数组直到遇到一个空。
static const struct of_device_id s3c_rtc_dt_match[] = { // 存储设备驱动程序和设备节点之间得匹配信息 { .compatible = "samsung,s3c2410-rtc", .data = &s3c2410_rtc_data, }, ...... { /* sentinel */ }, }; MODULE_DEVICE_TABLE(of, s3c_rtc_dt_match); static struct platform_driver s3c_rtc_driver = { .probe = s3c_rtc_probe, // rtc探测函数 .remove = s3c_rtc_remove, // rtc移除函数 .driver = { .name = "s3c-rtc", .pm = &s3c_rtc_pm_ops, .of_match_table = of_match_ptr(s3c_rtc_dt_match), }, };
2.3.2 address
几乎所有的设备都需要与CPU的IO口相连,所以其IO端口信息就需要在设备节点节点中说明。常用的属性有:
- #address-cells:用来描述子节点"reg"属性的地址表中用来描述首地址的cell的数量;也就是说需要用多少个u32位数来描述该地址;
- #size-cells:用来描述子节点"reg"属性的地址表中用来描述地址长度的cell的数量;也就是说需要用多少个u32的位数来描述地址长度;
有了这两个属性,子节点中的"reg"就可以描述一块连续的地址区域。
下例中,父节点中指定了#address-cells = <1>; #size-cells = <1>,则子节点i2c@54000000中的reg中:
- 使用1个u32数来表示地址,,即0x54000000,也就是I2C寄存器寄基地址;
- 使用1个u32数来表示地址跨度,即是0x100:
/ { #address-cells = <1>; #size-cells = <1>; i2c@54000000 { compatible = "samsung,s3c2410-i2c"; reg = <0x54000000 0x100>; interrupts = <0 0 27 3>; #address-cells = <1>; #size-cells = <0>; status = "disabled"; }; }; };
2.3.3 interrupts
一个计算机系统中大量设备都是通过中断请求CPU服务的,所以设备节点中就需要在指定中断号。常用的属性有:interrupt-controller、#interrupt-cells、interrupt-parent、interrupts。
interrupt-controller表示这个节点是一个中断控制器,需要注意的是,一个SoC中可能有不止一个中断控制器,下面是在文件"arch/arm/boot/dts/s3c24xx.dtsi"中对S3C2440的中断控制器节点描述:
intc:interrupt-controller@4a000000 { compatible = "samsung,s3c2410-irq"; reg = <0x4a000000 0x100>; interrupt-controller; #interrupt-cells = <4>; };
#interrupt-cells是中断控制器节点的属性,用来描述子节点中"interrupts"属性使用了几个cell才能确定所使用的中断。
以S3C2440位列,该属性的值是4,每一个参数的含义需要由中断控制器的驱动来解释,具体是有中断控制器的irq_domain_ops中的xlate来解释,对于S3C2440就是drivers/irqchip/irq-s3c24xx.c中的s3c24xx_irq_xlate_of。interrupts属性中的四个参数中的含义:
- 第0位:中断控制器编号:0表主中断控制器,1表示子中断控制器;
- 第1位:表示子中断所属的主中断控制器的硬件中断号:对于子中断,这个参数才有意义;
- 第2位:硬件中断编号:表示位于当前中断控制器的硬件中断号,比如EINT0~EINT3都是直接连接到主中断控制器的,所以其对应的中断控制器编号为0,硬件中断编号为0~3;
- 第3位:中断类型:1为上升沿触发、2位下降沿触发、3位双边沿触发、4位高电平触发、8位低电平触发;
中断类型在include/dt-bindings/interrupt-controller/irq.h文件定义:
#define IRQ_TYPE_NONE 0 #define IRQ_TYPE_EDGE_RISING 1 #define IRQ_TYPE_EDGE_FALLING 2 #define IRQ_TYPE_EDGE_BOTH (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING) #define IRQ_TYPE_LEVEL_HIGH 4 #define IRQ_TYPE_LEVEL_LOW 8
我们知道在不同的中断控制器里面相同的硬件中断号值对应的硬件中断是不一样的,因此interrupts即是利用这个特定来描述一个中断:
- 指定是属于哪一个中断控制器;
- 指定在该中断控制器里面的哪一个硬件中断号;
注意:S3C2440物理只有一个中断控制器,软件层面上抽象出来3个中断控制器,具体参考linux驱动移植-中断子系统执行流程:
- 一个主中断控制器管理主中断源;
- 两个子中断控制器,一个用于管理外部中断源(对应下图的gpio中断控制器)、另一个管理带有子中断的内部中断源(对应下图的子中断控制器);
关于gpio中断控制器并没有在intc节点中描述,intc仅仅描述了主中断控制器和子中断控制器。
示例:
uart0: serial@50000000 { compatible = "samsung,s3c2410-uart"; reg = <0x50000000 0x4000>; interrupts = <1 28 0 4>, <1 28 1 4>; status = "disabled"; };
这里配置的interrupts描述了串口0的收发中断:
- <1 28 0 4>:描述的是带有子中断控制器,主中断硬件中断号为28,子中断硬件中断编号为0,高定平触发;
- 定位到子中断INT_RXD0;主中断INT_UART0;
- <1 28 1 4>:描述的是带有子中断控制器,主中断硬件中断号为28,子中断硬件中断编号为1,高定平触发;
- 定位到子中断INT_TXD0;主中断INT_UART0;
interrupt-parent:标识此设备节点属于哪一个中断控制器,这个属性的值是一个指向中断控制器节点的引用。
如果当前设备节点没有interrupt-parent属性,那么该设备没有与中断控制器相连接的中断信号,而如果该属性存在,那么表示该设备节点产生的中断信号会通过指定的中断控制器进行处理。
举个例子有个网卡:当网卡有数据时会产生一个中断,接到gpio控制器里面,怎么描述这个网卡的中断信息呢?
- 指定网卡的中断控制器是谁(interrupt-parent= );
- 指定使用中断控制器里面的哪个中断(interrupts=);
网卡设备节点描述如下:
srom-cs4@20000000 { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; reg = <0x20000000 0x8000000>; ranges; ethernet@20000000 { compatible = "davicom,dm9000"; reg = <0x20000000 0x2 0x20000004 0x2>; interrupt-parent = <&gpf>; interrupts = <7 IRQ_TYPE_EDGE_RISING>; local-mac-address = [00 00 de ad be ef]; davicom,no-eeprom; }; };
其中:
-
interrupt-parent = <&gpf>:interrupt-parent指向一个中断控制器gpf;
-
interrupts = <7 IRQ_TYPE_EDGE_RISING>:表明使用中断控制器gpf里面的7号中断(硬件中断号),IRQ_TYPE_EDGE_RISING表明是中断触发方式;
对于gpio中断可以指定中断触发方式,对于其他中断可能就不需要指定中断触发方式。所以interrupts用多个u32表示是由interrupt-parent(它所属于的中断控制器)所决定的。
比如gpio中断控制器gpf使用2个u32来描述一个interrupts:
gpf: gpf { gpio-controller; #gpio-cells = <2>; interrupt-controller; #interrupt-cells = <2>; };
而gpf节点所属的中断控制器,可以通过该节点向上查找其父节点,找到第一个interrupt-parent,即:
interrupt-parent = <&intc>;
因此,中断控制器gpf、gpg和intc的关系如下:
2.3.4 reg
reg属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息。
reg属性的值,是一系列的“address size”,用多少个32位的数来表示address和size,由其父节点的#address-cells、#size-cells决定。示例:
/ { #address-cells = <1>; #size-cells = <1>; timer@51000000 { compatible = "samsung,s3c2410-pwm"; reg = <0x51000000 0x1000>; interrupts = <0 0 10 3>, <0 0 11 3>, <0 0 12 3>, <0 0 13 3>, <0 0 14 3>; #pwm-cells = <4>; }; };
上图描述了节点timer@51000000,timer节点描述了S3C2440的timer相关信息,重点是reg属性。
其中timer的父节点设置了#address-cells = <1>、#size-cells = <1>,因此reg属性中address=0x51000000,length=0x100。查阅S3C2440 datasheet可知,S3C2440的定时器寄存器首地址为0x51000000,但是定时器的地址长度(范围)并没有0x100这么多,这里我们重点是获取定时器寄存器首地址。
2.3.5 status
status标识了设备的状态,使用status可以去禁止设备或者启用设备,看下设备树规范中的status可选值:
- okay :表示设备正在运行;
- disabled :表示该设备目前尚未运行,但将来可能会运行;
- fail :表示设备无法运行。 在设备中检测到严重错误,确实如此没有修理就不可能投入运营;
- fail-sss:表示设备无法运行。 在设备中检测到严重错误,它是没有修理就不可能投入运营。 值的sss部分特定于设备并指示检测到的错误情况;
2.3.6 model
model属性值是一个字符串,一般model属性描述设备模块信息,比如名字什么的,比如:
model = "wm8960-audio";
2.3.7 name
name属性值为字符串, name属性用于记录节点名字, name属性已经被弃用,不推荐使用name属性,一些老的设备树文件可能会使用此属性。
2.3.8 device_type
device_type属性值为字符串, IEEE 1275会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于cpu节点或者memory节点。
memory@30000000 { device_type = "memory"; reg = <0x30000000 0x4000000>; };
2.3.9 ranges
ranges属性值可以为空或者按照“child-bus-address,parent-bus-address,length”格式编写的数字矩阵,ranges是一个地址映射/转换表,ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
- child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells确定此物理地址所占用的字长;
- parent-bus-address :父总线地址空间的物理地址,同样由父节点的#address-cells确定此物理地址所占用的字长;
- length:子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长;
示例:
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。
2.4 节点值
节点属性(property)值标识了设备的特性,它的值(value)是多种多样的。
2.4.1 空
可能是空,也就是没有值的定义,如下:
interrupt-controller;
2.4.2 字符串
可能为字符串,如下
compatible = "samsung,s3c24xx";
也可以为字符串数组,如下:
compatible = "firefly,firefly-rk3288", "rockchip,rk3288";
2.4.3 u32、u64数值
可能是一个u32数值,一个32位的数据,用尖括号包围起来,如:
#interrupt-cells = <4>;
一个64位数据(使用2个32位数据表示),用尖括号包围起来,如:
clock-frequency = <0x00000001 0x00000000>;
也可以是一个数值数组,如下:
interrupts = <0 0 0 3>,<0 0 1 3>,<0 0 2 3>,<0 0 3 3>,<0 0 4 4>,<0 0 5 4>;
2.4.4 16进制字节序列
字节序列,用中括号包围起来,如:
local-mac-address = [000 00 de ad be ef]
三、dtb文件
3.1 dtb文件布局
dtb文件是dts文件编译后的二进制格式的设备树文件,文件布局如下:
dtb文件主要包含四部分内容:
- struct ftd_header:用来表明各个部分的偏移地址,整个文件的大小,版本号等;
- memory reservation block:在设备树中使用/memreserve/ 定义的保留内存信息;
- structure block:保存节点的信息,节点的结构;
- strings block:保存属性的名字,单独作为字符串保存;
3.1.1 struct ftd_header
u-boot源码include/fdt.h文件中对struct ftd_header结构体进行了定义:
struct fdt_header { fdt32_t magic; /* magic word FDT_MAGIC */ fdt32_t totalsize; /* total size of DT block */ fdt32_t off_dt_struct; /* offset to structure */ fdt32_t off_dt_strings; /* offset to strings */ fdt32_t off_mem_rsvmap; /* offset to memory reserve map */ fdt32_t version; /* format version */ fdt32_t last_comp_version; /* last compatible version */ /* version 2 fields below */ fdt32_t boot_cpuid_phys; /* Which physical CPU id we're booting on */ /* version 3 fields below */ fdt32_t size_dt_strings; /* size of the strings block */ /* version 17 fields below */ fdt32_t size_dt_struct; /* size of the structure block */ };
其中:
- magic:它的值为0xd00dfeed,以大端模式保存;
- totalsize:整个dtb文件的大小;
- off_dt_struct:structure block的偏移地址;
- off_dt_strings:strings block的偏移地址;
- off_mem_rsvmap:memory reservation block的偏移地址;
- version:设备树版本信息;
- last_comp_version:向后兼容的最低设备树版本信息;
- boot_cpuid_phys:CPU ID;
- size_dt_strings:strings block的大小;
- size_dt_struct:structure block的大小;
在dtb文件中,数据的存放格式是大端模式,即数值的高位存放在低地址。对于一个值,比如0x12345678,存放方式如下:
注意:大端模式和小端模式只针对数值,对于字符串 abc,a在低地址,c在高地址。
在uboot支持设备树后,我们可以在uboot控制台中输出ftd header信息:
SMDK2440 # fdt addr 0x30001000 SMDK2440 # fdt header magic: 0xd00dfeed totalsize: 0x149a (5274) off_dt_struct: 0x38 off_dt_strings: 0x12d8 off_mem_rsvmap: 0x28 version: 17 last_comp_version: 16 boot_cpuid_phys: 0x0 size_dt_strings: 0x1c2 size_dt_struct: 0x12a0 number mem_rsv: 0x0
3.1.2 memory reservation block
ftd中使用struct fdt_reserve_entry,结构体用于表示memreserve的起始地址和内存空间的大小,它紧跟在struct ftdheader结构体后面:
struct fdt_reserve_entry { fdt64_t address; fdt64_t size; };
其中:
- address:64位地址;
- size:保留的内存空间的大小;
比如:
/memreserve/ 0x33000000 0x10000
3.1.3 structure block
structure block是用于描述设备树节点的结构,保存着节点的信息、节点的结构,它有5种标记类型:
(1)FDTBEGINNODE (0x00000001):表示节点的开始,它的后面紧跟的是节点的名字;
(2)FDTENDNODE (0x00000002):表示节点的结束;
(3)FDTPROP (0x00000003) :表示开始描述节点里面的一个属性,在FDTPROP后面紧跟一个结构体如下所示:
struct { uint32_t len; /*表示属性值的长度*/ uint32_t nameoff; /*属性的名字在string block的偏移*/ }
上面的这个结构体后紧跟着是属性值,属性的名字保存在字符串块(strings block)中。
(4)FDT_END (0x00000009):表示structure block的结束。
3.2 dtb文件分析
编译生成dtb文件的源设备树s3c2440-smdk2440.dts文件如下:
// SPDX-License-Identifier: GPL-2.0 /* * SAMSUNG SMDK2440 board device tree source * * Copyright (c) 2013 Heiko Stuebner <heiko@sntech.de> */ /dts-v1/; #include "s3c2440.dtsi" #include <dt-bindings/interrupt-controller/irq.h> / { model = "SMDK2440"; compatible = "samsung,s3c2440","samsung,mini2440"; memory@30000000 { device_type = "memory"; reg = <0x30000000 0x4000000>; }; clocks { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <0>; xti: xti@0 { compatible = "fixed-clock"; reg = <0>; clock-frequency = <12000000>; clock-output-names = "xti"; #clock-cells = <0>; }; }; /*添加DM9000 网卡,以便于后面的驱动开发调试*/ srom-cs4@20000000 { compatible = "simple-bus"; #address-cells = <1>; #size-cells = <1>; reg = <0x20000000 0x8000000>; ranges; ethernet@20000000 { compatible = "davicom,dm9000"; reg = <0x20000000 0x2 0x20000004 0x2>; interrupt-parent = <&gpf>; interrupts = <7 IRQ_TYPE_EDGE_RISING>; local-mac-address = [00 00 de ad be ef]; davicom,no-eeprom; }; }; }; &rtc { status = "okay"; }; &uart_0 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart0_data>; }; &uart_1 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart1_data>; }; &uart_2 { status = "okay"; pinctrl-names = "default"; pinctrl-0 = <&uart2_data>; }; &watchdog { status = "okay"; }; &nand0 { status = "okay"; nand,tacls = <0xa>; nand,twrph0 = <0x19>; nand,twrph1 = <0xa>; #address-cells = <1>; #size-cells = <1>; /*Nand Flash 分区*/ partitions { /* MTD partition table */ #address-cells = <1>; #size-cells = <1>; nr-chips = <1>; set-name = "mini2440-0"; partition@0 { label = "u-boot"; reg = <0x0000000 0x00040000>; read-only; }; partition@40000 { label = "params"; reg = <0x00040000 0x00020000>; read-only; }; partition@60000 { label = "kernel"; reg = <0x00060000 0x00400000>; read-only; }; partition@460000 { label = "rootfs"; reg = <0x00460000 0x0>; /*read-only;*/ /*若rootfs为read-only,内核加载文件系统后,文件系统只能读不能写*/ }; }; };
编译生成s3c2440-smdk2440.dtb文件,以16进制查看:
接下来我们对应上图的编号逐一分析。
其中地址范围0x00000000~0x00000027表示的是fdt_header 结构体的成员信息:
- 地址0x00000000:对应magic,表示设备树魔数,固定为0xd00dfeed;
- 地址0x00000004:对应totalsize,表示整个设备设dtb文件的大小,从上图可知0x0000149a正好是dtb文件的大小5274字节;
- 地址0x00000008:对应off_dt_struct,表示structure block的偏移地址,为 0x00000038;
- 地址0x0000000c:对应off_dt_strings,表示 strings block的偏移地址,为 0x000012d8;
- 地址0x00000010:对应off_mem_rsvmap:表示memory reservation block的偏移地址,为 0x00000028;
- 地址0x00000014:对应version,设备树版本的版本号为0x11;
- 地址0x00000018:对应last_comp_version:向下兼容版本号0x10;
- 地址0x0000001C:对应boot_cpuid_phys:在多核处理器中用于启动的主cpu的物理id,为0x0;
- 地址0x00000020:对应size_dt_strings,strings block的大小为 0x01C2;
- 地址0x00000024: 对应size_dt_struct,structure block的大小为 0x000012A0;
其中地址范围0x00000028~0x00000037表示的是fdt_reserve_entry结构体的成员信息:
- 对应结构体fdt_reserve_entry:它所在的地址为0x28,s3c2440-smdk2440.dts设备树文件没有设置 /memreserve/,所以address = 0x0,size = 0x0;
其中地址范围0x00000038~0x000012d7表示的是structure block信息:
- 地址0x00000038:值0x00000001表示的是设备节点的开始;
- 地址0x0000003c:表示设备节点的名字,这里是根节点,所以为0x00000000;
- 地址0x00000040:值0x00000003表示的是开始描述设备节点的一个属性;
- 地址0x00000044:表示这个属性值的长度为0x21;
- 地址0x00000048:表示这个属性的名字在strings block的偏移量是0,找到strings block的地址0x12d8的地方,可知这个属性的名字是compatible;
- 地址0x0000004c:这个compatible属性的值是"samsung,s3c2440.samsung,mini2440..",加上字符串的结束符,刚好33个字节;
参考文章
[2]Linux设备树语法详解
[5]【正点原子I.MX6U-MINI驱动篇】4、Linux设备树详解
[7]韦东山设备树文章地址:http://wiki.100ask.org/Linux_devicetree
文章来源: 博客园
- 还没有人评论,欢迎说说您的想法!