自Linux3.x版本后,arch/arm/plat-xxx和arch/arm/mach-xxx中,描述板级细节的代码(比如platform_device、i2c_board_info等)被大量取消,取而代之的是设备树,其目录位于arch/arm/boot/dts,今天来详细分析一下设备树。
设备树由1个dts文件+n个dtsi文件,它们编译而成的dtb二进制可执行文件就是真正的设备树文件。
.dts文件: 是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux在,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录
dtsi文件: soc厂商会把soc公共的特性和多块开发板公用的特性提炼为dtsi,而dts则负责描述某个具体的产品(开发板)的特性。dts直接或间接的包含多个dtsi(类似于c语言的头文件,使用include包含),就体现了一个完整的产品(开发板)所有的特性
dtb文件: dtb(Device Tree Blob),dts经过dtc编译之后会得到dtb文件,dtb通过Bootloader引导程序加载到内核。所以Bootloader需要支持设备树才行;Kernel也需要加入设备树的支持
Device Tree由一系列被命名的结点(node)和属性(property)组成,而结点本身可包含子结点。所谓属性,其实就是成对出现的name和value。在Device Tree中,可描述的信息包括(原先这些信息大多被hard code到kernel中):
CPU的数量和类别
内存基地址和大小
总线和桥
外设连接
中断控制器和中断使用情况
GPIO控制器和GPIO使用情况
Clock控制器和Clock使用情况
它本质上就是画一棵电路板上CPU、总线、设备组成的树,Bootloader会将这棵树传递给内核,然后内核可以识别这棵树,并根据它展开出Linux内核中的platform_device、i2c_client、spi_device等设备,而这些设备用到的内存、IRQ等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
下面根据一段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";
status = "okay";
};
child-node2 {
};
};
node2 {
an-empty-property;
a-cell-property = <1 2 3 4>; /* each number (cell) is a uint32 */
child-node1 {
status = "okay";
};
};
};
上述.dts文件并没有什么真实的用途,但它基本表征了一个Device Tree源文件的结构:
1、1个root结点"/";
2、root结点下面含一系列子结点,本例中为"node1" 和 “node2”;
3、结点"node1"下又含有一系列子结点,本例中为"child-node1" 和 “child-node2”;
4、status = “okay”:
在devicetree的节点下面可以方便的通过status这个变量来使能或者disable driver的probe
这样在driver的probe函数中可以通过of_device_is_available 来判断probe函数是否要继续下去
if (!of_device_is_available(node2)) {
ret = -ENODEV;
goto err_put_node;
}
各结点都有一系列属性。这些属性可能为空,如" an-empty-property";可能为字符串,如"a-string-property";可能为字符串数组,如"a-string-list-property";可能为Cells(由u32整数组成),如"second-child-property",可能为二进制数,如"a-byte-data-property"。
下面以一个最简单的machine为例来看如何写一个.dts文件。假设此machine的配置如下:
1个双核ARM Cortex-A9 32位处理器;
ARM的local bus上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10170000)、中断控制器(位于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@101f0000 {
compatible = "arm,pl011";
reg = <0x101f0000 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>;
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>;
interrupts = < 5 2 >;
};
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;
#size-cells = <0>;
reg = <1 0 0x1000>;
interrupts = < 6 2 >;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
上述.dts文件中,root结点"/“的compatible 属性compatible = “acme,coyotes-revenge”;定义了系统的名称,它的组织形式为:,。
Linux内核透过root结点”/“的compatible 属性即可判断它启动的是什么machine。
在.dts文件的每个设备,每个节点都有一个compatible 属性,compatible属性用户驱动和设备的绑定。compatible 属性是一个字符串的列表。
列表中的第一个字符串表征了结点代表的确切设备,形式为”,",其后的字符串表征可兼容的其他设备。
可以说前面的是特指,后面的则涵盖更广的范围。如在arch/arm/boot/dts/vexpress-v2m.dtsi中的Flash结点:
flash@0,00000000 {
compatible = "arm,vexpress-flash", "cfi-flash";
reg = <0 0x00000000 0x04000000>,
<1 0x00000000 0x04000000>;
bank-width = <4>;
};
compatible属性的第2个字符串"cfi-flash"明显比第1个字符串"arm,vexpress-flash"涵盖的范围更广。
再比如,Freescale MPC8349 SoC含一个串口设备,它实现了国家半导体(National Semiconductor)的ns16550 寄存器接口。则MPC8349串口设备的compatible属性为compatible = “fsl,mpc8349-uart”, “ns16550”。其中,fsl,mpc8349-uart指代了确切的设备, ns16550代表该设备与National Semiconductor 的16550 UART保持了寄存器兼容。
cpus {
#address-cells = <1>;
#size-cells = <0>;
cpu@0 {
compatible = "arm,cortex-a9";
reg = <0>;
};
cpu@1 {
compatible = "arm,cortex-a9";
reg = <1>;
};
};
接下来root结点"/“的cpus子结点下面又包含2个cpu子结点,描述了此machine上的2个CPU,并且二者的compatible 属性为"arm,cortex-a9”。
注意cpus和cpus的2个cpu子结点的命名,它们遵循的组织形式为:
<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述结点对应的设备类型,如3com Ethernet适配器对应的结点name宜为ethernet,而不是3com509。
如果一个结点描述的设备有地址,则应该给出@unit-address。
多个相同类型设备结点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点。
设备的unit-address地址也经常在其对应结点的reg属性中给出。ePAPR标准给出了结点命名的规范:
reg
#address-cells
#size-cells
reg: reg的组织形式为reg =
#address-cells: 为1个或多个32位的整型(即cell),基地址、片选号等绝对起始地址所占字长
#size-cells: 寄存器地址所占字长。
address 和 length 字段是可变长的,父结点的#address-cells和#size-cells分别决定了子结点的reg属性的address和length字段的长度。
在本例中,root结点的#address-cells = <1>;和#size-cells = <1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1。
cpus 结点的#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>;
开始的第一个cell(0、1、2)是对应的片选,第2个cell(0,0,0)是相对该片选的基地址,第3个cell(0x1000、0x1000、0x4000000)为length。特别要留意的是i2c结点中定义的 #address-cells = <1>;和#size-cells = <0>;又作用到了I2C总线上连接的RTC,它的address字段为0x58,是设备的I2C地址。
root结点的子结点描述的是CPU的视图,因此root子结点的address区域就直接位于CPU的memory区域。但是,经过总线桥后的address往往需要经过转换才能对应的CPU的memory映射。external-bus的ranges属性定义了经过external-bus桥后的地址范围如何映射到CPU的memory区域。
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2, i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR Flash
ranges是地址转换表,其中的每个项目是一个子地址、父地址以及在子地址空间的大小的映射。
映射表中的子地址、父地址分别采用子地址空间的#address-cells和父地址空间的#address-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个项目的含义可以类推。
Device Tree中还可以中断连接信息,对于中断控制器而言,它提供如下四个属性:
1、interrupt-controller : 这个属性为空,中断控制器应该加上此属性表明自己的身份;
2、#interrupt-cells : 与#address-cells 和 #size-cells相似,它表明连接此中断控制器的设备的interrupts属性的cell大小。
在整个Device Tree中,与中断相关的属性还包括:
3、interrupt-parent : 设备结点透过它来指定它所依附的中断控制器的phandle,当结点没有指定interrupt-parent 时,则从父级结点继承。对于本例而言,root结点指定了interrupt-parent = <&intc>;其对应于intc: interrupt-controller@10140000,而root结点的子结点并未指定interrupt-parent,因此它们都继承了intc,即位于0x10140000的中断控制器。
4、interrupts :用到了中断的设备结点透过它指定中断号、触发方法等,具体这个属性含有多少个cell,由它依附的中断控制器结点的#interrupt-cells属性决定。而具体每个cell又是什么含义,一般由驱动的实现决定,而且也会在Device Tree的binding文档中说明。如果有两个,第一个是中断号,第二个是中断类型,如高电平、低电平、边缘触发等触发特性。对于给定的中断控制器,应该仔细阅读相关文档来确定其中断标识该如何解析。
实例:
第一个值: 该中断位于他的中断控制器的索引;
第二个值:触发的type
固定的取值如下:
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active high level-sensitive
8 = active low level-sensitive
第一个值:中断号
第二个值:触发的类型
第三个值:优先级,0级是最高的,7级是最低的;其中0级的中断系统当做 FIQ处理。
另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而言都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;
除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述。
节点与节点之间的关联,通常通过“标号引用”和“包含”来实现
标号引用常常还作为节点的重写方式
1)phandle方式引用:
pic@10000000 {
phandle = <1>;
interrupt-controller;
};
another-device-node {
interrupt-parent = <1>; // 使用phandle值为1来引用上述节点
};
这种方式要自己确认,在设备树文件中phandle = <1>这个常量只能取值一次。
2)label 方式引用
标号引用,就是在节点名称前加上标号,这样设备树的其他位置就能够通过&符号来调用/访问该节点
引用的模块在该模块之前无需加’&’,而在其之后或者外面声明的,需要添加’&’
[label:] node-name[@unit-address] { //冒号前的label是为了方便引用给节点起的别名,此label一般使用为&label
[properties definitions] //就是属性定义,对当前节点描述,将硬件信息提供给内核处理
[child nodes] //子节点
}
PIC2: pic@11000000 {
interrupt-controller;
};
another-device-node {
interrupt-parent = <&PIC>; // 使用label来引用上述节点,
interrupt-parent = <&PIC2>; // 使用label来引用上述节点,
// 使用lable时实际上也是使用phandle来引用,
// 在编译dts文件为dtb文件时, 编译器dtc会在dtb中插入phandle属性
};
&PIC: pic@10000000 {
interrupt-controller;
};
这里的label方式其实原理和phandle方式是一样的,只不过lable对于我们使用来说更好辨认。dtc在编译的时候会在使用label的节点中增加一个phandle的属性,增加一个唯一的value,并把使用它的位置替换为该value。
覆盖规则:
同一层次的节点,后面的会覆盖前面的节点。
memory@30000000 {
device_type = "memory";
reg = <0x30000000 0x20000000>;
};
memory@30000000 {
reg = <0x30000000 0x10000000>;
};
直接引用方式覆盖(增加)节点属性:
假设下面节点定义在dtsi文件中
xusbxti: oscillator@1 {
compatible = "fixed-clock";
reg = <1>;
clock-frequency = <0>;
clock-output-names = "xusbxti";
#clock-cells = <0>;
};
某个dis文件包含了该dtsi文件,并定义了如下内容
&xusbxti {
clock-frequency = <24000000>;
};
在android中一般使用一下API来操作DTS中的硬件资源
常用的API
#include
#include
enum of_gpio_flags {
OF_GPIO_ACTIVE_LOW = 0x1,
};
int of_get_named_gpio_flags(struct device_node *np, const char *propname,
int index, enum of_gpio_flags *flags);
int gpio_is_valid(int gpio);
int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(int gpio);
int gpio_direction_output(int gpio, int v);
其中:
of_get_named_gpio:驱动中使用gpio号 = of_get_named_gpio(xxxnode, “reset-gpios”, 0);函数返回值来得到gpio号
gpio_request: 驱动中要想使用某一个gpio,就必须先调用gpio_request接口来向内核申请,得到允许后才可以去使用这个gpio
gpio_free:对应gpio_request,如果初始化过程出错,需要调用 gpio_free 来释放之前申请过且成功的 GPIO 。
gpiochip_is_requested: 接口用来判断某一个gpio是否已经被申请了
gpio_direction_input/gpio_direction_output: 接口用来设置GPIO为输入/输出模式(不推荐直接设置寄存器)
/*之前先要完成驱动的注册工作*/
...
ret = gpio_request(S5PV210_GPJ0(3), "led1_gpj0_3");
if (ret){
printk(KERN_INFO "gpio_request failed\n");
goto out_err_1;
}
/*申请完后可以利用接口设置该gpio。也可以直接操作寄存器来设置*/
gpio_direction_output(S5PV210_GPJ0(3), 1);
/*在remove函数中别忘了对应的释放操作*/
gpio_free(S5PV210_GPJ0(3));
of_get_named_gpio_flags :从设备树中读取 gpio 的 GPIO 配置编号和标志
gpio_is_valid :判断该 GPIO 编号是否有效
gpio_direction_output :在驱动中调用 gpio_direction_output 就可以设置输出高还是低电平,这里默认输出从DTS获取得到的有效电平GPIO_ACTIVE_HIGH,即为高电平。如果驱动正常工作,可以用万用表测得对应的引脚应该为高电平。
实际中如果要读出 GPIO,需要先设置成输入模式,然后再读取值:
int val;
gpio_direction_input(your_gpio);
val = gpio_get_value(your_gpio);
gpio_set_value(your_gpio,val);
内核中提供了虚拟文件系统debugfs,里面有一个gpio文件,提供了gpio的使用信息
使用 mount -t debugfs debugfs /tmp 把debugfs挂接到/tmp下,再重新进入/tmp后就能看到一个名为gpio的文件
cat /tmp/gpio即可得到gpio的所有信息,使用完后umount /tmp卸载掉debugfs
中断类型:
IRQ_TYPE_NONE //默认值,无定义中断触发类型
IRQ_TYPE_EDGE_RISING //上升沿触发
IRQ_TYPE_EDGE_FALLING //下降沿触发
IRQ_TYPE_EDGE_BOTH //上升沿和下降沿都触发
IRQ_TYPE_LEVEL_HIGH //高电平触发
IRQ_TYPE_LEVEL_LOW //低电平触发
中断的设置与获取
/*先确定中断所在的组*/
interrupt-parent = <&gpio6>;
/*表示中断,GPIO6中的第8个IO,2为触发类型,下降沿触发*/
interrupts = <8 2>;
在驱动中使用 中断号 =irq_of_parse_and_map(node, index)函数返回值来得到中断号
自定义属性,有点类似于老内核中的platform_data,我们在设备节点中可以随意添加自定义属性,比如下面这个节点里面的属性都是我们自己定义的:
reg_3p3v: 3p3v {
compatible = "regulator-fixed";
regulator-name = "3P3V";
regulator-min-microvolt = <3300000>;
regulator-max-microvolt = <3300000>;
regulator-always-on;
};
针对32位整形的属性,比如上面的regulator-min-microvolt,可以利用下面这个API来获取属性值,第一个参数是节点,第二个参数是属性名字,第三个是输出型参数(把读出来的值放进去)
of_property_read_u32(node, "regulator-min-microvolt", µvolt);
类似的读取数值的API还有:
int of_property_read_u8(const struct device_node *np, const char *propname, u8 *out_value)
int of_property_read_u16(const struct device_node *np, const char *propname, u16 *out_value)
下列API可检查节点中某个属性是否存在,存在则返回true,不存在则返回false
bool of_property_read_bool(const struct device_node *np, const char *propname)
当节点中存在字符串时,可以像下面那样读取,比如我们读取前面reg_3p3v节点中的字符串
of_property_read_string(node, "regulator-name", &string)
当节点中存在数组时,可以像下面那样读取
/*带有数组的某个节点*/
L2: cache-controller@1e00a000 {
compatible = "arm,pl310-cache";
arm,data-latency = <1 1 1>;
arm,tag-latency = <1 1 1>;
};