Linux设备树专有名词及语法规则详解(下)

2019-11-22 11:33发布

每个节点都有 compatible 属性,根节点“/”也不例外,imx6ull-alientek-emmc.dts 文件中根节点的 compatible 属性内容如下所示:

14 / {

15 model = "Freescale i.MX6 ULL 14x14 EVK Board";

16 compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

......

148 }

可以看出,compatible 有两个值:“fsl,imx6ull-14x14-evk”和“fsl,imx6ull”。前面我们说了,设备节点的compatible 属性值是为了匹配Linux 内核中的驱动程序,那么根节点中的 compatible属性是为了做什么工作的? 通过根节点的compatible 属性可以知道我们所使用的设备,一般第一个值描述了所使用的硬件设备名字,比如这里使用的是“imx6ull-14x14-evk”这个设备,第二个值描述了设备所使用的SOC,比如这里使用的是“imx6ull”这颗 SOC。Linux 内核会通过根节点的 compoatible 属性查看是否支持此设备,如果支持的话设备就会启动 Linux 内核。接下来我们就来学习一下Linux 内核在使用设备树前后是如何判断是否支持某款设备的。

1、使用设备树之前设备匹配方法

在没有使用设备树以前,uboot 会向 Linux 内核传递一个叫做 machine id 的值,machine id也就是设备 ID,告诉 Linux 内核自己是个什么设备,看看 Linux 内核是否支持。Linux 内核是支持很多设备的,针对每一个设备(板子),Linux 内核都用MACHINE_START 和MACHINE_END来定义一个 machine_desc 结构体来描述这个设备, 比如在文件 arch/arm/mach-imx/mach-mx35_3ds.c 中有如下定义:

613 MACHINE_START(MX35_3DS, "Freescale MX35PDK")

614 /* Maintainer: Freescale Semiconductor, Inc */ 615 .atag_offset = 0x100,

616 .map_io = mx35_map_io,

617 .init_early = imx35_init_early, 618 .init_irq = mx35_init_irq,

619 .init_time = mx35pdk_timer_init, 620 .init_machine = mx35_3ds_init, 621 .reserve = 

mx35_3ds_reserve,

622 .restart = mxc_restart, 623 MACHINE_END

上述代码就是定义了“ Freescale MX35PDK ” 这个设备, 其中 MACHINE_START 和MACHINE_END 定义在文件 arch/arm/include/asm/mach/arch.h 中,内容如下:

#define MACHINE_START(_type,_name) \

static const struct machine_desc mach_desc_##_type \

 used \

 attribute (( section (".arch.info.init"))) = { \

.nr = MACH_TYPE_##_type, \

.name = _name,

#define MACHINE_END \

};

根据 MACHINE_START 和 MACHINE_END 的宏定义,将代码展开后如下所示:

1 static const struct machine_desc mach_desc_MX35_3DS \

2 used \

3 attribute (( section (".arch.info.init"))) = {

4 .nr = MACH_TYPE_MX35_3DS,

5 .name = "Freescale MX35PDK",

6 /* Maintainer: Freescale Semiconductor, Inc */

7 .atag_offset = 0x100,

8 .map_io = mx35_map_io,

9 .init_early = imx35_init_early,

10 .init_irq = mx35_init_irq,

11 .init_time = mx35pdk_timer_init,

12 .init_machine = mx35_3ds_init,

13 .reserve = mx35_3ds_reserve,

14 .restart = mxc_restart, 15 }

从代码中可以看出,这里定义了一个 machine_desc 类型的结构体变量mach_desc_MX35_3DS , 这 个 变 量 存 储 在 “ .arch.info.init ” 段 中 。 第 4 行的 MACH_TYPE_MX35_3DS 就 是 “ Freescale MX35PDK ” 这 个 板 子 的 machine id 。MACH_TYPE_MX35_3DS 定义在文件 include/generated/mach-types.h 中,此文件定义了大量的machine id,内容如下所示:

15 #define MACH_TYPE_EBSA110 0

