基于mips架构的uboot 启动流程 (2)

来源:互联网 发布:自动化选型软件 编辑:程序博客网 时间:2024/05/20 16:40
 

对于计算机系统来说,从开机上电到操作系统启动需要一个引导过程,这个引导程序就叫作 Bootloader 。

 

Bootloader 是在操作系统运行之前执行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射表,从而建立适当的系统软硬件环境,为最终调用操作系统内核做好准备。

 

对于嵌入式系统, Bootloader 是基于特定硬件平台来实现的。因此,几乎不可能为所有的嵌入式系统建立一个通用的 Bootloader ,不同的处理器架构都有不同的 Bootloader 。 Bootloader 不但依赖于 CPU 的体系结构,而且依赖于嵌入式系统板级设备的配置。对于 2 块不同的嵌入式板而言,即使它们使用同一种处理器,要想让运行在一块板子上的 Bootloader 程序也能运行在另一块板子上,一般也都需要修改 Bootloader 的源程序。

 

反过来,大部分 Bootloader 仍然具有很多共性,某些 Bootloader 也能够支持多种体系结构的嵌入式系统。例如, U-Boot 就同时支持 PowerPC 、 ARM 、 MIPS 和 X86 等体系结构,支持的板子有上百种。通常,它们都能够自动从存储介质上启动,都能够引导操作系统启动,并且大部分都可以支持串口和以太网接口。

 

 

二、         U-Boot 工程简介 (ZZ) :

最早, DENX 软件工程中心的 Wolfgang Denk 基于 8xxrom 的源码创建了 PPCBOOT(PowerPC Boot) 工程,并且不断添加处理器的支持。后来, Sysgo Gmbh 把 ppcboot 移植到 ARM 平台上,创建了 ARMboot 工程。然后以 ppcboot 工程和 armboot 工程为基础,创建了 U-Boot 工程。

 

现在 U-Boot 已经能够支持 PowerPC 、 ARM 、 X86 、 MIPS 体系结构的上百种开发板,已经成为功能最多、灵活性最强并且开发最积极的开放源码 Bootloader。目前仍然由 DENX 的 Wolfgang Denk 维护。

 

U-Boot 的源码包可以从 sourceforge 网站下载,还可以订阅该网站活跃的 U-Boot Users 邮件论坛,这个邮件论坛对于 U-Boot 的开发和使用都很有帮助。

 

U-Boot 软件包下载网站: http://sourceforge.net/project/u-boot 。

U-Boot 邮件列表网站: http://lists.sourceforge.net/lists/listinfo/u-boot-users/ 。

DENX 相关的网站: http://www.denx.de/re/DPLG.html 。

 

 

三、         U-Boot 源码结构 (ZZ) :

从网站上下载得到 U-Boot 源码包,解压就可以得到全部 U-Boot 源程序。在顶层目录下有 18 个子目录,分别存放和管理不同的源程序。

 

这些目录中所要存放的文件有其规则,可以分为 3 类:

第 1 类目录与处理器体系结构或者开发板硬件直接相关;

第 2 类目录是一些通用的函数或者驱动程序;

第 3 类目录是 U-Boot 的应用程序、工具或者文档。

U-Boot 的源代码包含对几十种处理器、数百种开发板的支持。可是对于特定的开发板,配置编译过程只需要其中部分程序。

 

 

四、         U-Boot 的移植 (ZZ) :

U-Boot 能够支持多种体系结构的处理器,支持的开发板也越来越多。因为 Bootloader 是完全依赖硬件平台的,所以在新电路板上需要移植 U-Boot 程序。

 

开始移植 U-Boot 之前,先要熟悉硬件电路板和处理器。确认 U-Boot 是否已经支持新开发板的处理器和 I/O 设备。假如 U-Boot 已经支持一块非常相似的电路板,那么移植的过程将非常简单。

 

移植 U-Boot 工作就是添加开发板硬件相关的文件、配置选项,然后配置编译。

 

开始移植之前,需要先分析一下 U-Boot 已经支持的开发板,比较出硬件配置最接近的开发板。选择的原则是,首先处理器相同,其次处理器体系结构相同,然后是以太网接口等外围接口。还要验证一下这个参考开发板的 U-Boot ,至少能够配置编译通过。

 

移植 U-Boot 的基本步骤如下:

