Linux内核中设备树DTS详解及操作结点

一、引言

自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也需要加入设备树的支持

三、使用deb的启动流程

四、DTS的结构

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>;  
        };  
    };  
};  
解析:
1、compatible 属性 :

上述.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保持了寄存器兼容。

2、子节点
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>[@<unit-address>]
<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述结点对应的设备类型,如3com Ethernet适配器对应的结点name宜为ethernet,而不是3com509。
如果一个结点描述的设备有地址,则应该给出@unit-address。
多个相同类型设备结点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点。

3、reg属性

设备的unit-address地址也经常在其对应结点的reg属性中给出。ePAPR标准给出了结点命名的规范:
reg
#address-cells
#size-cells

reg: reg的组织形式为reg = <address1 length1 [address2 length2] [address3 length3] … >,其中的每一组address length表明了设备使用的一个地址范围。

#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个项目的含义可以类推。

4、interrupt属性

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文档中说明。如果有两个,第一个是中断号,第二个是中断类型,如高电平、低电平、边缘触发等触发特性。对于给定的中断控制器,应该仔细阅读相关文档来确定其中断标识该如何解析。
实例:

二个cell的情况

第一个值: 该中断位于他的中断控制器的索引;
第二个值:触发的type

固定的取值如下:
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active high level-sensitive
8 = active low level-sensitive

三个cell的情况

第一个值:中断号
第二个值:触发的类型

第三个值:优先级,0级是最高的,7级是最低的;其中0级的中断系统当做 FIQ处理。

另外,值得注意的是,一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,而言都是高电平触发,则该设备结点的interrupts属性可定义为:interrupts = <0 168 4>, <0 169 4>;
除了中断以外,在ARM Linux中clock、GPIO、pinmux都可以透过.dts中的结点和属性进行描述。

5、在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中的硬件资源

1、GPIO资源

常用的API

#include <linux/gpio.h>
#include <linux/of_gpio.h>  

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);
控制台中查看当前gpio占用情况的方法

内核中提供了虚拟文件系统debugfs,里面有一个gpio文件,提供了gpio的使用信息

使用 mount -t debugfs debugfs /tmp 把debugfs挂接到/tmp下,再重新进入/tmp后就能看到一个名为gpio的文件
cat /tmp/gpio即可得到gpio的所有信息,使用完后umount /tmp卸载掉debugfs

2、中断

中断类型:

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)函数返回值来得到中断号

3、DTS中自定义属性的设置与获取

自定义属性,有点类似于老内核中的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", &microvolt);

类似的读取数值的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>;
};
  • 39
    点赞
  • 77
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文艺小少年

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值