16 #define MACH_TYPE_RISCPC 1

17 #define MACH_TYPE_EBSA285 4

18 #define MACH_TYPE_NETWINDER 5

19 #define MACH_TYPE_CATS 6

20 #define MACH_TYPE_SHARK 15

21 #define MACH_TYPE_BRUTUS 16

22 #define MACH_TYPE_PERSONAL_SERVER 17

......

287 #define MACH_TYPE_MX35_3DS 1645

......

1000 #define MACH_TYPE_PFLA03 4575

第 287 行就是 MACH_TYPE_MX35_3DS 的值,为 1645。

前面说了,uboot 会给 Linux 内核传递 machine id 这个参数,Linux 内核会检查这个 machineid,其实就是将 machine id 与示例代码中的这些 MACH_TYPE_XXX 宏进行对比,看看有没有相等的,如果相等的话就表示 Linux 内核支持这个设备,如果不支持的话那么这个设备就没法启动Linux 内核。

2、使用设备树以后的设备匹配方法

当 Linux 内核引入设备树以后就不再使用 MACHINE_START 了, 而是换为了DT_MACHINE_START。DT_MACHINE_START 也定义在文件arch/arm/include/asm/mach/arch.h里面,定义如下:

#define DT_MACHINE_START(_name, _namestr) \

