> 技术文档 > imx6ull-驱动开发篇7——如何编写设备树_如何构建设备树

imx6ull-驱动开发篇7——如何编写设备树_如何构建设备树

目录

前言

创建小型模板设备树

添加 cpus 节点

添加 soc 节点

添加 ocram 节点

添加 aips1、 aips2 和 aips3

添加外设控制器节点

设备树在系统中的体现

根节点“/”各个属性

根节点“/”各子节点

aliases 子节点

chosen 子节点

chosen节点内容

fdt_chosen 函数

执行流程


前言

在上一讲内容里,Linux 设备树语法,我们学习了DTS的重要语法。

本讲内容里我们就根据前面讲解的语法,从头到尾编写一个小型的设备树文件

创建小型模板设备树

在编写设备树之前要先定义一个设备,以 正点原子开发板I.MX6ULL 这个 SOC 为例,我们需要在设备树里面描述的内容如下:

  •  I.MX6ULL 这个 Cortex-A7 架构的 32 位 CPU。
  • I.MX6ULL 内部 ocram,起始地址 0x00900000,大小为 128KB(0x20000)。
  • I.MX6ULL 内部 aips1 域下的 ecspi1 外设控制器,寄存器起始地址为 0x02008000,大小为 0x4000。
  • I.MX6ULL 内部 aips2 域下的 usbotg1 外设控制器,寄存器起始地址为 0x02184000,大小为 0x4000。
  • I.MX6ULL 内部 aips3 域下的 rngb 外设控制器,寄存器起始地址为 0x02284000,大小为 0x4000。

要描述这些内容,首先,我们搭建一个仅含有根节点“/”的基础的框架。

新建一个名为 myfirst.dts 文件,在里面输入如下所示内容:

/ { compatible = \"fsl,imx6ull-alientek-evk\", \"fsl,imx6ull\";}

代码很简单,就一个根节点“/”,根节点里面只有一个 compatible 属性。

接下来一步步完善这个框架。

添加 cpus 节点

I.MX6ULL 采用 Cortex-A7 架构,只有一个 CPU,也就是cpu0 节点。

添加 CPU 节点如下:

/ { /* 根节点定义 */ compatible = \"fsl,imx6ull-alientek-evk\", \"fsl,imx6ull\"; // 开发板兼容性标识,内核优先匹配\"fsl,imx6ull-alientek-evk\" // 若无匹配则尝试\"fsl,imx6ull\"通用驱动 /* CPU集群节点 */ cpus { #address-cells = ; // 子节点reg属性中地址字段占1个32位单元 #size-cells = ; // 子节点reg属性不需要长度字段 /* 单个CPU核心定义 */ cpu0: cpu@0 { // 标签cpu0,节点路径为cpus/cpu@0 compatible = \"arm,cortex-a7\"; // CPU架构标识 device_type = \"cpu\";  // 标准设备类型声明 reg = ;  }; };};

添加 soc 节点

uart, iic 控制器等等这些都属于 SOC 内部外设,因此一般会创建一个叫做 soc 的父节点来管理这些 SOC 内部外设的子节点。

添加 soc 节点,如下所示:

/ { // SoC 系统级外设容器节点 soc { #address-cells = ; // 子节点地址字段占用1个32位单元 #size-cells = ; // 子节点大小字段占用1个32位单元 compatible = \"simple-bus\"; // 简单内存映射总线 ranges; // 子地址空间与父地址空间直接映射,无需转换 };};

添加 ocram 节点

ocram 是 I.MX6ULL 内部 RAM,因此 ocram 节点应该是 soc 节点的子节点。

ocram 起始地址为 0x00900000,大小为 128KB(0x20000),

添加 ocram节点,如下所示:

/ { // SoC 系统级外设容器节点 soc { #address-cells = ; // 子节点reg属性中地址字段占1个32位单元 #size-cells = ; // 子节点reg属性中长度字段占1个32位单元 compatible = \"simple-bus\"; // 简单内存映射总线类型 ranges; // 子地址空间与父地址空间1:1直接映射 /* On-Chip RAM (OCRAM) 节点 */ ocram: sram@00900000 { // 标签ocram,节点名sram@00900000 compatible = \"fsl,lpm-sram\"; // 低功耗SRAM控制器 reg = ; // 物理地址0x00900000,大小128KB }; };};

添加 aips1、 aips2 和 aips3

.MX6ULL 内部分为三个域: aips1~3,这三个域分管不同的外设控制器。

 aips1~3 这三个域对应的内存范围如表:

域名称

起始地址

大小

大小

AIPS1

0x02000000

0x100000

1MB

AIPS2

0x02100000

0x100000

1MB

AIPS3

0x02200000

0x100000

1MB

在设备树中添加这三个域对应的子节点,aips1~3 这三个域都属于 soc 节点的子节点,

如下所示:

/ { /* AIPS1 总线域 - 外设控制区 */ aips1: aips-bus@02000000 { compatible = \"fsl,aips-bus\", \"simple-bus\"; // 总线类型声明 #address-cells = ; // 子节点地址字段占1个32位单元 #size-cells = ; // 子节点长度字段占1个32位单元 reg = ; // 物理地址范围:0x02000000~0x020FFFFF(1MB) ranges; // 子地址空间与父地址空间1:1映射 }; /* AIPS2 总线域 - 低速外设区 */ aips2: aips-bus@02100000 { compatible = \"fsl,aips-bus\", \"simple-bus\"; #address-cells = ; #size-cells = ; reg = ; // 0x02100000~0x021FFFFF ranges; }; /* AIPS3 总线域 - 高速外设区 */ aips3: aips-bus@02200000 { compatible = \"fsl,aips-bus\", \"simple-bus\"; #address-cells = ; #size-cells = ; reg = ; // 0x02200000~0x022FFFFF ranges; };};

添加外设控制器节点

在 myfirst.dts 文件中加入 ecspi1, usbotg1 和 rngb 这三个外设控制器对应的节点,

  • ecspi1 属于 aips1 的子节点,
  • usbotg1 属于 aips2 的子节点,
  • rngb 属于 aips3 的子节点。

最终的 myfirst.dts 文件内容如下:

/ { compatible = \"fsl,imx6ull-alientek-evk\", \"fsl,imx6ull\"; /* CPU 核心配置 */ cpus { #address-cells = ; // CPU地址用1个32位数表示 #size-cells = ; // 不需要大小字段 // Cortex-A7 单核处理器 cpu0: cpu@0 { compatible = \"arm,cortex-a7\"; device_type = \"cpu\"; reg = ; // CPU逻辑编号 }; }; /* SoC 系统外设容器 */ soc { #address-cells = ; // 子节点地址字段占1个32位 #size-cells = ; // 子节点大小字段占1个32位 compatible = \"simple-bus\"; ranges; // 1:1地址映射 /* 片上RAM (128KB) */ ocram: sram@00900000 { compatible = \"fsl,lpm-sram\"; reg = ; // 物理地址范围 }; /* AIPS1 外设总线域 (1MB) */ aips1: aips-bus@02000000 { compatible = \"fsl,aips-bus\", \"simple-bus\"; #address-cells = ; #size-cells = ; reg = ; ranges; // ECSPI1 控制器 ecspi1: ecspi@02008000 { #address-cells = ; #size-cells = ; compatible = \"fsl,imx6ul-ecspi\", \"fsl,imx51-ecspi\"; reg = ; // 16KB寄存器空间 status = \"disabled\"; // 默认禁用 }; }; /* AIPS2 外设总线域 (1MB) */ aips2: aips-bus@02100000 { compatible = \"fsl,aips-bus\", \"simple-bus\"; #address-cells = ; #size-cells = ; reg = ; ranges; // USB OTG1 控制器 usbotg1: usb@02184000 { compatible = \"fsl,imx6ul-usb\", \"fsl,imx27-usb\"; reg = ; status = \"disabled\"; }; }; /* AIPS3 外设总线域 (1MB) */ aips3: aips-bus@02200000 { compatible = \"fsl,aips-bus\", \"simple-bus\"; #address-cells = ; #size-cells = ; reg = ; ranges; // 硬件随机数发生器 rngb: rngb@02284000 { compatible = \"fsl,imx6sl-rng\", \"fsl,imx-rng\", \"imxrng\"; reg = ; }; }; };};

至此, myfirst.dts 这个小型的模板设备树就编写好了,学到的知识是不是得到了巩固呢~

设备树在系统中的体现

Linux 内核启动的时候会解析设备树中各个节点的信息,并且在根文件系统的/proc/device-tree 目录下根据节点名字创建不同文件夹。

输入cd proc/device-tree,如图:

这就是/proc/device-tree 目录下的内容:

  • 根节点“/”的所有属性
  • 子节点

我们依次来看一下这些属性和子节点。

根节点“/”各个属性

根节点属性属性表现为一个个的文件,比如:“#address-cells”、“#size-cells”、“compatible”、“model”和“name”这 5 个文件,它们在设备树中就是根节点的 5个属性。

既然是文件那么肯定可以查看其内容,输入 cat 命令来查看 model和 compatible 这两个文件的内容,结果如图:


打开文件 imx6ull-alientek-emmc.dts查看一下,打印的这些值,就是根节点“/”的 model 和 compatible 属性值。

根节点“/”各子节点

根文件系统的/proc/device-tree 目录下,各个文件夹就是根节点“/”的各个子节点,比如“aliases”、“ backlight”、“ chosen”和“ clocks”等等。

/proc/device-tree 目录就是设备树在根文件系统中的体现,同样是按照树形结构组织的,进入/proc/device-tree/soc 目录中就可以看到 soc 节点的所有子节点,如图:

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

aliases 子节点

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

aliases { /* CAN 总线控制器别名 */ can0 = &flexcan1; // CAN0 接口映射到 flexcan1 控制器 can1 = &flexcan2; // CAN1 接口映射到 flexcan2 控制器 /* 以太网控制器别名 */ ethernet0 = &fec1; // eth0 网卡对应 fec1 控制器 ethernet1 = &fec2; // eth1 网卡对应 fec2 控制器 /* GPIO 控制器别名 */ gpio0 = &gpio1; // gpio0 对应 GPIO1 控制器 gpio1 = &gpio2; // gpio1 对应 GPIO2 控制器 /* SPI 控制器别名 */ spi0 = &ecspi1; // spi0 对应 ECSPI1 控制器 spi1 = &ecspi2; // spi1 对应 ECSPI2 控制器 spi2 = &ecspi3; // spi2 对应 ECSPI3 控制器 spi3 = &ecspi4; // spi3 对应 ECSPI4 控制器 /* USB PHY 别名 */ usbphy0 = &usbphy1; // usbphy0 对应 USB PHY1 usbphy1 = &usbphy2; // usbphy1 对应 USB PHY2};

单词 aliases 的意思是“别名”,因此 aliases 节点的主要功能就是定义别名,定义别名的目的就是为了方便访问节点。

一般会在节点命名的时候会加上 label,然后通过&label来访问节点,这样也很方便,而且设备树里面大量的使用&label 的形式来访问节点。

chosen 子节点

chosen 并不是一个真实的设备, chosen 节点主要是为了 uboot 向 Linux 内核传递数据,重点是 bootargs 参数。

chosen节点内容

一般.dts 文件中 chosen 节点通常为空或者内容很少, imx6ull-alientekemmc.dts 中 chosen 节点内容如下所示:

chosen { stdout-path = &uart1;};

chosen 节点仅仅设置了属性“stdout-path”,表示标准输出使用 uart1。

但是当我们进入到/proc/device-tree/chosen 目录里面,会发现多了 bootargs 这个属性,如图:

输入 cat 命令查看 bootargs 这个文件的内容,可以看到bootargs 这个文件的内容为“console=ttymxc0,115200……”,这正是我们在 uboot 中设置的 bootargs 环境变量的值。

chosen 节点的 bootargs 属性不是我们在设备树里面设置的,而是 uboot 自己在 chosen 节点里面添加了 bootargs 属性!

fdt_chosen 函数

common/fdt_support.c 文件中,有个 fdt_chosen 函数,如下所示:

/** * fdt_chosen - 处理设备树中/chosen节点的设置 * @fdt: 设备树blob指针 * * 功能: * 1. 验证设备树头有效性 * 2. 创建或定位/chosen节点 * 3. 设置bootargs属性(从环境变量获取) * 4. 修复标准输出配置 * * 返回值:成功返回0,失败返回错误码(负数) */int fdt_chosen(void *fdt){ int nodeoffset; int err; char *str; /* 用于设置字符串属性 */ /* 步骤1:验证设备树头 */ err = fdt_check_header(fdt); if (err < 0) { printf(\"fdt_chosen: %s\\n\", fdt_strerror(err)); return err; } /* 步骤2:查找或创建/chosen节点 */ nodeoffset = fdt_find_or_add_subnode(fdt, 0, \"chosen\"); if (nodeoffset < 0) return nodeoffset; /* 步骤3:设置bootargs参数 */ str = getenv(\"bootargs\"); if (str) { err = fdt_setprop(fdt, nodeoffset, \"bootargs\", str,  strlen(str) + 1); if (err < 0) { printf(\"WARNING: could not set bootargs %s.\\n\",  fdt_strerror(err)); return err; } } /* 步骤4:配置标准输出 */ return fdt_fixup_stdout(fdt, nodeoffset);}

关键点就是:

  • 读取 uboot 中 bootargs 环境变量的内容。
  • 调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值就是环境变量 bootargs 的内容

执行流程

函数 do_bootm_linux 函数的执行流程:

在我们之前的博客里,bootz启动 Linux 内核,详细地分析了这部分的源码,有兴趣的朋友们可以去看一下。

我们通过 bootz 命令启动 Linux 内核的时候,会运行 do_bootm_linux 函数,

bootz 80800000 – 83000000

do_bootm_linux 函数会通过一系列复杂的调用,最终通过 fdt_chosen 函数在 chosen 节点中加入了 bootargs 属性。