一、 BootLoader 簡介 (ZZ) :
對於計算機系統來說,從開機上電到操作系統啓動需要一個引導過程,這個引導程序就叫作 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 以及它們上面的操作系統上用處不大並且帶來不穩定性,因而顯得不那麼重要。