第18章 ARM Linux设备树之二(设备树的组成和结构)

18.2.2 根节点兼容性

上述.dts文件中,第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-ca9arm,vexpress,v2p-ca5sarm,vexpress,v2p-ca15_a7

进一步地看,arch/arm/boot/dts/exynos4210-origen.dts的兼容性字段如下:

compatible = "insignal,origen", "samsung,exynos4210", "samsung,exynos4";

第一个字符串是板子名字(很特定),第2个字符串是芯片名字(比较特定),第3个字段是芯片系列的名字(比较通用)。

作为类比,arch/arm/boot/dts/exynos4210-universal_c210.dts的兼容性字段则如下:

compatible = "samsung,universal_c210", "samsung,exynos4210", "samsung,exynos4";

由此可见,它与exynos4210-origen.dts的区别只在于第1个字符串(特定的板子名字)不一样,后面芯片名和芯片系列的名字都一样。

在Linux 2.6内核中,ARM Linux针对不同的电路板会建立由MACHINE_START和MACHINE_END包围起来的针对这个设备的一系列回调函数,如代码清单18.3所示。

代码清单18.3 ARM Linux 2.6时代的设备

 1 MACHINE_START(VEXPRESS, "ARM-Versatile Express")
 2         .atag_offset    = 0x100,
 3         .smp            = smp_ops(vexpress_smp_ops),
 4         .map_io         = v2m_map_io,
 5         .init_early     = v2m_init_early,
 6         .init_irq       = v2m_init_irq,
 7         .timer          = &v2m_timer,
 8         .handle_irq     = gic_handle_irq,
 9         .init_machine   = v2m_init,
10        .restart        = vexpress_restart,
11 MACHINE_END

这些不同的设备会有不同的MACHINE ID,U-boot在启动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所示。

代码清单18.4 ARM Linux 3.x时代的设备

 1 static const char * const v2m_dt_match[] __initconst = { // 字符类型的指针数组
 2         "arm,vexpress",
 3         "xen,xenvm",
 4         NULL,
 5};
 6 DT_MACHINE_START(VEXPRESS_DT, "ARM-Versatile Express")
 7          .dt_compat      = v2m_dt_match,
 8          .smp            = smp_ops(vexpress_smp_ops),
 9          .map_io         = v2m_dt_map_io,
10         .init_early     = v2m_dt_init_early,
11         .init_irq       = v2m_dt_init_irq,
12         .timer          = &v2m_dt_timer,
13         .init_machine   = v2m_dt_init,
14         .handle_irq     = gic_handle_irq,
15         .restart        = vexpress_restart,
16 MACHINE_END

Linux倡导针对多个SoC、多个电路板的通用DT设备,即一个DT设备的.dt_compat包含多个电路板.dts文件的根节点兼容属性字符串。之后,如果这多个电路板的初始化序列不一样,可以通过int
of_machine_is_compatible(const char*compat)API判断具体的电路板是什么。在Linux内核中,常常使用如下API来判断根节点的兼容性:

int of_machine_is_compatible(const char *compat);

此API判断目前运行的板子或者SoC的兼容性,它匹配的是设备树根节点下的兼容属性。例如
drivers/cpufreq/exynos-cpufreq.c中就有判断运行的CPU类型是exynos4210、exynos4212、exynos4412还是exynos5250的代码,进而分别处理,如代码清单18.5所示。

代码清单18.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;
}


if (ret)
goto err_vdd_arm;

if (exynos_info->set_freq == NULL) {
dev_err(&pdev->dev, "No set_freq function (ERR)\n");
goto err_vdd_arm;
}
arm_regulator = regulator_get(NULL, "vdd_arm");
if (IS_ERR(arm_regulator)) {
dev_err(&pdev->dev, "failed to get resource vdd_arm\n");
goto err_vdd_arm;
}

/* Done here as we want to capture boot frequency */
locking_frequency = clk_get_rate(exynos_info->cpu_clk) / 1000;
if (!cpufreq_register_driver(&exynos_driver))

return 0;


dev_err(&pdev->dev, "failed to register cpufreq driver\n");
regulator_put(arm_regulator);
err_vdd_arm:
kfree(exynos_info);
return -EINVAL;
}

如果一个兼容包含多个字符串,譬如对于前面介绍的根节点兼容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")

18.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>;
 };

兼容属性的第2个字符串"cfi-flash"明显比第1个字符串"arm,vexpress-flash"涵盖的范围更广。

再如,Freescale MPC8349 SoC含一个串口设备,它实现了国家半导体(National Sem-iconductor)的NS16550寄存器接口。则MPC8349串口设备的兼容属性为compatible="fsl,mpc8349-uart","ns16550"。其中,fsl,mpc8349-uart指代了确切的设备,ns16550代表该设备与NS16550 UART保持了寄存器兼容。因此,设备节点的兼容性和根节点的兼容性是类似的,都是“从具体到抽象”。

