设备树学习笔记

学习正点原子《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.5.2.pdf》个人笔记

设备树编译

  设备树源文件扩展名为.dts, DTS 是设备树源码文件, DTB 是将DTS 编译以后得到的二进制文件。将.dts 编译为.dtb需要用到 DTC 工具,DTC 工具源码在 Linux 内核的 scripts/dtc 目录下。

  如果要编译 DTS 文件的话只需要进入到 Linux 源码根目录下,然后执行make all或者make dtbs
  “make all”命令是编译 Linux 源码中的所有东西,包括 zImage, .ko 驱动模块以及设备树,如果只是编译设备树的话建议使用“make dtbs”命令。

  基于 ARM 架构的 SOC 有很多种,一种 SOC 又可以制作出很多款板子,每个板子都有一个对应的 DTS 文件,如何确定编译哪一个 DTS 文件呢?以 I.MX6ULL 这款芯片对应的板子为例来看一下,打开 arch/arm/boot/dts/Makefile,有如下内容:

dtb-$(CONFIG_SOC_IMX6UL) += \
imx6ul-14x14-ddr3-arm2.dtb \
imx6ul-14x14-ddr3-arm2-emmc.dtb \
......
dtb-$(CONFIG_SOC_IMX6ULL) += \
imx6ull-14x14-ddr3-arm2.dtb \
imx6ull-14x14-ddr3-arm2-adc.dtb \
imx6ull-14x14-ddr3-arm2-cs42888.dtb \
imx6ull-14x14-ddr3-arm2-ecspi.dtb \
imx6ull-14x14-ddr3-arm2-emmc.dtb
......
dtb-$(CONFIG_SOC_IMX6SLL) += \
imx6sll-lpddr2-arm2.dtb \
imx6sll-lpddr3-arm2.dtb \
......

  可以看出,当选中 I.MX6ULL 这个 SOC 以后(CONFIG_SOC_IMX6ULL=y),所有使用到I.MX6ULL 这个 SOC 的板子对应的.dts 文件都会被编译为.dtb。如果我们使用 I.MX6ULL 新做了一个板子,只需要新建一个此板子对应的.dts 文件,然后将对应的.dtb 文件名添加到 dtb-$(CONFIG_SOC_IMX6ULL)下,这样在编译设备树的时候就会将对应的.dts 编译为二进制的.dtb文件。

语法

  关于设备树详细的语法规则请参考《Devicetree SpecificationV0.2.pdf 》和《Power_ePAPR_APPROVED_v1.12.pdf》这两份文档。

.dtsi 头文件

  和C语言一样,设备树也支持头文件,设备树的头文件扩展名为.dtsi。在 imx6ull-alientekemmc.dts 中有如下所示内容:

12 #include <dt-bindings/input/input.h>
13 #include "imx6ull.dtsi"

  第 12 行,使用“#include”来引用“input.h”这个.h 头文件。
  第 13 行,使用“#include”来引用“imx6ull.dtsi”这个.dtsi 头文件。
  .dts 文件支持引用 C 语言中的.h 文件,甚至也可以引用其他.dts 文件,打开 imx6ull-14x14-evk-gpmi-weim.dts 这个文件,此文件中有如下内容:

9 #include "imx6ull-14x14-evk.dts"

  可以看出,直接引用了.dts 文件,因此在.dts 设备树文件中,可以通过“#include”来引用.h、 .dtsi 和.dts 文件。只是,我们在编写设备树头文件的时候最好选择.dtsi 后缀。
  一般.dtsi 文件用于描述 SOC 的内部外设信息,比如 CPU 架构、主频、外设寄存器地址范围,比如 UART、 IIC 等等。

设备节点

1 / {
2 		aliases {
3 			can0 = &flexcan1;
4 		};
5 
6		cpus {
7 			#address-cells = <1>;
8 			#size-cells = <0>;
9
10 			cpu0: cpu@0 {
11 				compatible = "arm,cortex-a7";
12 				device_type = "cpu";
13 				reg = <0>;
14 			};
15 		};
16
17 		intc: interrupt-controller@00a01000 {
18 			compatible = "arm,cortex-a7-gic";
19 			#interrupt-cells = <3>;
20 			interrupt-controller;
21 			reg = <0x00a01000 0x1000>,
22				  <0x00a02000 0x100>;
23 		};
24 }

  第 1 行,“/”是根节点,每个设备树文件只有一个根节点。多个文件的“/”根节点的内容会合并成一个根节点。
  第 2、 6 和 17 行, aliases、 cpus 和 intc 是三个子节点,在设备树中节点命名格式如下:node-name@unit-address
  其中“node-name”是节点名字,为 ASCII 字符串,节点名字应该能够清晰的描述出节点的功能,比如“uart1”就表示这个节点是 UART1 外设。
  “unit-address”一般表示设备的地址或寄存器首地址,如果某个节点没有地址或者寄存器的话“unit-address”可以不要,比如“cpu@0”、“interruptcontroller@00a01000”。

  但是示例代码中我们看到的节点命名却如下所示:cpu0:cpu@0
  上述命令并不是“node-name@unit-address”这样的格式,而是用“:”隔开成了两部分,“:”前面的是节点标签(label),“:”后面的才是节点名字,格式如下所示:
