此文介绍Linux的设备树使用模范。
Open Firmware 设备树是用于描述硬件的数据结构和语言。
他是一种对硬件的描述,此描述是可被操作系统读的,所以OS不需要硬编码机器的详细信息。
从结构上看,DT是一个命名节点构成的树,节点可能有任意数量的属性,属性可以包含任意数量的数据。存在一种机制,可以创建任意连接,从一个节点到树结构外的任意节点。
通常,被成为 binding 的常见使用惯例的集合会被定义,用于描述数据应该如何显示在树中,以描述典型的硬件特征,包括总线数据,中断线,GPIO连接,外围设备。
尽可能的,使用已有的binding描述硬件,以最大化利用现有代码。但是因为属性和名称是简单字符串,所以很容易扩展现有binding,或创建新binding,通过定义新节点,和新属性。
创建binding时要小心,先检查下是否已经存在了。
1. 历史
DT最初被Open Firmware创建,作为通信工具的一部分,用于从Open Firmware到客户程序(如OS)传输数据。OS使用DT以发现运行时的硬件拓扑结构,因此不使用硬编码也能支持大部分硬件。
05年时,PowerPC Linux开始整理和合并32 64位支持,决定让DT支持所有powerpc平台,不论是否使用Open Firmware。为实现这点,一种平铺型DT被创建(FDT),FDT能被传输给kernel以二进制,不需要open firmware实列, u-boot等bootloader被修改以支持传输dtb和在启动时修改dtb。
后来,FDT广泛用于所有架构。
2. 数据模型
建议先读
http://devicetree.org/Device_Tree_Usage
2.1 整体描述
Linux 使用 DT以获得三个东西:
- 平台鉴定
- 运行时配置
3)获得设备信息
2.2 平台鉴定
kernel在启动前,使用 DT数据确定具体的机器。
平台需要将自己的详细信息写到DT 。
kernel会根据机器的SoC选择启动代码。
如arm,
setup_arch
setup_machine_fdt // 遍历 machine_desc[],选择和DT数据的最佳匹配的 matches_desc
// 通过找根节点的'compatible'数据以确定最佳匹配
// 将此属性和 matches_desc->dt_compat链表比较
compatible属性定义有序string链表,第一个元素是正确机器名字,后面是可选的板子列表,整个兼容性是递减的。
compatible = "ti,omap3-beagleboard", "ti,omap3450", "ti,omap3";
compatible = "ti,omap3-beagleboard-xm", "ti,omap3450", "ti,omap3";
需要注意,对于 compatible 属性的所有字符串,都需要写文档以说明其作用,文档添加在 Documentation/devicetree/bindings
2.3 运行时配置
DT能在运行时传输配置数据,如kernel参数,initrd位置。
大部分数据被包含在/chosen节点内,Linux启动时可能看见如下东西
chosen {
bootargs = "console=ttyS0,115200 loglevel=8";
initrd-start = <0xc8000000>;
initrd-end = <0xc8200000>;
};
注意 initrd-end 不是哨兵位置。
chosen节点能包含任意数量的其他属性。这些属性包含平台专属配置信息。
在boot早期阶段,架构启动代码调用 of_scan_flat_dt 多次,使用不同的helper回调,解析设备树, of_scan_flat_dt 扫描整个设备树,使用 helper 提取早期启动阶段需要的信息。
典型的,early_init_dt_scan_chosen 这个helper 用于解析 包含kernel参数的chosen 节点,early_init_dt_scan_root 这个helper用于初始化 DT 地址空间,early_init_dt_scan_memory 这个用于确定可用的内存大小和起始位置。
在arm中,setup_machine_fdt 有责任对设备树进行早期扫描,在选择了正确的 machine_desc 后。
2.4 设备信息
当板子被确定,早期配置被解析后,kernel开始普通初始化。在此阶段的一些时刻,unflatten_device_tree 被调用,以转换DT数据为运行更有效的形式。这时机器专属的hook会被使用,如 machine_desc .init_early(), .init_irq() and .init_machine() arm架构时,其他架构也类似。
根据名称可以猜到,.init_early() 用于在引导过程的早期执行任何特定于机器的设置,而 .init_irq() 则用于设置中断处理。使用设备树不会实质性地改变这两个函数的行为。如果提供了设备树,则 .init_early() 和 .init_irq() 都能够调用任何设备树查询函数(include/linux/of.h 中的 of_ 函数)来获取有关平台的其他数据。这种方法可以更灵活地适应不同的平台,并使设备树能够更好地管理硬件资源。
在设备树上下文中,最有趣的钩子是 .init_machine(),它主要负责向 Linux 设备模型填充关于平台的数据。在嵌入式平台上,这通常通过在板支持的 .c 文件中定义一组静态时钟结构、平台设备和其他数据,并在 .init_machine() 中批量注册它们来实现。当使用设备树时,不再为每个平台硬编码静态设备,而是可以通过解析设备树来获取设备列表,并动态地分配设备结构。这种方法可以更灵活地适应不同的平台,并且使设备树能够更好地管理硬件资源。
最简单的情况是 .init_machine() 仅负责注册一块 platform_device。Linux 中的 platform_devide 是指无法通过硬件检测到的内存或 I/O 映射设备,以及“组合”或“虚拟”设备(稍后会详细介绍)。虽然在设备树中没有“platform device”术语,但platform device可以和设备树的 device node 大致对应。
示例
/{
compatible = "nvidia,harmony", "nvidia,tegra20";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>;
chosen { };
aliases { };
memory {
device_type = "memory";
reg = <0x00000000 0x40000000>;
};
soc {
compatible = "nvidia,tegra20-soc", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
ranges;
intc: interrupt-controller@50041000 {
compatible = "nvidia,tegra20-gic";
interrupt-controller;
#interrupt-cells = <1>;
reg = <0x50041000 0x1000>, < 0x50040100 0x0100 >;
};
serial@70006300 {
compatible = "nvidia,tegra20-uart";
reg = <0x70006300 0x100>;
interrupts = <122>;
};
i2s1: i2s@70002800 {
compatible = "nvidia,tegra20-i2s";
reg = <0x70002800 0x100>;
interrupts = <77>;
codec = <&wm8903>;
};
i2c@7000c000 {
compatible = "nvidia,tegra20-i2c";
#address-cells = <1>;
#size-cells = <0>;
reg = <0x7000c000 0x100>;
interrupts = <70>;
wm8903: codec@1a {
compatible = "wlf,wm8903";
reg = <0x1a>;
interrupts = <347>;
};
};
};
sound {
compatible = "nvidia,harmony-sound";
i2s-controller = <&i2s1>;
i2s-codec = <&wm8903>;
};
};
在 .init_machine() 阶段,Tegra 板级支持代码需要查看该设备树并决定哪些节点需要创建 platform_devices。然而,仅从树形结构中看并不明显每个节点代表的设备类型,甚至有些节点并不代表设备。/chosen、/aliases 和 /memory 节点是信息节点,它们并不描述设备(尽管可以认为内存是设备)。/soc 节点的子节点是内存映射设备,但 codec@1a 是一个 I2C 设备,而 sound 节点并不代表设备,而是代表其他设备如何连接在一起以创建音频子系统。我知道每个设备的情况是因为我熟悉板子设计,但是内核如何知道该对每个节点做什么呢?
关键在于内核从树的根开始查找具有“compatible”属性的节点。首先,通常假定具有“compatible”属性的任何节点都表示某种设备,其次,可以假定树的根节点要么直接连接到处理器总线,要么是其他无法以其他方式描述的杂项系统设备。对于这些节点,Linux会分别分配并注册platform_device,这些设备可能会绑定到平台驱动程序。
为什么将这些节点使用 platform_device 是一个安全的假设呢?
嗯,对于 Linux 设备建模的方式,几乎所有的 bus_type 都假定它的设备是一个总线控制器的子设备。例如,每个 i2c_client 是一个 i2c_master 的子设备。每个 spi_device 是一个 SPI 总线的子设备。同样的道理适用于 USB、PCI、MDIO 等。在 DT 中也可以找到相同的层次结构,其中 I2C 设备节点只会出现在 I2C 总线节点的子节点中。SPI、MDIO、USB 等也是如此。唯一不需要特定类型的父设备的设备是 platform_device(以及 amba_device,但稍后再说),它们可以很愉快地生活在 Linux /sys/devices 树的底部。因此,如果一个 DT 节点位于树的根部,那么最好将其注册为 platform_device。
Linux板支持代码调用of_platform_populate(NULL, NULL, NULL, NULL)开启对树根设备的发现过程。这些参数都是NULL,因为从树的根开始时,不需要提供起始节点(第一个NULL),父struct device(最后一个NULL),并且我们还没有使用匹配表。对于仅需要注册设备的板子,.init_machine()可以完全为空,除了of_platform_populate()调用。
在Tegra示例中,这解释了/soc和/sound节点,但是SoC节点的子节点呢?它们不也应该被注册为平台设备吗?对于Linux DT支持,一般的行为是在父设备驱动程序的驱动程序.probe()时间注册子设备。因此,i2c总线设备驱动程序将为每个子节点注册i2c_client,SPI总线驱动程序将注册其spi_device子节点,其他总线类型也类似。
比如,根据这个模型,可以编写一个驱动程序,将绑定到SoC节点并为其每个子节点注册平台设备。板支持代码将分配和注册一个SoC设备,一个(理论上的)SoC设备驱动程序可以绑定到SoC设备,并在其.probe()挂钩中为/soc/interrupt-controller、/soc/serial、/soc/i2s和/soc/i2c注册平台设备。很容易,对吧?
实际上,事实证明将某些platform_devices的子设备注册为更多的platform_devices是一种常见的模式,设备树支持代码也反映了这一点,使上述示例更加简单。of_platform_populate()的第二个参数是一个of_device_id表,与该表中条目匹配的任何节点也将注册其子节点。对于Tegra案例,代码可能如下所示:
static void __init harmony_init_machine(void)
{
/* ... */
of_platform_populate(NULL, of_default_bus_match_table, NULL, NULL);
}
“simple-bus” 在 Devicetree 规范中定义为表示简单的内存映射总线的属性,因此 of_platform_populate() 代码可以被编写成仅假定带有 simple-bus compatible节点将始终被遍历。但是,我们将其作为参数传递,以便板级支持代码始终可以覆盖默认行为。
为什么不转换为AMBA设备
ARM Primecell 是一种连接到 ARM AMBA 总线的设备,其中包括一些硬件检测和电源管理的支持。在 Linux 中,使用 struct amba_device 和 amba_bus_type 来表示 Primecell 设备。但是,问题在于并非 AMBA 总线上的所有设备都是 Primecell,对于 Linux 而言,在同一总线段上,amba_device 和 platform_device 实例通常是同级的。
当使用DT时,这会给of_platform_populate()带来问题,因为它必须决定是否将每个节点注册为platform_device或amba_device。不幸的是,这会使设备创建模型变得有点复杂,但解决方案实际上不会太影响。如果一个节点与"arm,amba-primecell"兼容,则of_platform_populate()将其注册为amba_device,而不是platform_device。