DTS入门知识
本文目标是了解dts基本语法,可以尝试去看内核dts中的文件,follow去配置。
一、dts产生原因
DTS即Device Tree Source 设备树源码, DeviceTree是一种描述硬件的数据结构,它起源于OpenFirmware (OF)。
它替代arch/arm/plat-xxx和arch/arm/mach-xxx中的板级spec代码,便于code管理。
ARM平台的相关code相关规范调整:
1、ARM的核心代码仍然保存在arch/arm目录下
2、ARM SOC core architecture code保存在arch/arm目录下
3、ARM SOC的周边外设模块的驱动保存在drivers目录下
4、ARM SOC的特定代码在arch/arm/mach-xxx目录下
5、ARM SOC board specific的代码被移除,由Device Tree机制来负责传递硬件拓扑和硬件资源信息。
本质上,Device Tree改变了原来用hardcode方式将HW配置信息嵌入到内核代码的方法,改用bootloader传递一些参数。
如果我们认为kernel是一个black box,那么其输入参数应该包括:
a.识别platform的信息 b. runtime的配置参数 c.设备的拓扑结构以及特性
对于嵌入式系统,在系统启动阶段,bootloader会加载内核并将控制权转交给内核,此外,还需要把上述的三个参数信息传递给kernel,以便kernel可以有较大的灵活性。在linux kernel中,Device Tree的设计目标就
是如此。
二、dts基本知识
2.1 dts加载过程
如果要使用Device Tree,首先用户要了解自己的硬件配置和系统运行参数,并把这些信息组织成Device Tree source file。通过DTC(Device Tree Compiler),可以将这些适合人类阅读的Device Tree source file变成适合机器处理的Device Tree binary file(有一个更好听的名字,DTB,device tree blob)。在系统启动的时候,boot program(例如:firmware、bootloader)可以将保存在flash中的DTB copy到内存(当然也可以通过其他方式,例如可以通过bootloader的交互式命令加载DTB,或者firmware可以探测到device的信息,组织成DTB保存在内存中),并把DTB的起始地址传递给client program(例如OS kernel,bootloader或者其他特殊功能的程序)。对于计算机系统(computer system),一般是firmware->bootloader->OS,对于嵌入式系统,一般是bootloader->OS。
在高通msm8992平台:
sbl将烧写在emmc中的cdt.bin进行读取和处理,获得nPlatform和nSubtype,如果没有CDT分区,那么就使用代码里配置的cdt,存入共享内存;
lk将从共享内存中获得nPlatform和nSubtype进行相应处理,确定dtb entry,然后选择一个对应的dtb(所有dtb保存在boot.img中)从emmc中读进内存地址hdr->tags_addr,这个参数会传递给kernel;
kernel通过传入的dtb地址进行设备的创建。
2.2 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等资源,也被传递给了内核,内核会将这些资源绑定给展开的相应的设备。
是否Device Tree要描述系统中的所有硬件信息?答案是否定的。基本上,那些可以动态探测到的设备是不需要描述的,例如USB device。不过对于SOC上的usb host controller,它是无法动态识别的,需要在devicetree中描述。同样的道理,在computersystem中,PCI device可以被动态探测到,不需要在device tree中描述,但是PCI bridge如果不能被探测,那么就需要描述之。
.dts文件是一种ASCII 文本格式的Device Tree描述,此文本格式非常人性化,适合人类的阅读习惯。基本上,在ARM Linux在,一个.dts文件对应一个ARM的machine,一般放置在内核的arch/arm/boot/dts/目录。由于一个SoC可能对应多个machine(一个SoC可以对应多个产品和电路板),势必这些.dts文件需包含许多共同的部分,Linux内核为了简化,把SoC公用的部分或者多个machine共同的部分一般提炼为.dtsi,类似于C语言的头文件。其他的machine对应的.dts就include这个.dtsi。
譬如在arch/arm/boot/dts/qcom/目录下,高通的很多.dtsi都include了skeleton.dtsi或者skeleton64.dtsi。
正常情况下所有的dts文件以及dtsi文件都含有一个根节点”/”,这样include之后就会造成有很多个“根节点”。按理说 device tree既然是一个树,那么其只能有一个根节点,所有其他的节点都是派生于根节点的child node。其实Device TreeCompiler会对DTS的node进行合并,最终生成的DTB中只有一个root node.
device tree的基本单元是node。这些node被组织成树状结构,除了root node,每个node都只有一个parent。一个device tree文件中只能有一个root node。每个node中包含了若干的property/value来描述该node的一些特性。每个node用节点名字(node name)标识,节点名字的格式是node-name@unit-address。如果该node没有reg属性(后面会描述这个property),那么该节点名字中必须不能包括@和unit-address。unit-address的具体格式是和设备挂在那个bus上相关。例如对于cpu,其unit-address就是从0开始编址,依次加一。而具体的设备,例如以太网控制器,其unit-address就是寄存器地址。rootnode的node name是确定的,必须是“/”。
在一个树状结构的device tree中,如何引用一个node呢?要想唯一指定一个node必须使用full path,例如/node-name-1/node-name-2/node-name-N
2.3 dts组成结构示例
/ { "/" 表示root结点,该结点下有两个子结点node1和node2
node1 { 结点"node1"下又含有子结点,本例中为"child-node1" 和 "child-node2",各结点都有一系列属性
a-string-property = "Astring";属性是字符串
a-string-list-property = "firststring", "second string";字符串数组
a-byte-data-property = [0x01 0x23 0x340x56];二进制数组
child-node1 {
first-child-property;
second-child-property = <1>;Cells(由u32整数组成)
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 {
};
};
};
上述.dts文件并没有什么真实的用途,但它基本表征了一个DeviceTree源文件的结构。
2.4 dts语法
完整的Device Tree可以将一个PCB摆在你眼前,下面我们一起来看一下:
下面以一个最简单的machine为例来看如何写一个.dts文件。假设此machine的配置如下:
1个双核ARMCortex-A9 32位处理器;
ARM的local bus上的内存映射区域分布了2个串口(分别位于0x101F1000 和 0x101F2000)、GPIO控制器(位于0x101F3000)、SPI控制器(位于0x10115000)、中断控制器(位于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>; 子结点需要一个cell描述地址
#size-cells = <1>; 子结点需要一个cell描述长度
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@101f1000 { 串口
compatible = "arm,pl011";
reg = <0x101f1000 0x1000 >;
interrupts = < 1 0 >;
};
serial@101f2000 { 串口
compatible = "arm,pl011";
reg = <0x101f2000 0x1000 >;
interrupts = < 2 0 >;
};
gpio@101f3000 { GPIO控制器
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 { spi控制器
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >; 起始地址为0x10115000,长度为0x1000
interrupts = < 4 0 >;
};
external-bus { external bus桥
#address-cells = <2> 子结点需要两个cell描述地址,片选
#size-cells = <1>; 子结点需要一个cell描述长度
ranges = <0 0 0x10100000 0x10000 // Chipselect 1, Ethernet 片选0 0,地址0x10100000 ,长度0x10000
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>; rtc需要一个cell描述地址
#size-cells = <0>; rtc不需要0描述长度
reg = <1 0 0x1000>;
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
flash@2,0 {
compatible = "samsung,k8f1315ebm", "cfi-flash";
reg = <2 0 0x4000000>;
};
};
};
注释不是太多,下面来详细分类解释:
2.4.1 compatible
上述.dts文件中,root结点"/"的compatible 属性compatible = "acme,coyotes-revenge";定义了系统的名称,它的组织形式为:<manufacturer>,<model>。Linux内核透过root结点"/"的compatible 属性即可判断它启动的是什么machine。
在.dts文件的每个设备,都有一个compatible属性,compatible属性用户驱动和设备的绑定。compatible属性是一个字符串的列表,列表中的第一个字符串表征了结点代表的确切设备,形式为"<manufacturer>,<model>",其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。
如在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"涵盖的范围更广。
2.4.2 name@unit-address
接下来root结点"/"的cpus子结点下面又包含2个cpu子结点,描述了此machine上的2个CPU,并且二者的compatible 属性为"arm,cortex-a9"。
注意cpus和cpus的2个cpu子结点的命名,它们遵循的组织形式为:<name>[@<unit-address>],<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述结点对应的设备类型,如网卡适配器对应的结点name宜为ethernet,表示这个是网卡。如果一个结点描述的设备有地址,则应该给出@unit-address。多个相同类型设备结点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名结点。设备的unit-address地址也经常在其对应结点的reg属性中给出。
2.4.3 reg address-cells size-cells
设备的地址特性根据一下几个属性来控制:
● reg
● #address-cells
● #size-cells
reg意为region,区域。格式为:
reg =<address1 length1 [address2 length2] [address3 length3]>;
父类的address-cells和size-cells决定了子类的相关属性要包含多少个cell,如果子节点有特殊需求的话,可以自己再定义,这样就可以摆脱父节点的控制。
address-cells决定了address1/2/3包含几个cell,size-cells决定了length1/2/3包含了几个cell,用上面的dts文件内容举例子说明:
例子2.4.3.1
root结点的#address-cells = <1>;和#size-cells = <1>;决定了serial、gpio、spi等结点的address和length字段的长度分别为1
serial@101f1000 {
compatible = "arm,pl011";
reg = <0x101f1000 0x1000 >;地址0x101f1000,长度 0x1000
interrupts = < 1 0 >;
};
例子2.4.3.2
i2c控制器模块下的rtc模块。因为I2C设备只是被分配在一个地址上,不需要其他任何空间,所以只需要一个address的cell就可以描述完整,不需要size-cells
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>; 子结点需要一个cell描述地址
#size-cells = <1>; 子结点需要一个cell描述长度
interrupt-parent = <&intc>;
...
external-bus {
#address-cells = <2>
#size-cells = <1>;
...
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
#address-cells = <1>;重新写address-cells
#size-cells = <0>;重新写address-cells
reg = <1 0 0x1000>;
rtc@58 {
compatible ="maxim,ds1338";
reg = <58>;只需要一个address cell,不需要cell再描述长度
};
};
...
};
例子2.4.3.3
当需要描述的设备不是本地设备时,就需要描述一个从设备地址空间到CPU地址空间的映射关系,这里就需要用到ranges属性。还是以上边的external-bus举例
ranges属性为一个地址转换表。表中的每一行都包含了子地址、父地址、在自地址空间内的区域大小。他们的大小(包含的cell)分别由子节点的address-cells的值、父节点的address-cells的值和子节点的size-cells来决定。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>; 子结点需要一个cell描述地址
#size-cells = <1>; 子结点需要一个cell描述长度
interrupt-parent = <&intc>;
...
0 0 两个cell,由子节点external-bus的address-cells=<2>决定;
0x10100000 一个cell,由父节点(/)的address-cells=<1>决定;
0x10000 一个cell,由子节点external-bus的size-cells=<1>决定。
最终第一行说明的意思就是:片选0,偏移0(选中了网卡),被映射到CPU地址空间的0x10100000~0x10110000中,地址长度为0x10000。
external-bus {
#address-cells = <2>
#size-cells = <1>;
ranges = <00 0x10100000 0x10000 //Chipselect 1, Ethernet
1 0 0x10160000 0x10000 // Chipselect 2,i2c controller
2 0 0x30000000 0x1000000>; // Chipselect 3, NORFlash
};
进阶例子2.4.3.4
pci@0x10180000 {
compatible ="arm,versatile-pci-hostbridge", "pci";
reg = <0x10180000 0x1000>;
interrupts = <8 0>;
bus-ranges = <0 0>;
#address-cells = <3>
#size-cells = <2>;
ranges = <0x42000000 0 0x800000000x80000000 0 0x20000000
0x02000000 0 0xa00000000xa0000000 0 0x10000000
0x01000000 0 0x000000000xb0000000 0 0x01000000>;
};
像之前描述过的本地总线一样,PCI地址空间与CPU地址空间是完全分离的,所以这里需要通过定义ranges属性进行地址转化。
#address-cells定义PCI使用3个cell,并且PCI的地址范围通过两个单位就可以解读。所以,首先的问题就是,为什么需要用3个32位的cell来描述一个PCI地址。
这三个cell分别代表物理地址高位、中位、低位:
1 phys.high cell : npt000ss bbbbbbbb dddddfff rrrrrrrr
2 phys.mid cell : hhhhhhh hhhhhhhh hhhhhhhh hhhhhhh
3 phys.low cell : llllllll llllllll llllllll llllllll
PCI地址为64位宽度,编码在phys.mid和phys.low中。真正重要的东西在于phys.high这一位空间中:
n:代表重申请空间标志(这里没有使用)
p:代表预读空间(缓存)标志
t:别名地址标志(这里没有使用)
ss:空间代码
00:设置空间
01:IO空间
10:32位存储空间
11:64位存储空间
bbbbbbbb: PCI总线号。PCI有可能是层次性架构,所以我们可能需要区分一些子-总线
ddddd:设备号,通常由初始化设备选择信号IDSEL连接时申请。
fff:功能序号,有些多功能PCI设备可能用到。
rrrrrrrr:注册号,在设置周期使用。
回头再看这个ranges分表代表了什么。
ranges =<0x42000000 0 0x80000000 0x80000000 0 0x20000000
0x02000000 0 0xa0000000 0xa0000000 00x10000000
0x01000000 0 0x00000000 0xb0000000 00x01000000>;
父节点address-cells为1,子节点address-cells为3,子节点size-cells为2。则第一行可以这样划分:
0x42000000 00x80000000 子节点地址| 0x80000000 父节点地址| 0 0x20000000 地址空间长度|
0x42000000为phys.high,第一高字节为01000010,则p为1,ss为10,即申请32位存储空间为缓存空间。phys.mid为0,phys.low为0x80000000,他们共同组成了PCI地址,即表示从PCI总线的0x80000000地址处申请出一个32位的存储空间作为缓存。后边的那个cell0x80000000 0 0x20000000代表到CPU空间后的参数,申请的地址被映射到CPU空间的0x80000000地址处,大小共计0x20000000(512MB)。
2.4.5 interrupt
描述中断连接需要四个属性:
1. interrupt-controller 一个空属性用来声明这个node接收中断信号;
2. #interrupt-cells 这是中断控制器节点的属性,用来标识这个控制器需要几个单位做中断描述符;
3. interrupt-parent 标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的;
4. interrupts 一个中断标识符列表,表示每一个中断输出信号。
如果有两个,第一个是中断号,第二个是中断类型,如高电平、低电平、边缘触发等触发特性。对于给定的中断控制器,应该仔细阅读相关文档来确定其中断标识该如何解析。
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>; root结点指定中断控制器intc
...
intc: interrupt-controller@10140000 {
compatible = "arm,pl190";
reg = <0x10140000 0x1000 >;
interrupt-controller; 这个node接收中断信号
#interrupt-cells = <2>; 2个cell做中断描述符
};
对于ARM GIC中断控制器而言,#interrupt-cells为3,它3个cell的具体含义需要参考文档
kernel/Documentation/devicetree/bindings/arm/gic.txt
-#interrupt-cells : Specifies the number of cells needed to encode an
interrupt source. The type shall be a <u32> and the valueshall be 3.
The 1st cell is the interrupt type; 0 for SPIinterrupts, 1 for PPI
interrupts.
The 2nd cell contains the interrupt numberfor the interrupt type.
SPI interrupts are in the range [0-987]. PPI interrupts are in the
range [0-15].
The 3rd cell is the flags, encoded asfollows:
bits[3:0] trigger type and level flags.
1= low-to-high edge triggered
2= high-to-low edge triggered
4= active high level-sensitive
8= active low level-sensitive
一个设备还可能用到多个中断号。对于ARM GIC而言,若某设备使用了SPI的168、169号2个中断,且都是高电平触发,则该设备结点的interrupts属性可定义为:
interrupts =<0 168 4>, <0 169 4>;
三、dts与驱动
●没有使用dts之前的bsp和driver
板级spec代码
#ifdefCONFIG_LEDS_CTL
structplatform_device s3c_device_leds_ctl = {
.name = "leds",
.id = -1,
};
#endif
driver
static structplatform_driver leds_driver = {
.probe = leds_probe,
.remove = leds_remove,
.suspend = leds_suspend,
.resume = leds_resume,
.driver = {
.name= DRIVER_NAME,//leds
.owner= THIS_MODULE,
},
};
●使用dts之后的bsp和driver
dts文件
&soc {
i2c@f9928000 { /* BLSP1 QUP6 */
status= "ok";
nfc-nci@76{
compatible = "brcm,bcm2079x";
...
};
driver
static structof_device_id bcm_match_table[] = {
{.compatible = "brcm,bcm2079x"},
{}
}
static structi2c_driver bcm2079x_driver = {
.id_table = bcm2079x_id,
.probe = bcm2079x_probe,
.remove = bcm2079x_remove,
.driver = {
.owner= THIS_MODULE,
.name= "bcm2079x-i2c",
.of_match_table= bcm_match_table,
},
};
四、具体案例
用第三节提到的nfc进行分析,里面有很多的内容可以去Document/devicetree/bindings查找
root@icesky_msm8992:/sys/bus/i2c/devices/6-0076# cat name
bcm2079x
&soc {
i2c@f9928000 {/* BLSP1 QUP6 */
status= "ok";
nfc-nci@76 {
compatible = "brcm,bcm2079x";匹配
reg = <0x76>;地址cell=0x76,没有长度描述cell
gpio控制器msm_gpio
qcom,wake-gpio =<&msm_gpio 29 0x00>; 29pin,0x00 -- unused
qcom,en-gpio =<&msm_gpio 30 0x00>;30pin
qcom,irq-gpio =<&msm_gpio 9 0x00>;9pin
qcom,clk-src = "BBCLK2";nfc时钟源,跟驱动有关
interrupt-parent =<&msm_gpio>;属于msm_gpio中断控制器
interrupts = <9 0>;中断标识符列表 nfc readinterrupt, gpio-clk-req interrupt
interrupt-names = "nfc_irq";中断名称字符串类型,与驱动有关kernel/drivers/of/irq.c
pinctrl-names ="nfc_active","nfc_suspend";pin ctrl字符串类型
pinctrl-0 = <&nfc_int_active&nfc_disable_active &nfc_wake_active>;见4.3节
pinctrl-1 = <&nfc_int_suspend&nfc_disable_suspend &nfc_wake_suspend>;
clocks = <&clock_rpm clk_bb_clk2_pin>;
clock-names = "brcm_nfc_clk";
};
};
};
4.1 gpio
上面的wake-gpio、en-gpio、irq-gpio与driver相关
structbcm2079x_dev {
...
unsigned int wake_gpio;
unsigned int en_gpio;
unsigned int irq_gpio;
...
};
4.2 interrupt
msm_gpio既是中断控制器,也是gpio控制器
gp: gp{
qcom,num-pins = <146>;
#qcom,pin-cells = <1>;
msm_gpio: msm_gpio {
compatible = "qcom,msm-tlmm-gp";
gpio-controller;可以作为gpio控制器
#gpio-cells = <2>;2个cell做gpio描述符,第一个是pin number,第二个是指定可选参数
interrupt-controller;接收中断信号
#interrupt-cells = <2>;2个cell做中断描述符,第一个是pin number,第二个是指定可选参数中断类型
qcom,direct-connect-irqs = <8>;8个GPIO可以直接连中断信号
num_irqs = <146>;146个pins可以作为中断源
};
};
4.3 pinctl
我们挑下nfc_wake_active查查dts里面是什么
pmx_nfc_wake_reg{
qcom,pins = <&gp 9>; gp的配置见4.2节 pin number == 9
qcom,pin-func = <0>; 为pin设置function
qcom,num-grp-pins = <1>;pin group number = 1,因为本例只有一个gp 9
label = "pmx_nfc_wak";pin group中的身份识别
nfc_wake_active:active {
drive-strength= <6>;信号强度 6MA
bias-pull-up;上拉引脚
output-low;输出模式low level
};
nfc_wake_suspend: suspend {
drive-strength= <6>;
bias-pull-down;/* pull down */
output-low;
};
};
4.4 clk
nfc-nci@76 {
...
clocks = <&clock_rpm clk_bb_clk2_pin>;#defineclk_bb_clk2_pin 0x498938e5
clock-names = "brcm_nfc_clk";drivers将使用该name去匹配
};
clock_rpm: qcom,rpmcc@fc401880 {
compatible ="qcom,rpmcc-8992";
reg = <0xfc401880 0x4>;
reg-names = "cc_base";
#clock-cells = <1>; 0 表示单一时钟输出,1表示多时钟
};