label: node-name@unit-address
  引入 label 的目的就是为了方便访问节点,可以直接通过&label 来访问这个节点,比如通过&cpu0 就可以访问“cpu@0”这个节点,而不需要输入完整的节点名字。再比如节点 “intc:interrupt-controller@00a01000”,节点 label 是 intc,而节点名字就很长了,为“ interruptcontroller@00a01000”。很明显通过&intc 来访问“interrupt-controller@00a01000”这个节点要方便很多。
  第 10 行, cpu0 也是一个节点,只是 cpu0 是 cpus 的子节点。

  每个节点都有不同属性,不同的属性又有不同的内容,属性都是键值对,值可以为空或任意的字节流。设备树源码中常用的几种数据形式如下所示:
①、字符串
compatible = "arm,cortex-a7";
上述代码设置 compatible 属性的值为字符串“arm,cortex-a7”。
②、 32 位无符号整数
reg = <0>;
上述代码设置 reg 属性的值为 0, reg 的值也可以设置为一组值,比如:
reg = <0 0x123456 100>;
③、字符串列表
属性值也可以为字符串列表,字符串和字符串之间采用“,”隔开,如下所示:
compatible = "fsl,imx6ull-gpmi-nand", "fsl, imx6ul-gpmi-nand";
上述代码设置属性 compatible 的值为“fsl,imx6ull-gpmi-nand”和“fsl, imx6ul-gpmi-nand”。

标准属性

  节点是由一堆的属性组成,节点都是具体的设备,不同的设备需要的属性不同,用户可以自定义属性。除了用户自定义属性,有很多属性是标准属性, Linux 下的很多外设驱动都会使用这些标准属性,本节我们就来学习一下几个常用的标准属性。

1、 compatible 属性

  compatible 属性也叫做“兼容性”属性,这是非常重要的一个属性! compatible 属性的值是一个字符串列表, compatible 属性用于将设备和驱动绑定起来。字符串列表用于选择设备所要使用的驱动程序, compatible 属性的值格式如下所示:
"manufacturer,model"
  其中 manufacturer 表示厂商, model 一般是模块对应的驱动名字。比如 imx6ull-alientekemmc.dts 中 sound 节点是 I.MX6U-ALPHA 开发板的音频设备节点, I.MX6U-ALPHA 开发板上的音频芯片采用的欧胜(WOLFSON)出品的WM8960, sound 节点的 compatible 属性值如下:
compatible = "fsl,imx6ul-evk-wm8960","fsl,imx-audio-wm8960";
  属性值有两个,分别为“fsl,imx6ul-evk-wm8960”和“fsl,imx-audio-wm8960”,其中“fsl”表示厂商是飞思卡尔,“imx6ul-evk-wm8960”和“imx-audio-wm8960”表示驱动模块名字。 sound这个设备首先使用第一个兼容值在 Linux 内核里面查找,看看能不能找到与之匹配的驱动文件,如果没有找到的话就使用第二个兼容值查。
  一般驱动程序文件都会有一个 OF 匹配表,此 OF 匹配表保存着一些 compatible 值,如果设备节点的 compatible 属性值和 OF 匹配表中的任何一个值相等,那么就表示设备可以使用这个驱动。比如在驱动文件 imx-wm8960.c 中有如下内容:

632 static const struct of_device_id imx_wm8960_dt_ids[] = {
633 	{ .compatible = "fsl,imx-audio-wm8960", },
634 	{ /* sentinel */ }
635 };
636 MODULE_DEVICE_TABLE(of, imx_wm8960_dt_ids);
637
638 static struct platform_driver imx_wm8960_driver = {
639 	.driver = {
640 		.name = "imx-wm8960",
641 		.pm = &snd_soc_pm_ops,
642 		.of_match_table = imx_wm8960_dt_ids,
643 	},
644 	.probe = imx_wm8960_probe,
645 	.remove = imx_wm8960_remove,
646 };

  第 632~635 行的数组 imx_wm8960_dt_ids 就是 imx-wm8960.c 这个驱动文件的匹配表,此匹配表只有一个匹配值“fsl,imx-audio-wm8960”。如果在设备树中有哪个节点的 compatible 属性值与此相等,那么这个节点就会使用此驱动文件。
  第 642 行, wm8960 采用了 platform_driver 驱动模式,关于 platform_driver 驱动后面会讲解。此行设置.of_match_table 为 imx_wm8960_dt_ids,也就是设置这个 platform_driver 所使用的OF 匹配表。

