目录
前言
在上一讲内容里,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 = <1>; // 子节点reg属性中地址字段占1个32位单元
#size-cells = <0>; // 子节点reg属性不需要长度字段
/* 单个CPU核心定义 */
cpu0: cpu@0 { // 标签cpu0,节点路径为cpus/cpu@0
compatible = "arm,cortex-a7"; // CPU架构标识
device_type = "cpu"; // 标准设备类型声明
reg = <0>;
};
};
};
添加 soc 节点
uart, iic 控制器等等这些都属于 SOC 内部外设,因此一般会创建一个叫做 soc 的父节点来管理这些 SOC 内部外设的子节点。
添加 soc 节点,如下所示:
/ {
// SoC 系统级外设容器节点
soc {
#address-cells = <1>; // 子节点地址字段占用1个32位单元
#size-cells = <1>; // 子节点大小字段占用1个32位单元
compatible = "simple-bus"; // 简单内存映射总线
ranges; // 子地址空间与父地址空间直接映射,无需转换
};
};
添加 ocram 节点
ocram 是 I.MX6ULL 内部 RAM,因此 ocram 节点应该是 soc 节点的子节点。
ocram 起始地址为 0x00900000,大小为 128KB(0x20000),
添加 ocram节点,如下所示:
/ {
// SoC 系统级外设容器节点
soc {
#address-cells = <1>; // 子节点reg属性中地址字段占1个32位单元
#size-cells = <1>; // 子节点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 0x20000>; // 物理地址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>; // 子节点地址字段占1个32位单元
#size-cells = <1>; // 子节点长度字段占1个32位单元
reg = <0x02000000 0x100000>; // 物理地址范围:0x02000000~0x020FFFFF(1MB)
ranges; // 子地址空间与父地址空间1:1映射
};
/* AIPS2 总线域 - 低速外设区 */
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>; // 0x02100000~0x021FFFFF
ranges;
};
/* AIPS3 总线域 - 高速外设区 */
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>; // 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 = <1>; // CPU地址用1个32位数表示
#size-cells = <0>; // 不需要大小字段
// Cortex-A7 单核处理器
cpu0: cpu@0 {
compatible = "arm,cortex-a7";
device_type = "cpu";
reg = <0>; // CPU逻辑编号
};
};
/* SoC 系统外设容器 */
soc {
#address-cells = <1>; // 子节点地址字段占1个32位
#size-cells = <1>; // 子节点大小字段占1个32位
compatible = "simple-bus";
ranges; // 1:1地址映射
/* 片上RAM (128KB) */
ocram: sram@00900000 {
compatible = "fsl,lpm-sram";
reg = <0x00900000 0x20000>; // 物理地址范围
};
/* AIPS1 外设总线域 (1MB) */
aips1: aips-bus@02000000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02000000 0x100000>;
ranges;
// ECSPI1 控制器
ecspi1: ecspi@02008000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-ecspi", "fsl,imx51-ecspi";
reg = <0x02008000 0x4000>; // 16KB寄存器空间
status = "disabled"; // 默认禁用
};
};
/* AIPS2 外设总线域 (1MB) */
aips2: aips-bus@02100000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02100000 0x100000>;
ranges;
// USB OTG1 控制器
usbotg1: usb@02184000 {
compatible = "fsl,imx6ul-usb", "fsl,imx27-usb";
reg = <0x02184000 0x4000>;
status = "disabled";
};
};
/* AIPS3 外设总线域 (1MB) */
aips3: aips-bus@02200000 {
compatible = "fsl,aips-bus", "simple-bus";
#address-cells = <1>;
#size-cells = <1>;
reg = <0x02200000 0x100000>;
ranges;
// 硬件随机数发生器
rngb: rngb@02284000 {
compatible = "fsl,imx6sl-rng", "fsl,imx-rng", "imxrng";
reg = <0x02284000 0x4000>;
};
};
};
};
至此, 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 属性。