使用设备树后,驱动需要与.dts中描述的设备节点进行匹配,从而使驱动的probe()函数执行。对于platform_driver而言,需要添加一个OF匹配表,如前文的.dts文件的"acme,a1234-i2c-bus"兼容I2C控制器节点的OF匹配表,具体代码清单18.6所示。

代码清单18.6 platform设备驱动中的of_match_table

1 static const struct of_device_id a1234_i2c_of_match[] = {
2          { .compatible = "acme,a1234-i2c-bus", },
3          {},
4};
5 MODULE_DEVICE_TABLE(of, a1234_i2c_of_match);/* type ,name*/

 6
 7static struct platform_driver i2c_a1234_driver = {
 8          .driver = {
 9                  .name = "a1234-i2c-bus",
10                  .owner = THIS_MODULE,
11                  .of_match_table = a1234_i2c_of_match,
12          },
13          .probe = i2c_a1234_probe,
14          .remove = i2c_a1234_remove,
15};
16 module_platform_driver(i2c_a1234_driver);

对于I2C和SPI从设备而言,也可以通过of_match_table添加匹配的.dts中的相关节点的兼容属性,如sound/soc/codecs/wm8753.c中的针对Wolfson WM8753的of_match_table,具体如代码清单18.7所示。

代码清单18.7 I2C、SPI设备驱动中的of_match_table

static const struct of_device_id wm8753_of_match[] = {
{ .compatible = "wlf,wm8753", },// 第2行
{ }
};
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/vendor-prefixes.txt

对于I2C、SPI还有一点需要提醒的是,I2C和SPI外设驱动和设备树中设备节点的兼容属性还有一种弱式匹配方法,就是“别名”匹配。兼容属性的组织形式为<manufacturer>,<model>,别名就是去掉兼容属性中manufacturer前缀后的<model>部分。关于这一点,可查看drivers/spi/spi.c的源代码,函数spi_match_device()暴露了更多的细节,如果别名出现在设备spi_driver的id_table里面,或者别名与spi_driver的name字段相同,SPI设备和驱动都可以匹配上,代码清单18.8显示了SPI的别名匹配。

代码清单18.8 SPI的别名匹配

static const struct spi_device_id *spi_match_id(const struct spi_device_id *id,
const struct spi_device *sdev)
{
while (id->name[0]) {
if (!strcmp(sdev->modalias, id->name))
return id;
id++;
}
return NULL;
}

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;
}

通过这个别名匹配,实际上,SPI和I2C的外设驱动即使没有of_match_table,还是可以和设备树中的节点匹配上的。

一个驱动可以在of_match_table中兼容多个设备,在Linux内核中常常使用如下API来判断具体的设备是什么:

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表

 1#define L2C_ID(name, fns) { .compatible = name, .data = (void *)&fns }
 2static const struct of_device_id l2x0_ids[] __initconst = {
 3          L2C_ID("arm,l210-cache", of_l2c210_data),
 4          L2C_ID("arm,l220-cache", of_l2c220_data),
 5          L2C_ID("arm,pl310-cache", of_l2c310_data),
 6          L2C_ID("brcm,bcm11351-a2-pl310-cache", of_bcm_l2x0_data),
 7          L2C_ID("marvell,aurora-outer-cache", of_aurora_with_outer_data),
 8          L2C_ID("marvell,aurora-system-cache", of_aurora_no_outer_data),
 9          L2C_ID("marvell,tauros3-cache", of_tauros3_data),
10          /* Deprecated IDs */
11          L2C_ID("bcm,bcm11351-a2-pl310-cache", of_bcm_l2x0_data),
12          {}
13 };

在驱动中,通过如代码清单18.10的方法拿到了对应于L2缓存类型的.data成员,其中主要用到了of_match_node()这个API。

代码清单18.10 通过of_match_node()找到.data

 1int __init l2x0_of_init(u32 aux_val, u32 aux_mask)
 2{
 3          const struct l2c_init_data *data;
 4          struct device_node *np;
 5
 6          np = of_find_matching_node(NULL, l2x0_ids);
 7          if (!np)
 8                  return -ENODEV;
 9          …
10          data = of_match_node(l2x0_ids, np)->data;
11}

如果电路板的.dts文件中L2缓存是arm,pl310-cache,那么上述代码第10行找到的data就是of_l2c310_data,它是l2c_init_data结构体的一个实例。l2c_init_data是一个由L2缓存驱动自定义的数据结构,在其定义中既可以保护数据成员,又可以包含函数指针,如代码清单18.11所示。

代码清单18.11 与兼容对应的特定data实例

 1struct l2c_init_data {
 2          const char *type;
 3          unsigned way_size_0;
 4          unsigned num_lock;
 5          void (*of_parse)(const struct device_node *, u32 *, u32 *);
 6          void (*enable)(void __iomem *, u32, unsigned);
 7          void (*fixup)(void __iomem *, u32, struct outer_cache_fns *);
 8          void (*save)(void __iomem *);
 9          struct outer_cache_fns outer_cache;
10};