( 1 )在顶层 Makefile 中为开发板添加新的配置选项。

( 2 )创建一个新目录存放开发板相关的代码,并且添加文件。

( 3 )为开发板添加新的配置文件。如果是为一颗新的 CPU 移植,还要创建一个新的目录存放 CPU 相关的代码。

( 4 )配置开发板 (make XXXX_config) 。

( 5 )编译 U-Boot(make) 。执行 make 命令,编译成功可以得到 U-Boot 映像。 U-Boot 编译成功可以得到下表中的文件:

表 U-Boot 编译生成的映像文件

文 件 名 称

说     明

文 件 名 称

说     明

System.map

U-Boot 映像的符号表

u-boot.bin

U-Boot 映像原始的二进制格式

u-boot

U-Boot 映像的 ELF 格式

u-boot.srec

U-Boot 映像的 S-Record 格式

U-Boot 的 3 种映像格式都可以烧写到 Flash 中,但需要看加载器能否识别这些格式。一般 u-boot.bin 最为常用,直接按照二进制格式下载,并且按照绝对地址烧写到 Flash 中就可以了。 U-Boot 和 u-boot.srec 格式映像都自带定位信息。

 

( 6 )添加驱动或者功能选项。在能够编译通过的基础上,还要实现 U-Boot 的以太网接口、 Flash 擦写等功能。

( 7 )调试 U-Boot 源代码,直到 U-Boot 在开发板上能够正常启动。

 

 

 

五、         U-Boot 代码分析 (by MulinB)( 以某 Demo 板 Bootloader 代码工程为例, CPU 是 MIPS 架构的某多核 CPU) :

 

 

1)         史前时代:汇编在 FLASH 中运行的日子(汇编指令参见《 See MIPS Run 》一书):

U-Boot 的开始执行始于用汇编语言编写的 CPU 依赖的程序,程序是从 cpu/mips/start.S 文件中的 _start 代码段开始执行的。由于此时 DRAM 未初始化,所以程序是从存储 U-Boot 程序的 FLASH 中开始运行的。下面就从 _start 开始代码之旅。

 

/***************************************************************************************/

程序一开始就出现了一大片令人迷惑的代码:

_start:

        RVECENT(reset,0)        /* U-boot entry point */

        RVECENT(reset,1)        /* software reboot */

        RVECENT(romReserved,3)

        RVECENT(romReserved,4)

        ……

/* Reserve extra space so that when we use the boot bus local memory

        ** segment to remap the debug exception vector we don't overwrite

        ** anything useful */

               ……

 

而宏 RVECENT 的定义为:

#define RVECENT(f,n) /

   b f; nop

可见该指令只是一个简单的跳转指令 b Label 。

 

而 romReserved 代码为:

romReserved:

        b romReserved

        nop

               ……

可见是没有意义的死循环代码。

 

再结合注释,原来程序开始的一大片令人迷惑的代码的作用如下:

_start:

        RVECENT(reset,0)        /* U-boot entry point */  /*U-Boot 开始执行的代码起始地址 */

        RVECENT(reset,1)        /* software reboot */    /* 软重启时 U-Boot 开始执行的起始地址 */

        RVECENT(romReserved,3)  /* 保留本代码所在的地址,重新映射调试异常向量时可以使用该空间 */

        RVECENT(romReserved,4)  /* 同上…… */

               ……

 

 

/***************************************************************************************/

接着 reset 段的代码往下看:

首先是一些 COP0 的状态寄存器的设置:讲 COP0_STATUS_REG 寄存器的 5-7 三个 bit 置 1 。结合 CPU 手册可以看到三个 bit 的含义。

然后是调试模式下的 GPIO 初始化,然后是检查是否使用 FAILSAFE 模式加载 BootLoader ,接着才真正开始 CPU 初始化。

 

当看到一段注释时:

/* Check what core we are - if core 0, branch to init tlb

    ** loop in flash.  Otherwise, look up address of init tlb

    ** loop that was saved in the boot vector block.

    */

可以发现下面这段对每个 core 的 TLB ( Translation Lookaside Buffer ,详见 wikipedia )进行初始化时,是对 core0 与其他 cores 的 TLB 初始化有区别的:如果是 core0 ,由于 DRAM 没有初始化,代码只能继续在 FLASH 中执行;而如果是其他 cores ,则可以直接调转到 DRAM 中相应的这段代码的地址进行 TLB 初始化。

 

