精华内容
下载资源
问答
  • fdt_setprop
    2020-10-21 11:12:25

    第01节_传递dtb给内核 : r2

    a. u-boot中内核启动命令:
       bootm <uImage_addr>                            // 无设备树,bootm 0x30007FC0
       bootm <uImage_addr> <initrd_addr> <dtb_addr>   // 有设备树
       
       比如 :
       nand read.jffs2 0x30007FC0 kernel;     // 读内核uImage到内存0x30007FC0
       nand read.jffs2 32000000 device_tree;  // 读dtb到内存32000000
       bootm 0x30007FC0 - 0x32000000          // 启动, 没有initrd时对应参数写为"-"
    
    b. bootm命令怎么把dtb_addr写入r2寄存器传给内核?
       ARM程序调用规则(ATPCS)
       
          c_function(p0, p1, p2) // p0 => r0, p1 => r1, p2 => r2
          
          定义函数指针 the_kernel, 指向内核的启动地址,
          然后执行: the_kernel(0, machine_id, 0x32000000);
          
    
    c. dtb_addr 可以随便选吗?
       c.1 不要破坏u-boot本身
       c.2 不要挡内核的路:内核本身的空间不能占用, 内核要用到的内存区域也不能占用
                        内核启动时一般会在它所处位置的下边放置页表, 这块空间(一般是0x4000即16K字节)不能被占用
       
      JZ2440内存使用情况:
                         ------------------------------
      0x33f80000       ->|    u-boot                  |
                         ------------------------------
                         |    u-boot所使用的内存(栈等)|
                         ------------------------------
                         |                            |
                         |                            |
                         |        空闲区域             |
                         |                            |
                         |                            |
                         |                            |
                         |                            |
                         ------------------------------
      0x30008000       ->|      zImage                |
                         ------------------------------  uImage = 64字节的头部+zImage
      0x30007FC0       ->|      uImage头部             |
                         ------------------------------
      0x30004000       ->|      内核创建的页表          |  head.S
                         ------------------------------
                         |                            |
                         |                            |
                  -----> ------------------------------
                  |
                  |
                  --- (内存基址 0x30000000)
    

    命令示例:

    a. 可以启动:
        nand read.jffs2 30000000 device_tree
        nand read.jffs2 0x30007FC0 kernel
        bootm 0x30007FC0 - 30000000

    b. 不可以启动: 内核启动时会使用0x30004000的内存来存放页表,dtb会被破坏
        nand read.jffs2 30004000 device_tree
        nand read.jffs2 0x30007FC0 kernel
        bootm 0x30007FC0 - 30004000


    第02节_dtb的修改原理

    例子1. 修改属性的值,
    假设 老值: len
         新值: newlen (假设newlen > len)
    
    a. 把原属性val所占空间从len字节扩展为newlen字节:
       把老值之后的所有内容向后移动(newlen - len)字节
    
    b. 把新值写入val所占的newlen字节空间
    
    c. 修改dtb头部信息中structure block的长度: size_dt_struct
    
    d. 修改dtb头部信息中string block的偏移值: off_dt_strings
    
    e. 修改dtb头部信息中的总长度: totalsize
    
    例子2. 添加一个全新的属性
    a. 如果在string block中没有这个属性的名字,
       就在string block尾部添加一个新字符串: 属性的名
       并且修改dtb头部信息中string block的长度: size_dt_strings
       修改dtb头部信息中的总长度: totalsize
    
    b. 找到属性所在节点, 在节点尾部扩展一块空间, 内容及长度为: 
       TAG      // 4字节, 对应0x00000003
       len      // 4字节, 表示属性的val的长度
       nameoff  // 4字节, 表示属性名的offset
       val      // len字节, 用来存放val
    
    c. 修改dtb头部信息中structure block的长度: size_dt_struct
    
    d. 修改dtb头部信息中string block的偏移值: off_dt_strings
    
    e. 修改dtb头部信息中的总长度: totalsize
    


    可以从u-boot官网源码下载一个比较新的u-boot, 查看它的cmd/fdt.c
    fdt命令调用过程:
        fdt set <path> <prop> [<val>]

    a. 根据path找到节点
    b. 根据val确定新值长度newlen, 并把val转换为字节流
    c. fdt_setprop
            c.1 fdt_setprop_placeholder       // 为新值在DTB中腾出位置
                     fdt_get_property_w  // 得到老值的长度 oldlen
                     fdt_splice_struct_  // 腾空间
                            fdt_splice_             // 使用memmove移动DTB数据, 移动(newlen-oldlen)
                            fdt_set_size_dt_struct  // 修改DTB头部, size_dt_struct
                            fdt_set_off_dt_strings  // 修改DTB头部, off_dt_strings
                            
            c.2 memcpy(prop_data, val, len);  // 在DTB中存入新值
    

    第03节_dtb的修改命令fdt移植

        我们仍然使用u-boot 1.1.6, 在这个版本上我们实现了很多功能: usb下载,菜单操作,网卡永远使能等, 不忍丢弃.需要在里面添加fdc命令命令, 这个命令可以用来查看、修改dtb。

        从u-boot官网下载最新的源码, 把里面的 cmd/fdt.c移植过来.

      最终的补丁存放在如下目录: doc_and_sources_for_device_tree\source_and_images\u-boot\u-boot-1.1.6_device_tree_for_jz2440_add_fdt_20181022.patch
      补丁使用方法:
      export  PATH=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/work/system/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi/bin
      tar xjf u-boot-1.1.6.tar.bz2                                                # 解压
      cd u-boot-1.1.6                  
      patch -p1 < ../u-boot-1.1.6_device_tree_for_jz2440_add_fdt_20181022.patch   # 打补丁
      make 100ask24x0_config                                                      # 配置
      make                                                                        # 编译, 可以得到u-boot.bin
    

    a. 移植fdt命令

    a.1 先把代码移过去, 修改Makefile来编译
    u-boot-2018.11-rc2\lib\libfdt   主要用这个目录, 
                                    它里面的大部分文件是直接包含scripts\dtc\libfdt中的同名文件
                                    只有2个文件是自己的版本
    u-boot-2018.11-rc2\scripts\dtc\libfdt
    
    
    把新u-boot中cmd/fdt.c重命名为cmd_fdt.c , 和 lib/libfdt/* 一起复制到老u-boot的common/fdt目录
    修改 老u-boot/Makefile, 添加一行: LIBS += common/fdt/libfdt.a
    修改 老u-boot/common/fdt/Makefile, 仿照 drivers/nand/Makefile来修改
    
    
    a.2 根据编译的错误信息修改源码
    
    移植时常见问题:
    i. No such file or directory:
       要注意, 
       #include "xxx.h"  // 是在当前目录下查找xxx.h
       #include <xxx.h>  // 是在指定目录下查找xxx.h, 哪些指定目录呢?
                         // 编译文件时可以用"-I"选项指定头文件目录, 
                         // 比如: arm-linux-gcc -I <dir> -c -o ....
                         // 对于u-boot来说, 一般就是源码的 include目录
       
       解决方法: 
       确定头文件在哪, 把它移到include目录或是源码的当前目录
    
    ii. xxx undeclared :
       宏, 变量, 函数未声明/未定义
       
       对于宏, 去定义它;
       对于变量, 去定义它或是声明为外部变量;
       对于函数, 去实现它或是声明为外部函数;
       
    
    iii. 上述2个错误是编译时出现的,
       当一切都没问题时, 最后就是链接程序, 这时常出现: undefined reference to `xxx'
       这表示代码里用到了xxx函数, 但是这个函数没有实现
       
       解决方法: 去实现它, 或是找到它所在文件, 把这文件加入工程
    

    b. fdt命令使用示例

    nand read.jffs2 32000000 device_tree  // 从flash读出dtb文件到内存(0x32000000)
    fdt addr 32000000                     // 告诉fdt, dtb文件在哪
    fdt print /led pin                    // 打印/led节点的pin属性
    fdt get value XXX /led pin            // 读取/led节点的pin属性, 并且赋给环境变量XXX
    print XXX                             // 打印环境变量XXX的值
    fdt set /led pin <0x00050005>         // 设置/led节点的pin属性
    fdt print /led pin                    // 打印/led节点的pin属性
    nand erase device_tree                // 擦除flash分区
    nand write.jffs2 32000000 device_tree // 把修改后的dtb文件写入flash分区
    

    • 以上内容参考自韦东山老师设备树的教学资料
    更多相关内容
  • linux系统之驱动与FDT

    2020-10-29 08:50:27
    【分析一】FDT与TAG 【分析二】boot中对FDT的支持 【分析三】Uboot下调试FDT 【分析四】kernel中对FDT的支持 【分析五】FDT兼容TAG 【总结】 【附录】 【摘要】 为何要写此文? 随着linux 内核版本的逐渐提升,在...

    转自:https://blog.csdn.net/eleven_xiy/article/details/72835181

    【摘要】

    【分析一】FDT与TAG

    【分析二】boot中对FDT的支持

    【分析三】Uboot下调试FDT

    【分析四】kernel中对FDT的支持

    【分析五】FDT兼容TAG

    【总结】

    【附录】


    【摘要】

    为何要写此文?

    随着linux 内核版本的逐渐提升,在3.10.* 版本之后,linux系统arm架构中对驱动实现方式做了较大调整,驱动中广泛采用FDT的实现方式。因此对于linux系统上的驱动开发来说,了解FDT尤为重要。



    FDT对系统软件开发工作的实际影响?

    1) FDT取代了tag.这一点涉及bootloader和kernel的变动。

    例如:经常用到的uboot里面bootargs、环境变量等相关实现要有所改动。

    2) 驱动中板级信息的获取方式变动。这一点涉及到具体驱动实现,无论flash、usb还是音视频等驱动的实现都要涉及FDT,如果不了解,驱动就无法实现。

    例如:我们要获取一个驱动的中断号或者控制器相关基地址等信息,都要通过FDT。


    什么是FDT?

    FDT:Flattened Device Tree即扁平设备树。linux系统最早引入是在linux2.6*版本的powerpc架构中。早前曾做过powerpc架构的开发,不过当时系统已经对FDT做了很好的支持,驱动开发只是简单的配置了dts文件。当时想写一篇介绍FDT的文章,但一直未能如愿。

    直到arm架构也引入了FDT,写一篇这样的文档显得很有必要,一方面希望记录工作。另一方面也希望为涉及FDT开发的同仁提供一点参考。


    linux为何要引入FDT?

    引入FDT目的很明确–降低代码冗余。

    过去的linux源码,arch/arm/plat-xxx和arch/arm/mach-xxx中充斥着大量的垃圾代码,相当多代码都是描述板级信息,对于内核来将,都是垃圾。随着arm的不断普及,垃圾代码越来越多,已经到了kernel无法容忍的地步,因此在3.10*之后,linux系统采用了FDT的实现方式,裁剪掉这部分代码冗余。


    【分析一】FDT与TAG

    本节将介绍两种FDT实现方案:FDT取代TAG 和FDT兼容TAG。不过在介绍之前,我们先明确下FDT到底是什么?我们需要做哪些工作?

    1 .首先要实现FDT功能,必须有一个*.dtb文件的输出。

    dtb文件是通过dtc工具生成,dtc(device tree complier)是开源工具,可以理解为dts文件的编译器。一般uboot和kernel代码中都有集成,直接拿来用即可。为了生成dtb ,需要修改makefile (一般可以修改uboot的顶级Makefile或者linux中arch/arm/boot/Makefile)如下:

    $(DTB_OBJS):FORCE
    $(obj)/dts/dtc -I dts -O dtb -i $(obj)/dts -p 1024 -o $(DTB_OBJS) $(obj)/dts/example_plat_version.dts

    观察上述命令:
    1) 首先输出 dtb文件,如DTB_OBJS=example_plat_version.dts.dtb
    dtb文件如何使用?这个文件可以烧录到flash上的任何位置。
    Ps:要想使用fdt兼容tag,则必须把dtb放到uImage后面,这是因为kernel在启动阶段有限制。如果用FDT替代tag则可放到任何位置。
    2) dtc是开源工具,主要功能是解析dts文件,并把解析的结果保存到dtb文件中。
    什么是dts文件? dts文件是设备树的源码。
    举例:example_plat_version.dts
    /include/ "example-plat.dtsi" 
    /{
      mode = "example plat evm Board";
      compatible = "example,**";
      chosen{
        bootargs = "console=ttyS0,115200 mem=100M root=/dev/mtdblock0 init=/linuxrc"; 
      };
      apb@80000000{
        i2c0:i2c@e80030000{
          status= "ok";
          ak4293: codec@12{
            compatible = "example,ak4293";
        reg = <0x12>;
          };
          it66121@4c{
               compatible = "example,it66121";
        reg = <0x44>;
          };
        };
      };
     };
    

    如上是dts文件的片段,这个文件是需要我们正确配置的。
    1) 其中 chosen和apb@80000000可以被称为一个节点--node,而i2c0可以被称为一个子节点—subnode,”/”是唯一的根节点。
    2) 不难发现,chosen中保存的bootargs就是我们uboot下使用的bootargs,以前它被保存在tag中,如今放到了dts文件里。
    3) apb@80030000中保存的是i2c驱动的信息。以前的系统中,这一部分是被定义在arch/arm/plat-xxx和arch/arm/mach-xxx 代码中,如今放到dts文件中。
    至此kernel代码得到精简。
    Ps:dtc是如何解析dts文件的不必详查,如感兴趣可以参考dtc源码。

    【分析二】boot中对FDT的支持

    如果要使用tag,需boot和kernel同时支持,那么用FDT取代TAG也同样如此.

    uboot中对FDT的支持:

    1) uboot代码中已经对fdt有支持,可以通过开启CONFIG_OF_LIBFDT配置,实现FDT支持,如此会省掉一部分工作。例如:fdt调试命令可以直接使用。

    2) 如前所述dtb文件被烧录到flash上。在Uboot引导内核启动之前会将dtb拷贝到内存上,并对dtb的内容进行修改,这一步是为了支持对环境变量的动态修改。

    以下介绍u-boot中如何修改dts文件中定义的内容:重点观察example_plat_version.dts文件里chosen节点中bootargs参数的修改,关键函数fdt_chosen:

    int fdt_chosen(void *fdt, int force)
    {
      int nodeoffset;
      char *str;
      const char *path;
      nodeoffset= fdt_path_offset(fdt, "/chosen");
      if(nodeoffset<0)
      {
        nodeoffset=fdt_add_subnode(fdt,0,"chosen");
        if(nodeoffset<0)
        {
          return nodeoffset;
        }
      }
      str = getenv("bootargs");
      path = fdt_set_prop(fdt,nodeoffset,"bootargs",str,strlen(str)+1);
      return 0;
    }
    上述函数的fdt_path_offset(fdt,”chosen”)获取的是dts文件中的:
      chosen{
        bootargs = "console=ttyS0,115200 mem=100M root=/dev/mtdblock0 init=/linuxrc"; 
      };
    要想修改bootargs则调用fdt_setprop_string;
    Ps:其中fdt_setprop_string等接口是FDT提供的标准接口,boot和kernel源码中都可调用。
    3)将dtb从flash拷贝到dram上。假设拷贝到dram=0x200100的地址,则uboot跳转到kernel时再通过cpu的通用寄存器r2告知系统这个地址,这一点与tag没有分别。至此完成boot下对fdt的支持。

    【分析三】Uboot下调试FDT

    Uboot支持标准的fdt命令修改dtb的内容。

    1 举例:常用调试命令

    1>fdt list :列出系统中所有节点。即上面dts文件中的节点。
    #fdt list
    /{
      #address-cells = <0x1000000>;
      #size-cells = <0x1000000>;
      compatible = "example,**";
      model = "example plat evm Board";
      chosen{
      };
      aliases{
      };
      memory{
      };
      cpus{
      };
      apb@80000000{
      };
    };
    2>列出aliases节点:
    #fdt list /aliases
    aliases{
      serial0 = "/apb@80000000/uart@8005000";
      nand = "/apb@90000000/nand@9001000";
      i2c0 = "/ahb@70000000/i2c@7001000";
    };
    
    3>查看boot分区信息:
    #fdt list nand
    nand@9001000{
      compatible = "example,nand";
      #address-cells = <0x1000000>;
      #size-cells = <0x1000000>;
      partition@0{
      };
      partition@1{
      };
    }; 
    #fdt list nand/partition@0
    partition@0{
      reg = <0x0 0x40000>;
      label = "uboot";
    };
    2 修改dtb举例:
    #fdt list /memory
    memory{
      device_type = "memory";
      reg = <0x2000,0xe007>;
    };
    #fdt set /memory reg <0x200000 0x6e00000>
    #
    #fdt list /memory
    memory{
      device_type = "memory";
      reg = <0x2000,0xe006>;
    };
    1>修改前 reg= <0x2000 0xe007>(字节序反了):实际表示linux 系统内存从0x200000开始大小为0x7e00000
    2>修改后 reg=<0x2000 0xe006>:实际表示linux 系统内存从0x200000开始大小为0x6e00000
    3>以上介绍了boot下显示和修改dtb的命令,其他命令可实际摸索一下,不逐一举例。

    【分析四】kernel中对FDT的支持

    1、 设备树初始化及解析

    分析Linux内核的源码,可以看到其对扁平设备树的解析流程如下:

    (1)首先在内核入口处将从u-boot传递过来的镜像基地址。这一步系统已经有实现好,不需要再开发。

    (2)通过调用early_init_dt_scan()函数来获取内核前期初始化所需的bootargs,cmd_line等系统引导参数。这一步需要二次开发,可以在系统搭建的框架中,加入项目特有的初始参数。

    (3)根据bootargs,cmd_line等系统引导参数进入start_kernel()函数,进行内核的第二阶段初始化。

    (4)调用unflatten_device_tree()函数来解析dtb文件,构建一个由device_node结构连接而成的单项链表,并使用全局变量of_allnodes指针来保存这个链表的头指针。这一步不需要二次开发

    (5)内核调用OF提供的API函数获取of_allnodes链表信息来初始化内核其他子系统、设备等。这一步可能会二次开发。

    如上:步骤1-4主要通过如下函数实现:

    
    void __init setup_arch(char **cmdline_p)
    {   /*实现步骤1-3*/
     mdesc = setup_machine_fdt(__atags_pointer);
     /* 实现步骤4 */
     unflatten_device_tree();
    }
    如上:步骤5主要通过如下函数实现:
    
    int of_platform_populate(struct device_node *root,
       const struct of_device_id *matches,
       const struct of_dev_auxdata *lookup,
       struct device *parent)
    {
     root = root ? of_node_get(root) : of_find_node_by_path("/");
    /* 
    为每个node创建dev.  实现步骤5
    struct platform_device也是在此时创建的 
    */
     for_each_child_of_node(root, child) {
      rc = of_platform_bus_create(child, matches, lookup, parent, true);
      if (rc)
       break;
     }
    }
    在此不对代码进行逐行分析,只对重点函数的功能予以说明,并且对可能需要二次开发的地方重点举例说明。实际工作中,主要集中在第(2)、(5)步。

    2 、重点函数分析.

    1)setup_machine_fdt()

    该函数主要解析设备树中和系统初始启动有关的参数,比如bootargs、Linux使用的物理内存等。实际项目中新添加的uboot参数,也可以在此解析。如下:

    举例:如何通过FDT将uboot参数example_para传入kernel:

    第一步:将example_para作为一个子节点,加入dts的根节点中。 如下example_plat_version.dts文件:
    
    /include/ "example-plat.dtsi" 
    /{
      mode = "example plat evm Board";
      compatible = "example,**";
      chosen{
        bootargs = "console=ttyS0,115200 mem=100M root=/dev/mtdblock0 init=/linuxrc"; 
      };
      apb@80000000{
      };
      /* 新添加的节点*/
      example_para{
        test = <1>;
        ethaddr = "0x00";
      };
     };
    第二步kernel中解析example_para。如下early_init_dt_scan_example_para需要我们自己来实现。
    
    struct machine_desc *__init setup_machine_fdt(unsigned int dt_phys)
    {
      of_scan_fdt_dt(early_init_dt_scan_chosen,boot_command_line);
      of_scan_fdt_dt(early_init_dt_scan_memory,NULL);
      of_scan_fdt_dt(early_init_dt_scan_example_para,NULL);
    }
    第三步解析dtb文件 查找到example_para
    
    int __init early_init_dt_scan_example_para(unsigned node,char *uname,int depth,void *data)
    {
      char *pchar;
      unsigned int leng;
      unsigned int *pvar;
      if(strcmp(uname,"example_para")==0)
      {
        pvar=of_get_flat_dt_prop(node,"ethaddr",&leng);
        pchar=of_get_flat_dt_prop(node,"ethaddr",&leng);
      }
    }
    至此,完成example_para节点添加,大部分初始参数都可以如此实现.以下简单分析一下上述实现过程.
    2) of_scan_flat_dt()实现功能:
    dtc工具在解析dts时,会生成一个描述dts的结构,并放在dtb的头部。
    dtc工具在解析dts时,example_para被保存到dtb的结构块中,因此从结构块的起始偏移地址开始遍历,能够在dtb中找到name为example_para的节点。
    Ps:注意此时dtb被拷贝到了DRAM.
    DTB的头结构如下:
     struct boot_param_header {
        __be32 magic;                //设备树魔数,固定为0xd00dfeed
        __be32 totalsize;            //整个设备树的大小
        __be32 off_dt_struct;        //保存结构块在整个设备树中的偏移
        __be32 off_dt_strings;        //保存的字符串块在设备树中的偏移
        __be32 off_mem_rsvmap;        //保留内存区,该区保留了不能被内核动态分配的内存空间
        __be32 version;            //设备树版本
        __be32 last_comp_version;    //向下兼容版本号
        __be32 boot_cpuid_phys;    //为在多核处理器中用于启动的主cpu的物理id
        __be32 dt_strings_size;    //字符串块大小
        __be32 dt_struct_size;     //结构块大小
    };
    3) early_init_dt_scan_example_para()->of_get_flat_dt_prop()实现功能。
    找到example_para节点后,继续查找property,例如test对应一个属性名为test的属性结构体即struct property。
    struct property {
        char *name;        //属性名
        int length;        //属性值长度
        void *value;        //属性值
        struct property *next; //指向下一个属性
        unsigned long _flags; //标志
        unsigned int unique_id;
    };
    到此,linux系统可以解析出example_para的值。
    如下函数中实现example_para解析:
    Ps: 注意查找方式遵循dtb的生成方式,即dtc对dts的解析规则。
    4)unflatten_device_tree()实现功能。
    实际上 如前所述,系统启动初期已经解析了dtb,不过只是查找少部分启动所需的初始化参数,如chosen(example_para)。系统真正完成对整个dtb文件解析是在该函数中实现。只要合理配置dts文件,该函数就能自动解析出对应node,不需要我么再次开发。unflatten_device_tree()函数解析dtb文件,构建一个由device_node结构连接而成的单项链表,并使用全局变量of_allnodes指针来保存这个链表的头指针。内核调用OF提供的API函数获取of_allnodes链表信息来初始化内核其他子系统、设备。
    设备节点结构体:
    struct device_node {
        const char *name;    //设备name
        const char *type; //设备类型
        phandle phandle;
        const char *full_name; //设备全称,包括父设备名
        struct property *properties; //设备属性链表
        struct property *deadprops; //removed properties
        struct device_node *parent; //指向父节点
        struct device_node *child; //指向子节点
        struct device_node *sibling; //指向兄弟节点
        struct device_node *next; //相同设备类型的下一个节点
        struct device_node *allnext; //next in list of all nodes
        struct proc_dir_entry *pde; //该节点对应的proc
        struct kref kref;
        unsigned long _flags;
        void *data;
    };
    其中,dts文件中每个{ }内的内容对应一个device_node,即上述结构体。 5)of_platform_populate() 该函数能完成驱动设备初始化,其实在此并未实现真正意义上的硬件初始化,只是把驱动所需资源 关联到了platform设备。
    实现过程如下:
    (1) 找到根节点。从of_allnodes开始遍历,找到 name为 “/” 的节点。
    (2) 为根节点的每个子节点创建 struct platform_device *dev;并把子节点对应的device_node结构关联到platform_dev,如下实现:
    of_device_alloc()中 dev->dev.of_node = of_node_get(np); (3) 注册驱动 driver_register()时,通过platform_device找到device_node,从而找到dts中配置的驱动资源。




    【分析五】FDT兼容TAG

    Linux系统在实现 FDT时,除了考虑直接用FDT替代tag外,还考虑的兼容tag的做法。

    考虑FDT兼容tag的情况,不需要对uboot进行修改。即前面所述的分析二和分析三都可以不用实现,不过内核中要做好兼容措施。下面介绍一下实现步骤:

    第一步:打开内核对FDT兼容TAG的配置,make menuconfig如下:

    在这里插入图片描述
    在这里插入图片描述

    第二步 分析内核启动第一阶段,将tag转换成FDT。 分析下面的小段代码arch/arm/boot/compressed/head.s:

    1) 至此 r6中保存的是dtb在dram上的起始地址,r8中保存atag在dram上地址。把dtb 头部的magic信息保存到r1,并校验是否为0xd00dfeed,这一步实际是检查是否有dtb文件。

    2) 只考虑存在dtb的情况,此时会跳转到atag_to_fdt()中去把tag信息转换为fdt信息。

      ldr lr, [r6, #0]
    #ifndef __ARMEB__
      ldr r1, =0xedfe0dd0  @ sig is 0xd00dfeed big endian
    #else
      ldr r1, =0xd00dfeed
    #endif
      cmp lr, r1
      bne dtb_check_done  @ not found
    #ifdef CONFIG_ARM_ATAG_DTB_COMPAT
      /*
       * OK... Let's do some funky business here.
       * If we do have a DTB appended to zImage, and we do have
       * an ATAG list around, we want the later to be translated
       * and folded into the former here.  To be on the safe side,
       * let's temporarily move  the stack away into the malloc
       * area.  No GOT fixup has occurred yet, but none of the
       * code we're about to call uses any global variable.
      */
      add sp, sp, #0x10000
      stmfd sp!, {r0-r3, ip, lr}
      mov r0, r8
      mov r1, r6
      sub r2, sp, r6
      bl atags_to_fdt
      /*
       * If returned value is 1, there is no ATAG at the location
       * pointed by r8.  Try the typical 0x100 offset from start
       * of RAM and hope for the best.
       */
      cmp r0, #1
      sub r0, r4, #TEXT_OFFSET
      bic r0, r0, #1
      add r0, r0, #0x100
      mov r1, r6
      sub r2, sp, r6
      bleq atags_to_fdt
      ldmfd sp!, {r0-r3, ip, lr}
      sub sp, sp, #0x10000
    #endif
      mov r8, r6   @ use the appended device tree
    第三步 观察atags_to_fdt()内核接口,该函数已经实现了将ATAG_CMDLINE转换为FDT的过程,若要实现其他转换,可参照实现:
    /*
     * Convert and fold provided ATAGs into the provided FDT.
     *
     * REturn values:
     *    = 0 -> pretend success
     *    = 1 -> bad ATAG (may retry with another possible ATAG pointer)
     *    < 0 -> error from libfdt
     */
    int atags_to_fdt(void *atag_list, void *fdt, int total_space)
    {
     for_each_tag(atag, atag_list) 
     {
       if (atag->hdr.tag == ATAG_CMDLINE) 
       {
         if (do_extend_cmdline)
            merge_fdt_bootargs(fdt,
             atag->u.cmdline.cmdline);
         else
           setprop_string(fdt, "/chosen", "bootargs",
                atag->u.cmdline.cmdline);
        } 
     }
    }



    【总结】

    系统切换到linux3.10之后,驱动中获取资源的方式、启动中获取初始信息的方式都有所改变,在系统移植或者驱动编写过程要尤为注意。
    也许你会发现,没有配置dts文件,驱动也能正常运行,那很可能是dts中的默认配置已经满足了驱动的要求,而不是不需要配置dts。






    【附录】
    网上找到一些OF提供的常用API函数,驱动开发中可能会用到,一并奉上:
    OF提供的函数主要集中在drivers/of/目录下,有address.c,base.c,device.c,fdt.c,irq.c,platform.c等等

    1. 用来查找在dtb中的根节点
      unsigned long __init of_get_flat_dt_root(void)
    2. 根据deice_node结构的full_name参数,在全局链表of_allnodes中,查找合适的device_node
      struct device_node *of_find_node_by_path(const char *path)
      例如:
      struct device_node *cpus;
      cpus=of_find_node_by_path("/cpus");
    3. 若from=NULL,则在全局链表of_allnodes中根据name查找合适的device_node
      struct device_node *of_find_node_by_name(struct device_node *from,const char *name)
      例如:
      struct device_node *np;
      np = of_find_node_by_name(NULL,“firewire”);
    4. 根据设备类型查找相应的device_node
      struct device_node *of_find_node_by_type(struct device_node *from,const char *type)
      例如:
      struct device_node *tsi_pci;
      tsi_pci= of_find_node_by_type(NULL,“pci”);
    5. 根据compatible字符串查找device_node
      struct device_node *of_find_compatible_node(struct device_node *from,const char *type, const char *compatible)
    6. 根据节点属性的name查找device_node
      struct device_node *of_find_node_with_property(struct device_node *from,const char *prop_name)
    7. 根据phandle查找device_node
      struct device_node *of_find_node_by_phandle(phandle handle)
    8. 根据alias的name获得设备id号
      int of_alias_get_id(struct device_node *np, const char *stem)
    9. device node计数增加/减少
      struct device_node *of_node_get(struct device_node *node)
      void of_node_put(struct device_node *node)
    10. 根据property结构的name参数,在指定的device node中查找合适的property
      struct property *of_find_property(const struct device_node *np,const char *name,int *lenp)
    11. 根据property结构的name参数,返回该属性的属性值
      const void *of_get_property(const struct device_node *np, const char *name,int *lenp)
    12. 根据compat参数与device node的compatible匹配,返回匹配度
      int of_device_is_compatible(const struct device_node *device,const char *compat)
    13. 获得父节点的device node
      struct device_node *of_get_parent(const struct device_node *node)
    14. 将matches数组中of_device_id结构的name和type与device node的compatible和type匹配,返回匹配度最高的of_device_id结构
      const struct of_device_id *of_match_node(const struct of_device_id *matches,const struct device_node *node)
    15. 根据属性名propname,读出属性值中的第index个u32数值给out_value
      int of_property_read_u32_index(const struct device_node *np,const char *propname,u32 index, u32 *out_value)
    16. 根据属性名propname,读出该属性的数组中sz个属性值给out_values
      int of_property_read_u8_array(const struct device_node *np,const char *propname, u8 *out_values, size_t sz)
      int of_property_read_u16_array(const struct device_node *np,const char *propname, u16 *out_values, size_t sz)
      int of_property_read_u32_array(const struct device_node *np,const char *propname, u32 *out_values,size_t sz)
    17. 根据属性名propname,读出该属性的u64属性值
      int of_property_read_u64(const struct device_node *np, const char *propname,u64 *out_value)
    18. 根据属性名propname,读出该属性的字符串属性值
      int of_property_read_string(struct device_node *np, const char *propname,const char **out_string)
    19. 根据属性名propname,读出该字符串属性值数组中的第index个字符串
      int of_property_read_string_index(struct device_node *np, const char *propname,int index, const char **output)
    20. 读取属性名propname中,字符串属性值的个数
      int of_property_count_strings(struct device_node *np, const char *propname)
    21. 读取该设备的第index个irq号
      unsigned int irq_of_parse_and_map(struct device_node *dev, int index)
    22. 读取该设备的第index个irq号,并填充一个irq资源结构体
      int of_irq_to_resource(struct device_node *dev, int index, struct resource *r)
    23. 获取该设备的irq个数
      int of_irq_count(struct device_node *dev)
    24. 获取设备寄存器地址,并填充寄存器资源结构体
      int of_address_to_resource(struct device_node *dev, int index,struct resource *r)
      const __be32 *of_get_address(struct device_node *dev, int index, u64 *size,unsigned int *flags)
    25. 获取经过映射的寄存器虚拟地址
      void __iomem *of_iomap(struct device_node *np, int index)
    26. 根据device_node查找返回该设备对应的platform_device结构
      struct platform_device *of_find_device_by_node(struct device_node *np)
    27. 根据device node,bus id以及父节点创建该设备的platform_device结构
      struct platform_device *of_device_alloc(struct device_node *np,const char *bus_id,struct device *parent)
      static struct platform_device *of_platform_device_create_pdata(struct device_node *np,const char *bus_id,
      void *platform_data,struct device *parent)
    28. 遍历of_allnodes中的节点挂接到of_platform_bus_type总线上,由于此时of_platform_bus_type总线上还没有驱动,所以此时不进行匹配
      int of_platform_bus_probe(struct device_node *root,const struct of_device_id *matches,struct device *parent)
    29. 遍历of_allnodes中的所有节点,生成并初始化platform_device结构
      int of_platform_populate(struct device_node *root,const struct of_device_id *matches,
      const struct of_dev_auxdata *lookup,struct device *parent)
    展开全文
  • QEMU Device Tree —— 传递流程

    千次阅读 2020-09-18 22:13:25
    fdt_create:初始化设备树 fdt_add_subnode:设备树中添加节点 fdt_setprop_cell:设置设备树属性键值对,值为数字 fdt_setprop_string:设置设备属性键值对,值为字符串 wrapper api qemu对libfdt的接口做了二次...

    dtb创建

    • 我们以下面的设备树信息为例,介绍设备树的创建过程:
    {   /* 根节点信息 */
        interrupt-parent = <0x00008001>;
        #size-cells = <0x00000002>;
        #address-cells = <0x00000002>;
        compatible = "linux,dummy-virt";
        
        /* gpio-keys信息 */
        gpio-keys {
            #address-cells = <0x00000001>;
            #size-cells = <0x00000000>;
            compatible = "gpio-keys";
            poweroff {
                gpios = <0x00008003 0x00000003 0x00000000>;
                linux,code = <0x00000074>;
                label = "GPIO Key Poweroff";
            };
        };
        
        /* pl011串口芯片信息 */
        pl011@9000000 {
            clock-names = "uartclk", "apb_pclk";
            clocks = <0x00008000 0x00008000>;
            interrupts = <0x00000000 0x00000001 0x00000004>;
            reg = <0x00000000 0x09000000 0x00000000 0x00001000>;
            compatible = "arm,pl011", "arm,primecell";
        };
     };
    

    api

    libfdt api

    • dtb一词在qemu中被fdt(Flat Device Tree)一词取代,以下所有提到fdt的地方指代dtb,linux平台为设备树的创建提供了libfdt库,由libfdt-devel rpm包提供,这个库提供了fdt操作的各种接口,部分如下:
    1. fdt_create:初始化设备树
    2. fdt_add_subnode:设备树中添加节点
    3. fdt_setprop_cell:设置设备树属性键值对,值为数字
    4. fdt_setprop_string:设置设备属性键值对,值为字符串

    wrapper api

    qemu对libfdt的接口做了二次封装,部分接口举例如下:

    1. create_device_tree:初始化一颗设备树
    2. qemu_fdt_setprop_cell:设置设备树的节点的属性,值为数字
    3. qemu_fdt_setprop_string:设置设备树的节点的属性,值为字符串
    4. qemu_fdt_add_subnode:添加一个子节点

    创建流程

    根节点创建

    • qemu在初始化virt架构板级信息时调用machvirt_init函数,首先创建设备树的基本属性,如下:
    machvirt_init
        create_fdt
    static void create_fdt(VirtMachineState *vms)
    {
        ....
        void *fdt = create_device_tree(&vms->fdt_size); 						/* 1 */
        vms->fdt = fdt;
        /* Header */
        qemu_fdt_setprop_string(fdt, "/", "compatible", "linux,dummy-virt"); 	/* 2 */
        qemu_fdt_setprop_cell(fdt, "/", "#address-cells", 0x2); 				/* 3 */
        qemu_fdt_setprop_cell(fdt, "/", "#size-cells", 0x2);
        ....
    }
    
    1. 初始化设备树,返回一个设备树内存地址
    2. 往设备树根节点添加compatible = "linux,dummy-virt"键值对
    3. 往设备树根节点添加#address-cells = <0x00000001>和#address-cells = <0x00000002>键值对
    
    • 经过以上流程后,设备树信息如下:
    {
        #size-cells = <0x00000002>;
        #address-cells = <0x00000002>;
        compatible = "linux,dummy-virt";
    }
    

    设备节点创建

    • 设备树基本属性设置后,一些基本硬件对应的设备在创建后,也会随之设置其设备树属性,比如gpio-key,如下:
    machvirt_init
        create_gpio
    static void create_gpio(const VirtMachineState *vms)
    {
        ....
        qemu_fdt_add_subnode(vms->fdt, "/gpio-keys");
        qemu_fdt_setprop_string(vms->fdt, "/gpio-keys", "compatible", "gpio-keys");
        qemu_fdt_setprop_cell(vms->fdt, "/gpio-keys", "#size-cells", 0);
        qemu_fdt_setprop_cell(vms->fdt, "/gpio-keys", "#address-cells", 1);
    
        qemu_fdt_add_subnode(vms->fdt, "/gpio-keys/poweroff");
        qemu_fdt_setprop_string(vms->fdt, "/gpio-keys/poweroff", "label", "GPIO Key Poweroff");
        qemu_fdt_setprop_cell(vms->fdt, "/gpio-keys/poweroff", "linux,code", KEY_POWER);
        qemu_fdt_setprop_cells(vms->fdt, "/gpio-keys/poweroff", "gpios", phandle, 3, 0);
        ....
    }
    
    • 以上流程生成如下设备树节点信息:
    gpio-keys {
        #address-cells = <0x00000001>;
        #size-cells = <0x00000000>;
        compatible = "gpio-keys";
        poweroff {
            gpios = <0x00008003 0x00000003 0x00000000>;
            linux,code = <0x00000074>;
            label = "GPIO Key Poweroff";
        };
    };
    

    dtb传递

    数据结构

    arm_boot_info

    • 设备树信息dtb生成之后,qemu会在最后阶段将其加载到内存。在arm架构下,qemu定义了一个arm_boot_info用于存放启动的信息,这些信息可能是qemu命令行配置的,也可能是qemu自己维护,数据结构如下:
    struct arm_boot_info {
        uint64_t ram_size;									/* 1 */
        const char *kernel_filename;						/* 2 */
        const char *kernel_cmdline;							/* 3 */
        const char *initrd_filename;						/* 4 */
        const char *dtb_filename;							/* 5 */
        hwaddr loader_start;								/* 6 */
        hwaddr dtb_start;									/* 7 */
        hwaddr dtb_limit;									
        ....
        hwaddr initrd_start;								/* 8 */
        hwaddr initrd_size;									
        ....
    };
    
    1. 虚拟机内存大小
    2. 内核镜像的文件路径,通常是linux内核编译出的镜像:arch/arm64/boot/Image,通过-kernel参数指定
    3. 传递给内核的命令行参数,通过-append指定
    4. 内存根文件系统ramdisk.img路径,通过-initrd指定
    5. 设备树dtb路径,qemu传递给kernel的设备树dtb信息有两个来源,一个是qemu自动生成的dtb传递给kernel,另一个是qemu去用户指定路径下
    加载dtb文件,然后传递给kernel。这里保存的是第二种方式下用户指定的路径,通过-dtb参数可以指定
    6. cpu运行时的起始物理地址,这段地址用来存放固件代码,其作用是加载dtb地址和kernel地址到寄存器,后文中会提到
    7. dtb被加载到内存后,它所在的虚机物理地址以及长度
    8. ramdisk.img文件被加载到内存后,它所在的虚机物理地址及大小
    

    VirtMachineState

    • 对于virt 架构的arm板,还有一个数据结构用来保存板级相关的信息,它继承自MachineState,qemu中定义的所有类型架构都继承自MachineState,如下:
    typedef struct {
        MachineState parent;
        ......
        struct arm_boot_info bootinfo;     	/* 1 */
        void fdt;                         	/* 2 */
        int fdt_size;                   	/* 3 */
        ......
    } VirtMachineState;
    
    1. 指向虚拟机的启动信息
    2. 保存虚拟机dtb信息的内存地址(HVA)
    3. 保存虚拟机dtb信息的大小
    

    ARMInsnFixup

    • ARMInsnFixup是一个临时的数据结构,它用于存放虚机引导过程中,跳转到kernel之前的一段固件代码,固件代码是ARMv8汇编指令对应的机器码,结构体的insn就用于存放这个机器码
    typedef struct ARMInsnFixup {
        uint32_t insn;
        FixupType fixup;
    } ARMInsnFixup;
    

    dtb加载

    • qemu生成dtb的过程就是在一段内存区间内不断添加设备树的信息,包括节点和属性信息,最终这段内存区域存放着dtb的内容,在host侧看来这段内存区间就是qemu申请的虚拟内存区间
    machvirt_init
    	vms->bootinfo.get_dtb = machvirt_dtb;
    		vms->machine_done.notify = virt_machine_done;
    			virt_machine_done
    				arm_load_dtb
    int arm_load_dtb(hwaddr addr, const struct arm_boot_info *binfo,
                     hwaddr addr_limit, AddressSpace *as)
    {
    	......
        if (binfo->dtb_filename) {										/* 1 */
            char *filename;										
            filename = qemu_find_file(QEMU_FILE_TYPE_BIOS, binfo->dtb_filename);
            fdt = load_device_tree(filename, &size);					
        } else {
            fdt = binfo->get_dtb(binfo, &size);							/* 2 */
        }
    	......
    	qemu_fdt_dumpdtb(fdt, size);									/* 3 */
    	rom_add_blob_fixed_as("dtb", fdt, size, addr, as);				/* 4 */
    	......
    
    1. 如果命令行指定了dtb文件路径,将器加载到内存中
    2. 如果命令行没有指定,通过get_dtb获取,get_dtb的对应实现machvirt_dtb,它返回qemu保存fdt的内存地址,存放在VirtMachineState的fdt成员中。
    3. 判断qemu命令行参数,如果指定了dumpdtb选项,将dtb信息打印出来,然后退出
    4. 封装一个qemu Rom对象并设置其物理地址,将dtb的内容拷贝到Rom的数据区域,然后将Rom插入到全局的roms链表中。roms链表最终会被qemu整
    个映射到虚机的物理地址空间,从而让虚机可以访问包括设备树在内的rom数据。
    
    • 打印fdt指向的内存内容前40字节如下,可以看到,前40字节就是设备树规范中定义的头部信息,header是大端字节对齐,高位在低内存地址。内存中保存的dtb信息和磁盘文件上一致:
      在这里插入图片描述

    地址传递

    传递规范

    • 在设备树规范中,初始化系统硬件的组件(比如固件,bootloader,hypervisors)被称为启动程序(boot programe),启动程序在硬件初始化完成后需要将控制权交给另外一个程序(比如kernel,bootloader)被称为客户程序(client program),设备树规范就是在boot programe和client programe之间定义一组接口用于传递不容易被自动探测的硬件信息。boot programe负责初始化并搜集硬件信息,生成设备树dtb然后传递给client program。client program解析dtb并根据其提供的信息加载对应的设备驱动,使用这些设备。在两者之间,如何传递设备树的内存地址信息,需要一个约定。内核的Documentation/arm64/booting.rst定义了启动规范,boot program需要在启动过程中完成如下几个任务:
    1. 设置并初始化系统内存
    2. 设置设备树信息
    3. 解压内核镜像
    4. 跳转到内核镜像,移交控制权
    • 其中描述了内核设备树地址如何传递给内核:通过寄存器x0。
      Primary CPU general-purpose register settings:
      x0 = physical address of device tree blob (dtb) in system RAM.
      x1 = 0 (reserved for future use)
      x2 = 0 (reserved for future use)
      x3 = 0 (reserved for future use)
    • 上面的规范是针对aarch64架构,对于32-bit arm,是通过r2寄存器传递的dtb地址。

    传递流程

    • qemu在加载好所有镜像后(包括kernel Image,ramdisk.img,dtb),跳转到kernel之前,这中间还有一件事情需要做,就是加载一个临时生成的bootloader固件到内存,然后跳转到这个固件执行代码,最终通过这个固件跳转到内核。这个固件做了三个动作,一是将dtb的地址加载到x0寄存器,二是将内核地址加载到x4寄存器,三是跳转到kernel入口地址。至此才算移交控制权给kernel。
    • dtb和kernel加载的地址可能每次有变化,因此固件的内容是由qemu动态生成的,其信息存放在bootloader_aarch64变量中,内容是aarch64架构汇编指令对应的机器码,如下:
    static const ARMInsnFixup bootloader_aarch64[] = {
        { 0x580000c0 }, /* ldr x0, arg ; Load the lower 32-bits of DTB /    			/* 1 */
        { 0xaa1f03e1 }, /* mov x1, xzr */                                    			/* 2 */
        { 0xaa1f03e2 }, /* mov x2, xzr */
        { 0xaa1f03e3 }, /* mov x3, xzr */
        { 0x58000084 }, /* ldr x4, entry ; Load the lower 32-bits of kernel entry */	/* 3 */
        { 0xd61f0080 }, /* br x4 ; Jump to the kernel entry point /     				/* 4 */
        { 0, FIXUP_ARGPTR_LO }, /* arg: .word @DTB Lower 32-bits */						/* 5 */
        { 0, FIXUP_ARGPTR_HI}, /* .word @DTB Higher 32-bits */						
        { 0, FIXUP_ENTRYPOINT_LO }, /* entry: .word @Kernel Entry Lower 32-bits */		/* 6 */
        { 0, FIXUP_ENTRYPOINT_HI }, /* .word @Kernel Entry Higher 32-bits */
        { 0, FIXUP_TERMINATOR }
    };
    
    1. 将dtb的物理地址加载到x0寄存器中,0x580000c0是ldr指令对应机器码(0b1011000000000000000000011000000)
    2. 将x1,x2,x3写入0
    3. 将内核的入口地址加载到x4寄存器中,0x58000084同上,是ldr指令对应机器码(0b1011000000000000000000010000100)
    4. 跳转到内核
    5. 存放dtb的物理内存地址(GPA)
    6. 存放kernel的入口地址(GPA)
    
    • 参考ARMv8-A_Architecture_Refernce_Manual的C5章节,查找ldr指令对应机器码格式如下:
      在这里插入图片描述在这里插入图片描述
    • 分析第一条汇编指令机器码,提取关键字段如下:
    1. opc: 01,表示操作64-bit变量
    2. imm19: 110,立即数为6
    3. Rt: 0,要加载的寄存器是x0
    4. label: 表示当前pc指针的偏移offset,寄存器的值从PC + offset的地方取值加载。它的值是imm19 * 4 = 6 * 4 = 24。
    • 上面这条汇编指令的含义,就是从相对pc指针偏移的24字节的地方,取64字节内容,加载的x0寄存器中,再对比bootloader_aarch64数组的内容,可以确定第一条指令的意思,就是从数组的第6,7个元素中读取值然后加载到x0寄存器中。从注释中也可以确认,第6,7个元素分别放的就是dtb地址的低32bit和高32bit。
    • bootloader_aarch64存放的是汇编指令的机器码,它被作为固件加载到虚机的物理地址空间,其加载流程如下:
    arm_setup_direct_kernel_boot													
    	primary_loader = bootloader_aarch64;										/* 5 */
    	write_bootloader("bootloader", info->loader_start, 							/* 6 */
    						primary_loader, fixupcontext, as);
    		rom_add_blob_fixed_as(name, code, len * sizeof(uint32_t), addr, as);
    
    11. 将bootloader固件代码的指针放到primary_loader中
    12. 封装成Rom,添加到roms全局链表中,动作与dtb的加载类似,从这里可以看到,loader_start保存的虚机物理地址,存放的是固件代码,主要用于
    加载dtb地址和内核地址,并跳转
    
    • primary_loader,kernel Image,dtb,ramdisk都被加载到内存后,一切准备就绪,最后的动作就是让cpu跳转到到primary_loader处执行固件代码,那怎么跳转过去呢?通过初始化cpu的pc寄存器,将其设置为primary_loader的地址,然后运行cpu,那么虚拟机cpu自然就从primary_loader开始运行,流程如下:
    arm_load_kernel
    	qemu_register_reset(do_cpu_reset, ARM_CPU(cs))		/* 7 */
    		do_cpu_reset,								
    			if (cs == first_cpu) {
    				cpu_set_pc(cs, info->loader_start)		/* 8 */
    				......
    			}
    arm_cpu_set_pc											/* 9 */
    	ARMCPU *cpu = ARM_CPU(cs);			
        CPUARMState *env = &cpu->env;
    	env->pc = value;									
    
    13. qemu在加载内存镜像时,注册了cpu复位的回调函数。当cpu复位时,会触发do_cpu_reset
    14. do_cpu_reset中有一个动作就是设置pc指针,这里将loader_start保存的虚机物理地址设置成pc指针,这个地址在arm64架构下就是
    primary_loader固件的起始地址,将其作为cpu复位时pc寄存器的初始值,那么当cpu运行时,第一条读取的指令就是这个固件存放的指令
    15. 每个架构下pc指针对应的cpu寄存器不同,qemu对应的定义了不同的数据结构,对于arm就是ARMCPU这个数据结构。
    
    • 以上所有这些设置好之后,qemu中虚机pc的值存放的时primary_loader固件的物理地址,当cpu进入guest态时,将这个pc值加载的cpu对应的物理pc寄存器中,然后开始cpu开始运行,执行固件代码,固件的动作是加载dtb和kernel的物理内存地址,然后跳转到kernel入口,启动虚机。
    展开全文
  • 第四课:u-boot对设备树的支持

    万次阅读 2018-11-22 14:06:38
    boot/ 查看它的cmd/fdt.c,里面构造了fdt的命令 fdt命令调用过程: fdt set [] 根据path找到节点 根据val确定新值长度newlen, 并把val转换为字节流 fdt_setprop 3.1 fdt_setprop_placeholder // 为新值在DTB中腾出...

    在线课堂:https://www.100ask.net/index(课程观看)
    论  坛:http://bbs.100ask.net/(学术答疑)
    开 发 板:https://100ask.taobao.com/ (淘宝)
         https://weidongshan.tmall.com/(天猫)
    交流群一:QQ群:869222007(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
    交流群二:QQ群:536785813(单片机-嵌入式)
    公 众 号:百问科技


    版本日期作者说明
    V12020韦东山技术文档

    第01节_传递dtb给内核

    先把设备树文件读到内存,在启动内核时把设备树的地址写到r2寄存器中
    a. u-boot中内核启动命令:

    bootm <uImage_addr> // 无设备树,bootm 0x30007FC0
    bootm <uImage_addr> <initrd_addr> <dtb_addr> // 有设备树

    比如 :

    nand read.jffs2 0x30007FC0 kernel; // 读内核uImage到内存0x30007FC0
    nand read.jffs2 32000000 device_tree; // 读dtb到内存32000000
    bootm 0x30007FC0 - 0x32000000 // 启动, 没有initrd时对应参数写为"-"

    b. bootm命令怎么把dtb_addr写入r2寄存器传给内核?
    在百度搜索ARM程序调用规则(ATPCS)
    写一个c函数
    c_function(p0, p1, p2) // p0 => r0, p1 => r1, p2 => r2(3个参数分别保存到相应的寄存器)
    定义函数指针 the_kernel, 指向内核的启动地址,然后执行: the_kernel(0, machine_id, 0x32000000);

    armlinux.c中

         /* 100ask for device tree, no initrd image used */
    	if (argc == 4) {
    		//第三个参数0x32000000就是设备树地址
    		of_flat_tree = (char *) simple_strtoul(argv[3], NULL, 16);
    
    		if  (be32_to_cpu(*(ulong *)of_flat_tree) == OF_DT_HEADER) {
    			printf ("\nStarting kernel with device tree at 0x%x...\n\n", of_flat_tree);
    
    			cleanup_before_linux ();
    			//把dtb的地址传到r2寄存器里			
    			theKernel (0, bd->bi_arch_number, of_flat_tree);
    					
    		} else {
    			printf("Bad magic of device tree at 0x%x!\n\n", of_flat_tree);
    		}
    		
    	}
    

    c. dtb_addr 可以随便选吗?
        c.1 不要破坏u-boot本身
        c.2 不要挡内核的路: 内核本身的空间不能占用, 内核要用到的内存区域也不能占用
    内核启动时一般会在它所处位置的下边放置页表, 这块空间(一般是0x4000即16K字节)不能被占用
    JZ2440内存使用情况:

                         ------------------------------
      0x33f80000       ->|    u-boot                  | 分析lds链接文件
                         ------------------------------
                         |    u-boot所使用的内存(栈等)|
                         ------------------------------
                         |                            |
                         |                            |
                         |        空闲区域            |
                         |                            |
                         |                            |
                         |                            |
                         |                            |
                         ------------------------------
      0x30008000       ->|      zImage                |
                         ------------------------------  uImage = 64字节的头部+zImage
      0x30007FC0       ->|      uImage头部            |
                         ------------------------------
      0x30004000       ->|      内核创建的页表        |  head.S
                         ------------------------------
                         |                            |
                         |                            |
                  -----> ------------------------------
                  |
                  |
                  --- (内存基址 0x30000000)
    

    我如何知道内核放在 0x30008000
    在内核目录下执行 mkimage -l arch/arm/boot/uImage
    里面显示内核的load address = 0x30008000 最终运行也在0x30008000位置

    命令示例:
    a. 可以启动:

    nand read.jffs2 30000000 device_tree
    nand read.jffs2 0x30007FC0 kernel
    bootm 0x30007FC0 - 30000000

    b. 不可以启动: 内核启动时会使用0x30004000的内存来存放页表,dtb会被破坏

    nand read.jffs2 30004000 device_tree
    nand read.jffs2 0x30007FC0 kernel
    bootm 0x30007FC0 - 30004000

    第02节_dtb的修改原理

    如果修改设备树中的led设备引脚,有两种办法

    • 修改dts文件,重新编译得到dtb并上传烧写
    • 使用uboot提供的一些命令来修改dtb文件,修改后再把它保存到板子上,以后就使用这个修改后的dtb文件
      移动值,也就是通过memmove处理

    memmove(dst,src,len)

    拷贝值

    memcpy(dst,src,len)

    例子1. 修改属性的值,

    假设 老值: len
    新值: newlen (假设newlen > len)

    • a. 把原属性val所占空间从len字节扩展为newlen字节:
      把老值之后的所有内容向后移动(newlen - len)字节
    • b. 把新值写入val所占的newlen字节空间
    • c. 修改dtb头部信息中structure block的长度: size_dt_struct
    • d. 修改dtb头部信息中string block的偏移值: off_dt_strings
    • e. 修改dtb头部信息中的总长度: totalsize

    扩充 string block
    并且修改dtb头部信息中string block的长度: size_dt_strings
    修改dtb头部信息中的总长度: totalsize

    例子2. 添加一个全新的属性

    a. 如果在string block中没有这个属性的名字,
    就在string block尾部添加一个新字符串: 属性的名
    并且修改dtb头部信息中string block的长度: size_dt_strings
    修改dtb头部信息中的总长度: totalsize

    b. 找到属性所在节点, 在节点尾部扩展一块空间, 内容及长度为:

       TAG      // 4字节, 对应0x00000003
       len      // 4字节, 表示属性的val的长度
       nameoff  // 4字节, 表示属性名的offset 
       val      // len字节, 用来存放val
    

    c. 修改dtb头部信息中structure block的长度: size_dt_struct
    d. 修改dtb头部信息中string block的偏移值: off_dt_strings
    e. 修改dtb头部信息中的总长度: totalsize

    我们需要在 0000,0001与 0000,0002之间加入属性,我需要把0002后面这段空间移动若干字节
    加入属性需要移动若干字节,

    可以从u-boot官网源码下载一个比较新的u-boot, ftp://ftp.denx.de/pub/u-boot/ 查看它的cmd/fdt.c,里面构造了fdt的命令

    fdt命令调用过程:
    fdt set []

    • 根据path找到节点
    • 根据val确定新值长度newlen, 并把val转换为字节流
    • fdt_setprop
    		3.1 fdt_setprop_placeholder       // 为新值在DTB中腾出位置
    		         fdt_get_property_w  // 得到老值的长度 oldlen
    				 fdt_splice_struct_  // 腾空间
    						fdt_splice_  // 使用memmove移动DTB数据, 移动(newlen-oldlen)
    						fdt_set_size_dt_struct  // 修改DTB头部, size_dt_struct 
    						fdt_set_off_dt_strings  // 修改DTB头部, off_dt_strings
    						
    		3.2 memcpy(prop_data, val, len);  // 在DTB中存入新值
    

    第03节_dtb的修改命令fdt移植

    我们仍然使用u-boot 1.1.6, 因为在这个版本上我们实现了很多功能: usb下载,菜单操作,网卡永远使能等, 不忍丢弃。
    现在比较新的uboot,已经自带fdc命令,我们使用老版本需要在里面添加fdc命令, 这个命令可以用来查看、修改dtb。

    从u-boot官网下载最新的源码, 把里面的 cmd/fdt.c移植过来.
    u-boot官网源码:
    ftp://ftp.denx.de/pub/u-boot/

    如果不想看本节的移植过程,可以直接使用补丁文件打补丁,得到移植后的uboot。
    最终的补丁存放在如下目录: doc_and_sources_for_device_tree\source_and_images\u-boot\u-boot-1.1.6_device_tree_for_jz2440_add_fdt_20181022.patch

    补丁使用方法

    1.设置交叉编译工具链

      export  PATH=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/work/system/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi/bin
    

    2.解压1.1.6版本的uboot

      tar xjf u-boot-1.1.6.tar.bz2  // 解压
    

    3.进入解压的uboot

    cd u-boot-1.1.6       
    

    4.打补丁

    patch -p1 < ../u-boot-1.1.6_device_tree_for_jz2440_add_fdt_20181022.patch   // 打补丁
    

    5.重新配置,编译uboot

      make 100ask24x0_config     // 配置
      make                       // 编译, 可以得到u-boot.bin
    

    移植fdt

    a.1 先把代码移过去, 修改Makefile来编译
    u-boot-2018.11-rc2\lib\libfdt主要用这个目录,它里面的大部分文件是直接包含scripts\dtc\libfdt中的同名文件,只有2个文件是自己的版本,即fdt_region.cfdt_ro.c
    把新u-boot中cmd/fdt.c重命名为cmd_fdt.c , 和 lib/libfdt/*一起复制到老u-boot的common/fdt目录;
    修改老u-boot中u-boot/Makefile,添加一行:LIBS += common/fdt/libfdt.a
    修改老u-boot中u-boot/common/fdt/Makefile, 仿照drivers/nand/Makefile来修改;

    a.2 根据编译的错误信息修改源码

    移植时常见问题:
    i. No such file or directory:

       #include "xxx.h"  // 是在当前目录下查找xxx.h
       #include <xxx.h>  // 是在指定目录下查找xxx.h
    

    这里的指定目录,在编译文件时可以用"-I"选项指定头文件目录,比如: arm-linux-gcc -I

    -c -o …,对于u-boot来说, 一般就是源码的 include目录。

    • 解决方法:
      确定头文件在哪, 把它移到include目录或是源码的当前目录。

    ii. xxx undeclared :
    宏, 变量, 函数未声明/未定义

    • 解决方法:
      对于宏, 去定义它;
      对于变量, 去定义它或是声明为外部变量;
      对于函数, 去实现它或是声明为外部函数;

    iii. 上述2个错误是编译时出现*

    当一切都没问题时, 最后就是链接程序, 这时常出现: undefined reference to `xxx’
    这表示代码里用到了xxx函数, 但是这个函数没有实现

    • 解决方法:
      去实现它, 或是找到它所在文件, 把这文件加入工程

    fdt命令使用示例

    nand read.jffs2 32000000 device_tree  // 从flash读出dtb文件到内存(0x32000000)
    fdt addr 32000000                     // 告诉fdt, dtb文件在哪
    fdt print /led pin                    // 打印/led节点的pin属性
    fdt get value XXX /led pin            // 读取/led节点的pin属性, 并且赋给环境变量XXX
    print XXX                             // 打印环境变量XXX的值
    fdt set /led pin <0x00050005>         // 设置/led节点的pin属性
    fdt print /led pin                    // 打印/led节点的pin属性
    nand erase device_tree                // 擦除flash分区
    nand write.jffs2 32000000 device_tree // 把修改后的dtb文件写入flash分区
    

    朋友您好,上面的内容过瘾不?如果还想了解更多可以去淘宝搜索"设备树" 进入"韦东山老师个人店"购买设备树视频,29节只要69元,超级划算!

    展开全文
  • u-boot对设备树的支持

    2022-05-22 17:47:36
    目录一、传递dtb给内核 : r21、u-boot中内核启动命令... 添加一个全新的属性三、dtb的修改命令fdt移植 一、传递dtb给内核 : r2 1、u-boot中内核启动命令 bootm <uImage_addr> // 无设备树,bootm 0x30007FC0 bootm
  • err = fdt_setprop(fdt, nodeoffset,"bootargs", str, strlen(str)+1); //将bootargs 设置为设备树的属性 fixup_memory_node(*of_flat_tree); for (bank = 0; bank < CONFIG_NR_DRAM_BANKS; bank++) { ...
  • MTK平台LCM驱动加载过程-lk阶段

    千次阅读 2018-12-14 11:02:25
    原创文章,转载请注明出处 注:该博客基于MT6739 Android O分析,贴出来的代码会针对性删减,只留重点部分 总体流程 platform_init -&gt; mt_disp_init -&gt; primary_display_init -&...
  • 第 294 行,调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值就是环境变量 bootargs 的内容。 上图中框起来的部分就是函数 do_bootm_linux 函数的执行流程,也就是说do_bootm_linux...
  • 设备树基础知识

    2020-12-11 16:53:23
    //获取环境变量bootargs的值 if (str) { //给设备树fdt下的chosen节点(nodeoffset)添加属性名称为 bootargs 的属性,属性值为booatargs环境变量的值(str) err = fdt_setprop(fdt, nodeoffset, "bootargs", str,...
  • fdt_create_phandle() 增加phandle节点 fdt_add_subnode() 增加子节点 删除: fdt_del_node() fdt_del_node_and_alias() fdt_del_node() 修改: do_fixup_by_prop_u32() 修改属性值 do_fixup_by_compat
  • 48 ret = fdt_setprop_u64(fdt, offset, "cpu-release-addr", 49 (unsigned long)&spin_table_cpu_release_addr); 50 if (ret) 51 return -ENOSPC; 52 } 53 //设置设备树的保留内存 :添加一个内存区域为16和17...
  • 高通android bootloader

    千次阅读 2017-03-13 16:03:54
    ret=fdt_setprop_string(fdt,offset,(constchar*)”bootargs”,(constvoid*)cmdlin e); /*Addingtheinitrd-start tothechosennode*/ ret=fdt_setprop_u32(fdt,offset,”linux,initrd-start”,(uint32_t)ramdisk); /*...
  • } if((strcmp(screen_buf,"inch8rgb")==0) || (strcmp(screen_buf,"g104x1-l04")==0) || (strcmp(screen_buf,"g101ice")==0) || (strcmp(screen_buf,"ldb15xga")==0)) { fdt_setprop_string(blob,panel,...
  • 第 294 行,调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值就是环境变量 bootargs 的内容。 上图中框起来的部分就是函数 do_bootm_linux 函数的执行流程,也就是说do_bootm_linux...
  • atags_to_fdt.c

    千次阅读 2016-08-04 11:18:59
    178 setprop_cell(fdt, "/chosen", "linux,initrd-start", 179 initrd_start); 180 setprop_cell(fdt, "/chosen", "linux,initrd-end", 181 initrd_start + initrd_size); 182 } 183 } 184  185 if ...
  • 设备树(三)

    2019-09-01 14:19:05
     c.1 fdt_setprop_placeholder // 为新值在DTB中腾出位置  fdt_get_property_w // 得到老值的长度 oldlen  fdt_splice_struct_ // 腾空间  fdt_splice_ // 使用memmove移动DTB数据, 移动(newlen-oldlen)  ...
  • 在efi_entry 中会通过下面的code从uefi中拿到cmdline  cmdline_ptr = efi_convert_cmdline(sys_table, image,...在update_fdt中可以看到是通过fdt_setprop 将cmdline_ptr 中的内容copy到chosen节点中的bootargs中.
  • 在很久之前的版本,uboot还需要传machine id和参数区地址给内核,但现在只传递设备树(fdt)的地址给内核了,这些参数全在fdt中做了。uboot将设备树bin文件和kernel加载到内存,然后将fdt地址传给kernel,跳到kernel...
  • if (ret) /* Adding the initrd-end to the chosen node */ ret = fdt_setprop_u32(fdt, offset, "linux,initrd-end", ((uint32_t)ramdisk + ramdisk_size)); fdt_pack(fdt); return ret; } 2,Kernel中的处理 主要...
  • 3)、调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值 就是环境变量 bootargs 的内容。 对函数fdt_chosen()继续进行查找,发现他的调用关系是这个样子的: 框起来的部分就是函数 ...
  • 第 294 行,调用函数 fdt_setprop 向 chosen 节点添加 bootargs 属性,并且 bootargs 属性的值就是环境变量bootargs 的内容。 证据“石锤”了,就是 uboot 中的 fdt_chosen 函数在设备树的chosen 节点中加入了 ...
  • setprop_cell(fdt, "/chosen", "linux,initrd-end", initrd_end); setprop_string(fdt, "/chosen", "bootargs", cmdline); r = fdt_pack(fdt); CHECK(r ); /* 打印相关信息 */ msg("kernel command line: \"%s...
  • } else if(sim_count == 2) { fdt_setprop_string(fdt, nodeoffset, "product.hardware.sku", "dsds"); } // end of P version return 0; } 调用的方法可以参考如下的代码,第6行。 extern int *target_fdt_...
  • 添加一个全新的属性2.3、fdt命令调用过程3、dtb的修改命令fdt移植3.1、体验移植好fdt命令的uboot3.2、移植fdt命令3.2.1、先把代码移过去, 修改Makefile来编译3.2.2、根据编译的错误信息修改源码3.2.3、fdt命令使用...
  • 最近一段时间在荔枝派zero上调试simplefb进行显示视频和图片,经过一段时间的研究,基本上搞清楚了simplefb参数的设置、uboot和kernel之间关于参数的传递流程,在这里记录一下,以备查阅。 首先描述一下simplefb使...

空空如也

空空如也

1 2 3 4 5 ... 7
收藏数 137
精华内容 54
关键字:

fdt_setprop