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

发布于:2025-08-05 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

前言

创建小型模板设备树

添加 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 = <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 属性。


网站公告

今日签到

点亮在社区的每一天
去签到