接着下面的代码可以连续看到两个 Label :

.globl InitTLBStart

InitTLBStart:

InitTLBStart_local:

第一个 Label 是为了将下面的代码拷贝到 DRAM 后可以直接在 C 语言中用函数的方式调用,第二个 Label 是为了 core0 中执行 TLB 初始化时跳转。

从下面的注释中可以证实这一点:

/* This code run on all cores - core 0 from flash,

    ** the rest from DRAM.  When booting from PCI, non-zero cores

    ** come directly here from the boot vector - no earlier code in this

    ** file is executed.

    */

/* Some generic initialization is done here as well, as we need this done on

    ** all cores even when booting from PCI

    */

对 TLB 初始化的代码中使用了很多 mfc0 与 mtc0 指令,可见是对一些 COP0 的寄存器的读写。

接着往下又是一些 COP0 的状态寄存器的设置,设置 scratch memory 等。

 

 

/***************************************************************************************/

再往下可以看到一段注释:

/* Check if we are core 0, if we are not then we need

        ** to vector to code in DRAM to do application setup, and

        ** skip the rest of the bootloader.  Only core 0 runs the bootloader

        ** and sets up the tables that the other cores will use for configuration

        */

可见以下的代码执行在不同的 core 上开始出现不同: core0 继续往下执行汇编代码;而如果是其余 cores ,则从内存中找到 BOOT_VECTOR_BASE 地址,直接跳入内存执行应用程序的初始化。

 

假设当前仍是 core0 ,继续往下看。看到注释:

/* If we don't have working memory yet configure a bunch of

              ** scratch memory, and set the stack pointer to the top

        ** of it.  This allows us to go to C code without having

              ** memory set up

        */

可见如果内存还没有初始化,这里首先初始化一块临时内存作为栈空间,这使得程序可以在内存初始化之前用来调用 C 程序。

 

 

/***************************************************************************************/

再往下是:

/* Initialize GOT pointer.

        ** Global symbols can't be resolved before this is done, and as such we can't

        ** use any global symbols in this code.  We use the bal/ move xxx,ra combination to access

        ** data in a PC relative manner to avoid this.  This code will correctly set the

        ** gp regardless of whether the code has already been relocated or not.

        ** This code determines the current gp by computing the link time (gp - pc)

        ** and adding this to the current pc.

        ** runtime_gp = runtime_pc + (linktime_gp - linktime_pc)

        ** U-boot is running from the address it is linked at at this time, so this

        ** general case code is not strictly necessary here.

        */

其中, GOT=Global Offset Table , GP = GOT pointer , PC=Program counter 。可见程序开始为调用其他汇编文件中定义的函数或 C 程序中定义的函数进行准备而建立符号表指针( GOT pointer )。

 

 

/***************************************************************************************/

初始化完 GOT pointer 后,接着往下就可以调用其他汇编文件中定义的函数(或代码段),可以看到初始化内存、缓存的代码:

/* Initialize any external memory. */

        jal     memsetup  /*memsetup 是定义在 board/tb0229/ 文件夹下的 memsetup.S 中的代码段 */

        nop

/* Initialize caches... */

        sync

        cache 0, 0($0)

        sync

        cache 9, 0($0)

        sync

        jal     mips_cache_reset  /*mips_cache_reset 也是定义在其他文件中的代码段 */

        nop

/* ... and enable them. */

        li      t0, CONF_CM_CACHABLE_NONCOHERENT

        mtc0    t0, CP0_CONFIG

/* Set up temporary stack. */

        li      a0, CFG_INIT_SP_OFFSET

        jal     mips_cache_lock   /*mips_cache_lock 同样是定义在其他文件中的代码段 */

        nop

这段代码主要是调用依赖某个板子的对 memory 进行参数设置、对 cache 进行初始化的代码,借以完成对某个板子的内存、缓存初始化。

 

 

/***************************************************************************************/

接着再往下可以看到代码:

        la   t9, board_init_f  /* doesn't return... */  /*board_init_f 是定义在 lib_mips/board.c 中的 C 函数 */

        j    t9

        nop

