“第一课--OpenWRT uboot介绍”的版本间的差异
(→microWRT Uboot 启动分析) |
(→microWRT Uboot 启动分析) |
||
第167行: | 第167行: | ||
只有前者返回值小于0时,我们才需要执行后者。 | 只有前者返回值小于0时,我们才需要执行后者。 | ||
− | 5. 最后进入main_loop() | + | 5. 最后进入main_loop()。在这个函数中,主要是通过bootm 命令来启动kernel的。具体内容如下: |
− | + | ||
main_loop() | main_loop() | ||
{ | { | ||
第193行: | 第193行: | ||
setup_videolfb_tag ((gd_t *) gd) | setup_videolfb_tag ((gd_t *) gd) | ||
setup_end_tag (bd) | setup_end_tag (bd) | ||
− | |||
启动内核 | 启动内核 | ||
theKernel (0, machid, bd->bi_boot_params); --> 启动kernel后一去不复返了。 | theKernel (0, machid, bd->bi_boot_params); --> 启动kernel后一去不复返了。 |
2015年3月17日 (二) 08:43的最新版本
关于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 = .; }
首先从_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 (¶ms) setup_revision_tag (¶ms) 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后一去不复返了。 }
|