第一课--OpenWRT uboot介绍

来自Microduino Wikipedia
Shengkai81@gmail.com讨论 | 贡献2015年3月17日 (二) 08:43的版本 microWRT Uboot 启动分析
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳转至: 导航搜索

关于uboot的介绍,在网上有很多blog,并且很多文章都详细介绍了uboot的启动过程,这些启动过程大多是基于ARM core的S3C2440. Uboot的作用简单概括就是设置硬件的初始状态,并引导kernel。其实在系统上电之后,uboot被运行之前还有一端代码,一般称为bootrom。 这部分内容对我们开发人员来说是不可见的,我们也不用关心。 我们只需要知道uboot是我们开始设计系统的第一步就可以了。

本篇教程主要基于microWRT介绍uboot的启动。MicroWRT所用过的芯片MTK7620A是一款MIPS 架构的芯片, 所以首先我们就要介绍mips处理器的架构。


MIPS 处理器架构

地址空间映射

MIPS 地址空间,这里说的是CPU的寻址空间,不是内存空间。内存只是映射在一部分地址空间上而已。 每个芯片的地址空间映射是很重要的,这些信息都会从datasheet中获得。Mips处理器的地址空间一般分为4段 (Kuseg、Kseg0、Kseg1、Kseg2),其中:

Kseg0 (0x80000000 ~ 0x9fffffff) 为缓存段,直接映射在物理地址段上。在 CPU 复位时,缓存未被初始化,只有 Kseg1 能够被直接访问。

kseg1: 虚拟空间0xA000 0000 - 0xBFFF FFFF(512M): 这些地址通过把最高3位清零的方法来映射到相应的物理地址上, 与kseg0映射的物理地址一样。但kseg1是非cache存取的。kseg1是唯一的在系统重启时能正常工作的地址空间。 这也是为什么重新启动时的入口向量是0xBFC0 0000。这个向量相应的物理地址是0x1FC0 0000。 你将使用这段地址空间去存取你的初始化ROM。大多数人在这段空间使用I/O寄存器。

kseg2: 虚拟空间0xC000 0000 - 0xFFFF FFFF (1G): 这段地址空间只能在核心态下使用并且要经过MMU的转换。 在MMU设置好之前,不能存取这段区域。除非你在写一个真正的操作系统,一般来说你不需要使用这段地址空间。

综上可以看到,MIPS32 CPU下面的不经过MMU转换的内存窗口只有kseg0和kseg1 的512M的大小,而且这两个内存窗口映射到同一个512M的物理地址空间。 其余的3G虚拟地址空间需要经过MMU转换成物理地址,这个转换规则是由CPU 厂商实现的。换 句话说,在MIPS32 CPU下面访问高于512M的物理地址空间,必须通过MMU地址转换, 在 CPU 复位时,缓存未被初始化,只有 Kseg1 能够被直接访问。

基于MIPS的这种特殊设计,在移植uboot的时候,往往要对cache进行一些特殊设置。

microWRT地址映射

1. 物理段:

内存、Flash、IO寄存器等都被映射在物理段上访问时需要通过宏 KSEG0ADDR(_addr) 来通过 Kseg0 访问或 KSEG1ADDR(_addr) 来通过 Kseg1 访问。 物理地址一般不能直接访问,都需要通过 Kseg0 或 Kseg1 来访问。

2. 主要的映射范围:

DDR:0~0x0fffffff (这一段直接映射物理内存,一般通过带缓存的 Kseg0 来访问,以便加快速度) I/O空间:0x10000000 ~ 0x1dffffff (这一段直接控制硬件,必须通过 Kseg1 来访问) SPI Flash 空间:0x1f000000 ~ 0x1fffffff (这一段映射闪存的前 16MB 数据)

microWRT Uboot 启动分析