这里开始转到 board_init_f 代码段开始执行程序, board_init_f 实质上是 C 语言中定义的函数,虽然后面的代码仍在 flash 中存放,但是已经可以使用一部分 scratch memory 作为临时栈空间进行函数调用,可以用 C 语言进行批量初始化了,纯汇编的时代暂时告一段落。

 

 

 

2)         石器时代: FLASH 中的 C 代码在临时栈空间中活跃:

这部分的代码的使命是致力于建立一个“正常”的 C 运行环境,主要是内存的初始化及整个寻址空间的部分初始化。而这部分代码本身所运行的环境受到较多限制,只有一个大小受限的 scratch memory 作为临时运行的栈空间。

 

 

/***************************************************************************************/

board_init_f() 函数一开始出现一个宏, DECLARE_GLOBAL_DATA_PTR ,查看该宏的定义 (include/asm-mips/Global_data.h) :

#define DECLARE_GLOBAL_DATA_PTR     register volatile gd_t *gd asm ("k0")

 

结合注释可以了解到,这些关于系统信息的结构体 (GD 是指 Global Data, BD 是指 Board info Data) 应该存放于在 DRAM 控制器未初始化之前就能使用的空间中,比如锁定的缓存中。在这里我们可以暂时把它放在已经初始化好的临时栈空间 scratch memory 中。

GD 和 BD 是很重要的结构体,后面当 DRAM 初始化完成后,会将其拷贝入 DRAM 空间保存。

 

 

/***************************************************************************************/

接着往下是循环调用 init_sequence 函数指针数组中的成员,来依次调用数组列表中的函数进行初始化。

init_sequence 的定义如下(将部分预编译指令去掉后的代码):

init_fnc_t *  init_sequence[] = {

    octeon_boot_bus_init,

       timer_init,

       env_init,         /* initialize environment */

    early_board_init,

       init_baudrate,         /* initialze baudrate settings */

       serial_init,             /* serial communications setup */

       console_init_f,

       display_banner,             /* say that we are here */

init_dram,

    dram_test,

    init_func_ram,

       NULL,

};

 

 

/***************************************************************************************/

从调用完 init_sequence 中的函数后往下看:

/*

        * Now that we have DRAM mapped and working, we can

        * relocate the code and continue running from DRAM.

        */

#if defined(CONFIG_NO_RELOCATION) && defined(CONFIG_RAM_RESIDENT)

        /* If loaded into ram, we can skip relocation , and run from the spot we were loaded into */

       addr = CFG_MONITOR_BASE;

       u_boot_mem_top = CFG_SDRAM_BASE + MIN(gd->ram_size, (1*1024*1024));

#else

    /* Locate at top of first Megabyte */

       addr = CFG_SDRAM_BASE + MIN(gd->ram_size, (1*1024*1024));

       u_boot_mem_top = addr;

 

其中 CFG_SDRAM_BASE = 0x8000 0000 ,是 MIPS 虚拟寻址空间中 kseg0 段的起始地址(参考《 See MIPS Run 》),它经过 CPU TLB 翻译后是 DRAM 内存的起始物理地址。

这里显然是将 u_boot_mem_top 和 addr 指向了 DRAM 中 1M 地址处 ( 从 DRAM 起始地址到 1M 地址处的 1M 空间是为 U-boot 自己运行分配的 ) , 即从 0x8000 0000 到 0x8010 0000 的 1M 空间是 U-boot 自己活跃的天下了。

 

 

/***************************************************************************************/

现在 U-boot 有了对自己来说很富裕的 1M 字节可以自由分配的 DRAM 空间了,下面很大一段代码都是对这 1M 内存的划分和分配。

 

这部分代码精简后,加入详细注释如下:

/* We can reserve some RAM "on top" here. */  // 从 0x8010 0000 处开始向下划分势力范围

/* round down to next 4 kB limit. */

       addr &= ~(4096 - 1);  //addr &= ~0x0FFF 这种计算是常用的地址对齐的算法,这里是向下 4K 字节对齐

/* Reserve memory for U-Boot code, data & bss*/

       addr -= MAX(len, (512*1024));  // 为 code, data, bss 段保留 512K 的空间

/* round down to next 64k (allow us to create image at same addr for debugging)*/

       addr &= ~(64 * 1024 - 1);  // 向下 64K 字节对齐