2、 model 属性

  model 属性值也是一个字符串,一般 model 属性描述设备模块信息,比如名字什么的,比如:model = "wm8960-audio";

3、 status 属性

  status 属性看名字就知道是和设备状态有关的, status 属性值也是字符串,字符串是设备的状态信息,可选的状态如表:
在这里插入图片描述

4、 #address-cells 和#size-cells 属性

  这两个属性的值都是无符号 32 位整形, #address-cells 和#size-cells 这两个属性可以用在任何拥有子节点的设备中,用于描述子节点的地址信息。
  #address-cells 属性值决定了子节点 reg 属性中地址信息所占用的字长(32 位)
  #size-cells 属性值决定了子节点 reg 属性中长度信息所占的字长(32 位)。
  #address-cells 和#size-cells 表明了子节点应该如何编写 reg 属性值,一般 reg 属性都是和地址有关的内容,和地址相关的信息有两种:起始地址和地址长度, reg 属性的格式一为:
reg = <address1 length1 address2 length2 address3 length3……>
  每个“address length”组合表示一个地址范围,其中 address 是起始地址, length 是地址长度, #address-cells 表明 address 这个数据所占用的字长, #size-cells 表明 length 这个数据所占用的字长,比如:

1  spi4 {
2 		compatible = "spi-gpio";
3 		#address-cells = <1>;
4 		#size-cells = <0>;
5 
6 		gpio_spi: gpio_spi@0 {
7 			compatible = "fairchild,74hc595";
8 			reg = <0>;
9 		};
10 };
11
12 aips3: aips-bus@02200000 {
13 		compatible = "fsl,aips-bus", "simple-bus";
14 		#address-cells = <1>;
15 		#size-cells = <1>;
16
17 		dcp: dcp@02280000 {
18 			compatible = "fsl,imx6sl-dcp";
19 			reg = <0x02280000 0x4000>;
20 		};
21 };

  第 3, 4 行,节点 spi4 的#address-cells = <1>, #size-cells = <0>,说明 spi4 的子节点 reg 属性中起始地址所占用的字长为 1,地址长度所占用的字长为 0。
  第 8 行,子节点 gpio_spi: gpio_spi@0 的 reg 属性值为 <0>,因为父节点设置了#addresscells = <1>, #size-cells = <0>,因此 addres=0,没有 length 的值,相当于设置了起始地址,而没有设置地址长度。
  第 14, 15 行,设置 aips3: aips-bus@02200000 节点#address-cells = <1>, #size-cells = <1>,说明 aips3: aips-bus@02200000 节点起始地址长度所占用的字长为 1,地址长度所占用的字长也为 1。
  第 19 行,子节点 dcp: dcp@02280000 的 reg 属性值为<0x02280000 0x4000>,因为父节点设置了#address-cells = <1>, #size-cells = <1>, address= 0x02280000, length= 0x4000,相当于设置了起始地址为 0x02280000,地址长度为 0x40000。

5、 reg 属性

  reg 属性前面已经提到过了, reg 属性的值一般是(address, length)对。 reg 属性一般用于描述设备地址空间资源信息,一般都是某个外设的寄存器地址范围信息,比如在 imx6ull.dtsi 中有如下内容:

323 uart1: serial@02020000 {
324 	compatible = "fsl,imx6ul-uart",
325 	             "fsl,imx6q-uart", "fsl,imx21-uart";
326 	reg = <0x02020000 0x4000>;
327 	interrupts = <GIC_SPI 26 IRQ_TYPE_LEVEL_HIGH>;
328 	clocks = <&clks IMX6UL_CLK_UART1_IPG>,
329 	         <&clks IMX6UL_CLK_UART1_SERIAL>;
330		clock-names = "ipg", "per";
331 	status = "disabled";
332 };

  上述代码是节点 uart1, uart1 节点描述了 I.MX6ULL 的 UART1 相关信息,重点是第 326 行的 reg 属性。其中 uart1 的父节点 aips1: aips-bus@02000000 设置了#address-cells = <1>、 #sizecells = <1>,因此 reg 属性中 address=0x02020000, length=0x4000。查阅《I.MX6ULL 参考手册》可知, I.MX6ULL 的 UART1 寄存器首地址为 0x02020000,但是 UART1 的地址长度(范围)并没有 0x4000 这么多,这里我们重点是获取 UART1 寄存器首地址。