uboot的入口函数,我们可以从板子的lds文件里找到。一般都是start.s, 每个板子都有自己的lds文件,其主要用来说明编译生成的命令, 以及运行过程中用到的数据放置的位置。下面是个lds的例子。

 OUTPUT_FORMAT(“elf32-tradbigmips”, “elf32-tradbigmips”, “elf32-tradbigmips”)
  /* 这里是生成格式为elf。大端,mips */
 OUTPUT_ARCH(mips)
  /* 平台为mips */
 ENTRY(_start) /* 入口点为_start */
 SECTIONS
 {
   . = 0×00000000;
   . = ALIGN(4);
   .text : /* 这个是程序存放的地方 */
   {
     *(.text)
   }
 . = ALIGN(4); /* 表示以4字节对齐 */
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(4);
 .data : { *(.data) }
 . = .;
 _gp = ALIGN(16) + 0×7ff0;
 .got : {
 __got_start = .; /* 表示该处地址的值给__got_start */
   *(.got)
 __got_end = .;
 }
 .sdata : { *(.sdata) }
 .u_boot_cmd : {
 __u_boot_cmd_start = .;
 *(.u_boot_cmd)
 __u_boot_cmd_end = .;
 }
 uboot_end_data = .;
 num_got_entries = (__got_end – __got_start) >> 2;
 . = ALIGN(4);
 .sbss (NOLOAD) : { *(.sbss) }
 .bss (NOLOAD) : { *(.bss) . = ALIGN(4); }
 uboot_end = .;
 }


通过分析lds,我们就可以知道uboot的入口是start.s 文件,下面我就就来分析这个文件。限于篇幅的限制,下面给出了uboot运行的一个大概流程,没有对源码做详细分析。

首先从_start开始。前面是128个字(不是字节),是留给异常入口点的。

1. 最前面两个分别是硬复位和软复位,这两个都跳到reset处。

2. 下面就是清一些CP0(协处理器0,mips对CPU的控制都是通过它实现的)的一些主要位。

3. 然后是关闭cache。

4. 下面这个比较有意思。为什么还非要跳一下呢?这样就可以知道代码的位置,而不是标号值。比如可能在RAM中或ROM中。

 bal 1f
 nop
 .word _gp
 1:
 lw gp, 0(ra)

5. 这里执行lowlevel_init。这是第一个需要我们自己定义的函数。由于没有初始化堆栈,这里只能用汇编。我们看到在jalr后跟了个nop,这就是分支延迟槽,在这里什么也没有执行。

6. 下面执行了mips_cache_reset,它会来清理数据和指令的cache,并设置为正确的值。然后就可以打开cache了。

7. 由于我们的内存可能还没有始初化(有些人会有lowlevel_init中初始化,但有的人没有这样做)。但我们使用C函数的话,就需要堆栈,所以需要一个内存空间。 于是这里执行了mips_cache_lock,将cache的地址锁定,就是将cache当内存用了。然后我们将堆栈的地址设定在我们锁定的cache的最高地址(因为堆栈是向下生长的)。 这时我们就可以用C函数了,当然你还用不了malloc,也不可以太多的浪费堆栈。

8. 这里就跑到C的初始化函数中去了--board_init_f。

对于mips,board_init_f在lib_mips/board.c下。在board_init_f()函数中,主要完成了一些功能初始化,和划分RAM。 再看一下都初始化了什么功能。初始化的函数都在init_fnc_t *init_sequence[]里。有些不同版本的uboot,会直接在上级函数里调用这些函数。

 init_fnc_t *init_sequence[] = {
 board_early_init_f, /* 一些必要,需在之前做的初始化,如想使用需定义CONFIG_BOARD_EARLY_INIT_F */
 timer_init, /* 初始化时钟计数,cp0的 */
 env_init, /* 环境变量保存在flash中 */
 #ifdef CONFIG_INCA_IP
 incaip_set_cpuclk, /* 根据cpuclk环境变量设定CPU主频 */
 #endif
 init_baudrate, /* 根据baudrate环境变量设定gd->baudrate */
 serial_init, /* 设定串口速率,需要我们自己写(包括其它serial的) */
 console_init_f, /* 设置gd->have_console=1,有CONFIG_SILENT_CONSOLE则查看silent */
 display_banner, /* 打印uboot信息 */
 checkboard, /* 检测板子,可以在这打印设备信息,需要我们自己写 */
 init_func_ram, /* 设置gd->ram_size,initdram需要我们自己写 */
 NULL, /* 最后这个空必须留着,检查结束 */
 };