/* Reserve memory for malloc() arena. */

       addr_sp = addr - TOTAL_MALLOC_LEN;  // 划分 malloc() 使用的空间,即所谓的堆空间,大小有宏来确定

/* (permanently) allocate a Board Info struct and a permanent copy of the "global" data*/

       addr_sp -= sizeof(bd_t);  // 分配 BD 结构体大小的空间

       bd = (bd_t *)addr_sp;

       gd->bd = bd;  //GD 中的指针关联到此处的 BD 结构体地址

       addr_sp -= sizeof(gd_t);  // 分配 GD 结构体大小的空间

       id = (gd_t *)addr_sp;  //id 指针指向 GD 结构体地址

/* Reserve memory for boot params. */

       addr_sp -= CFG_BOOTPARAMS_LEN;  // 分配 boot param 的空间,这里的宏大小是 128K 字节

       bd->bi_boot_params = addr_sp;  // 在 BD 中记录此 boot param 空间的地址

/* Finally, we set up a new (bigger) stack.  Leave some safety gap for SP, force alignment on 16 byte boundary */

       addr_sp -= 16;  // 向下一帧,保证栈空间的开始没有和之前分配的空间冲突

       addr_sp &= ~0xF;  // 栈空间 16 字节对齐

#define STACK_SIZE  (16*1024UL)

    bd->bi_uboot_ram_addr = (addr_sp - STACK_SIZE) & ~(STACK_SIZE - 1);  // 将栈地址 16K 对齐后记录入 BD

    bd->bi_uboot_ram_used_size = u_boot_mem_top - bd->bi_uboot_ram_addr;  // 在 BD 中记录使用的 DRAM 大小

/* Save local variables to board info struct */

       bd->bi_memstart    = CFG_SDRAM_BASE;       /* start of  DRAM memory */  //0x80000000

       bd->bi_memsize     = gd->ram_size;            /* size  of  DRAM memory in bytes */

       bd->bi_baudrate     = gd->baudrate;            /* Console Baudrate */

       memcpy (id, (void *)gd, sizeof (gd_t));  // 将在临时栈空间 scratch memory 中的 GD 数据拷贝入 DRAM 中,至此, BD 和 GD 都已经存在于 DRAM 中了。

 

根据这部分代码可以画出 U-boot 这 1M 空间的示意图:

 

 

/***************************************************************************************/

1M 空间瓜分完毕后,出现一条语句:

relocate_code (addr_sp, id, addr);

该语句使程序回到 cpu/mips/start.S 的汇编中,在之后的汇编中, U-boot 将自己的代码段、数据段、 BSS 段等搬到在 DRAM 中新家,为以后跨入速度时代而过渡。

 

 

 

3)         青铜时代:短暂的回归 cpu/mips/start.S :

 

 

 

/***************************************************************************************/

重新回到汇编的天下,找到代码:

.globl  relocate_code

        .ent    relocate_code

relocate_code:

下面的代码就是搬家了。

 

直到出现代码:

move    a0, a1

la       t9, board_init_r  /* doesn't return, runs main_loop() */

j        t9

程序搬家基本完成,后面的程序就可以全部在内存 DRAM 中执行了,速度会比之前在 FLASH 和 scratch memory 中运行的速度快上很多。这里跳入的代码段 board_init_r 是在 C 程序中定义的函数,仍然在刚才的那个 C 语言文件 lib_mips/board.c 中。

 

 

 

4)         白银时代:终于有正常的 C 环境接着进行初始化了:

 

 

 

/***************************************************************************************/

进入 board_init_r 函数之前,有一段让人振奋的注释:

/* This is the next part if the initialization sequence: we are now

 * running from RAM and have a "normal" C environment, i. e. global

 * data can be written, BSS has been cleared, the stack size in not

 * that critical any more, etc.

 */

然后注意到该函数有两个传入的参数,参数是之前汇编中用 a0 寄存器传入的,在这里可以看出这两个参数的含义:

id: 之前在 U-boot 的 1M 空间中分配的 GD 结构体的地址

dest_addr: U-boot 重新定位到 DRAM 之后的代码起始地址

 

程序接着向下是将 id 的值用 k0 寄存器保存,将 GD 结构体中的一些字段进行设置,包括记录 U-boot 自身的代码在内存中的偏移地址等。

 

 

/***************************************************************************************/