6、 ranges 属性

  ranges属性值可以为空或者按照(child-bus-address,parent-bus-address,length)格式编写的数字矩阵, ranges 是一个地址映射/转换表, ranges 属性每个项目由子地址、父地址和地址空间长度这三部分组成:
  child-bus-address:子总线地址空间的物理地址,由父节点的#address-cells 确定此物理地址所占用的字长。
  parent-bus-address: 父总线地址空间的物理地址,同样由父节点的#address-cells 确定此物理地址所占用的字长。
  length: 子地址空间的长度,由父节点的#size-cells 确定此地址长度所占用的字长。
  如果 ranges 属性值为空值,说明子地址空间和父地址空间完全相同,不需要进行地址转换,对于我们所使用的 I.MX6ULL 来说,子地址空间和父地址空间完全相同,因此会在 imx6ull.dtsi中找到大量的值为空的 ranges 属性,如下所示:

137 soc {
138 	#address-cells = <1>;
139 	#size-cells = <1>;
140 	compatible = "simple-bus";
141 	interrupt-parent = <&gpc>;
142 	ranges;
......
1177 }

第 142 行定义了 ranges 属性,但是 ranges 属性值为空。
ranges 属性不为空的示例代码如下所示:

1 soc {
2 	compatible = "simple-bus";
3 	#address-cells = <1>;
4 	#size-cells = <1>;
5 	ranges = <0x0 0xe0000000 0x00100000>;
6 
7 	serial {
8 		device_type = "serial";
9 		compatible = "ns16550";
10 		reg = <0x4600 0x100>;
11 		clock-frequency = <0>;
12 		interrupts = <0xA 0x8>;
13 		interrupt-parent = <&ipic>;
14 	};
15 };

  第 5 行,节点 soc 定义的 ranges 属性,值为<0x0 0xe0000000 0x00100000>,此属性值指定了一个 1024KB(0x00100000)的地址范围,子地址空间的物理起始地址为 0x0,父地址空间的物理起始地址为 0xe0000000。
  第 10 行, serial 是串口设备节点, reg 属性定义了 serial 设备寄存器的起始地址为 0x4600,寄存器长度为 0x100。经过地址转换, serial 设备可以从 0xe0004600 开始进行读写操作,0xe0004600=0x4600+0xe0000000。(ranges 属性相当于为子节点提供了基地址?)

7、 name 属性

  name 属性值为字符串, name 属性用于记录节点名字, name 属性已经被弃用,不推荐使用name 属性,一些老的设备树文件可能会使用此属性。

8、 device_type 属性

  device_type 属性值为字符串, IEEE 1275 会用到此属性,用于描述设备的 FCode,但是设备树没有 FCode,所以此属性也被抛弃了。此属性只能用于 cpu 节点或者 memory 节点。imx6ull.dtsi 的 cpu0 节点用到了此属性,内容如下所示:

54 cpu0: cpu@0 {
55 	compatible = "arm,cortex-a7";
56 	device_type = "cpu";
57 	reg = <0>;
......
89 };

关于标准属性就讲解这么多,其他的比如中断、 IIC、 SPI 等使用的标准属性等到具体的例程再讲解。

根节点 compatible 属性

  每个节点都有 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/machmx35_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 内核会检查这个 machine id,其实就是将 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。说明引入设备树以后不会再根据 machine id 来检查 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-alientekemmc.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 内核,结果如图所示的错误提示:
在这里插入图片描述
  当我们修改了根节点 compatible 属性内容以后,因为 Linux 内核找不到对应的设备,因此Linux 内核无法启动。在 uboot 输出 Starting kernel…以后就再也没有其他信息输出了。

向节点追加或修改内容

  产品开发过程中可能面临着频繁的需求更改,比如第一版硬件上有一个 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 = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
943     clocks = <&clks IMX6UL_CLK_I2C1>;
944     status = "disabled";
945 };

  现在要在 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 = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
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这颗六轴芯片。
  因为示例代码 43.3.5.4 中的内容是 imx6ull-alientek-emmc.dts 这个文件内的,所以不会对使用 I.MX6ULL 这颗 SOC 的其他板子造成任何影响。这个就是向节点追加或修改内容,重点就是通过&label 来访问节点,然后直接在里面编写要追加或者修改的内容。

设备树在系统中的体现

  Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/devicetree 目录下根据节点名字创建不同文件夹,如图:
在这里插入图片描述
  /proc/device-tree 目录下是根节点“/”的 所有属性和子节点,我们依次来看一下这些属性和子节点。

  根节点属性属性表现为一个个的文件(图中细字体文件),比如图中的“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设 备树中就是根节点的 5 个属性。既然是文件那么肯定可以查看其内容,输入 cat 命令来查看 model 和 compatible 这两个文件的内容,结果如图:
在这里插入图片描述
  从图可以看出,文件 model 的内容是“Freescale i.MX6 ULL 14x14 EVK Board”,文 件 compatible 的内容为“fsl,imx6ull-14x14-evkfsl,imx6ull”。打开文件 imx6ull-alientek-emmc.dts 查看一下,这不正是根节点“/”的 model 和 compatible 属性值吗!同样的,也可以查看子节点。

特殊节点

  在根节点“/”中有两个特殊的子节点:aliases 和 chosen,我们接下来看一下这两个特殊的 子节点。

aliases子节点

打开 imx6ull.dtsi 文件,aliases 节点内容如下所示:

18 aliases {
19     can0 = &flexcan1;
20     can1 = &flexcan2;
21     ethernet0 = &fec1;
22     ethernet1 = &fec2;
23     gpio0 = &gpio1;
24     gpio1 = &gpio2;
......
42     spi0 = &ecspi1;
43     spi1 = &ecspi2;
44     spi2 = &ecspi3;
45     spi3 = &ecspi4;
46     usbphy0 = &usbphy1;
47     usbphy1 = &usbphy2;
48 };

  单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。不过我们一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。

chosen子节点

  chosen 并不是一个真实的设备,chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。一般.dts 文件中 chosen 节点通常为空或者内容很少,imx6ull-alientek- emmc.dts 中 chosen 节点内容如下所示:

18 chosen {
19     stdout-path = &uart1;
20 };

  从示例代码可以看出,chosen 节点仅仅设置了属性“stdout-path”,表示标准输 出使用 uart1。但是当我们进入到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个属性,如图所示:
在这里插入图片描述
输入 cat 命令查看 bootargs 这个文件的内容,结果如图:
在这里插入图片描述
  可以看出,bootargs 这个文件的内容为“console=ttymxc0,115200……”,这个 不就是我们在 uboot 中设置的 bootargs 环境变量的值吗?
  说明uboot 自己在 chosen 节点里面添加了 bootargs 属性!并且设置 bootargs 属性的值为 bootargs环境变量的值。

绑定信息文档

  设备树是用来描述板子上的设备信息的,不同的设备其信息不同,反映到设备树中就是属性不同。那么我们在设备树中添加一个硬件对应的节点的时候从哪里查阅相关的说明呢?在 Linux 内核源码中有详细的.txt 文档描述了如何添加节点,这些.txt 文档叫做绑定文档,路径为: Linux 源码目录/Documentation/devicetree/bindings

  比如我们现在要想在 I.MX6ULL 这颗 SOC 的 I2C 下添加一个节点,那么就可以查看 Documentation/devicetree/bindings/i2c/i2c-imx.txt,此文档详细的描述了 I.MX 系列的 SOC 如何 在设备树中添加 I2C 设备节点。
  有时候使用的一些芯片在 Documentation/devicetree/bindings 目录下找不到对应的文档,这个时候就要咨询芯片的提供商,让他们给你提供参考的设备树文件。

设备树常用 OF 操作函数

  设备树描述了设备的详细信息,这些信息包括数字类型的、字符串类型的、数组类型的,我们在编写驱动的时候需要获取到这些信息。比如设备树使用 reg 属性描述了某个外设的寄存器地址为 0X02005482 , 长度为 0X400 , 我们在编写驱动的时候需要获取到 reg 属性的0X02005482 和 0X400 这两个值,然后初始化外设。Linux 内核给我们提供了一系列的函数来获取设备树中的节点或者属性信息,这一系列的函数都有一个统一的前缀“of_”,所以在很多资 料里面也被叫做 OF 函数。这些 OF 函数原型都定义在 include/linux/of.h 文件中。

查找节点的 OF 函数

  设备都是以节点的形式“挂”到设备树上的,因此要想获取这个设备的其他属性信息,必须先获取到这个设备的节点。Linux 内核使用 device_node 结构体来描述一个节点,此结构体定义在文件 include/linux/of.h 中,定义如下:

49 struct device_node {
50     const char *name; /* 节点名字 */
51     const char *type; /* 设备类型 */
52     phandle phandle;
53     const char *full_name; /* 节点全名 */
54     struct fwnode_handle fwnode;
55
56     struct property *properties; /* 属性 */
57     struct property *deadprops; /* removed 属性 */
58     struct device_node *parent; /* 父节点 */
59     struct device_node *child; /* 子节点 */
60     struct device_node *sibling;
61    struct kobject kobj;
62     unsigned long _flags;
63     void *data;
64 #if defined(CONFIG_SPARC)
65     const char *path_component_name;
66     unsigned int unique_id;
67     struct of_irq_controller *irq_trans;
68 #endif
69 };

与查找节点有关的 OF 函数有 5 个,我们依次来看一下。

1、of_find_node_by_name

of_find_node_by_name 函数通过节点名字查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_name(struct device_node *from,
                                         const char *name);

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
name:要查找的节点名字。
返回值:找到的节点,如果为 NULL 表示查找失败。

2、of_find_node_by_type

of_find_node_by_type 函数通过 device_type 属性查找指定的节点,函数原型如下:

struct device_node *of_find_node_by_type(struct device_node *from, const char *type)

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值。
返回值:找到的节点,如果为 NULL 表示查找失败。

3、of_find_compatible_node

of_find_compatible_node 函数根据 device_type 和 compatible 这两个属性查找指定的节点, 函数原型如下:

struct device_node *of_find_compatible_node(struct device_node *from,
                                            const char *type,
                                            const char *compatible)

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
type:要查找的节点对应的 type 字符串,也就是 device_type 属性值,可以为 NULL,表示 忽略掉 device_type 属性。
compatible:要查找的节点所对应的 compatible 属性列表。
返回值:找到的节点,如果为 NULL 表示查找失败

4、of_find_matching_node_and_match

of_find_matching_node_and_match 函数通过 of_device_id 匹配表来查找指定的节点,函数原 型如下:

struct device_node *of_find_matching_node_and_match(struct device_node *from,
const struct of_device_id *matches,
const struct of_device_id **match)

函数参数和返回值含义如下:
from:开始查找的节点,如果为 NULL 表示从根节点开始查找整个设备树。
matches:of_device_id 匹配表,也就是在此匹配表里面查找节点。
match 找到的匹配的 of_device_id。
返回值:找到的节点,如果为 NULL 表示查找失败

5、of_find_node_by_path

of_find_node_by_path 函数通过路径来查找指定的节点,函数原型如下:

 	inline struct device_node *of_find_node_by_path(const char *path) 	 

函数参数和返回值含义如下:
path:带有全路径的节点名,可以使用节点的别名,比如“/backlight”就是 backlight 这个节点的全路径。
返回值:找到的节点,如果为 NULL 表示查找失败

查找父/子节点的 OF 函数

Linux 内核提供了几个查找节点对应的父节点或子节点的 OF 函数,我们依次来看一下。

1、of_get_parent

of_get_parent 函数用于获取指定节点的父节点(如果有父节点的话),函数原型如下:

struct device_node *of_get_parent(const struct device_node *node) 	

函数参数和返回值含义如下:
node:要查找的父节点的节点。
返回值:找到的父节点。

2、of_get_next_child

of_get_next_child 函数用迭代的方式查找子节点,函数原型如下:

struct device_node *of_get_next_child(const struct device_node *node,
struct device_node *prev)

函数参数和返回值含义如下:
node:父节点。
prev:前一个子节点,也就是从哪一个子节点开始迭代的查找下一个子节点。可以设置为NULL,表示从第一个子节点开始。
返回值:找到的下一个子节点。

提取属性值的 OF 函数

节点的属性信息里面保存了驱动所需要的内容,因此对于属性值的提取非常重要,Linux 内 核中使用结构体 property 表示属性,此结构体同样定义在文件include/linux/of.h 中,内容如下:

35 struct property {
36     char *name; /* 属性名字 */
37     int length; /* 属性长度 */
38     void *value; /* 属性值 */
39     struct property *next; /* 下一个属性 */
40     unsigned long _flags;
41     unsigned int unique_id;
42     struct bin_attribute attr;
43 };

Linux 内核也提供了提取属性值的 OF 函数,我们依次来看一下。

1、of_find_property

of_find_property 函数用于查找指定的属性,函数原型如下:

property *of_find_property(const struct device_node *np,
const char *name,
int *lenp)

函数参数和返回值含义如下:
np:设备节点。
name: 属性名字。
lenp:属性值的字节数
返回值:找到的属性。

2、of_property_count_elems_of_size

of_property_count_elems_of_size 函数用于获取属性中元素的数量,比如 reg 属性值是一个 数组,那么使用此函数可以获取到这个数组的大小,此函数原型如下:

int of_property_count_elems_of_size(const struct device_node *np,
const char *propname,
int elem_size)