static const struct machine_desc mach_desc_##_name \

 used \

 attribute (( section (".arch.info.init"))) = { \

.nr = ~0, \

.name = _namestr,

可以看出,DT_MACHINE_START 和 MACHINE_START 基本相同,只是.nr 的设置不同,在 DT_MACHINE_START 里面直接将.nr 设置为~0。说明引入设备树以后不会再根据 machineid 来检查 Linux 内核是否支持某个设备了。

打开文件 arch/arm/mach-imx/mach-imx6ul.c,有如下所示内容:

208 static const char *imx6ul_dt_compat[] initconst = {

209 "fsl,imx6ul",

210 "fsl,imx6ull",

211 NULL,

212 };

213

214 DT_MACHINE_START(IMX6UL, "Freescale i.MX6 Ultralite (Device Tree)")

215 .map_io = imx6ul_map_io,

216 .init_irq = imx6ul_init_irq,

217 .init_machine = imx6ul_init_machine,

218 .init_late = imx6ul_init_late,

219 .dt_compat = imx6ul_dt_compat,

220 MACHINE_END

machine_desc 结构体中有个.dt_compat 成员变量,此成员变量保存着本设备兼容属性,示例代码 中设置.dt_compat = imx6ul_dt_compat,imx6ul_dt_compat 表里面有"fsl,imx6ul"和"fsl,imx6ull" 这两个兼容值。只要某个设备( 板子) 根节点“/”的 compatible 属性值与imx6ul_dt_compat 表中的任何一个值相等,那么就表示 Linux 内核支持此设备。imx6ull-alientek-emmc.dts 中根节点的 compatible 属性值如下:

compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ull";

其中“fsl,imx6ull”与 imx6ul_dt_compat 中的“fsl,imx6ull”匹配,因此 I.MX6U-ALPHA 开发板可以正常启动Linux 内核。如果将 imx6ull-alientek-emmc.dts 根节点的compatible 属性改为其他的值,比如:compatible = "fsl,imx6ull-14x14-evk", "fsl,imx6ullll"重新编译DTS,并用新的DTS 启动 Linux 内核,结果如图所示的错误提示:

86ffdb86ef724464a5e681864a61eb07.jpg

系统启动信息

当我们修改了根节点 compatible 属性内容以后,因为 Linux 内核找不到对应的设备,因此Linux 内核无法启动。在 uboot 输出 Starting kernel…以后就再也没有其他信息输出了。接下来我们简单看一下 Linux 内核是如何根据设备树根节点的 compatible 属性来匹配出对应的 machine_desc,Linux 内核调用 start_kernel 函数来启动内核,start_kernel 函数会调用setup_arch 函数来匹配 machine_desc,setup_arch 函数定义在文件 arch/arm/kernel/setup.c 中,函数内容如下(有缩减):

913 void init setup_arch(char **cmdline_p)

914 {

915 const struct machine_desc *mdesc;

916

917 setup_processor();

918 mdesc = setup_machine_fdt( atags_pointer);

919 if (!mdesc)

920 mdesc = setup_machine_tags( atags_pointer,

 machine_arch_type);

921 machine_desc = mdesc;

922 machine_name = mdesc->name;

......

986 }

第 918 行,调用 setup_machine_fdt 函数来获取匹配的 machine_desc,参数就是 atags 的首地址,也就是 uboot 传递给 Linux 内核的dtb 文件首地址,setup_machine_fdt 函数的返回值就是找到的最匹配的machine_desc。

函数 setup_machine_fdt 定义在文件 arch/arm/kernel/devtree.c 中,内容如下(有缩减):

204 const struct machine_desc * init setup_machine_fdt(unsigned int

dt_phys)

205 {

206 const struct machine_desc *mdesc, *mdesc_best = NULL;

......

214

215 if (!dt_phys || !early_init_dt_verify(phys_to_virt(dt_phys)))

216 return NULL;

217

218 mdesc = of_flat_dt_match_machine(mdesc_best,

arch_get_next_mach);

219

......

247

 machine_arch_type = mdesc->nr;

248

249 return mdesc;

250 }

第 218 行,调用函数 of_flat_dt_match_machine 来获取匹配的machine_desc,参数 mdesc_best是默认的 machine_desc ,参数 arch_get_next_mach 是个函数, 此函数定义在定义在arch/arm/kernel/devtree.c 文件中。找到匹配的 machine_desc 的过程就是用设备树根节点的compatible 属性值和 Linux 内核中保存的所以 machine_desc 结构的. dt_compat 中的值比较,看看那个相等,如果相等的话就表示找到匹配的 machine_desc,arch_get_next_mach 函数的工作就是获取 Linux 内核中下一个 machine_desc 结构体。

最后在来看一下 of_flat_dt_match_machine 函数,此函数定义在文件 drivers/of/fdt.c 中,内容如下(有缩减):

705 const void * init of_flat_dt_match_machine(const void

*default_match,

706 const void * (*get_next_compat)(const char * const**))

707 {

708 const void *data = NULL;

709 const void *best_data = default_match; 710 const char *const *compat;

711 unsigned long dt_root;

712 unsigned int best_score = ~1, score = 0; 713

714 dt_root = of_get_flat_dt_root();

715 while ((data = get_next_compat(&compat))) { 716 score = of_flat_dt_match(dt_root, 

compat); 717 if (score > 0 && score < best_score) {

718 best_data = data;

719 best_score = score; 720 }

721 }

......

739

740 pr_info("Machine model: %s\n", of_flat_dt_get_machine_name());

741

742 return best_data;

743 }

第 714 行,通过函数 of_get_flat_dt_root 获取设备树根节点。

第 715~720 行,此循环就是查找匹配的 machine_desc 过程,第 716 行的 of_flat_dt_match 函数会将根节点compatible 属性的值和每个machine_desc 结构体中. dt_compat 的值进行比较,直至找到匹配的那个 machine_desc。

总结一下,Linux 内核通过根节点 compatible 属性找到对应的设备的函数调用过程,如图所示:

f87546a59fba4b9da369a6babb66d7d8.jpg

查找匹配设备的过程

产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 IIC 接口的六轴芯片 MPU6050,第二版硬件又要把这个 MPU6050 更换为 MPU9250 等。一旦硬件修改了,我们就要同步的修改设备树文件,毕竟设备树是描述板子硬件信息的文件。假设现在有个六轴芯片fxls8471,fxls8471 要接到 I.MX6U-ALPHA 开发板的 I2C1 接口上,那么相当于需要在 i2c1 这个节点上添加一个 fxls8471 子节点。先看一下 I2C1 接口对应的节点,打开文件 imx6ull.dtsi 文件,找到如下所示内容:

937 i2c1: i2c@021a0000 {

938 #address-cells = <1>;

939 #size-cells = <0>;

940 compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c"; 941 reg = <0x021a0000 0x4000>;

942 interrupts = ; 943 clocks = <&clks IMX6UL_CLK_I2C1>;

944 status = "disabled"; 945 };

上面代码就是 I.MX6ULL 的 I2C1 节点,现在要在 i2c1 节点下创建一个子节点,这个子节点就是 fxls8471,最简单的方法就是在 i2c1 下直接添加一个名为 fxls8471 的子节点,如下所示:

937 i2c1: i2c@021a0000 {

938 #address-cells = <1>;

939 #size-cells = <0>;

940 compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c"; 941 reg = <0x021a0000 0x4000>;

942 interrupts = ; 

943 clocks = <&clks IMX6UL_CLK_I2C1>;

944 status = "disabled";

945

946 //fxls8471 子节点

947 fxls8471@1e {

948 compatible = "fsl,fxls8471"; 
 
949 reg = <0x1e>;

950 };

951 };

第 947~950 行就是添加的 fxls8471 这个芯片对应的子节点。但是这样会有个问题!i2c1 节点是定义在 imx6ull.dtsi 文件中的,而 imx6ull.dtsi 是设备树头文件,其他所有使用到 I.MX6ULL这颗 SOC 的板子都会引用 imx6ull.dtsi 这个文件。直接在 i2c1 节点中添加 fxls8471 就相当于在其他的所有板子上都添加了 fxls8471 这个设备,但是其他的板子并没有这个设备啊!因此,按照示例代码这样写肯定是不行的。

这里就要引入另外一个内容,那就是如何向节点追加数据,我们现在要解决的就是如何向i2c1 节点追加一个名为 fxls8471 的子节点,而且不能影响到其他使用到 I.MX6ULL 的板子。 I.MX6U-ALPHA 开发板使用的设备树文件为 imx6ull-alientek-emmc.dts, 因此我们需要在 imx6ull-alientek-emmc.dts 文件中完成数据追加的内容,方式如下:

1 &i2c1 {

2 /* 要追加或修改的内容 */

3 };

第 1 行,&i2c1 表示要访问 i2c1 这个 label 所对应的节点,也就是 imx6ull.dtsi 中的“i2c1: i2c@021a0000”。

第 2 行,花括号内就是要向 i2c1 这个节点添加的内容,包括修改某些属性的值。打开 imx6ull-alientek-emmc.dts,找到如下所示内容:

224 &i2c1 {

225 clock-frequency = <100000>;

226 pinctrl-names = "default";

227 pinctrl-0 = <&pinctrl_i2c1>;

228 status = "okay"; 229

230 mag3110@0e {

231 compatible = "fsl,mag3110";

232 reg = <0x0e>;

233 position = <2>;

234 };

235

236 fxls8471@1e {

237 compatible = "fsl,fxls8471";

238 reg = <0x1e>;

239 position = <0>;

240 interrupt-parent = <&gpio5>;

241 interrupts = <0 8>;

242 };

243 };

代码就是向 i2c1 节点添加/修改数据,比如第 225 行的属性“clock-frequency”就表示 i2c1 时钟为 100KHz。“clock-frequency”就是新添加的属性。

第 228 行,将 status 属性的值由原来的disabled 改为 okay。

第 230~234 行,i2c1 子节点 mag3110,因为 NXP 官方开发板在 I2C1 上接了一个磁力计芯片 mag3110,正点原子的 I.MX6U-ALPHA 开发板并没有使用 mag3110。

第 236~242 行,i2c1 子节点 fxls8471,同样是因为 NXP 官方开发板在 I2C1 上接了 fxls8471这颗六轴芯片。

因为代码中的内容是 imx6ull-alientek-emmc.dts 这个文件内的,所以不会对使用 I.MX6ULL 这颗SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。