接着是重新计算命令表 (cmd table) 的地址。什么是命令表?因为 U-boot 启动完成后可以进入命令行模式,这时候用户可以从串口输入命令来指示 U-boot 下一步做什么,每个命令对应的名称、用法、描述、执行的函数等信息,用一个命令表结构体保存,这样每一个命令在内存中有对应的一个命令表。结构体的定义在 include/Command.h 中,定义如下:

struct cmd_tbl_s {

       char        *name;           /* Command Name               */

       int          maxargs;     /* maximum number of arguments       */

       int          repeatable;      /* autorepeat allowed?          */

       int          (*cmd)(struct cmd_tbl_s *, int, int, char *[]);   /* Implementation function    */

       char        *usage;           /* Usage message   (short)     */

       char        *help;            /* Help  message   (long)     */

} __attribute__ ((aligned (8)));

而这里给命令表重新计算地址其实只是将从 __u_boot_cmd_start 到 __u_boot_cmd_end 之间的每个命令表中的成员指针的地址加上 U-boot 在 DRAM 中的偏移地址,这样获得命令表在 DRAM 中的地址。看来转换之前的命令表中的地址应该是相对地址( ?! )。

注: 这里,注意到 __attribute((XXX)) 比较奇特的语法,其实这个是 GCC 对 C 语言的扩充, GCC 允许声明函数、变量和类型的特殊属性,以便手工的代码优化和更仔细的代码检查。要指定一个声明的属性,在声明后写

    __attribute__ (( ATTRIBUTE ))

其中 ATTRIBUTE 是属性说明,多个属性以逗号分隔。 GNU C 支持十几个属性,如 noreturn, unused, aligned 等。

 

 

/***************************************************************************************/

然后是初始化 malloc() 堆空间:

mem_malloc_init();

其实是将全局变量 mem_malloc_start 和 mem_malloc_end 和 mem_malloc_brk 三个指针指向之前分配好的堆空间。

 

然后是重定位或者初始化环境变量的指针:

env_relocate();

将 env_ptr 指针及其指向的地址初始化,用来存放环境变量结构体,然后将 flash 中的环境变量拷贝到内存中。

 

然后是其余设备的初始化 devices_init() ,这是在前面的堆空间 (malloc) 、环境变量、 PCI 总线初始化后的基础之上才能进行的,这里的设备包括:

i2c_init ();

drv_lcd_init ();

drv_video_init ();

drv_keyboard_init ();

drv_logbuff_init ();

drv_system_init ();

drv_usbtty_init ();

...

 

然后是将标准的输入输出 std* 变量由通过串口改为通过 pci_console_active 进行输入输出。

然后是 jump table 的初始化,这里似乎是将一些函数指针记录进 GD 结构体。

然后是 console 初始化。

然后是再次打印 board/chip info ,这里打印是为了自检和板子确认。

然后是确认 loadaddr 和 bootfile 环境变量的有效性。

然后是 miscellaneous platform dependent 的初始化,函数 misc_init_r () 。

然后是网卡的初始化, eth_initialize() 。

然后是 IDE 的检测和初始化, ide_init() 。

然后是 debugger 的设置。

最后是一个空函数 late_board_init() ,用来添加比较晚的初始化代码。

下面就进入了一个死循环,循环调用 main_loop() 函数,这意味着 U-boot 基本启动完毕,进入命令行模式。

 

 

 

5)         钻石时代:专题篇

 

 

下面是一些个人学习的专题。记录备忘。

 

/***************************************************************************************/

u MIPS CPU 地址空间简介 ( 整理自《 See MIPS Run 》和 CPU 文档 ) :       

注:首先需要明确的是 CPU 物理地址空间 不仅仅包括 RAM 物理内存的空间,还包括 CPU 内部的一些总线、寄存器的编址。

 

一个 MIPS CPU 可以运行在两种优先级别上,用户态和核心态。 MIPS CPU 从核心态到用户态的变化并不是 CPU 工作不一样,而是对于有些操作认为是非法的。在用户态,任何一个程序地址的首位是 1 的话,这个地址是非法的,对其存取将会导致异常处理。另外,在用户态下,一些特殊的指令将会导致 CPU 进入异常状态。

 

 

