Linux DTS 设备树源码
1. 简介1.1 引入DTS的原因1.2 文件格式 2. DTS语法2.1 .dtsi头文件2.2 设备节点2.2.1 设备树中节点命名格式2.2.2 设备树源码中常用的几种数据类型2.2.3 标准属性 2.3 DTS的加载过程2.4 DTS的描述信息2.3 DTS举例 3. 在系统中查看设备树4. Linux内核解析DTB文件流程5. 绑定信息文档6. 设备树常用操作函数6.1 查找节点的 OF 函数6.2 查找父/子节点的 OF 函数6.3 提取属性值的 OF 函数6.4 其它常用OF函数 7. MIPI配置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 是为 Linux 提供一种硬件信息的描述方法,以此代替源码中的 硬件编码 (hard code)。DTS 即 Device Tree Source 设备树源码, Device Tree 是一种描述硬件的数据结构,起源于 OpenFirmware (OF). 在 Linux 2.6 中, ARM 架构的板级硬件细节过多的被硬编码在 arch/arm/plat-xxx 和 arch/arm/mach-xxx (比如板上的 platform 设备,resource, i2c_board_info, spi_board_info 以及各种硬件的 platform_data), 这些板级细节代码对内核来讲只不过是垃圾代码。而采用 Device Tree 后, 许多硬件的细节可以直接透过它传递给 Linux,而不再需要在 kernel 中 进行大量的冗余编码。
DTS设备树描述文件中什么代表总线,什么代表设备
设备:一个含有compatible属性的节点就是一个设备总线:包含一组设备节点的父节点即为总线
1.1 引入DTS的原因
对ARM平台的相关code做出如下相关规范调整,这个也正是引入DTS的原因: ARM的核心代码仍然保存在arch/arm目录下ARM SoC core architecture code保存在arch/arm目录下ARM SOC的周边外设模块的驱动保存在drivers目录下ARM SOC的特定代码在arch/arm/mach-xxx目录下ARM SOC board specific的代码被移除,由DeviceTree机制来负责传递硬件拓扑和硬件资源信息。 本质上,Device Tree改变了原来用hardcode方式将HW 配置信息嵌入到内核代码的方法,改用bootloader传递一个DB的形式。如果我们认为kernel是一个black box,那么其输入参数应该包括: 识别platform的信息runtime的配置参数设备的拓扑结构以及特性 对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外, 还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就是如此。1.2 文件格式
设备树相关文件均在 arch/arm/boot/dts/ 文件夹DTC 工具源码在 Linux 内核的 scripts/dtc 目录下DTC 工具依赖于 dtc.c、flattree.c、fstree.c 等文件,最终编译并链接出 DTC 这个主机文件在 arch/arm/boot/dts/Makefile 中新增需要编译的DTS文件。如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行如下命令: make all:编译 Linux 源码中的所有东西,包括 zImage,.ko 驱动模块以及设备 树make dtbs:仅编译设备树2. DTS语法
DTS中常用符号的含义2.1 .dtsi头文件
在 .dts 设备树文件中,可以通过“#include ”来引用 .h 、 .dtsi 和 .dts 文件。.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART 、 IIC 等等。2.2 设备节点
设备树是采用树形结构来描述板子上的设备信息的文件,每个设备都是一个节点,叫做设备节点,每个节点都通过一些属性信息来描述节点信息,属性就是 键值对。每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。.dts举例说明/ {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>;};}
“/”是根节点,每个设备树文件只有一个根节点。
2.2.1 设备树中节点命名格式
节点标签:节点名@设备的地址或寄存器首地址label: node-name@unit-address // e.g.:cpu0:cpu@0
2.2.2 设备树源码中常用的几种数据类型
1) 字符串compatible ="arm,cortex-a7";上述代码设置 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”。
2.2.3 标准属性
节点是由一堆属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性,Linux 下的很多外设驱动都会使用这些标准属性2.3 DTS的加载过程
如果要使用Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把这些信息组织成Device Tree source file。 通过DTC(Device Tree Compiler),可以将这些适合人类阅读的Device Tree source file变成适合机器处理的 Device Tree binary file(有一个更好听的名字,DTB,device tree blob)。在系统启动的时候,boot program (例如:firmware、bootloader)可以将保存在flash中的DTB copy到内存(当然也可以通过其他方式, 例如可以通过bootloader的交互式命令加载DTB,或者firmware可以探测到device的信息,组织成DTB保存在内存中), 并把DTB的起始地址传递给client program(例如OS kernel,bootloader或者其他特殊功能的程序)。 对于计算机系统(computer system),一般是firmware->bootloader->OS,对于嵌入式系统,一般是bootloader->OS。2.4 DTS的描述信息
设备树的组成:节点+属性
Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性, 其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):CPU的数量和类别内存基地址和大小总线和桥外设连接中断控制器和中断使用情况GPIO控制器和GPIO使用情况Clock控制器和Clock使用情况
CPU、总线和设备组成的树
它基本上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树, 并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源, 也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
只描述无法动态探测到的设备
是否Device Tree要描述系统中的所有硬件信息?答案是否定的。基本上,那些可以动态探测到的设备是不需要描述的, 例如USB device。不过对于SOC上的usb hostcontroller,它是无法动态识别的,需要在device tree中描述。同样的道理, 在computersystem中,PCI device可以被动态探测到,不需要在device tree中描述,但是PCI bridge如果不能被探测,那么就需要描述之。
.dtsi包含公共部分
.dts文件是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux中,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。由于一个SoC可能对应多个machine(一个SoC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分, Linux内核为了简化,把SoC公用的部分或者多个machine共同的部分一般提炼为.dtsi,类似于C语言的头文件。 其他的machine对应的.dts就include这个.dtsi。例如,对于RK3288而言, rk3288.dtsi就被rk3288-chrome.dts所引用, rk3288-chrome.dts有如下一行:#include“rk3288.dtsi”, 对于rtd1195,在 rtd-119x-nas.dts中就包含了/include/ ”rtd-119x.dtsi” 当然,和C语言的头文件类似,.dtsi也可以include其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi,即#include”skeleton.dtsi“ 或者 /include/ “skeleton.dtsi”
DTS合并
正常情况下所有的dts文件以及dtsi文件都含有一个根节点”/”,这样include之后就会造成有很多个根节点? 按理说 device tree既然是一个树,那么其只能有一个根节点,所有其他的节点都是派生于根节点的child node. 其实Device Tree Compiler会对DTS的node进行合并,最终生成的DTB中只有一个 root node.
节点
device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。 一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该node没有reg属性(后面会描述这个property), 那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于cpu, 其unit-address就是从0开始编址,以此加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。root node的node name是确定的,必须是“/”。 在一个树状结构的device tree中,如何引用一个node呢?要想唯一指定一个node必须使用full path,例如/node-name-1/node-name-2/node-name-N。
2.3 DTS举例
1个双核ARM Cortex-A9 32位处理器; ARM的local bus:上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、 GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10115000)、中断控制器(位于0x10140000)和一个external bus桥;External bus:桥上又连接了SMC SMC91111 Ethernet(位于0x10100000)、I2C控制器(位于0x10160000)、64MB NOR Flash(位于0x30000000);External bus桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58) 其对应的.dts文件为:/ {compatible = "acme,coyotes-revenge"; #address-cells = <1>; #size-cells = <1>; interrupt-parent = <&intc>; cpus {#address-cells = <1>; #size-cells = <0>; cpu@0 {compatible = "arm,cortex-a9"; reg = <0>; }; cpu@1 {compatible = "arm,cortex-a9"; reg = <1>; }; }; serial@101f1000 {compatible = "arm,pl011"; reg = <0x101f1000 0x1000 >; interrupts = < 1 0 >; }; serial@101f2000 {compatible = "arm,pl011"; reg = <0x101f2000 0x1000 >; interrupts = < 2 0 >; }; gpio@101f3000 {compatible = "arm,pl061"; reg = <0x101f3000 0x1000 0x101f4000 0x0010>; interrupts = < 3 0 >; }; intc: interrupt-controller@10140000 {compatible = "arm,pl190"; reg = <0x10140000 0x1000 >; interrupt-controller; #interrupt-cells = <2>; }; spi@10115000 {compatible = "arm,pl022"; reg = <0x10115000 0x1000 >; interrupts = < 4 0 >; }; external-bus {#address-cells = <2> #size-cells = <1>; // address-cells=<2> 一个是片选序号,另一个是片选序号上的偏移量,表示挂载在外部总线上,//需要通过片选线工作的一些模块// 片选0,偏移0,被映射到CPU地址空间的0x10100000~0x10110000中,地址长度为0x10000(第一行)ranges = <0 0 0x10100000 0x10000// Chipselect 1, Ethernet 1 0 0x10160000 0x10000// Chipselect 2, i2c controller 2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash ethernet@0,0 {compatible = "smc,smc91c111"; reg = <0 0 0x1000>; // 片选、偏移量、地址长度 (前2个cell为地址,后一个cell为长度)interrupts = < 5 2 >; }; i2c@1,0 {compatible = "acme,a1234-i2c-bus"; #address-cells = <1>; #size-cells = <0>; reg = <1 0 0x1000>; // 片选、偏移量、地址长度rtc@58 {compatible = "maxim,ds1338"; reg = <58>; interrupts = < 7 3 >; // 第一个值: 该中断位于它的中断控制器的索引// 第二个值:触发的type}; }; flash@2,0 {compatible = "samsung,k8f1315ebm", "cfi-flash"; reg = <2 0 0x4000000>; // 片选、偏移量、地址长度}; }; };
说明如下:
root结点”/”的compatible 属性
compatible = “acme,coyotes-revenge”; 定义了系统的名称, 它的组织形式为:, 。Linux内核通过过root结点"/"的compatible 属性即可判断它启动的是什么machineLinux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程:
每个设备的compatible属性
在.dts文件的每个设备,都有一个compatible属性,compatible属性用于驱动和设备的绑定compatible 属性是一个字符串的列表, 列表中的第一个字符串表征了结点代表的确切设备,形式为”,",其后的字符串表征可兼容的其他设备。可以说前面的是特指, 后面的则涵盖更广的范围
#address-cells和#size-cells
父结点的#address-cells和#size-cells分别决定了子结点的reg属性的address和length字段的长度在本例中,root结点的#address-cells = <1>;和#size-cells =<1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1cpus 结点的#address-cells= <1>;和#size-cells =<0>;决定了2个cpu子结点的address为1,而length为空, 于是形成了2个cpu的reg =<0>;和reg =<1>;external-bus结点的#address-cells= <2>和#size-cells =<1>; 决定了其下的ethernet、i2c、flash的reg字段形如reg = <0 0 0x1000>;、reg = <1 0 0x1000>;和reg = <2 0 0x4000000>;
ranges
ranges属性为一个地址转换表。表中的每一行都包含了子地址、父地址、在子地址空间内的区域大小。他们的大小(包含的cell) 分别由子节点的address-cells的值、父节点的address-cells的值和子节点的size-cells来决定。对于本例而言,子地址空间的#address-cells为2, 父地址空间的#address-cells值为1,因此0 0 0x10100000 0x10000的前2个cell为external-bus上片选0上偏移0, 第3个cell表示external-bus上片选0上偏移0的地址空间被映射到CPU的0x10100000位置,第4个cell表示映射的大小为0x10000。ranges的后面2个项目的含义可以类推。以第一行为例: 0 0 两个cell,由子节点external-bus的address-cells=<2>决定0x10100000 一个cell,由父节点的address-cells=<1>决定0x10000 一个cell,由子节点external-bus的size-cells=<1>决定 即第一行的含义:片选0,偏移量0(选中了网卡),被映射到CPU地址空间的0x10100000~0x10110000中,地址长度为0x10000
#address-cells= <1>;#size-cells= <1>;...external-bus{#address-cells = <2>#size-cells = <1>;ranges = <0 0 0x10100000 0x10000// Chipselect 1,Ethernet1 0 0x10160000 0x10000// Chipselect 2, i2c controller2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash};
节点名
node的名字自己可以随便定义,当然最好是见名知意,可以通过驱动程序打印当前使用的设备树节点 printk(“now dts node name is %s\n”,pdev->dev.of_node->name);
compatible
compatible选项是用来和驱动程序中的of_match_table指针所指向的of_device_id结构里的compatible字段匹配的,只有dts里的compatible字段的名字和驱动程序中of_device_id里的compatible字段的名字一样,驱动程序才能进入probe函数
3. 在系统中查看设备树
Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的 /proc/device-tree 目录下根据节点名字创建不同文件夹和文件,/proc/device-tree 目录就是设备树在根文件系统中的体现。cd /proc/device-treels# 即可查看所有的设备和属性
特殊节点 aliases 子节点:主要功能就是定义别名,定义别名的目的就是为了方便访问节点chosen 子节点:主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。bootargs 环境变量的值是在uboot 中设置的,而 uboot 中的 fdt_chosen 函数在设备树的 chosen 节点中加入了 bootargs 属性,并且还设置了 bootargs 属性值。