可以学设备树的课程:https://edu.csdn.net/course/detail/9511?utm_source=blog2edu
1 ARM设备树
DT: Device Tree
FDT: Flattened DeviceTree
OF: Open Firmware(打开固件,这个前缀在后面的api中会用到)
DTS : device tree souke
DTSI: device tree source include
DTB: device tree blob
DTC:device tree compiler
2 设备树的组成和结构
整个设备树牵涉面比较广,既增加了新的用于描述设备硬件信息的文本格式,又增加了编译这个文本的工具,同时Bootloader也需要支持将编译后的设备树传递给Linux内核
2.1 DTS、 DTC和DTB等
1.DTS
一般放置在内核的arch/arm/boot/dts/目录中。也会在arch/powerpc/boot/dts、 arch/powerpc/boot/dts、
arch/c6x/boot/dts、 arch/openrisc/boot/dts等目录中。
由于一个SoC可能对应多个设备(一个SoC可以对应多个产品和电路板),这些.dts文件势必须包含许多共同的部分,Linux内核为了简化,把SoC公用的部分或者多个设备共同的部分一般提炼为.dtsi,类似于C语言的头文件。其他的设备对应的.dts就包括这个.dtsi。譬如,对于VEXPRESS而言, vexpressv2m.dtsi就被vexpress-v2p-ca9.dts所引用, vexpress-v2p-ca9.dts
有如下一行代码:
/include/ "vexpress-v2m.dtsi"
当然,和C语言的头文件类似, .dtsi也可以包括其他的.dtsi,譬如几乎所有的ARM SoC的.dtsi都引用了skeleton.dtsi。
文件.dts(或者其包括的.dtsi)的基本元素即为前文所述的节点和属性,代码清单18.1给出了一个设备树结构的模版。
下面以一个最简单的设备为例来看如何写一个.dts文件。如图18.1所示,假设此设备的配置如下
- 1个双核ARM Cortex-A932位处理器;
- ARM本地总线上的内存映射区域分布有两个串口(分别位于0x101F1000和0x101F2000)、 GPIO控制器(位于0x101F3000)、 SPI控制器(位于0x10170000)、中断控制器(位于0x10140000)和一个外部总线桥;
- 外部总线桥上又连接了SMC SMC91111以太网(位于0x10100000)、 I2C控制器(位于0x10160000)、 64MBNOR Flash(位于0x30000000);
- 外部总线桥上连接的I2C控制器所对应的I2C总线上又连接了Maxim DS1338实时钟(I2C地址为0x58)。
对于图18.1所示硬件结构图,如果用“.dts”描述,则其对应的“.dts”文件如代码清单18.2所示。
/ {
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, Ethe
1 0 0x10160000 0x10000 // Chipselect 2, i
2 0 0x30000000 0x1000000>; // Chipselect 3, NOR
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>;
};
};
};
2.DTC(Device Tree Compiler)
DTC是将.dts编译为.dtb的工具。 DTC的源代码位于内核的scripts/dtc目录中,在Linux内核使能了设备树的情况下,编译内核的时候主机工具DTC会被编译出来,对应于scripts/dtc/Makefile中“hostprogs -y: =dtc”这一hostprogs的编译目标。
当然, DTC也可以在Ubuntu中单独安装,命令如下:
sudo apt-get install device-tree-compiler
在Linux内核的arch/arm/boot/dts/Makefile中,描述了当某种SoC被选中后,哪些.dtb文件会被编译出来,如与VEXPRESS对应的.dtb包括:
dtb-$(CONfiG_ARCH_VEXPRESS) += vexpress-v2p-ca5s.dtb \
vexpress-v2p-ca9.dtb \
vexpress-v2p-ca15-tc1.dtb \
vexpress-v2p-ca15_a7.dtb \
xenvm-4.2.dtb
在Linux下,我们可以单独编译设备树文件。当我们在Linux内核下运行make dtbs时,若我们之前选择了ARCH_VEXPRESS,上述.dtb都会由对应的.dts编译出来,因为arch/arm/Makefile中含有一个.dtbs编译目标项目。DTC除了可以编译.dts文件以外,其实也可以“反汇编”.dtb文件为.dts文件,其指令格式为:
./scripts/dtc/dtc -I dtb -O dts -o xxx.dts arch/arm/boot/dts/xxx.dtb
3.DTB(Device Tree Blob)
文件.dtb是.dts被DTC编译后的二进制格式的设备树描述,可由Linux内核解析,当然U-Boot这样的bootloader也是可以识别.dtb的。
通常在我们为电路板制作NAND、 SD启动映像时,会为.dtb文件单独留下一个很小的区域以存放之,之后bootloader在引导内核的过程中,会先读取该.dtb到内存。
Linux内核也支持一种变通的模式,可以不把.dtb文件单独存放,而是直接和zImage绑定在一起做成一个映像文件,类似
cat zImage xxx.dtb>zImage_with_dtb的效果。当然内核编译时候要使能CONFIG_ARM_APPENDED_DTB这个选项,以支持“Use appended device tree blob to zImage”(见Linux内核中的菜单)。
4.绑定(Binding)
对于设备树中的节点和属性具体是如何来描述设备的硬件细节的,一般需要文档来进行讲解,文档的后缀名一般为.txt。在这个.txt文件中,需要描述对应节点的兼容性、必需的属性和可选的属性。这些文档位于内核的Documentation/devicetree/bindings目
录下,其下又分为很多子目录。譬如,
Documentation/devicetree/bindings/i2c/i2c-xiic.txt描述了Xilinx的I2C控制器,其内容如下
Xilinx IIC controller:
Required properties:
- compatible : Must be "xlnx,xps-iic-2.00.a"
- reg : IIC register location and length //IIC寄存器的位置和长度
- interrupts : IIC controller unterrupt
- #address-cells = <1>
- #size-cells = <0>
Optional properties:
- Child nodes conforming to i2c bus binding
Example:
axi_iic_0: i2c@40800000 {
compatible = "xlnx,xps-iic-2.00.a";
interrupts = < 1 2 >;
reg = < 0x40800000 0x10000 >;
#size-cells = <0>;
#address-cells = <1>;
};
基本可以看出,设备树绑定文档的主要内容包括:
- 关于该模块最基本的描述。
- 必需属性(Required Properties)的描述。
- 可选属性(Optional Properties)的描述。
- 一个实例。
Linux内核下的scripts/checkpatch.pl会运行一个检查,如果有人在设备树中新添加了compatible字符串,而没有添加相应的文档进行解释, checkpatch程序会报出警告:
UNDOCUMENTED_DT_STRINGDT compatible string xxxappears un-documented
因此程序员要养成及时写DT Binding文档的习惯。
5.Bootloader
设置启动参数
bootargs=root=/dev/nfs nfsroot=192.168.43.30:/home/liu/ARM/rootfs/rootfs2 ip=192.168.43.10:192.168.43.30:192.168.43.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200
bootcmd=tftp 30008000 zImage; tftp 30008020 test.dtb; bootm 30008000 - 30008020
2.2 根节点兼容性
根节点"/"的兼容属性compatible="acme, coyotes-revenge";它的组织形式为: <manufacturer>, <model>。
Linux内核通过根节点"/"的兼容属性即可判断它启动的是什么设备。在真实项目中,这个顶层设备的兼容属性一般包括两个或者两个以上的兼容性字符串,
- 第一个兼容性字符串是板子级别的名字
- 第二个兼容性是芯片级别(或者芯片系列级别)的名字
譬如板子arch/arm/boot/dts/vexpress-v2p-ca9.dts兼容于
arm, vexpress, v2p-ca9和“arm, vexpress”:
compatible = "arm,vexpress,v2p-ca9", "arm,vexpress";
板子arch/arm/boot/dts/vexpress-v2p-ca5s.dts的兼容性则为:
compatible = "arm,vexpress,v2p-ca5s", "arm,vexpress";
板子arch/arm/boot/dts/vexpress-v2p-ca15_a7.dts的兼容性为:
compatible = "arm,vexpress,v2p-ca15_a7", "arm,vexpress";
可以看出,上述各个电路板的共性是兼容于arm,vexpress,而特性是分别兼容于arm, vexpress, v2p-ca9、
arm, vexpress, v2p-ca5s和arm, vexpress, v2p-ca15_a7。
进一步地看, arch/arm/boot/dts/exynos4210-origen.dts的兼容性字段如下:
compatible = "insignal,origen", "samsung,exynos4210", "samsung,exynos4";
- 第1个字符串是板子名字(很特定)
- 第2个字符串是芯片名字(比较特定)
- 第3个字段是芯片系列的名字(比较通用)
作为类比, arch/arm/boot/dts/exynos4210-universal_c210.dts的兼容性字段则如下:
compatible = "samsung,universal_c210", "samsung,exynos4210", "samsung,ex";
由此可见,它与exynos4210-origen.dts的区别只在于第1个字符串(特定的板子名字)不一样,后面芯片名和芯片系列的名字都一样。
在Linux 2.6内核中, ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个设备的一系列回调函数,如mach-x210.c中
#ifdef CONFIG_MACH_SMDKC110
MACHINE_START(SMDKC110, "SMDKC110")
#elif CONFIG_MACH_SMDKV210
MACHINE_START(SMDKV210, "SMDKV210")
#endif
/* Maintainer: Kukjin Kim <kgene.kim@samsung.com> */
.phys_io = S3C_PA_UART & 0xfff00000,
.io_pg_offst = (((u32)S3C_VA_UART) >> 18) & 0xfffc,
.boot_params = S5P_PA_SDRAM + 0x100,
//.fixup = smdkv210_fixup,
.init_irq = s5pv210_init_irq,
.map_io = smdkc110_map_io,
.init_machine = smdkc110_machine_init,
.timer = &s5p_systimer,
MACHINE_END
这些不同的设备会有不同的MACHINE ID, Uboot在启动Linux内核时会将MACHINE ID存放在r1寄存器, Linux启动时会匹配Bootloader传递的MACHINE ID和MACHINE_START声明的MACHINE ID,然后执行相应设备的一系列初始化函数。
ARM Linux 3.x在引入设备树之后, MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性兼容关系。
如果Bootloader传递给内核的设备树中根节点的兼容属性出现在某设备的.dt_compat表中,相关的设备就与对应的兼容匹配,从而引发这一设备的一系列初始化函数被执行。一个典型的DT_MACHINE如代码清单18.4所示。
static const char * const v2m_dt_match[] __initconst = {
"arm,vexpress",
"xen,xenvm",
NULL,
};
DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
.dt_compat = v2m_dt_match,
.smp = smp_ops(vexpress_smp_ops),
.map_io = v2m_dt_map_io,
.init_early = v2m_dt_init_early,
.init_irq = v2m_dt_init_irq,
.timer = &v2m_dt_timer,
.init_machine = v2m_dt_init,
.handle_irq = gic_handle_irq,
.restart = vexpress_restart,
MACHINE_END
Linux倡导针对多个SoC、多个电路板的通用DT设备,即一个DT设备的.dt_compat包含多个电路板.dts文件的根节点兼容属性字符串。之后,如果这多个电路板的初始化序列不一样,可以通过下面的API判断具体的电路板是什么。
int of_machine_is_compatible(const char *compat);
此API判断目前运行的板子或者SoC的兼容性,它匹配的是设备树根节点下的兼容属性。
例如drivers/cpufreq/exynoscpufreq.c中就有判断运行的CPU类型是exynos4210、exynos4212、 exynos4412还是exynos5250的代码,进而分别处理,
代码5 of_machine_is_compatible()的案例
static int exynos_cpufreq_probe(struct platform_device *pdev)
{
int ret = -EINVAL;
exynos_info = kzalloc(sizeof(*exynos_info), GFP_KERNEL);
if (!exynos_info)
return -ENOMEM;
exynos_info->dev = &pdev->dev;
if (of_machine_is_compatible("samsung,exynos4210")) {
exynos_info->type = EXYNOS_SOC_4210;
ret = exynos4210_cpufreq_init(exynos_info);
} else if (of_machine_is_compatible("samsung,exynos4212")) {
exynos_info->type = EXYNOS_SOC_4212;
ret = exynos4x12_cpufreq_init(exynos_info);
} else if (of_machine_is_compatible("samsung,exynos4412")) {
exynos_info->type = EXYNOS_SOC_4412;
ret = exynos4x12_cpufreq_init(exynos_info);
} else if (of_machine_is_compatible("samsung,exynos5250")) {
exynos_info->type = EXYNOS_SOC_5250;
ret = exynos5250_cpufreq_init(exynos_info);
} else {
pr_err("%s: Unknown SoC type\n", __func__);
return -ENODEV;
}
//...
}
如果一个兼容包含多个字符串,譬如对于前面介绍的根节点兼容compatible="samsung, universal_c210", "samsung,exynos4210", "samsung, exynos4"的情况,如下的3个表达式都为真。
of_machine_is_compatible("samsung,universal_c210")
of_machine_is_compatible("samsung,exynos4210")
of_machine_is_compatible("samsung,exynos4")
2.3 设备节点兼容性
在.dts文件的每个设备节点中,都有一个兼容属性,兼容属性用于驱动和设备的绑定。
兼容属性是一个字符串的列表,列表中的第一个字符串表征了节点代表的确切设备,形式为"<manufacturer>, <model>",其后的字符串表征可兼容的其他设备。可以说前面的是特指,后面的则涵盖更广的范围。如在vexpress-v2m.dtsi中的Flash节点如下:
flash@0,00000000 {
compatible = "arm,vexpress-flash", "cfi-flash";
reg = <0 0x00000000 0x04000000>,
<1 0x00000000 0x04000000>;
bank-width = <4>;
};
int of_device_is_compatible(const struct device_node *device,const char *compat);
此函数用于判断设备节点的兼容属性是否包含compat指定的字符串。这个API多用于一个驱动支持两个以上设备的时候。
当一个驱动支持两个或多个设备的时候,这些不同.dts文件中设备的兼容属性都会写入驱动OF匹配表。因此驱动可以通过Bootloader传递给内核设备树中的真正节点的兼容属性以确定究竟是哪一种设备,从而根据不同的设备类型进行不同的处理。
如arch/powerpc/platforms/83xx/usb.c中的mpc831x_usb_cfg()就进行了类似处理:
if (immr_node && (of_device_is_compatible(immr_node, "fsl,mpc8315-immr") ||
of_device_is_compatible(immr_node, "fsl,mpc8308-immr")))
clrsetbits_be32(immap + MPC83XX_SCCR_OFFS,
MPC8315_SCCR_USB_MASK,
MPC8315_SCCR_USB_DRCM_01);
else
clrsetbits_be32(immap + MPC83XX_SCCR_OFFS,
MPC83XX_SCCR_USB_MASK,
MPC83XX_SCCR_USB_DRCM_11);
/* Configure pin mux for ULPI. There is no pin mux for UTMI */
if (prop && !strcmp(prop, "ulpi")) {
if (of_device_is_compatible(immr_node, "fsl,mpc8308-immr")) {
clrsetbits_be32(immap + MPC83XX_SICRH_OFFS,
MPC8308_SICRH_USB_MASK,
MPC8308_SICRH_USB_ULPI);
} else if (of_device_is_compatible(immr_node, "fsl,mpc8315-immr")){
clrsetbits_be32(immap + MPC83XX_SICRL_OFFS,
MPC8315_SICRL_USB_MASK,
MPC8315_SICRL_USB_ULPI);
clrsetbits_be32(immap + MPC83XX_SICRH_OFFS,
MPC8315_SICRH_USB_MASK,
MPC8315_SICRH_USB_ULPI);
} else {
clrsetbits_be32(immap + MPC83XX_SICRL_OFFS,
MPC831X_SICRL_USB_MASK,
MPC831X_SICRL_USB_ULPI);
clrsetbits_be32(immap + MPC83XX_SICRH_OFFS,
MPC831X_SICRH_USB_MASK,
MPC831X_SICRH_USB_ULPI);
}
}
它根据具体的设备是fsl, mpc8315-immr和fsl, mpc8308-immr、中的哪一种来进行不同的处理。
当一个驱动可以兼容多种设备的时候,除了of_device_is_compatible()这种判断方法以外,还可以采用在驱动的of_device_id表中填充.data成员的形式。譬如,arch/arm/mm/cache-l2x0.c支持“arm, l210-cache”“arm, pl310-cache”“arm, l220-cache”等多种设备,其of_device_id表如代码清单18.9所示。
代码清单18.9 支持多个兼容性以及.data成员的of_device_id表
#define L2C_ID(name, fns)
{
.compatible = name,
.data = (void *)&fns,
}
static const struct of_device_id l2x0_ids[] __initconst = {
L2C_ID("arm,l210-cache", of_l2c210_data),
L2C_ID("arm,l220-cache", of_l2c220_data),
L2C_ID("arm,pl310-cache", of_l2c310_data),
L2C_ID("brcm,bcm11351-a2-pl310-cache", of_bcm_l2x0_data),
L2C_ID("marvell,aurora-outer-cache", of_aurora_with_outer_data),
L2C_ID("marvell,aurora-system-cache", of_aurora_no_outer_data),
L2C_ID("marvell,tauros3-cache", of_tauros3_data),
/* Deprecated IDs */
L2C_ID("bcm,bcm11351-a2-pl310-cache", of_bcm_l2x0_data),
{}
};
在驱动中,通过下面的代码的方法拿到了对应于L2缓存类型的.data成员,其中主要用到了of_match_node()这个API。
int __init l2x0_of_init(u32 aux_val, u32 aux_mask)
{
const struct l2c_init_data *data;
struct device_node *np;
np = of_find_matching_node(NULL, l2x0_ids);
if (!np)
return -ENODEV;
//...
/*10*/ data = of_match_node(l2x0_ids, np)->data;
}
如果电路板的.dts文件中L2缓存是arm, pl310-cache,那么上述代码第10行找到的data就是of_l2c310_data,它是l2c_init_data结构体的一个实例。 l2c_init_data是一个由L2缓存驱动自定义的数据结构,在其定义中既可以保护数据成员,又可以包含函数指针,如下面所示。
与兼容对应的特定data实例
struct l2c_init_data {
const char *type;
unsigned way_size_0;
unsigned num_lock;
void (*of_parse)(const struct device_node *, u32 *, u32 *);
void (*enable)(void __iomem *, u32, unsigned);
void (*fixup)(void __iomem *, u32, struct outer_cache_fns *);
void (*save)(void __iomem *);
struct outer_cache_fns outer_cache;
};
通过这种方法,驱动可以把与某个设备兼容的私有数据寻找出来,如此体现了一种面向对象的设计思想,避免了大量的if, else或者switch, case语句。
2.4 设备节点及label的命名 @@@@@@
代码2的.dts文件中,根节点“/”的cpus子节点下面又包含两个cpu子节点,描述了此设备上的两个CPU,并且两者
的兼容属性为: "arm, cortex-a9"。
cpus,cpus的两个cpu子节点的命名形式为<name>[@<unit-address>]
<>中的内容是必选项
[]中的则为可选项
name是一个ASCII字符串,用于描述节点对应的设备类型,如3com Ethernet适配器对应的节点name宜为ethernet,而不3com509。
设备的unit-address地址也经常在其对应节点的reg属性中给出。
对于挂在内存空间的设备而言, @字符后跟的一般就是该设备在内存空间的基地址,譬如arch/arm/boot/dts/exynos4210.dtsi中存在的:
sysram@02020000 {
compatible = "mmio-sram";
reg = <0x02020000 0x20000>;
}
但是也可以和reg中的地址不一样,对于挂在I2C总线上的外设而言, @后面一般跟的是从设备的I2C地址(不是物理地址),譬如arch/arm/boot/dts/exynos4210-trats.dts中的mms114-touchscreen:
i2c@13890000 {
mms114-touchscreen@48 { //I2C地址为0x48
compatible = "melfas,mms114";
reg = <0x48>;
};
};
具体的节点命名规范可见ePAPR(embedded PowerArchitecture Platform Reference)标准,在https://www.power.org中可下载该标准。
我们还可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的。
例如,第3组GPIO有gpio3这个label
代码12 在设备树中定义label
//在设备树中定义labe
gpio3: gpio@48057000 { //lable 为 gpio3
compatible = "ti,omap4-gpio";
reg = <0x48057000 0x200>;
interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
ti,hwmods = "gpio3";
gpio-controller;
#gpio-cells = <2>;
interrupt-controller;
#interrupt-cells = <2>;
};
而hsusb2_phy这个USB的PHY复位GPIO用的是这组GPIO中的一个,所以它通过phandle引用了“gpio3”,如下面所示。
代码13:通过phandle引用其他节点
/* HS USB Host PHY on PORT 2 */
hsusb2_phy: hsusb2_phy {
compatible = "usb-nop-xceiv";
reset-gpios = <&gpio3 12 GPIO_ACTIVE_LOW>; /* gpio3_76 HUB_RESET*/
};
上面代码12的gpio3是gpio@48057000节点的label,而代码13的hsusb2_phy则通过&gpio3引用了这个节点,表明自己要使用这一组GPIO中的第12个GPIO。很显然,这种phandle引用其实表明硬件之间的一种关联性。
再举一例,从下面实例可以看出, label习惯以<设备类型><index>进行命名:
i2c1: i2c@48070000 {
}
i2c2: i2c@48072000 {
}
i2c3: i2c@48060000 {
}
读者也许发现了一个奇怪的现象,就是代码13中居然引用了GPIO_ACTIVE_LOW这个类似C语言的宏。文件.dts的编译过程确实支持C的预处理,相应的.dts文件也包括了包含GPIO_ACTIVE_LOW这个宏定义的头文件
#include <dt-bindings/gpio/gpio.h>
状态:status
'status'属性用来表示节点的状态的,其实就是硬件的状态,用字符串表示。
- ”okay“ 表示硬件正常工作
- “disabled” 表示硬件当前不可用
- “fail” 表示因为出错不可用
- “fail-sss” 表示因为某种原因出错不可用
- “sss” 表示具体的出错原因
实际中,基本只用'okay'和'disabled'。
2.5 地址编码 @@@@@@@
父节点的#address-cells和#size-cells分别决定了子节点reg属性中address和length占多少单元。
- 如:
- #address-cells=<2> :用来表示地址的数占用一个单元,则reg()中前两个数表示地址
- #sizecells=<1> :用来表示地址的范围的数占用1个单元
第一种:#address-cells=<1>和#size-cells=<0>
如reg=<0>, reg=<1>
这里的reg仅仅是编号
第二种:#address-cells=<1>和#size-cells=<1>
第一个单元:相对该片选的基地址
第二个单元:为length
第三种:#address-cells=<2>和#size-cells=<1>;
如reg=<0 0 0x1000>;、 reg=<1 0 0x1000>;和reg=<2 0 0x4000000>;。
每个reg共有三个单元:
第一个单元:对应的片选
第二个单元:相对该片选的基地址
第三个单元:为length
第四种:
#address-cells = <1>;
#size-cells = <0>;
i2c@1,0{
reg = <0x48>;
};
这里是i2c设备,reg的属性值表示该i2c设备地址
注意:地址编码属性只能对子节点起作用
根节点的直接子书点描述的是CPU的视图,因此根子节点的address区域就直接位于CPU的内存区域。但是,经过总线桥后的address往往需要经过转换才能对应CPU的内存映射。external-bus的ranges属性定义了经过external-bus桥后的地址范围如何映射到CPU的内存区域。
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是地址转换表,由一个子地址、父地址以及在子地址空间的大小的映射。
0 0 0x10100000 0x10000
第一个:片选0
第二个:偏移0
第三个:表示external-bus上片选0偏移0的地址空间被映射到CPU的本地总线的0x10100000位置
第四个:表示映射的大小为0x10000
2.6 中断连接 @@@@@@
- interrupt-controller
- 一个空属性用来声明这个node接收中断信号,即这个node是一个中断控制器。
- #interrupt-cells
- 用来标识这个控制器需要几个单位做中断描述符,用来描述子节点中"interrupts"属性使用了父节点中的interrupts属性的具体的哪个值。
- 如果父节点的该属性的值是3,则子节点的interrupts分别为:<中断域 中断 触发方式>
- 如果父节点的该属性是2,则是<中断 触发方式>
- interrupt-parent
- 标识此设备节点属于哪一个中断控制器,如果没有设置这个属性,会自动依附父节点的
- interrupts
- 一个中断标识符列表,表示每一个中断输出信号
实例:
/ {
compatible = "acme,coyotes-revenge";
#address-cells = <1>;
#size-cells = <1>;
interrupt-parent = <&intc>; //标识此设备节点属于中断控制器“intc”
gpio@101f3000 {
compatible = "arm,pl061";
reg = <0x101f3000 0x1000
0x101f4000 0x0010>;
interrupts = <0 3 1 >;
};
spi@10115000 {
compatible = "arm,pl022";
reg = <0x10115000 0x1000 >;
interrupts = <0 4 2 >;
};
};
GIC:中断控制器
关于这几个属性的具体含义在 Documentation/devicetree/bindings/arm/gic.txt 中有详细说明:
The 1st cell is the interrupt type; 0 for SPI interrupts, 1 for PPI
interrupts.
The 2nd cell contains the interrupt number for 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 as follows:
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
bits[15:8] PPI interrupt cpu mask. Each bit corresponds to each of
the 8 possible cpus attached to the GIC. A bit set to '1' indicated
the interrupt is wired to that CPU. Only valid for PPI interrup
第一个单元格是中断类型;SPI中断为0,PPI为1中断。
第二个单元格包含中断类型的中断号。SPI中断在范围内[0-987]。PPI中断在范围(0-15)。
第三个单元格是标志,编码如下:
bits[3:0] 触发类型和等级标志
1 = 上升沿触发
2 = 下降沿触发
4 = 高电平触发
8 = 低电平触发
位[15:8]PPI中断cpu掩码。每个位对应于每个位
连接到GIC的8个可能的cpu。位设置为“1”表示
中断连接到那个CPU。仅对PPI交互有效
Example:
interrupt-controller@2c001000 {
compatible = "arm,cortex-a15-gic";
#interrupt-cells = <3>;
interrupt-controller;
reg = <0x2c001000 0x1000>,
<0x2c002000 0x1000>,
<0x2c004000 0x2000>,
<0x2c006000 0x2000>;
interrupts = <1 9 0xf04>;
};
一个设备可以有多个中断号,如:定义为interrupts=<0 168 4>, <0 169 4>;
由于系统的中断号不够用,于是内核发明了内核域的概览,有中断进程分层(父子层)
于是中断源--interrupt parent-->GPIO--interrupt parent-->GIC1--interrupt parent-->GIC2--...-->CPU
对于平台设备:
简单的通过如下API就可以指定想取哪一个中断,其中的参数num就是中断的index。
int platform_get_irq(struct platform_device *dev, unsigned int num);
当然在.dts文件中可以对中断进行命名,而后在驱动中通过platform_get_irq_byname()来获取对应的中断号。譬如代码14演示了在drivers/dma/fsl-edma.c中通过platform_get_irq_byname()获取IRQ,以及arch/arm/boot/dts/vf610.dtsi与fsl-edma驱动对应节点的中断描述。
代码14 设备树中的中断名称以及驱动获取中断
static int
fsl_edma_irq_init(struct platform_device *pdev,struct fsl_edma_engine
{
fsl_edma->txirq = platform_get_irq_byname(pdev, "edma-tx");
fsl_edma->errirq = platform_get_irq_byname(pdev, "edma-err");
}
edma0: dma-controller@40018000 {
#dma-cells = <2>;
compatible = "fsl,vf610-edma";
reg = <0x40018000 0x2000>,
<0x40024000 0x1000>,
<0x40025000 0x1000>;
interrupts = <0 8 IRQ_TYPE_LEVEL_HIGH>,
<0 9 IRQ_TYPE_LEVEL_HIGH>;
interrupt-names = "edma-tx", "edma-err";
dma-channels = <32>;
clock-names = "dmamux0", "dmamux1";
clocks = <&clks VF610_CLK_DMAMUX0>,
<&clks VF610_CLK_DMAMUX1>;
};
第4行、第5行的platform_get_irq_byname()的第2个参数与.dts中的interrupt-names是一致的。
2.7 GPIO、时钟、 pinmux连接 @@@@@
1.GPIO
GPIO控制器:对应的设备节点需声明gpio-controller属性,并设置#gpio-cells的大小,如下。
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:
- 命名:通过定义命名xxx-gpios属性来引用GPIO控制器的设备节点
- 第1个cell:GPIO号
- 第2个cell:GPIO的极性,为0的时候是高电平有效,为1的时候则是低电平有效。
sdhci@c8000400 {
status = "okay";
cd-gpios = <&gpio01 0>; //gpio1_0
wp-gpios = <&gpio02 0>; //gpio2_0
power-gpios = <&gpio03 0>; //gpio3_0
bus-width = <4>;
};
而具体的设备驱动则通过类似如下的方法来获取GPIO:
cd_gpio = of_get_named_gpio(np, "cd-gpios", 0);
wp_gpio = of_get_named_gpio(np, "wp-gpios", 0);
power_gpio = of_get_named_gpio(np, "power-gpios", 0);
of_get_named_gpio()这个API的原型如下:
static inline int of_get_named_gpio(struct device_node *np,
const char *propname, int index);
在.dts和设备驱动不关心GPIO名字的情况下,也可以直接通过of_get_gpio()获取GPIO,此函数原型为:
static inline int of_get_gpio(struct device_node *np, int index);
如对于compatible="gpio-control-nand"的基于GPIO的NAND控制器而言,在.dts中会定义多个gpio属性:
gpio-nand@1,0 {
compatible = "gpio-control-nand";
reg = <1 0x0000 0x2>;
#address-cells = <1>;
#size-cells = <1>;
gpios = <&banka 1 0 /* rdy */
&banka 2 0 /* nce */
&banka 3 0 /* ale */
&banka 4 0 /* cle */
0 /* nwp */>;
partition@0 {
//...
};
};
在相应的驱动代码drivers/mtd/nand/gpio.c中是这样获取这些GPIO的:
plat->gpio_rdy = of_get_gpio(dev->of_node, 0);
plat->gpio_nce = of_get_gpio(dev->of_node, 1);
plat->gpio_ale = of_get_gpio(dev->of_node, 2);
plat->gpio_cle = of_get_gpio(dev->of_node, 3);
plat->gpio_nwp = of_get_gpio(dev->of_node, 4);
2.时钟
时钟和GPIO也是类似的,时钟控制器的节点被使用时钟的模块引用:
clocks = <&clks 138>, <&clks 140>, <&clks 141>;
clock-names = "uart", "general", "noc";
而驱动中则使用上述的clock-names属性作为clk_get()或devm_clk_get()的第二个参数来申请时钟,譬如获取第2个时钟:
devm_clk_get(&pdev->dev, "general");
<&clks 138>里的138这个index是与相应时钟驱动中clk的表的顺序对应的,很多开发者也认为这种数字出现在设备树中不太好,因此他们把clk的index作为宏定义到了arch/arm/boot/dts/include/dt-bindings/clock中。譬如include/dt-bindings/clock/imx6qdl-clock.h中存在这样的宏:
#define IMX6QDL_CLK_STEP 16
#define IMX6QDL_CLK_PLL1_SW 17
#define IMX6QDL_CLK_ARM 104
而arch/arm/boot/dts/imx6q.dtsi则是这样引用它们的:
clocks = <&clks IMX6QDL_CLK_ARM>,
<&clks IMX6QDL_CLK_PLL2_PFD2_396M>,
<&clks IMX6QDL_CLK_STEP>,
<&clks IMX6QDL_CLK_PLL1_SW>,
<&clks IMX6QDL_CLK_PLL1_SYS>;
3.pinmux(引出线)
在设备树中,某个设备节点使用的pinmux的引脚群是通过phandle来指定的。譬如在arch/arm/boot/dts/atlas6.dtsi的pinctrl节点中包含所有引脚群的描述,如代码清单18.15所示。
代码15 设备树中pinctrl控制器的引脚群
gpio: pinctrl@b0120000 {
#gpio-cells = <2>;
#interrupt-cells = <2>;
compatible = "sirf,atlas6-pinctrl";
lcd_16pins_a: lcd0@0 {
lcd {
sirf,pins = "lcd_16bitsgrp";
sirf,function = "lcd_16bits";
};
};
spi0_pins_a: spi0@0 {
spi {
sirf,pins = "spi0grp";
sirf,function = "spi0";
};
};
spi1_pins_a: spi1@0 {
spi {
sirf,pins = "spi1grp";
sirf,function = "spi1";
};
};
};
而SPI0这个硬件实际上需要用到spi0_pins_a对应的spi0grp这一组引脚,因此在atlas6-evb.dts中通过pinctrl-0引用了它,如下面所示。
spi@b00d0000 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&spi0_pins_a>;
};
到目前为止,我们可以勾勒出一个设备树的全局视图,图18.2显示了设备树中的节点、属性、 label以及phandle等信息。
3 由设备树引发的BSP和驱动变更 ########
有了设备树后,不再需要大量的板级信息,譬如过去经常在arch/arm/plat-xxx和arch/arm/mach-xxx中实施如下事情。
1.注册platform_device,绑定resource,即内存、 IRQ等板级信息
通过设备树后,形如:
static struct resource xxx_resources[] = {
[0] = {
.start = …,
.end = …,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = …,
.end = …,
.flags = IORESOURCE_IRQ,
},
};
static struct platform_device xxx_device = {
.name = "xxx",
.id = -1,
.dev = {
.platform_data = &xxx_data,
},
.resource = xxx_resources,
.num_resources = ARRAY_SIZE(xxx_resources),
};
之类的platform_device代码都不再需要,其中platform_device会由内核自动展开。
而这些resource实际来源于.dts中设备节点的reg、 interrupts属性。
典型的,大多数总线都与“simple_bus”兼容,而在与SoC对应的设备的.init_machine成员函数中,调用
of_platform_bus_probe(NULL, xxx_of_bus_ids, NULL);
即可自动展开所有的platform_device。
2.注册i2c_board_info,指定IRQ等板级信息
形如:
static struct i2c_board_info __initdata afeb9260_i2c_devices[] = {
{
I2C_BOARD_INFO("tlv320aic23", 0x1a),
}, {
I2C_BOARD_INFO("fm3130", 0x68),
}, {
I2C_BOARD_INFO("24c64", 0x50),
},
};
之类的i2c_board_info代码目前不再需要出现,现在只需要把tlv320aic23、 fm3130、 24c64这些设备节点填充作为相应的控制器节点的子节点即可,类似于前面的代码:
i2c@1,0 {
compatible = "acme,a1234-i2c-bus";
rtc@58 {
compatible = "maxim,ds1338";
reg = <58>;
interrupts = < 7 3 >;
};
};
设备树中的I2C客户端会通过在I2C host驱动的probe()函数中调用的of_i2c_register_devices(&i2c_dev->adapter);被自动展开。
3.注册spi_board_info,指定IRQ等板级信息
形如:
static struct spi_board_info afeb9260_spi_devices[] = {
{ /* DataFlash chip */
.modalias = "mtd_dataflash",
.chip_select = 1,
.max_speed_hz = 15 * 1000 * 1000,
.bus_num = 0,
},
};
之类的spi_board_info代码目前不再需要出现,与I2C类似,现在只需要把mtd_dataflash之类的节点作为SPI控制器的子节点即可, SPI host驱动的probe()函数通过spi_register_master()注册主机的时候,会自动展开依附于它的从机, spear1310-evb.dts中的st, m25p80SPI接口的NOR Flash节点如下:
spi0: spi@e0100000 {
status = "okay";
num-cs = <3>;
m25p80@1 {
compatible = "st,m25p80";
};
}
4.多个针对不同电路板的设备,以及相关的回调函数
在过去, ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围的设备,引入设备树之后, MACHINE_START变更为DT_MACHINE_START,其中含有一个.dt_compat成员,用于表明相关的设备与.dts中根节点的兼容属性的兼容关系。
这样可以显著改善代码的结构并减少冗余的代码,在不支持设备树的情况下,光是一个S3C24xx就存在多个板文件,
譬如mach-amlm5900.c、 mach-gta02.c、 mach-smdk2410.c、 machqt2410.c、 mach-rx3715.c等,其累计的代码量是相当大的,板级信息都用C语言来实现。而采用设备树后,我们可以对多个SoC和板子使用同一个DT_MACHINE和板文件,板子和板子之间的差异更多只是通过不同的.dts文件来体现。
5.设备与驱动的匹配方式
使用设备树后,驱动需要与在.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。新的驱动、设备的匹配变成了设备树节点的兼容属性和设备驱动中的OF匹配表的匹配。
6.设备的平台数据属性化
在Linux 2.6下,驱动习惯自定义platform_data,在arch/arm/mach-xxx注册platform_device、 i2c_board_info、spi_board_info等的时候绑定platform_data,而后驱动通过标准API获取平台数据。譬如,在arch/arm/mach-at91/board-sam9263ek.c下用如下代码注册gpio_keys设备,它通过gpio_keys_platform_data结构体来定义platform_data。
static struct gpio_keys_button ek_buttons[] = {
{ /* BP1, "leftclic" */
.code = BTN_LEFT,
.gpio = AT91_PIN_PC5,
.active_low = 1,
.desc = "left_click",
.wakeup = 1,
},
{ /* BP2, "rightclic" */
//...
}
};
static struct gpio_keys_platform_data ek_button_data = {
.buttons = ek_buttons,
.nbuttons = ARRAY_SIZE(ek_buttons),
};
static struct platform_device ek_button_device = {
.name = "gpio-keys",
.id = -1,
.num_resources = 0,
.dev = {
.platform_data= &ek_button_data,
}
};
设备驱动drivers/input/keyboard/gpio_keys.c则通过如下简单方法取得这个信息。
static int gpio_keys_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
const struct gpio_keys_platform_data *pdata = dev_get_platdata(dev);
//...
}
在转移到设备树后, platform_data便不再喜欢放在arch/arm/mach-xxx中了,它需要从设备树的属性中获取,比如一个电路板上有gpio_keys,则只需要在设备树中添加类似arch/arm/boot/dts/exynos4210-origen.dts中的如代码17所示的信息则可。
代码17 在设备树中添加GPIO按键信息
gpio_keys {
compatible = "gpio-keys";
#address-cells = <1>;
#size-cells = <0>;
up {
label = "Up";
gpios = <&gpx2 0 1>;
linux,code = <KEY_UP>;
gpio-key,wakeup;
};
down {
label = "Down";
gpios = <&gpx2 1 1>;
linux,code = <KEY_DOWN>;
gpio-key,wakeup;
};
//...
};
而drivers/input/keyboard/gpio_keys.c则通过以of_开头的读属性的API来读取这些信息,并组织出gpio_keys_platform_data结构体,如代码18所示。
代码18 在GPIO按键驱动中获取.dts中的键描述
static struct gpio_keys_platform_data *gpio_keys_get_devtree_pdata(struct device *dev)
{
struct device_node *node, *pp;
struct gpio_keys_platform_data *pdata;
struct gpio_keys_button *button;
int error;
int nbuttons;
int i;
node = dev->of_node;
if (!node)
return ERR_PTR(-ENODEV);
nbuttons = of_get_child_count(node);
if (nbuttons == 0)
return ERR_PTR(-ENODEV);
pdata = devm_kzalloc(dev, sizeof(*pdata) + nbuttons * sizeof(*butt), GFP_KERNEL);
if (!pdata)
return ERR_PTR(-ENOMEM);
pdata->buttons = (struct gpio_keys_button *)(pdata + 1);
pdata->nbuttons = nbuttons;
pdata->rep = !!of_get_property(node, "autorepeat", NULL);
i = 0;
for_each_child_of_node(node, pp) {
int gpio;
enum of_gpio_flags flags;
if (!of_find_property(pp, "gpios", NULL)) {
pdata->nbuttons--;
dev_warn(dev, "Found button without gpios\n");
continue;
}
gpio = of_get_gpio_flags(pp, 0, &flags);
if (gpio < 0) {
error = gpio;
if (error != -EPROBE_DEFER){
dev_err(dev,"Failed to get gpio flags", ererror);
}
return ERR_PTR(error);
}
button = &pdata->buttons[i++];
button->gpio = gpio;
button->active_low = flags & OF_GPIO_ACTIVE_LOW;
if (of_property_read_u32(pp, "linux,code", &button->code)){
dev_err(dev, "Button without keycode: 0x%x\n", button->gpio);
return ERR_PTR(-EINVAL);
}
button->desc = of_get_property(pp, "label", NULL);
if (of_property_read_u32(pp, "linux,input-type", &button->type)
button->type = EV_KEY;
button->wakeup = !!of_get_property(pp, "gpio-key,wakeup", NULL);
if (of_property_read_u32(pp, "debounce-interval",&button->debounce_interval))
button->debounce_interval = 5;
}
if (pdata->nbuttons == 0)
return ERR_PTR(-EINVAL);
return pdata;
}
上述代码通过第31行的for_each_child_of_node()遍历gpio_keys节点下的所有子节点,并通过of_get_gpio_flags()、of_property_read_u32()等API读取出来与各个子节点对应的GPIO、与每个GPIO对应的键盘键值等。
4 常用的OF API
关于设备树的API通常被冠以of_前缀,它们的实现代码位于内核的drivers/of/目录下。
1.寻找节点
struct device_node *of_find_compatible_node(struct device_node *from,
const char *type, const char *compatible);
根据兼容属性,获得设备节点。遍历设备树中的设备节点,看看哪个节点的类型、兼容属性与本函数的输入参数匹配,在大多数情况下, from、 type为NULL,则表示遍历了所有节点。
2.读取属性
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()。如在arch/arm/mm/cache-l2x0.c中,通过如下语句可读取
L2cache的"arm, data-latency"属性:
of_property_read_u32_array(np, "arm,data-latency",data, ARRAY_SIZE(data));
在arch/arm/boot/dts/vexpress-v2p-ca9.dts中,对应的含有"arm, data-latency"属性的L2cache节点如下:
L2: cache-controller@1e00a000 {
compatible = "arm,pl310-cache";
reg = <0x1e00a000 0x1000>;
interrupts = <0 43 4>;
cache-level = <2>;
arm,data-latency = <1 1 1>;
arm,tag-latency = <1 1 1>;
}
在有些情况下,整型属性的长度可能为1,于是内核为了方便调用者,又在上述API的基础上封装出了更加简单的读单一整形属性的API,它们为int of_property_read_u8()、of_property_read_u16()等,实现于include/linux/of.h中,如代
码19所示。
代码19 设备树中整型属性的读取API
static inline int of_property_read_u8(const struct device_node *np,
const char *propname, u8 *out_value)
{
return of_property_read_u8_array(np, propname, out_value, 1);
}
static inline int of_property_read_u16(const struct device_node *np,
const char *propname, u16 *out_value)
{
return of_property_read_u16_array(np, propname, out_value, 1)
}
static inline int of_property_read_u32(const struct device_node *np,
const char *propname, u32 *out_value)
{
return of_property_read_u32_array(np, propname, out_value, 1)
}
除了整型属性外,字符串属性也比较常用,其对应的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);
如:test_list_string = "red fish", "fly fish", "blue, fish";
首先系统会把这几个字符串合在一起,对应节点属性的value成员,这个 value 是 void指针
前者是把value直接赋值给 *out_string,而里面是多个字符串组合而成,还要自己去分开
后者是把value分成单个字符串,你要第index个就给你第index个的地址
如drivers/clk/clk.c中的
of_clk_get_parent_name()函数就通过
of_property_read_string_index()遍历clkspec节点的所有"clock-output-names"字符串数组属性。
代码20 在驱动中读取第index个字符串的例子
const char *of_clk_get_parent_name(struct device_node *np, int index)
{
struct of_phandle_args clkspec;
const char *clk_name;
int rc;
if (index < 0)
return NULL;
rc = of_parse_phandle_with_args(np, "clocks", "#clock-cells", index, &clkspec);
if (rc)
return NULL;
if (of_property_read_string_index(clkspec.np, "clock-output-names",
clkspec.args_count clkspec.args[0] : 0, &clk_name) < 0){
clk_name = clkspec.np->name;
}
of_node_put(clkspec.np);
return clk_name;
}
EXPORT_SYMBOL_GPL(of_clk_get_parent_name);
3.内存映射
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参数指向的结构体中。
4.解析中断
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);
通过设备树获得设备的中断号,实际上是从.dts中的interrupts属性里解析出中断号。若设备使用了多个中断, index指定中断的索引号。
5.获取与节点对应的platform_device
struct platform_device *of_find_device_by_node(struct device_node *np);
在可以拿到device_node的情况下,如果想反向获取对应的platform_device,可使用上述API。当然,在已知platform_device的情况下,想获取device_node则易如反掌,例如:
static int sirfsoc_dma_probe(struct platform_device *op)
{
struct device_node *dn = op->dev.of_node;
}
5 设备和驱动之间如何匹配
当我们在DTS文件中写了一个设备节点,如
test_node@12345678{
compatible = "farsight,tst";
reg = <0x12345678 0x12
0x87654321 0x34>;
tesprop,mytest;
test_list_string = "red fish", "fly fish", "blue, fish";
interrupts = <&gpx1 2 3>, //对应gpx1_2 ,上升沿和下降沿触发
<&gpx1 3 3>, //对应gpx1_3 ,上升沿和下降沿触发
<&gpx3 2 3>; //对应gpx3_2 ,上升沿和下降沿触发
}
就会生成一个platform_device,最后用compatible进行匹配
使用设备树后,驱动需要与.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器节点的OF匹配表,
struct of_device_id
{
char name[32];
char type[32];
char compatible[128];
#ifdef __KERNEL__
void *data;
#else
kernel_ulong_t data;
#endif
};
static const struct of_device_id a1234_i2c_of_match[] = {
{
.compatible = "acme,a1234-i2c-bus",
},
{},
};
MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);
static struct platform_driver i2c_a1234_driver = {
.driver = {
.name = "a1234-i2c-bus",
.owner = THIS_MODULE,
.of_match_table = a1234_i2c_of_match,
},
.probe = i2c_a1234_probe,
.remove = i2c_a1234_remove,
};
module_platform_driver(i2c_a1234_driver);
对于I2C和SPI从设备而言,同样也可以通过of_match_table添加匹配的.dts中的相关节点的兼容属性,如sound/soc/codecs/wm8753.c中的针对WolfsonWM8753的of_match_table,具体如代码清单18.7所示。
static const struct of_device_id wm8753_of_match[] = {
{ .compatible = "wlf,wm8753", },
{ }
};
MODULE_DEVICE_TABLE(of, wm8753_of_match);
static struct spi_driver wm8753_spi_driver = {
.driver = {
.name = "wm8753",
.owner = THIS_MODULE,
.of_match_table = wm8753_of_match,
},
.probe = wm8753_spi_probe,
.remove = wm8753_spi_remove,
};
static struct i2c_driver wm8753_i2c_driver = {
.driver = {
.name = "wm8753",
.owner = THIS_MODULE,
.of_match_table = wm8753_of_match,
},
.probe = wm8753_i2c_probe,
.remove = wm8753_i2c_remove,
.id_table = wm8753_i2c_id,
};
第2行显示WM8753的供应商是“wlf”,它其实是对应于Wolfson Microe-lectronics的前缀。
设备树绑定供应商前缀注册表
这不是一个详尽的列表,但是您应该在它之前添加新的前缀使用它们来避免名称空间冲突。
位于内核文档: Documentation/devicetree/bindings/vendorprefixes.txt
注意: I2C和SPI外设驱动和设备树中设备节点的兼容属性还有一种弱式匹配方法,就是“别名”匹配。
兼容属性的组织形式为<manufacturer>,<model>,别名其实就是去掉兼容属性中manufacturer前缀后的<model>部分。关于这一点,可查看drivers/spi/spi.c的源代码,函数spi_match_device()暴露了更多的细节,如果别名出现在设备spi_driver的id_table里面,或者别名与spi_driver的name字段相同, SPI设备和驱动都可以匹配上,代码清单18.8显示了SPI的别名匹配
static int spi_match_device(struct device *dev, struct device_driver *drv)
{
const struct spi_device *spi = to_spi_device(dev);
const struct spi_driver *sdrv = to_spi_driver(drv);
/* Attempt an OF style match */
if(of_driver_match_device(dev, drv))
return 1;
/* Then try ACPI */
if (acpi_driver_match_device(dev, drv))
return 1;
if (sdrv->id_table)
return !!spi_match_id(sdrv->id_table, spi);
return strcmp(spi->modalias, drv->name) == 0;
}
static const struct spi_device_id *spi_match_id(const struct spi_devic_id *id,
const struct spi_device *sdev)
{
while (id->name[0]) {
if (!strcmp(sdev->modalias, id->name))
return id;
id++;
}
return NULL;
}
通过这个别名匹配,实际上, SPI和I2C的外设驱动即使没有of_match_table,还是可以和设备树中的节点匹配上的。