在 32 位 CPU 下,程序地址空间 划分为 4 个大区域。每个区域有一个传统的名字。对于在这些区域的地址,各自有不同的属性:

 

 

kuseg: 虚拟空间 0x0000 0000 - 0x7FFF FFFF ( 低端 2G) :这些地址是用户态 可用的地址。在有 MMU 的机器里,这些地址将一概被 MMU 作转换 ,除非 MMU 的设置被建立好,否则这 2G 地址是不可用的。对于没有 MMU 的机器,存取这 2G 地址的方法依具体机器相关,你的 CPU 具体厂商提供的手册将会告诉你关于这方面的信息。如果想要你的代码在有或没有 MMU 的 MIPS 处理器之间有兼容性,尽量避免 这块区域的存取。

 

kseg0: 虚拟空间 0x8000 0000 - 0x9FFF FFFF(512M): 这些地址映射到物理地址简单的通过把最高位清零,然后把它们映射到物理地址低段 512M(0x0000 0000 - 0x1FFF FFFF) 。因为这种映射是很简单的,通常称之为“非转换的” 地址区域。几乎全部的对这段地址的存取都会通过快速缓存 (cache) 。因此在 cache 设置好之前,不能随便使用这段地址。通常一个没有 MMU 的系统会使用这段地址作为其绝大多数程序和数据的存放位置。对于有 MMU 的系统,操作系统核心会存放在这个区域。

 

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

 

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

 

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

 

在核心态下 (CPU 启动时 ) , CPU 可以作任何事情。在用户态下, 2G 之上的地址空间是非法的,任何存取将会导致系统异常处理。注意的是,如果一个 CPU 有 MMU ,这意味着所有的用户地址在真正访问到物理地址之前必须经过 MMU 的转换 , 从而使得 OS 可以防止用户程序随便乱用。对於一个没有内存映射的 OS , MIPS CPU 的用户态其实是多余的。在核心态下, CPU 可以存取低段地址空间,这个存取也是通过 MMU 的转换。

 

 

下面来谈谈 MIPS64 CPU 的虚拟地址空间。

64 位 CPU 的地址空间的最低 2G 和最高 2G 区域是和 32 位情况下一样的, 64 位扩展的地址部分在这两者之间。 64 位下那些大块的不需要 MMU 转换的窗口可以克服 kseg0 和 kseg1 512M 的局限,但是 32 位下我们可以通过对 MMU 编程来同样达到这一点。

 

/***************************************************************************************/

u MIPS CPU 内存管理与 TLB( 整理自《 See MIPS Run 》 ) :       

早期的 MIPS CPU 定位于支持运行在 UNIX 工作站与服务器上的应用程序,因此内存管理硬件被构想为一个最小化的能帮助 BSD UNIX ——一个经过完善设计并拥有充分多虚拟存储需求的操作系统的典型——提供内存管理功能的硬件。我们将从 MIPS 的设计起点开始,面对着一个 unix 类型的操作系统以及它的虚存系统的众多需求。我们将会展示一下 MIPS 的硬件是如何满足这些需求的。结尾时,我们会讨论一下在不能像通常一样使用内存管理硬件的嵌入式系统中,您可以采取的几种使用方式。

 

UNIX 内存管理工作的本质是为了能运行众多不同的任务(即 multitasking —— 多进程),并且每个任务各自拥有自己的内存空间。如果这个工作圆满完成,那么各任务的命运将彼此独立开来(操作系统自身也因此得以保护):一个任务自身崩溃或者错误的做某些事不会影响整个系统。显然,对一个使用分布终端来运行学生们程序的大学而言,这是一个很有用的特性;然而不仅如此,甚至是要求最严格的 商业系统环境也需要能够在运行的同时支持实验软件或原型软件一并进行调试和测试。

 

MMU 并不仅仅为了建立巨大而完备的虚拟存储系统,小的嵌入式程序也能在重定位和更有效的内存分配里受益。如果能把应用程序观念上的地址映射到任何可获得的物理地址,系统在不同时刻运行不同程序就会更加容易。

 

嵌入式应用中常常会明确的运用多进程机制,但几乎没有多少嵌入式操作系统使用隔离的地址空间。或许这归咎于这种机制在嵌入式 CPU 以及它们上面的操作系统上用处不大并且带来不稳定性,因而显得不那么重要。

 

 

原创粉丝点击