函数参数和返回值含义如下:
np:设备节点。
proname: 需要统计元素数量的属性名字。
elem_size:元素长度。
返回值:得到的属性元素数量。

3、of_property_read_u32_index
of_property_read_u32_index 函数用于从属性中获取指定标号的 u32 类型数据值(无符号 32 位),比如某个属性有多个 u32 类型的值,那么就可以使用此函数来获取指定标号的数据值,此 函数原型如下:

int of_property_read_u32_index(const struct device_node *np,
const char *propname,
u32 index,
u32 *out_value)

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
index:要读取的值标号。
out_value:读取到的值
返回值:0 读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没有要读取的数据,-EOVERFLOW 表示属性值列表太小。

4、of_property_read_u8_array

of_property_read_u16_array
of_property_read_u32_array
of_property_read_u64_array
这 4 个函数分别是读取属性中 u8、u16、u32 和 u64 类型的数组数据,比如大多数的 reg 属性都是数组数据,可以使用这 4 个函数一次读取出 reg 属性中的所有数据。这四个函数的原型如下:

int of_property_read_u8_array(const struct device_node *np,
const char *propname,
u8 *out_values,
size_t sz)

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值,分别为 u8、u16、u32 和 u64。
sz 要读取的数组元素数量。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没 有要读取的数据,-EOVERFLOW 表示属性值列表太小。

5、of_property_read_u8

of_property_read_u16
of_property_read_u32
of_property_read_u64
有些属性只有一个整形值,这四个函数就是用于读取这种只有一个整形值的属性,分别用于读取 u8、u16、u32 和 u64 类型属性值,函数原型如下:

 int of_property_read_u8(const struct device_node *np,
const char *propname,
u8 *out_value)

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_value:读取到的数组值。
返回值:0,读取成功,负值,读取失败,-EINVAL 表示属性不存在,-ENODATA 表示没 有要读取的数据,-EOVERFLOW 表示属性值列表太小。

6、of_property_read_string

of_property_read_string 函数用于读取属性中字符串值,函数原型如下:

int of_property_read_string(struct device_node   *np, 
const char	*propname,
const char	**out_string)

函数参数和返回值含义如下:
np:设备节点。
proname: 要读取的属性名字。
out_string:读取到的字符串值。
返回值:0,读取成功,负值,读取失败。

7、of_n_addr_cells

of_n_addr_cells 函数用于获取#address-cells 属性值,函数原型如下:

int of_n_addr_cells(struct device_node *np) 	

函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#address-cells 属性值。

8、of_n_size_cells

of_size_cells 函数用于获取#size-cells 属性值,函数原型如下:

int of_n_size_cells(struct device_node *np) 	

函数参数和返回值含义如下:
np:设备节点。
返回值:获取到的#size-cells 属性值。

其他常用的 OF 函数

1、of_device_is_compatible

of_device_is_compatible 函数用于查看节点的 compatible 属性是否有包含 compat 指定的字 符串,也就是检查设备节点的兼容性,函数原型如下:

int of_device_is_compatible(const struct device_node *device,
const char *compat)

函数参数和返回值含义如下:
device:设备节点。
compat:要查看的字符串。
返回值:0,节点的 compatible 属性中不包含 compat 指定的字符串;正数,节点的 compatible属性中包含 compat 指定的字符串。

2、of_get_address

of_get_address 函数用于获取地址相关属性,主要是“reg”或者“assigned-addresses”属性 值,函数原型如下:

const __be32 *of_get_address(struct device_node *dev,
int index,
u64 *size,
unsigned int *flags)

函数参数和返回值含义如下:
dev:设备节点。
index:要读取的地址标号。
size:地址长度。
flags:参数,比如 IORESOURCE_IO、IORESOURCE_MEM 等
返回值:读取到的地址数据首地址,为 NULL 的话表示读取失败。

3、of_translate_address

of_translate_address 函数负责将从设备树读取到的地址转换为物理地址,函数原型如下:

u64 of_translate_address(struct device_node *dev,
const __be32 *in_addr)

函数参数和返回值含义如下:
dev:设备节点。
in_addr:要转换的地址。
返回值: 得到的物理地址,如果为 OF_BAD_ADDR 的话表示转换失败。

4、of_address_to_resource

IIC、SPI、GPIO 等这些外设都有对应的寄存器,这些寄存器其实就是一组内存空间,Linux 内核使用 resource 结构体来描述一段内存空间,“resource”翻译出来就是“资源”,因此用 resource 结构体描述的都是设备资源信息,resource 结构体定义在文件 include/linux/ioport.h 中,定义如下:

18 struct resource {
19     resource_size_t start;
20     resource_size_t end;
21     const char *name;
22     unsigned long flags;
23     struct resource *parent, *sibling, *child;
24 };

对于 32 位的 SOC 来说,resource_size_t 是 u32 类型的。其中 start 表示开始地址,end 表示结束地址,name 是这个资源的名字,flags 是资源标志位,一般表示资源类型,可选的资源标志 定义在文件 include/linux/ioport.h 中,如下所示:

1 #define IORESOURCE_BITS 0x000000ff
2 #define IORESOURCE_TYPE_BITS 0x00001f00
3 #define IORESOURCE_IO 0x00000100
4 #define IORESOURCE_MEM 0x00000200
5 #define IORESOURCE_REG 0x00000300
6 #define IORESOURCE_IRQ 0x00000400
7 #define IORESOURCE_DMA 0x00000800
8 #define IORESOURCE_BUS 0x00001000
9 #define IORESOURCE_PREFETCH 0x00002000
10 #define IORESOURCE_READONLY 0x00004000
11 #define IORESOURCE_CACHEABLE 0x00008000
12 #define IORESOURCE_RANGELENGTH 0x00010000
13 #define IORESOURCE_SHADOWABLE 0x00020000
14 #define IORESOURCE_SIZEALIGN 0x00040000
15 #define IORESOURCE_STARTALIGN 0x00080000
16 #define IORESOURCE_MEM_64 0x00100000
17 #define IORESOURCE_WINDOW 0x00200000
18 #define IORESOURCE_MUXED 0x00400000
19 #define IORESOURCE_EXCLUSIVE 0x08000000
20 #define IORESOURCE_DISABLED 0x10000000
21 #define IORESOURCE_UNSET 0x20000000
22 #define IORESOURCE_AUTO 0x40000000
23 #define IORESOURCE_BUSY 0x80000000

大家一般最常见的资源标志就是IORESOURCE_MEM 、 IORESOURCE_REG 和
IORESOURCE_IRQ 等。接下来我们回到 of_address_to_resource 函数,此函数看名字像是从设 备树里面提取资源值,但是本质上就是将 reg 属性值,然后将其转换为 resource 结构体类型, 函数原型如下所示

int of_address_to_resource(struct device_node *dev,
int index,
struct resource *r)

函数参数和返回值含义如下:
dev:设备节点。
index:地址资源标号。
r:得到的 resource 类型的资源值。
返回值:0,成功;负值,失败。

5、of_iomap

of_iomap 函数用于直接内存映射,以前我们会通过 ioremap 函数来完成物理地址到虚拟地 址的映射,采用设备树以后就可以直接通过 of_iomap 函数来获取内存地址所对应的虚拟地址, 不需要使用 ioremap 函数了。当然了,你也可以使用 ioremap 函数来完成物理地址到虚拟地址 的内存映射,只是在采用设备树以后,大部分的驱动都使用 of_iomap 函数了。of_iomap 函数本 质上也是将 reg 属性中地址信息转换为虚拟地址,如果 reg 属性有多段的话,可以通过 index 参 数指定要完成内存映射的是哪一段,of_iomap 函数原型如下:

void __iomem *of_iomap(struct device_node *np,
int index)

函数参数和返回值含义如下:
np:设备节点。
index:reg 属性中要完成内存映射的段,如果 reg 属性只有一段的话 index 就设置为 0。
返回值:经过内存映射后的虚拟内存首地址,如果为 NULL 的话表示内存映射失败。

关于设备树常用的 OF 函数就先讲解到这里,Linux 内核中关于设备树的 OF 函数不仅仅只有前面讲的这几个,还有很多 OF 函数我们并没有讲解,这些没有讲解的 OF 函数要结合具体 的驱动,比如获取中断号的 OF 函数、获取 GPIO 的 OF 函数等等,这些 OF 函数我们在后面的 驱动实验中再详细的讲解。
关于设备树就讲解到这里,关于设备树我们重点要了解一下几点内容:
①、DTS、DTB 和 DTC 之间的区别,如何将.dts 文件编译为.dtb 文件。
②、设备树语法,这个是重点,因为在实际工作中我们是需要修改设备树的。
③、设备树的几个特殊子节点。
④、关于设备树的 OF 操作函数,也是重点,因为设备树最终是被驱动文件所使用的,而 驱动文件必须要读取设备树中的属性信息,比如内存信息、GPIO 信息、中断信息等等。要想在 驱动中读取设备树的属性值,那么就必须使用 Linux 内核提供的众多的 OF 函数。

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

【ql君】qlexcel

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

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

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

打赏作者

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

抵扣说明:

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

余额充值