起源
在过去的 ARM Linux 中, arch/arm/plat-xxx 和 arch/arm/mach-xxx 中充斥着大量的垃圾代码,很多代码只是在描述板级细节,而这些板级细节对于内核来讲,不过是垃圾,如板上的 platform 设备、resource 、i2c_board_info 、spi_board_info 以及各种硬件的platform_data 。设备树是一种描述硬件的数据结构,它起源于 OpenFirmware ( OF )。
在 Linux 2.6 中, ARM 架构的板极硬件细节过多地被硬编码在 arch/arm/plat-xxx 和 arch/arm/mach-xxx 中,采用设备树后,许多硬件的细节可以直接通过它传递给 Linux ,而不再需要在内核中进行大量的冗余编码。
设备树由一系列被命名的节点( Node )和属性( Property )组成,而节点本身可包含子节点。所谓属性,其实就是成对出现的名称和值。在设备树中,可描述的信息包括(原先这些信息大多被硬编码在内核中):
·CPU 的数量和类别。
· 内存基地址和大小。
· 总线和桥。
· 外设连接。
· 中断控制器和中断使用情况。
·GPIO 控制器和 GPIO 使用情况。
· 时钟控制器和时钟使用情况。
它基本上就是画一棵电路板上 CPU 、总线、设备组成的树, Bootloader 会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出 Linux 内核中的 platform_device 、 i2c_client 、 spi_device 等设备,而这些设备用到的内存、 IRQ 等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
设备树的组成和结构
整个设备树牵涉面比较广,即增加了新的用于描述设备硬件信息的文本格式,又增加了编译这个文本的工具,同时 Bootloader 也需要支持将编译后的设备树传递给 Linux 内核。
DTS 、 DTC 和 DTB 等
1.DTS
文件 .dts 是一种 ASCII 文本格式的设备树描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在 ARM Linux 中,一个 .dts 文件对应一个 ARM 的设备,一般放置在内核的 arch/arm/boot/dts/ 目录中。值得注意的是,在 arch/powerpc/boot/dts 、arch/powerpc/boot/dts 、 arch/c6x/boot/dts 、 arch/openrisc/boot/dts 等目录中,也存在大量的 .dts 文件,这证明 DTS 绝对不是 ARM 的专利。
由于一个 SoC 可能对应多个设备(一个 SoC 可以对应多个产品和电路板),这些 .dts 文件势必须包含许多共同的部分, Linux 内核为了简化,把 SoC 公用的部分或者多个设备共同的部分一般提炼为 .dtsi ,类似于 C 语言的头文件。其他的设备对应的 .dts 就包括这个 .dtsi 。譬如,对于 VEXPRESS 而言, vexpress-v2m.dtsi 就被 vexpress-v2p-ca9.dts 所引用, vexpress-v2p-ca9.dts 有如下一行代码:
/include/ "vexpress-v2m.dtsi"
当然,和 C 语言的头文件类似, .dtsi 也可以包括其他的 .dtsi ,譬如几乎所有的 ARM SoC 的 .dtsi 都引用了 skeleton.dtsi 。文件 .dts (或者其包括的 .dtsi )的基本元素即为前文所述的节点和属性,代码清单 给出了一个设备树结构的模版。
上述 .dts 文件并没有什么真实的用途,但它基本表征了一个设备树源文件的结构:
1 个 root 节点 “/” ; root 节点下面含一系列子节点,本例中为 node1 和 node2 ;节点 node1 下又含有一系列子节点,本例中为 child-node1 和 child-node2 ;各节点都有一系列属性。这些属性可能为空,如 an-empty-property ;可能为字符串,如 a-string-property ;可能为字符串数组,如 a-string-list-property ;可能为 Cells (由 u32 整数组成),如 second-child-property ;可能为二进制数,如 a-byte-data-property 。
下面以一个最简单的设备为例来看如何写一个 .dts 文件。如图所示,假设此设备的配置如下:
1 个双核 ARM Cortex-A932 位处理器; ARM 本地总线上的内存映射区域分布有两个串口(分别位于 0x101F1000 和0x101F2000 )、 GPIO 控制器(位于 0x101F3000 )、 SPI 控制器(位于 0x10170000 )、中断控制器(位于 0x10140000 )和一个外部总线桥;外部总线桥上又连接了 SMC SMC91111 以太网(位于 0x10100000 )、 I 2 C 控制器(位于 0x10160000 )、 64MBNOR Flash (位于 0x30000000 );外部总线桥上连接的 I 2 C 控制器所对应的 I 2 C 总线上又连接了 Maxim DS1338 实时钟( I 2 C 地址为 0x58 )。
对于上图所示硬件结构图,如果用 “.dts” 描述,则其对应的 “.dts” 文件如代码清单所示:
DTC ( Device Tree Compiler )
DTC 是将 .dts 编译为 .dtb 的工具。 DTC 的源代码位于内核的 scripts/dtc 目录中,在 Linux 内核使能了设备树的情况下,编译内核的时候主机工具 DTC 会被编译出来,对应于 scripts/dtc/Makefile 中“hostprogs-y : =dtc” 这一 hostprogs 的编译目标。
在 Linux 内核的 arch/arm/boot/dts/Makefile 中,描述了当某种 SoC 被选中后,哪些 .dtb 文件会被编译出来,如与 VEXPRESS 对应的 .dtb 包括:
在 Linux 下,我们可以单独编译设备树文件。当我们在 Linux 内核下运行 make dtbs 时,若我们之前选择了 ARCH_VEXPRESS ,上述 .dtb 都会由对应的 .dts 编译出来,因为 arch/arm/Makefile 中含有一个 .dtbs 编译目标项目。
DTC 除了可以编译 .dts 文件以外,其实也可以 “ 反汇编 ”.dtb 文件为 .dts 文件,其指令格式为:
./scripts/dtc/dtc -I dtb -O dts -o xxx.dts arch/arm/boot/dts/xxx.dtb
DTB ( Device Tree Blob )
文件 .dtb 是 .dts 被 DTC 编译后的二进制格式的设备树描述,可由 Linux 内核解析,当然 U-Boot 这样的 bootloader 也是可以识别 .dtb的。
通常在我们为电路板制作 NAND 、 SD 启动映像时,会为 .dtb 文件单独留下一个很小的区域以存放之,之后 bootloader 在引导内核的过程中,会先读取该 .dtb 到内存。
Linux 内核也支持一种变通的模式,可以不把 .dtb 文件单独存放,而是直接和 zImage 绑定在一起做成一个映像文件,类似 cat zImage xxx.dtb>zImage_with_dtb 的效果。当然内核编译时候要使能 CONFIG_ARM_APPENDED_DTB 这个选项,以支持 “Use appended device tree blob to zImage” (见 Linux 内核中的菜单)。
绑定( Binding )
对于设备树中的节点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为 .txt 。在这个 .txt 文件中,需要描述对应节点的兼容性、必需的属性和可选的属性。这些文档位于内核的 Documenta tion/devicetree/bindings 目录下,其下又分为很多子目录。譬如, Documentation/devicetree/bindings/i2c/i2c-xiic.txt 描述了 Xilinx 的 I 2 C 控制器,其内容如下:
Linux 内核下的 scripts/checkpatch.pl 会运行一个检查,如果有人在设备树中新添加了 compatible 字符串,而没有添加相应的文档进行解释, checkpatch 程序会报出警告:UNDOCUMENTED_DT_STRINGDT compatible string xxx appears un-documented ,因此程序员要养成及时写 DT Binding 文档的习惯。
Bootloader
Uboot 设备从 v1.1.3 开始支持设备树,其对 ARM 的支持则是和 ARM 内核支持设备树同期完成。为了使能设备树,需要在编译 Uboot 的时候在 config 文件中加入:
#define CONfiG_OF_LIBFDT
在 Uboot 中,可以从 NAND 、 SD 或者 TFTP 等任意介质中将 .dtb 读入内存,假设 .dtb 放入的内存地址为 0x71000000 ,之后可在Uboot 中运行 fdt addr 命令设置 .dtb 的地址,如:
对于 ARM 来讲,可以通过 bootz kernel_addr initrd_address dtb_address 的命令来启动内核,即 dtb_address 作为 bootz 或者 bootm 的最后一次参数,第一个参数为内核映像的地址,第二个参数为 initrd 的地址,若不存在 initrd ,可以用 “-” 符号代替。
根节点兼容性
上述 .dts 文件中,第 2 行根节点 “/” 的兼容属性 compatible=“acme , coyotes-revenge” ;定义了整个系统(设备级别)的名称,它的组织形式为: < manufacturer > , < model > 。
Linux 内核通过根节点 “/” 的兼容属性即可判断它启动的是什么设备。在真实项目中,这个顶层设备的兼容属性一般包括两个或者两个以上的兼容性字符串,首个兼容性字符串是板子级别的名字,后面一个兼容性是芯片级别(或者芯片系列级别)的名字。
譬如板子 arch/arm/boot/dts/vexpress-v2p-ca9.dts 兼容于 arm , vexpress , v2p-ca9 和 “arm , vexpress” :
compatible = "arm,vexpress,v2p-ca9", "arm,vexpress";
板子 arch/arm/boot/dts/vexpress-v2p-ca5s.dts 的兼容性则为:
compatible = "arm,vexpress,v2p-ca5s", "arm,vexpress";
板子 arch/arm/boot/dts/vexpress-v2p-ca15_a7.dts 的兼容性为:
compatible = "arm,vexpress,v2p-ca15_a7", "arm,vexpress";
进一步地看, arch/arm/boot/dts/exynos4210-origen.dts 的兼容性字段如下:
compatible = "insignal,origen", "samsung,exynos4210", "samsung,exynos4";
第一个字符串是板子名字(很特定),第 2 个字符串是芯片名字(比较特定),第 3 个字段是芯片系列的名字(比较通用)。作为类比, arch/arm/boot/dts/exynos4210-universal_c210.dts 的兼容性字段则如下:
compatible = "samsung,universal_c210", "samsung,exynos4210", "samsung,exynos4";
由此可见,它与 exynos4210-origen.dts 的区别只在于第 1 个字符串(特定的板子名字)不一样,后面芯片名和芯片系列的名字都一样。
在 Linux 2.6 内核中, ARM Linux 针对不同的电路板会建立由 MACHINE_START 和 MACHINE_END 包围起来的针对这个设备的一系列回调函数
ARM Linux 2.6 时代的设备:
这些不同的设备会有不同的 MACHINE ID , Uboot 在启动 Linux 内核时会将 MACHINE ID 存放在 r1 寄存器, Linux 启动时会匹配 Bootloader 传递的 MACHINE ID 和 MACHINE_START 声明的 MACHINE ID ,然后执行相应设备的一系列初始化函数。
ARM Linux 3.x 在引入设备树之后, MACHINE_START 变更为 DT_MACHINE_START ,其中含有一个 .dt_compat成员,用于表明相关的设备与 .dts 中根节点的兼容属性兼容关系。如果 Bootloader 传递给内核的设备树中根节点的兼容属性出现在某设备的 .dt_compat 表中,相关的设备就与对应的兼容匹配,从而引发这一设备的一系列初始化函数被执行。
ARM Linux 3.x 时代的设备:
Linux 倡导针对多个 SoC 、多个电路板的通用 DT 设备,即一个 DT 设备的 .dt_compat 包含多个电路板 .dts 文件的根节点兼容属性字符串。之后,如果这多个电路板的初始化序列不一样,可以通过 intof_machine_is_compatible ( const char*compat ) API 判断具体的电路板是什么。在 Linux 内核中,常常使用如下 API来判断根节点的兼容性:
int of_machine_is_compatible(const char *compat);
此 API 判断目前运行的板子或者 SoC 的兼容性,它匹配的是设备树根节点下的兼容属性。例如drivers/cpufreq/exynos-cpufreq.c 中就有判断运行的 CPU 类型是 exynos4210 、 exynos4212 、 exynos4412 还是exynos5250 的代码,进而分别处理
of_machine_is_compatible ()的案例:
如果一个兼容包含多个字符串,譬如对于前面介绍的根节点兼容compatible=“samsung , universal_c210” , “samsung , exynos4210” , “samsung , exynos4” 的情况,如下 3 个表达式都是成立的。
设备节点兼容性
在 .dts 文件的每个设备节点中,都有一个兼容属性,兼容属性用于驱动和设备的绑定。兼容属性是一个字符串的列表,列表中的第一个字符串表征了节点代表的确切设备,形式为 “< manufacturer > , < model >” ,其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。如在 vexpress-v2m.dtsi 中的 Flash 节点如下:
再如, Freescale MPC8349SoC 含一个串口设备,它实现了国家 半导体( National Sem-iconductor )的 NS16550 寄存器接口。则 MPC8349 串口设备的兼容属性为 compatible=“fsl , mpc8349-uart” , “ns16550” 。其中, fsl , mpc8349-uart 指代了确切的设备, ns16550 代表该设备与 NS16550UART 保持了寄存器兼容。因此,设备节点的兼容性和根节点的兼容性是类似的,都是 “ 从具体到抽象 ” 。
使用设备树后,驱动需要与 .dts 中描述的设备节点进行匹配,从而使驱动的 probe ()函数执行。对于platform_driver 而言,需要添加一个 OF 匹配表,如前文的 .dts 文件的 “acme , a1234-i2c-bus” 兼容 I 2 C 控制器节点的OF 匹配表
platform 设备驱动中的 of_match_table:
其中的of_match_table结构体为:
对于 I 2 C 和 SPI 从设备而言,同样也可以通过 of_match_table 添加匹配的 .dts 中的相关节点的兼容属性,如sound/soc/codecs/wm8753.c 中的针对 WolfsonWM8753 的 of_match_table
I 2 C 、 SPI 设备驱动中的 of_match_table:
对于 I 2 C 、 SPI 还有一点需要提醒的是, I 2 C 和 SPI 外设驱动和设备树中设备节点的兼容属性还有一种弱式匹配方法,就是 “ 别名 ” 匹配。兼容属性的组织形式为 < manufacturer > , < model > ,别名其实就是去掉兼容属性中manufacturer 前缀后的 < model > 部分。关于这一点,可查看 drivers/spi/spi.c 的源代码,函数 spi_match_device ()暴露了更多的细节,如果别名出现在设备 spi_driver 的 id_table 里面,或者别名与 spi_driver 的 name 字段相同, SPI 设备和驱动都可以匹配上,代码清单显示了 SPI 的别名匹配。
SPI 的别名匹配:
一个驱动可以在 of_match_table 中兼容多个设备,在 Linux 内核中常常使用如下 API 来判断具体的设备是什么:
当一个驱动支持两个或多个设备的时候,这些不同 .dts 文件中设备的兼容属性都会写入驱动 OF 匹配表。因此驱动可以通过 Bootloader 传递给内核设备树中的真正节点的兼容属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。如 arch/powerpc/platforms/83xx/u***.c 中的 mpc831x_u***_cfg ()就进行了类似处理:
|