1 设备树起源
Linux 3.x之前,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,很多代码只是在描述板级细节,并且有大量重复性的描述,而这些板级细节对内核来讲并没有什么用。
设备树是一种描述硬件的数据结构,它起源于OpenFirmware(OF)。在Linux 2.6中,ARM架构的板级硬件细节过多地被硬编码在arch/arm/plat-xxx和arch/arm/mach-xxx中,采用设备树后,许多硬件的细节可以直接通过它传递给Linux,而不需要在内核里进行大量的冗余编码。
设备树由一系列被命名的节点(node)和属性(property)组成,而节点本身可包含子节点,属性就是成对出现的名称和值。在设备树中,可描述的信息包括(原先此类信息大多被硬编码到内核中):
- CPU的数量和类型;
- 内存基地址和大小;
- 总线和桥;
- 外设连接;
- 中断控制器和中断使用情况;
- GPIO控制器和GPIO使用情况;
- 时钟控制器和时钟使用情况;
2 设备树的目的
设备树是一个本质是一个描述硬件的数据结构,并不能解决所有的硬件配置问题。设备树只是给我们提供了一种语言,将板级细节从Linux内核源码中提取出来。设备树使得目标板和设备变成数据驱动的,它们必须基于传递给内核的数据进行初始化,而不是像以前一样采用硬编码的方式。理论上,这种方式可以带来较少的代码重复率,使单个内核镜像能够支持很多硬件平台。
Linux使用设备树主要有三个目的:
2.1 平台识别
Linux内核通过设备树中数据来识别特定的板卡。最完美的情况是内核与特定硬件平台无关,因为所有硬件平台的细节都由设备树来描述,然而硬件平台并不是完美的,所以内核需要在早期初始化阶段识别机器,这样内核才能有机会运行与特定机器相关的初始化序列。
内核和设备树通过compatible属性来进行匹配,当内核和设备树匹配成功后,内核会调用对应的probe函数;
内核中compatible:
static const struct of_device_id dw8250_of_match[] = {
{ .compatible = "snps,dw-apb-uart" },
{ .compatible = "cacium,octeon-3860-uart" },
{ }
};
设备树中的compatibe:
compatible = "snps,dw-apb-uart";
2.2 实时配置
NULL
2.3 设备植入
NULL
3 Linux设备树的组成和使用
3.1 基本数据格式
在Linux中,设备树文件的类型有 .dts 、.dtsi、.dtb,其中.dtsi是被包含的设备树源文件,类似C语言的头文件;.dts是设备树文件,可以包含其他.dtsi文件,然后由dtc编译生产.dtb文件,.dtb文件是二进制文件会在内核里被进一步转换为device_node,device_node又会转换成platform_device。
设备树是一个包含节点和属性的简单树状结构,属性就是键值对,节点可同时包含属性和子节点;
.dts结构:
/{
node1{
a-string-property = "A string";
a-string-list-property = "first string", "second string";
a-byte-data-property =[0x01 0x23 0x34 0x56];
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
{
};
};
};
该设备树包含了下面的内容:
- 1个root节点”/”;
- root节点下面含一系列子节点,“node1” and “node2”
- 节点node1和下又含有一系列子节点,“child-node1” and “child-node2”
- 一些分散在节点中的属性,属性是简单的键值对,它的值可以为空或者包含一个任意字节流:
- 空:an-empty-property
- 字符串:用双引号表示,如:a-string-property = "A string";
- 字符串列表:使用逗号连在一起,如:a-string-list-property = "first string", "second string";
- Cells:u32整数组成,用角括号限定,如:second-child-property = < 1 >;
- 二进制数据:用方括号限定,如:a-byte-data-property =[0x01 0x23 0x34 0x56];
- 混合表示:使用逗号连在一起;
3.2 DTS的组成
3.2.1 包含其他的".dtsi"文件:
#include "ks23xx.dtsi"
3.2.2 节点名称
一个节点的命名形式:
[label:] node-name[@unit-address] {
[properties definitions]
[child nodes]
};
方括号中的内容不是必须的,所以可以定义一个只有name的空节点,name是一个不超过31位的简单字符串 用于描述节点对应的设备类型;
如果一个节点描述的设备有地址,则应该给出@unit-address。多个相同类型设备节点的name可以一样,只要unit-address不同即可。对于挂在内存空间的设备而言,@字符后跟的一般就是该设备在内存空间的基地址。
多个节点可使用一样的通用节点,如:
serial@0a000000
serial@0a010000
3.2.3 compatible属性
系统中的每个设备都会被表示为一个设备树节点,系统通过compatible属性来找到节点对应的设备驱动。
compatible属性的形式为一个字符串列表,其中第一个字符串指定了确切的设备,剩下的字符串表示兼容的设备,通常形式为:compatible = "<厂商>,<型号>";
compatible = "snps,dw-apb-uart";
3.2.4 地址属性
reg
#address-cells // 子节点reg的address为几个32bit的整型数据
#size-cells // 长度为几个32bit整型数据,如果为0,则没有lenth字段
reg的组织形式为reg=<addr1 len1[addr2 len2] [addr3len3] ...>;
其中的每一组address length表明了设备使用的一个地址范围。address为1个或多个32位的整型(即cell),而length的意义则意味着从address到address+length–1的地址范围都属于该节点。若#size-cells=0,则length字段为空。
address和length字段是可变长的,父节点的#address-cells和#size-cells分别决定了子节点reg属性的address和length字段的长度。
soc {
#address-cells = <1>;
#size-cells = <1>;
serial {
compatible = "ns16550";
reg = <0x4600 0x100>;
clock-frequency = <0>;
interrupts = <0xA 0x8>;
interrupt-parent = <&ipic>;
};
};
3.2.4.1 Memory Mapped Devices
与在cpu节点中单独的address值不同,内存映射设备被分配了一系列将要响应的地址,因此不仅需要包含内存的基地址而且还需要映射地址的长度,因此需要使用#size-cells用来表示在每个子reg元组中长度字段的大小。在以下示例中,每个address值为1 cell(32 bits),每个长度值也是1 cell,这在32 bit系统是比较典型的。64 bit设备也许会为#address-cells和#size-cells使用数值2,在device tree中获取64 bit addressing。
/ {
#address-cells = <1>;
#size-cells = <1>;
...
serial@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 0x1000 >;
};
serial@101f2000 {
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
};
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
};
interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
};
...
};
3.2.4.2 Non Memory Mapped Devices
处理器总线的其它设备为非内存映射设备。他们有地址范围,但不能被CPU直接寻址。母设备的驱动程序将代替CPU进行间接访问。以i2c设备为例,每个设备都分配了一个地址,但没有长度或范围与之相匹配。这与CPU地址分配很相似。
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>;
};
};
3.2.4.2 Ranges (Address Translation)
我们已经讨论过如何向设备分配地址,但此时这些地址只是本地设备节点,还没有说明如何从那些地址里映射到cpu可以使用的地址。根节点经常描述地址空间的CPU视图。根节点的子节点已经使用了CPU的address domain,所以不需要任何明确的映射。例如,serial@101f0000设备被直接分配了地址0x101f0000。
根节点的非直接子节点是无法使用CPU的address domain的。为了在deivce tree获取内存映射地址必须指定如何从一个域名将地址转换到另一个。Ranges属性就用于此目的。以下是添加了ranges属性的device tree示例。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
...
external-bus {
#address-cells = <2>
#size-cells = <1>;
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>;
};
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>;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
Ranges是一个地址转换列表。每个输入ranges表格的是包含子地址的元组,母地址和子地址空间的范围大小。每个字段的大小都由获取的子地址的#address-cells值,母地址的#address-cell值和子地址的#size-cells值而定。以外部总线为例,子地址是2 cells,母地址是1 cell,大小也为1 cell。转换三个ranges:
Offset 0 from chip select 0 is mapped to address range 0x10100000…0x1010ffff
Offset 0 from chip select 1 is mapped to address range 0x10160000…0x1016ffff
Offset 0 from chip select 2 is mapped to address range 0x30000000…0x30ffffff
例如上面的总线是有片选的,就需要描述片选及片选的偏移量,在说明地址时,还需要说明地址映射范围。
3.2.5 中断属性
描述中断连接需要四个属性:
- interrupt-controller–这个属性为空,中断控制器应该加上此属性表明自己的身份;
- #interrupt-cells–与#address-cells和#size-cells相似,它表明连接此中断控制器的设备的中断属性的cell大小。#interrupt-cells属性设置为2表示每个中断描述符包含2个cells,一般第一个cells表示表示中断线号,第二个cells表示一个标记号,比如表示该中断是高电平触发、是低电平触发还是电平触发等等。对于所有给定的中断控制器,请参考控制器绑定文档以便获取对象中断编码。
- interrupt-parent–设备节点通过它来指定它所依附的中断控制器的phandle,当节点没有指定interrupt-parent时,则从父级节点继承。
- interrupts–用到了中断的设备节点,通过它指定中断号、触发方法等,这个属性具体含有多少个cell,由它依附的中断控制器节点的#interrupt-cells属性决定。而每个cell具体又是什么含义,一般由驱动的实现决定,而且也会在设备树的绑定文档中说明。
soc {
compatible = "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
open-pic {
clock-frequency = <0>;
interrupt-controller;
#address-cells = <0>;
#interrupt-cells = <2>;
};
pci {
#interrupt-cells = <1>;
#size-cells = <2>;
#address-cells = <3>;
interrupt-map-mask = <0xf800 0 0 7>;
interrupt-map = <
/ * IDSEL 0x11 - PCI slot 1* /
0x8800 0 0 1 &open-pic 2 1 / * INTA* /
0x8800 0 0 2 &open-pic 3 1 / * INTB* /
0x8800 0 0 3 &open-pic 4 1 / * INTC* /
0x8800 0 0 4 &open-pic 1 1 / * INTD* /
/ * IDSEL 0x12 - PCI slot 2* /
0x9000 0 0 1 &open-pic 3 1 / * INTA* /
0x9000 0 0 2 &open-pic 4 1 / * INTB* /
0x9000 0 0 3 &open-pic 1 1 / * INTC* /
0x9000 0 0 4 &open-pic 2 1 / * INTD* /
>;
};
};
3.2.6 clock属性和reset属性
clocks = <&clk_sys FCK_AP_UART0_CLK>, <&clk_sys FCK_AP_UART0_PCLK>;
clock-names = "baudclk", "apb_pclk";
resets = <&reset_sys FRST_APPERI_UART0>, <&reset_sys FRST_APPERI_UART0_APB>;
reset-names = "reset", "apb_reset";
3.2.7 GPIO属性
对于GPIO控制器而言,其对应的设备节点需声明gpio-controller属性,并设置#gpio-cells的大小。譬如,对于兼容性为fsl,imx28-pinctrl的pinctrl驱动而言,其GPIO控制器的设备节点类似于:
pinctrl @ 80018000
{
compatible = "fsl,imx28-pinctrl", "simple-bus";
reg = < 0x80018000 2000 >;
gpio0:gpio @ 0
{
compatible = "fsl,imx28-gpio";
interrupts = < 127 >;
gpio - controller;
#gpio -cells = <2>;
interrupt - controller;
#interrupt -cells = <2>;
};
gpio1:gpio @ 1
{
compatible = "fsl,imx28-gpio";
interrupts = < 126 >;
gpio - controller;
#gpio - cells = <2>;
interrupt - controller;
#interrupt -cells = <2>;
};
...
};
其中,#gpio- cells为2,第1个cell为GPIO号,第2个为GPIO的极性。为0的时候是高电平有效,为1的时候则是低电平有效。
使用GPIO的设备则通过定义命名xxx-gpios属性来引用GPIO控制器的设备节点,如:
sdhci@c8000400 {
status = "okay";
cd-gpios = <&gpio01 0>;
wp-gpios = <&gpio02 0>;
power-gpios = <&gpio03 0>;
bus-width = <4>;
};
3.2.8 aliases节点
aliases节点为了解决节点路径名过长的问题,引入了节点别名的概念,可以引用到一个全路径的节点。如/external-bus/ethernet@0,0,但当用户想知道具体内容的时候显得太累赘,“哪个设备是eth0?”
aliases {
ethernet0 = ð0;
serial0 = &serial0;
};
3.2.9 chosen节点
chosen节点并不代表一个真正的设备,只是一个为固件和操作系统传递数据的地方,如引导参数。chosen节点里的数据也不代表硬件。
chosen {
bootargs = "console=ttyS3,460800n8";
stdout-path = &uart3;
};
3.2.10 设备特定数据
用于定义特定于某个具体设备的一些属性,这些属性可以自由命名定义,但要注意避免和现有标准属性名相冲突。
3.3 常用of API
int of_device_is_compatible(const struct device_node *device,const char *compat);
此函数用于判断设备节点的兼容属性是否包含compat指定的字符串。这个API多用于一个驱动支持两个以上设备的时候。
读取属性:
- int of_property_read_u8_array(const struct device_node *np, const char *propname, u8 *out_values, size_t sz);
- int of_property_read_u16_array(const struct device_node *np, const char *propname, u16 *out_values, size_t sz);
- int of_property_read_u32_array(const struct device_node *np, const char *propname, u32 *out_values, size_t sz);
- int of_property_read_u64(const struct device_node *np, const char*propname, u64 *out_value);
读取设备节点np的属性名,为propname,属性类型为8、16、32、64位整型数组。对于32位处理器来讲,最常用的是of_property_read_u32_array()。
除了整型属性外,字符串属性也比较常用,其对应的API包括:
- int of_property_read_string(struct device_node *np, const char*propname,const char **out_string);
- int of_property_read_string_index(struct device_node *np, const char*propname,int index, const char **output);
除整型、字符串以外的最常用属性类型就是布尔型,其对应的API很简单:
static inline bool of_property_read_bool(const struct device_node *np, const char *propname);
如果设备节点np含有propname属性,则返回true,否则返回false。一般用于检查空属性是否存在。
内存映射:
void __iomem *of_iomap(struct device_node *node, int index);
上述API可以直接通过设备节点进行设备内存区间的ioremap(),index是内存段的索引。若设备节点的reg属性有多段,可通过index标示要ioremap()的是哪一段,在只有1段的情况,index为0。采用设备树后,一些设备驱动通过of_iomap()而不再通过传统的ioremap()进行映射,当然,传统的ioremap()的用户也不少。
int of_address_to_resource(struct device_node *dev, int index,struct resource *r);
上述API通过设备节点获取与它对应的内存资源的resource结构体。其本质是分析reg属性以获取内存基地址、大小等信息并填充到struct resource*r参数指向的结构体中。
解析中断:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
通过设备树获得设备的中断号,实际上是从.dts中的interrupts属性里解析出中断号。若设备使用了多个中断,index指定中断的索引号。