最后这个函数调用了relocate_code (addr_sp, id, addr)。注意,这个函数,准确的说不是函数(因为不能返回),是不返回的。

现在我们又回到start.S中了。我们可以看到,这里和C语言传递参数是用a0,a1,a2。relocate_code的工作就是将代码搬移到RAM中执行。这里做的工作是:

1. 移动gp指针

2. 复制代码到RAM中

3. 刷新一下cache

4. 跳到RAM代码当中去(in_ram)

in_ram的主要工作是:更新GOT;清空BSS段;最后跳到board_init_r。我们可以看到board_init_r最后一个参数是在分支延迟槽中赋值的。

这其实这里主要说一下GOTs(global offset tables)这个东东,这是uboot能跳转到不同空间运行的原理。uboot编译时用到了PIC(position-independent code) (也可以说成position-independent executable (PIE))。这个其实是很早之前,在没有MMU的年代引进来的东西。为了在没有MMU时,不同进程也能同时运行, 就需要他们的运行地址可以改变。GOTs用来保存所有的全局变量地址,所以我们只要改变GOTs的值就可以了。gp就是指向GOTs位置的指针。 这个功能需要在gcc编译时指定-fpic。然后就像我们看到的,我们只要改变GOTs里的值,加上地址偏移就可以了。

下面再看一下board_init_r,这里的工作包括下面几个内容:

1. 复制cmd段的信息过来。这里只复制了cmd,name,usage,usage。帮助信息的字符串还在flash中。

2. 然后是初始化malloc功能。注意这里env有malloc的方式分配到了空间,并复制到RAM。

3. 再就是stdio,串口,入口函数,以及全局变最根据env的初始化了。

4. 再接着就是网络的初始化。eth_initialize(gd->bd)。对于mips,如果设了CONFIG_NET_MULTI。我们需要自己写board_eth_init和cpu_eth_init两个函数。 只有前者返回值小于0时,我们才需要执行后者。

5. 最后进入main_loop()。在这个函数中,主要是通过bootm 命令来启动kernel的。具体内容如下:

 main_loop()
 {
    s = getenv ("bootcmd");
    debug ("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");
    run_command (s, 0);
 }

执行上面的bootcmd,第一步就是copy kernel 到 RAM 中。这里涉及到nand的读写 do_nand() 完成kernle的copy后,就接着执行bootm,实际的执行顺序为bootm -->do_bootm-->do_bootm_linux

 do_bootm() --> 在文件cmd_bootm.c 中
 {
    bootm_start()  --> 读取头部获得加载地址和入口地址
    do_bootm_linux() --> 启动,当然要首先设置启动参数,然后跳到入口地址  bootm.c 
          设置参数
      setup_start_tag (bd)
      setup_serial_tag (&params)
      setup_revision_tag (&params)
      setup_memory_tags (bd) --> 这个是在start_armboot 的dram_init() 里设定好的。
      setup_commandline_tag (bd, commandline)  --> commandline 就是bootargs 
      if (images->rd_start && images->rd_end)
        setup_initrd_tag (bd, images->rd_start, images->rd_end)
      setup_videolfb_tag ((gd_t *) gd)
      setup_end_tag (bd)
      启动内核
      theKernel (0, machid, bd->bi_boot_params); --> 启动kernel后一去不复返了。
 }