2.4 设备树简介
设备树规范(DTSpec)源自IEEE 1275标准——该标准主要用于解决通用计算机上某个单一版本的操作系统如何运行在隶属于同一计算家族的不同计算机上。设备树规范主要针对嵌入式系统,因此IEEE 1275标准的很多重要部分被删除,但是引导程序和客户程序的接口定义则被保留了下来。该接口定义允许引导程序描述并传递系统硬件信息给客户程序,客户程序因此可以避免对系统硬件描述做硬编码。
设备树规范定义了一个被称作设备树(DT)的结构来描述系统硬件。引导程序将设备树加载到客户程序的内存中并将指向该设备树的指针传递给客户程序。以下内容摘自设备树规范对设备树的定义:
设备树是一个由节点构成的树状数据结构,节点用于描述系统中的设备。每个节点通过属性/值对来描述其所表示的设备的特性。除了根节点没有父节点外,其他所有节点有且仅有一个父节点。
你可以从https://github.com/devicetree-org/devicetree-specification/releases下载完整的设备树规范,通过该规范你可以找到关于设备树结构与规范的详细描述。
从概念上来说,一组通用的使用规范(称为设备树绑定),定义了描述一个新设备的典型硬件特性时数据该如何在设备树中展示。对于设备必备属性的全面描述应该通过创建设备树绑定来实现。为了给Linux设备驱动提供关于设备的必要属性,设备树绑定中的属性描述必须充分。在嵌入式系统中,这些属性包括数据总线、中断线、GPIO连接、外设等。应该尽可能通过已有的设备树绑定来描述硬件,以最大化利用现有的支持代码。由于属性和节点名字都是纯文本,可以很容易地通过定义新的节点和属性来新建或者扩展已有的绑定。对于一般的绑定来说,这些绑定由设备树规范来描述,这些规范位于Linux软件组件文档中,例如Linux内核(https://www.kernel.org/doc/Documentation/devicetree/bindings/)以及U-Boot文档(https://github.com/ARMsoftware/u-boot/tree/master/doc/device-tree-bindings)。
在内核设备驱动开发过程中,设备树的compatible
属性是最为重要的。该属性包含一个或者多个字符串,每个字符串定义了设备兼容的特定编程模型。compatible
属性由一组以null结尾的字符串连接而成,按照“最精确”到“最通用”的规则排序。
设备树在内核代码中通过一组文本文件描述。在arch/arm/boot/dts/
中可以找到两种文件类型:
- *.dtsi文件是设备树的头文件。用来描述多个平台共用的硬件结构并被包含在这些平台的*.dts文件中。
- *.dts文件是设备树源文件。它们描述了某种具体的硬件平台。
在Linux中使用设备树主要有3个目的:
1. 平台区分:内核会通过设备树中的信息来识别机器类型。理想情况下,特定的平台不应该影响内核,因为所有的平台细节都会完美地被设备树以一致且可靠的方式描述。但是硬件并不完美,因此内核必须在启动早期识别出具体的机型,这样就有机会执行机型特有的硬件修复代码。大多数时候,机型并不重要,内核根据机型的CPU或者SoC来选择执行相应的启动代码。以ARM为例,arch/arm/kernel/setup.c
中的setup_arch()
函数会调用位于arch/arm/kernel/devtree.c
中的setup_machine_fdt()
函数。该函数会查找machine_desc
表并选择与设备树最为匹配的machine_desc
。通过比较设备树根节点的compatible
属性和定义在arch/arm/include/asm/mach/arch.h
中的machine_desc
数据结构的dt_compat
列表来选择最佳匹配。
compatible
属性包含了以确切机器名开始的一组有序字符串。比如,arch/arm/boot/dts
目录下的sama5d2.dtsi
文件包含了如下的compatible
属性:
仍然以ARM为例,对于每一个machine_desc
,内核会检查其dt_compat
列表中的条目是否出现在compatible
属性中。如果有的话,对应的machine_desc
就会作为驱动机器的一个候选。以arch/arm/mach-at91/sama5.c
文件中声明的sama5_alt_dt_board_compat[]
和DT_MACHINE_START
为例。它们用来填充一个machine_desc
结构。
当machine_descs
表被全部检索后,setup_machine_fdt()
函数返回一个最佳匹配的machine_desc
——依据machine_desc
与compatible
属性的哪一个条目相匹配来确定。如果没有匹配的machine_desc
,那么该函数返回NULL。在选择了machine_desc
之后,setup_machine_fdt()
还负责早期的设备树扫描。
2. 运行时配置:大多数情况下,设备树将作为u-boot与内核传递数据的唯一方法。所以内核参数、initrd镜像的位置等运行时配置项也可以通过设备树传递。这些数据一般存放在/chosen
节点,启动Linux内核时的代码看上去像这样:
bootargs
属性包含了内核参数,initrd-
开头的属性则定义了initrd数据块的起始地址和大小。在启动阶段的早期,分页机制还没有开启,setup_machine_fdt()
借助不同的辅助函数调用of_scan_flat_dt()
对设备树的数据进行扫描。of_scan_flat_dt()
扫描整个设备树,利用传入的辅助函数来提取启动阶段早期所需要的信息。辅助函数early_init_dt_scan_chosen()
主要用来解析包含内核启动参数的chosen节点,early_init_dt_scan_root()
则负责初始化设备树的地址空间模型,early_init_dt_scan_memory()
的调用决定了可用内存的位置和大小。
3. 设备填充:在机型识别完成并且解析完早期的配置信息之后,内核的初始化就可以按常规方式继续了。在这个过程中的某个时间点,unflatten_device_tree()
函数负责将设备树的数据转化为更为有效的运行时描述方式。在ARM平台上,这里也是机型特有的启用钩子(比如.init_early()
、.init_irq()
和.init_machine()
)被调用的地方。这些函数的目的可以从名字看出来,.init_early()
负责特定机型在启动早期需要执行的设置,.init_irq()
则负责中断处理相关的设置。
在设备树上下文中最有趣的钩子函数当属.init_machine()
,主要负责根据平台相关的信息填充Linux设备模型。设备列表可以通过解析设备树获取,然后动态地为这些设备分配device
数据结构。对于SAMA5D2处理器,.init_machine()
会调用sama5_dt_device_init()
,后者又会接着调用of_platform_populate()
函数。在arch/arm/mach-at91/sama5.c
中可以查看sama5_dt_device_init()
函数:
位于drivers/of/platform.c
的of_platform_populate()
函数遍历设备树的节点并创建相应的平台设备。of_platform_populate()
函数的第二个参数是一个of_device_id
类型的表,任何一个节点只要和该表中的某一个条目匹配的话,该节点的子节点也会被注册。