通过这种方法,驱动可以把与某个设备兼容的私有数据寻找出来,体现了一种面向对象的设计思想,避免了大量的if,else或者switch,case语句。

18.2.4 设备节点及label(标号)的命名

代码清单18.2的.dts文件中,根节点“/”的cpus子节点下面又包含两个cpu子节点,描述了此设备上的两个CPU,并且两者的兼容属性为:"arm,cortex-a9"。

注意cpus和cpus的两个cpu子节点的命名,遵循的组织形式为<name>[@<unit-address>],<>中的内容是必选项,[]中的则为可选项。name是一个ASCII字符串,用于描述节点对应的设备类型,如3comEthernet适配器对应的节点name宜为ethernet,而不是3com509。如果一个节点描述的设备有地址,则应该给出@unit-address。多个相同类型设备节点的name可以一样,只要unit-address不同即可,如本例中含有cpu@0、cpu@1以及serial@101f0000与serial@101f2000这样的同名节点。设备的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 {
              compatible = "melfas,mms114";
              reg = <0x48>;
              …
       };
};

上述节点的reg属性标示的I2C从地址与@后面的地址一样

具体的节点命名规范可见ePAPR(embedded Power Architecture Platform Reference)标准,在https://www.power.org中可下载该标准。

还可以给一个设备节点添加label,之后可以通过&label的形式访问这个label,这种引用是通过phandle(pointer handle)进行的

例如,在arch/arm/boot/dts/omap5.dtsi中,第3组GPIO有gpio3这个label,如代码清单18.12所示。

代码清单18.12 在设备树中定义label

 1 gpio3: gpio@48057000 {
 2         compatible = "ti,omap4-gpio";
 3         reg = <0x48057000 0x200>;
 4         interrupts = <GIC_SPI 31 IRQ_TYPE_LEVEL_HIGH>;
 5         ti,hwmods = "gpio3";
 6         gpio-controller;
 7         #gpio-cells = <2>;
 8         interrupt-controller;
 9         #interrupt-cells = <2>;
10 };

而hsusb2_phy这个USB的PHY复位GPIO用的是这组GPIO中的一个,所以它通过phandle引用了“gpio3”,如代码清单18.13所示。

代码清单18.13 通过phandle引用其他节点

 1 /* HS USB Host PHY on PORT 2 */
 2 hsusb2_phy: hsusb2_phy {
 3          compatible = "usb-nop-xceiv";
 4          reset-gpios = <&gpio3 12 GPIO_ACTIVE_LOW>; /* gpio3_76 HUB_RESET */
 5};

代码清单18.12第1行的gpio3是gpio@48057000节点的label,而代码清单18.13的hsusb2_phy则通过&gpio3引用了这个节点,表明自己要使用这一组GPIO中的第12个GPIO。这种phandle引用其实表明硬件之间的一种关联性。

再举一例,在arch/arm/boot/dts/omap5.dtsi中,可以看到类似如下的label,从这些实例可以看出,label习惯以<设备类型><index>进行命名:

i2c1: i2c@48070000 {
};
i2c2: i2c@48072000 {
};
i2c3: i2c@48060000 {

};

代码清单18.13中居然引用了GPIO_ACTIVE_LOW这个类似C语言的宏。文件.dts的编译过程确实支持C的预处理,相应的.dts文件也包括了包含GPIO_ACTIVE_LOW这个宏定义的头文件:

#include <dt-bindings/gpio/gpio.h>

对于ARM而言,dt-bindings头文件位于内核的arch/arm/boot/dts/include/dt-bindings目录中。它实际上是一个符号链接:

xz@ubuntu2018:~/share/vichip_qualcomm_8953/kernel/msm-3.18/arch/arm/boot/dts/include$ ls -al
total 40
drwxrwxr-x 2 xiezhi xiezhi  4096 May 10 11:56 .
drwxrwxr-x 4 xiezhi xiezhi 36864 May 10 11:56 ..
lrwxrwxrwx 1 xiezhi xiezhi    34 May 10 11:56 dt-bindings -> ../../../../../include/dt-bindings

从内核的scripts/Makefile.lib这个文件可以看出,文件.dts的编译过程确实是支持C预处理的。

cmd_dtc = $(CPP) $(dtc_cpp_flags) -x assembler-with-cpp -o $(dtc-tmp) $< ; \
        $(objtree)/scripts/dtc/dtc -O dtb -o $@ -b 0 \
                -i $(dir $<) $(DTC_FLAGS) \
                -d $(depfile).dtc.tmp $(dtc-tmp) ; \
cat $(depfile).pre.tmp $(depfile).dtc.tmp > $(depfile)

它是先做了$(CPP)$(dtc_cpp_flags)-x assembler-with-cpp-o$(dtc-tmp)$<,再做的.dtc编译。

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页