在Linux中可以將一部分內存mount為分區來使用,通常稱之為RamDisk,分為:
Ramdisk, ramfs, tmpfs。
① 第一種就是傳統意義上的,可以格式化,然后加載。這在Linux內核2.0/2.2就已經支持,其不足之處是大小固定,之后不能改變。為了能夠使用 Ramdisk,我們在編譯內核時須將block device中的Ramdisk支持選上,它下面還有兩個選項,一個是設定Ramdisk的大小,默認是4096k;另一個是initrd的支持。
如果對Ramdisk的支持已經編譯進內核,我們就可以使用它了:首先查看一下可用的RamDisk,使用 ls /dev/ram*;首先創建一個目錄,比如test,運行 mkdir /mnt/test;然后對/dev/ram0 創建文件系統,運行 mke2fs /dev/ram0;最后掛載/dev/ram0,運行mount /dev/ram /mnt/test,就可以象對普通硬盤一樣對它進行操作了。
② 另兩種則是內核2.4才支持的,通過Ramfs或者Tmpfs來實現:它們不需經過格式化,用起來靈活,其大小隨所需要的空間而增加或減少。
Ramfs顧名思義是內存文件系統,它處于虛擬文件系統(VFS)層,而不像ramdisk那樣基于虛擬在內存中的其他文件系統(ex2fs)。因而,它無需格式化,可以創建多個,只要內存足夠,在創建時可以指定其最大能使用的內存大小。
如果你的Linux已經將Ramfs編譯進內核,你就可以很容易地使用Ramfs了。創建一個目錄,加載Ramfs到該目錄即可:
# mkdir /testRam # mount -t ramfs none /testRAM缺省情況下,Ramfs被限制最多可使用內存大小的一半。可以通過maxsize(以kbyte為單位)選項來改變。
# mount -t ramfs none /testRAM -o maxsize=2000 (創建了一個限定最大使用內存為2M的ramdisk)③ Tmpfs是一個虛擬內存文件系統,它不同于傳統的用塊設備形式來實現的Ramdisk,也不同于針對物理內存的Ramfs。
Tmpfs 可以使用物理內存,也可以使用交換分區。在Linux內核中,虛擬內存資源由物理內存(RAM)和交換分區組成,這些資源是由內核中的虛擬內存子系統來負 責分配和管理。Tmpfs向虛擬內存子系統請求頁來存儲文件,它同Linux的其它請求頁的部分一樣,不知道分配給自己的頁是在內存中還是在交換分區中。同Ramfs一樣,其大小也不是固定的,而是隨著所需要的空間而動態的增減。
使用tmpfs,首先你編譯內核時得選擇“虛擬內存文件系統支持(Virtual memory filesystem support)”。然后就可以加載tmpfs文件系統了:
# mkdir -p /mnt/tmpfs
# mount tmpfs /mnt/tmpfs -t tmpfs
同樣可以在加載時指定tmpfs文件系統大小的最大限制:
# mount tmpfs /mnt/tmpfs -t tmpfs -o size=32m
FAT: bogus logical sector size 21072
具體的文件系統FAT格式。虛擬邏輯扇區大小為20K,linux-2.4.22/fs/fat/Inode.c。
在初始化MS-DOS文件系統時,讀MS-DOS文件系統的superblock,函數fat_read_super中輸出的上面的信息。
UMSDOS: msdos_read_super failed, mount aborted.
UMSDOS:一種文件系統,特點容量大 但相對而言不大穩定。是Linux 使用的擴展了的DOS文件系統。它在 DOS 文件系統下增加了長文件名、 UID/GID、POSIX 權限和特殊文件 (設備、命名管道等)功能,而不犧牲對 DOS 的兼容性。允許一個普通的msdos文件系統用于Linux,而且無須為它建立單獨的分區,特別適合早期的硬盤空間不足的硬件條件。
VFS: Mounted root (romfs filesystem) readonly
虛擬文件系統VFS(Virtual Filesystem Switch)的輸出信息。
再 次強調一下一個概念。VFS 是一種軟件機制,也可稱它為 Linux 的文件系統管理者,它是用來管理實際文件系統的掛載點,目的是為了能支持多種文件系統。kernel會先在內存中建立一顆 VFS 目錄樹,是內存中的一個數據對象,然后在其下掛載rootfs文件系統,還可以掛載其他類型的文件系統到某個子目錄上。
Mounted devfs on /dev
加載devfs設備管理文件系統到dev安裝點上。/dev是我們經常會用到的一個目錄。在2.4的kernel中才有使用到。每次啟動時內核會自動掛載devfs。
devfs 提供了訪問內核設備的命名空間。它并不是建立或更改設備節點,devfs只是為你的特別文件系統進行維護。一般我們可以手工mknod創件設備節點。 /dev目錄最初是空的,里面特定的文件是在系統啟動時、或是加載模組后驅動程序載入時建立的。當模組和驅動程序卸載時,文件就消失了。
Freeing init memory: 72K
釋放1號用戶進程init所占用的內存
*************************************************************
第三節:加載linux內核完畢,轉入cpu_idle進程
系統啟動過程中進程情況:
① init進程
一 般來說, 系統在跑完 kernel bootstrapping 內核引導自舉后(被裝入內存、已經開始運行、已經初始化了所有的設備驅動程序和數據結構等等), 就去運行 init『萬process之父』, 有了它, 才能開始跑其他的進程,因此,init進程,它是內核啟動的第一個用戶級進程,它的進程號總是1。你可以用進程查看命令來驗證:
# ps aux
PID Uid VmSize Stat Command
1 0 SW init
2 0 SW [keventd]
3 0 SWN [ksoftirqd_CPU0]
4 0 SW [kswapd]
5 0 SW [bdflush]
6 0 SW [kupdated]
7 0 SW [rbwdg]
9 0 SW [mtdblockd]
10 0 SW [khubd]
80 0 SW [loop0]
另外 Linux 有兩個 kernel 類的 process 也開始跑了起來,一個是 kflushd/bdflush,另一個是 kswapd。只有這個init 是完全屬于 user 類的進程, 后兩者是 kernel假借 process 進程之名掛在進程上。
init 有許多很重要的任務,比如象啟動getty(用于用戶登錄)、實現運行級別、以及處理孤立進程。init 一開始就去讀 /etc/inittab (init初始化表),初始化表是按一定格式排列的關于進程運行時的有關信息的。init程序需要讀取/etc/inittab文件作為其行為指針。這個 inittab 中對于各個runlevel運行級別要跑哪些 rc 或 spawn 生出什么有很清楚的設定。
一 般, 在Linux中初始化腳本在/etc/inittab 文件(或稱初始化表)中可以找到關于不同運行級別的描述。inittab是以行為單位的描述性(非執行性)文本,每一個指令行都是固定格式。 inittab中有respawn項,但如果一個命令運行時失敗了,為了避免重運行的頻率太高,init將追蹤一個命令重運行了多少次,并且如果重運行的 頻率太高,它將被延時五分鐘后再運行。
② kernel進程
A》 請注意init是1號進程,其他進程id分別是kflushd/ bdflush, kupdate, kpiod and kswapd。這里有一個要指出的:你會注意到虛擬占用(SIZE)和實際占用(RSS)列都是0,進程怎么會不使用內存呢?
這些進程就是內核守護進程。大部分內核并不顯示在進程列表里。守護進程在init之后啟動,所以他們和其他進程一樣有進程ID,但是他們的代碼和數據都存放在內核占有的內存中。在列表中使用中括號來區別與其他進程。
B》 輸入和輸出是通過內存中的緩沖來完成的,這讓事情變得更快,程序的寫入會存放在內存緩沖中,然后再一起寫入硬盤。守護進程kflushd和kupdate 管理這些工作。kupdate間斷的工作(每5秒)來檢查是否有寫過的緩沖,如過有,就讓kflushd把它們寫入磁盤。
C》 進程有時候無事可做,當它運行時也不一定需要把其所有的代碼和數據都放在內存中。這就意味著我們可以通過把運行中程序不用的內容切換到交換分區來更好的是利用內存。把這些進程數據移入/移出內存通過進程IO管理守護進程kpiod和交換守護進程kswapd,大約每隔1秒,kswapd醒來并檢查內存情 況。如果在硬盤的東西要讀入內存,或者內存可用空間不足,kpiod就會被調用來做移入/移出操作。
D》 bdflush - BUF_DIRTY, 將dirty緩存寫回到磁盤的核心守護進程。對于有許多臟的緩沖區(包含必須同時寫到磁盤的數據的緩沖區)的系統提供了動態的響應。它在系統啟動的時候作為一個核心線程啟動,它叫自己為 “kflushd”,而這是你用ps顯示系統中的進程的時候你會看得的名字。即定期(5秒)將臟(dirty)緩沖區的內容寫入磁盤,以騰出內存;
E》 ksoftirqd_CPUx 是一個死循環, 負責處理軟中斷的。它是用來對軟中斷隊列進行緩沖處理的進程。當發生軟中斷時,系統并不急于處理,只是將相應的cpu的中斷狀態結構中的active 的相應的位,置位,并將相應的處理函數掛到相應的隊列,然后等待調度時機來臨,再來處理。
ksoftirqd_CPUx是由 cpu_raise_softirq() 即cpu觸發中斷,喚醒的內核線程,這涉及到軟中斷,ksoftirqd的代碼參見[kernel/softirq.c]。
F》 keventd,它的任務就是執行 scheduler 調度器隊列中的任務,keventd 為它運行的任務提供了可預期的進程上下文。
G》 khubd, 是用來檢測USB hub設備的,當usb有動態插拔時,將交由此內核進程來處理。在檢測到有hub事件時會有相應的動作(usb_hub_events())
H》 mtdblockd是用來對flash塊設備進行寫操作的守護進程。NAND類型的Flash需要MTD(Memory Technology Devices 內存技術驅動程序)驅動的支持才能被linux所使用。NAND的特點是不能在芯片內執行(XIP,eXecute In Place),需要把代碼讀到系統RAM中再執行,傳輸效率不是最高,最大擦寫次數量為一百萬次,但寫入和擦除的速度很快,擦除單元小,是高數據存儲密度 的最佳選擇。NAND需要I/O接口,因此使用時需要驅動程序。
I》 loop0 是負責處理loop塊設備的(回環設備)。loopback device指的就是拿文件來模擬塊設備, 在我們這里,loop設備主要用來處理需要mount到板上的文件系統,類似mount /tmp/rootfs /mnt -o loop。。我們的實例有:mount -o loop -t cramfs /xxx.bin /xxx 也就是將xxx.bin這個文件mount到板上來模擬cramfs壓縮ram文件系統。loop0進程負責對loop設備進行操作。
loopback設備和其他的塊設備的使用方法相同。特別的是,可以在該設備上建立一個文件系統,然后利用mount命令把該系統映射到某個目錄下以便訪問。這種整個建立在一個普通磁盤文件上的文件系統,就是虛擬文件系統 (virtual file system)。
總結
上面的內容是本人為了在實際開發中更加清楚地了解uclinux的啟動過程而做的一個總結性的文章。在對uclinux的啟動過程做了一個詳細注釋后,大家 會對涉及到嵌入系統的各個概念有了一個更加明確的認識,并能對嵌入系統的軟硬件環境的有關設置更加清楚。當你自己動手結合linux源代碼來分析時,將會有一個清楚的全局觀。
=============================================================
1. 運行bootloader初始化程序
SRAM 、SDRAM等存儲設備屬于揮發性的存儲器,掉電以后其中的內容就會全部丟失,所以必須把操作系統的內核鏡像存放在Flash等不揮發性存儲介質上。但是操作系統在運行時,需要動態的創建一些如數據段、堆棧、頁表(針對使用虛擬地址的操作系統)等內容,所以需要在RAM中運行操作系統。
因此,就需要一個引導程序把操作系統的內核鏡像從Flash存儲器拷貝到RAM中,然后再從RAM中執行操作系統的內核。Bootloader就是可以完成這樣一種功能的程序。
從本質上來講,bootloader不屬于操作系統內核。它采用匯編語言編寫,因此針對不同的CPU體系結構,這一部分代碼不具有可移植性。在移植操作系統時,這部分代碼必須加以改寫
具體來講,bootloader在系統啟動時主要完成以下幾項工作:
(1) 將操作系統內核從Flash拷貝到SDRAM中,如果是壓縮格式的內核,還要將之解壓縮。
(2) 改寫系統的memory map,原先flash起始地址映射為0地址,這時需要將RAM的起始地址映射為0。
(3) 設置堆棧指針并將bss段清零。
將來執行C語言程序和調用子函數時要用到
(4) 改變pc值,使得CPU開始執行真正的操作系統內核。
2. 運行操作系統內核
bootloader程序執行完上述的各項工作后,通過一條跳轉指令,轉而執行init目錄下C語言源文件main.c中的函數start_kernel()。
因為在此之前bootloader已經創建好一個初始化環境,C函數可以開始執行了。
整個操作系統內核的初始化工作從這里才算是真正開始。這個函數的長度比較短,代碼如下:
void __init start_kernel(void)
{
char * command_line;
unsigned long mempages;
extern char saved_command_line[];
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
lock_kernel();
printk(linux_banner);
setup_arch(&command_line);
printk(“Kernel command line: %s/n”, saved_command_line);
parse_options(command_line);
trap_init();
init_IRQ();
sched_init();
softirq_init();
time_init();
/*
* HACK ALERT! This is early. We‘re enabling the console before
* we’ve done PCI setups etc, and console_init() must be aware of
* this. But we do want output early, in case something goes wrong.
*/
console_init();
#ifdef CONFIG_MODULES
init_modules();
#endif
if (prof_shift) {
unsigned int size;
/* only text is profiled */
prof_len = (unsigned long) &_etext - (unsigned long) &_stext;
prof_len 》》= prof_shift;
size = prof_len * sizeof(unsigned int) + PAGE_SIZE-1;
prof_buffer = (unsigned int *) alloc_bootmem(size);
}
kmem_cache_init();
sti();
calibrate_delay();
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && !initrd_below_start_ok && initrd_start 《 min_low_pfn 《《 PAGE_SHIFT)
{
printk(KERN_CRIT “initrd overwritten (0x%08lx 《 0x%08lx) - ”
“disabling it./n”,initrd_start,min_low_pfn 《《 PAGE_SHIFT);
initrd_start = 0;
}
#endif
mem_init();
kmem_cache_sizes_init();
pgtable_cache_init();
mempages = num_physpages;
fork_init(mempages);
proc_caches_init();
vfs_caches_init(mempages);
buffer_init(mempages);
page_cache_init(mempages);
#if defined(CONFIG_ARCH_S390)
ccwcache_init();
#endif
signals_init();
#ifdef CONFIG_PROC_FS
proc_root_init();
#endif
#if defined(CONFIG_SYSVIPC)
ipc_init();
#endif
check_bugs();
printk(“POSIX conformance testing by UNIFIX/n”);
/*
* We count on the initial thread going ok
Like idlers init is an unlocked kernel thread,which will
* make syscalls (and thus be locked)。
*/
smp_init();
rest_init();
}
內核啟動之后需要執行的第一個函數是start_kernel()(在linux/init/main.c文件中)。
start_kernel()
完成下面一系列初始化的工作。
◆printk(1inux_banner),顯示Linux內核的版本信息。
◆ setup_arch(&command_line),做與體系結構相關的初始化工作。
◆ parse_options(command_line),解釋系統參數。
◆ trap_init(),設置系統異常的入口點。
◆ init_IRQ(),初始化系統中斷服務。
◆ sched_init(),系統調度器的初始化。
◆ time_init(),時鐘、定時器初始化。
◆ softirq_init(),系統軟中斷的初始化。
◆console_init(),控制臺初始化。
◆kmem_cache_init(),內核cache的初始化。
◆calibrate_delay(),校準時鐘。
◆mem_init(),內存初始化。
◆kmem_cache_sizes_init(),創建及設置通用cache。
◆fork_init(mempages),建立uidcache,并且根據系統內存大小來確定最大進程數目。
◆buffer_init(mempages),塊設備緩沖區的初始化。初始化一系列的cache。
◆check_bugs(),檢查體系結構漏洞。
◆kernel_thread(init NULL,CLONE_FS | CLONE_FILES | CLONE_SIGNAL),創建第一個核心進程,啟動init進程。
◆cpu_idle(),運行idle進程
接下去做的工作由init()函數來完成。init()首先要鎖定內核,然后調用do_basic_setup( )來完成外部設備以及驅動程序的初始化。外設的初始化要根據內核的配置來決定,一般需要做下面的初始化工作:
◆PCI總線初始化。
◆網絡初始化。
◆一系列其他設備的初始化。
◆start_context_thread()創建事件管理核心進程keventd。
◆通過do_initcalls()函數來啟動任何使用__initcall標識的函數。
◆文件系統初始化。
◆加載文件系統。
在do_basic_setup()調用完成之后,init()會釋放初始化函數所用的內存,并且打開/dev/console設備重新定向控制臺,讓系統調用execve來執行程序init。
到這里為止,Linux 內核的初始化工作已經完成。然后開始用戶態進程的初始化。
=============================================================
uClinux中內存模塊的啟動初始化
arch/armnommu/kernel/entry_armv.S是一個匯編文件,他包含了一個kernel_entry的定義,這是整個內核的進入點。在完成某些平臺相關的初始化工作之后,執行流程跳轉到start_kerne()處。從這里開始考察uClinux的內存模塊啟動初始化是如何實現的。
start_kernel()中與內存模塊相關的函數調用流程如下:
setup_arch() paging_init() free_area_init() mem_init()
下面分別分析這些函數各自的功能以及uClinux對他們的改造。
(1) setup_arch()
setup_arch()首先根據目前內核所配置的平臺向某些特定地址寫入特殊字符序列,以完成對特定硬件的初始化,比如工作狀態發光二極管、錯誤和報警發光二極管。
存儲了可用物理內存起始地址的變量memory_start的初始化是通過一個ld腳本中定義的變量_end進行的。ld腳本是用來控制GNU ld連接器在連接內核各個目標文件部分的時候的配置動作,比如這樣一個腳本:
SECTION
{
。=0x10000;
.text:{*(.text)}
。=0x8000000;
.data:{*(.data)}
.bss :{*(.bss)}
}
用來配置ld連接目標文件的時候將所有的目標文件中的存儲程序正文的.text段(section)連接到一起,并且映射到輸出文件的地址0x10000處,將所有目標文件中已初始化的數據.data段連接到一起并放置到輸出可執行文件的0x8000000地址處,而所有目標文件中還未初始化的數據段.bss連接起來后影射到輸出文件中緊跟在.data段之后的位置。
這個ld配置腳本文件對每個平臺都是不同的。如為MICETEK上所使用的uClinux版本使用的ld配置文件為arch/armnomm/vmLinux.lds。可以通過修改某個平臺上的ld腳本配置文件中的_end變量來達到配置其可用物理內存起始地址的目的。
setup_arch()在完成對memory_start變量的初始化之后,通過某些特定手段檢測不同類型的內存分布情況。比如為檢測某段地址范圍是否為RAM的方法是通過將某個地址的數據讀出來,將它加1后寫回內存地址中,然后再讀出來和原始數據比較看看其值是否成功增加了1,這樣反復操作兩次,最后將數據恢復。如果是可讀可寫的RAM,那么這個測試的結果就是每次比較都是成功的,否則就不能將這個地址當作RAM。
在setup_arch()中還可能根據所用平臺進行對flash memory和ROM的測試。在這些平臺相關的工作完成之后,setup_arch()將對系統運行的第一個進程init_task的mm_struct結構中描述地址空間分布的變量start_code,end_code,end_data和brk進行初始化,start_code為0,其他三個數值分別為來自于ld腳本配置文件中定義的相關變量_etext、_edata和_end。
此后setup_arch()將根據Linux中為系統中的第一塊rom/flash memory card所分配的固定的主/從設備號(可以從Document/devices.txt中得到)來創建根文件系統的設備號,并存儲在后來將要用到的全局變量ROOT_DEV中。
setup_arch()最后完成對系統啟動參數的保存。
在調用setup_arch()返回之后,start_kernel()中得到了系統可用物理內存的起始和結束地址,以及命令啟動時的命令行參數。
(2) paging_init()
在Linux中,paging_init()的一項主要功能是建立頁目錄和頁表,而且將Linux移植到不同平臺的過程中非常重要的一個步驟就是修改這個函數來適應新的硬件平臺的虛擬內存體系。但是由于在uClinux中不再使用虛擬內存機制,也就不再需要維護頁目錄和頁表數據結構了,所以paging_init()在這里只是為系統啟動的時候保留一部分特殊用途的內存區間。它返回后,從可以使用的內存空間開始,依次是如下的數據結構:
empty_bad_page_table 占用1頁(4KB)
empty_bad_page 占用1頁(4KB)
empty_zero_page 占用1頁,并初始化為全0
mem_map
bitmap
paging_init()函數在返回前通過調用free_area_init(start_mem,end_mem)進行建立buddy system的映射位圖關系,以及建立空閑物理頁面鏈表的操作。
(3)free_area_init()
這個函數用于建立管理物理頁幀的數據結構mem_map,有多少物理頁幀就有多少mem_map_t類型的結構體與之相對應。每個頁面的mem_map_t結構中的flags被標明為PG_DMA和PG_reserved,并且頁幀號被賦給相應的數值。同時建立了管理空閑頁面的bitmap映射表,并且所有的位都被清零。
(3) mem_init()
mem_init()函數遍歷整個可用物理內存地址空間,將每個頁面相對應的struct page結構中flags的PG_reserved 標志位清除,標志用戶個數的count計數器置1,并同時統計可用物理頁面數量,然后打印系統的各個內存參數,如可用RAM和ROM的大小、內核代碼段和數據段大小等。
======================================================
摘 要:本文采用三星公司的S3C44B0微處理器,對uClinux操作系統內核的引導過程進行了剖析。
關鍵字:S3C44B0X;uClinux;嵌入式系統;內核引導
1 前言
伴隨著微電子的發展,用于嵌入式設備的處理器速度越來越快,功能也越來越強大。三星公司生產的S3C44B0微處理器,采用的是ARM7TDMI內核。該內核因為有著功耗小、成本低等特點,因此非常適合作為移動手持終端的處理器核心。Linux操作系統因為它的開放性,使得它不斷的被應用到各個領域。在嵌入式領域同樣也出現了各種各樣的Linux變體,最常用的是uClinux。也正是因為uClinux操作系統支持不帶MMU單元的ARM處理器,因此該系統可以對S3C44B0微處理器有很好的支持。
在嵌入式系統開發中,第一個部分便是系統的引導。而系統的引導過程是通過BootLoader來完成的。BootLoader程序是與硬件緊密相關的一段代碼,而且編寫的時候比較復雜,它主要的功能是初始化微處理器以及周邊的硬件資源,并且引導操作系統的啟動。下面我將以S3C44B0微處理器來作為例子,對uClinux操作系統內核的引導過程進行一個剖析。
2 BootLoader程序概念
簡單的說Boot Lodaer就是在操作系統內核運行之前運行的一段小程序,通過這段小程序,可以初始化硬件設備、建立系統的內存空間映射圖,從而將系統的軟硬件環境設置成一個適合的狀態,以便為最終調用操作系統內核準備好正確的環境。最終,BootLoader把操作系統內核映象加載到RAM中,并將系統控制權傳遞給它。
2.1 典型的BootLoader程序框架
操作系統角度來說,Boot Loader的總目標就是正確的調用內核來執行。
由于Boot Loader的實現依賴于CPU的體系結構,因此大多數Boot Loader都分為Stage1和Stage2兩大部分。依賴于CPU體系結構的代碼,例如設備初始化代碼等,通常都放在Stage1中,而且通常都用匯編語言來實現,以達到短小精悍的目的。而Stage2通常用C語言來實現,這樣可以實現更加復雜的功能,而且代碼會具有更好的可讀性和可移植性。
Boot Loader的Stage1通常包括如下步驟:
1) 硬件設備初始化
2) 為加載Boot Loader的Stage2準備RAM空間
3) 復制Boot Loader的Stage2到RAM空間中
4) 設置好堆棧
5) 跳轉到Stage2的C入口點
Boot Loader的Stage2通常包括如下步驟:
1) 始化本階段要使用的硬件設備
2) 檢測系統內存映射(Memory Map)
3) 將Kernel映象和根文件系統映象從FLASH上讀取到RAM空間中
4) 為內核設置啟動參數
5) 調用內核
2.2 系統內存組織
由于嵌入式設備具有很好的制定性,因此通常硬件環境會變的千差萬別。就算是用戶使用了相同的處理器芯片,但是也很有可能因為外圍設備電路設計的不同,而存在差異。對于BootLoader程序來說,存儲設備的與處理器的連接方式,與其息息相關。對于我們采用的S3C44B0微處理器來說,在系統加電之后,指令指針是指向0x00000000的,也就是說系統是從0x00000000開始之行。正是因為這個原因,通常這個地址空間我們會安排給FLASH存儲器。這樣我們可以將BootLoader啟動代碼以及我們之后將會要啟動的uClinux操作系統映像燒寫到Flash里。對于RAM地址空間,S3C44B0芯片將其設定為從0x0C000000到0x0FFFFFFF一共64MB的范圍里。我們可以通過設定存儲器控制寄存器來重新設定RAM的大小。例如我們試驗采用的存儲設備安排如下:
0x00000000 – 0x003FFFFF 4MB Flash
0x0C000000 – 0x0C7FFFFF 8MB RAM
通常來說對于系統的引導和操作系統的啟動,可以完全都在Flash中進行,但是Flash存儲器的速度相對于RAM來說會慢很多,因此出于速度上的考慮,我們通常會將啟動代碼和uClinux操作系統的內核映像文件拷貝到RAM中之行。
下面我將對典型的BootLoader程序框架進行分析。
2.3 Stage1階段
該階段的主要工作是完成對系統中斷向量的設置,初始化微處理器內部寄存器,初始化堆棧,初始化RAM地址空間,并且將Stage2部分的C代碼拷貝到RAM空間的指定地點,然后跳轉到C代碼入口點繼續執行。對于這段代碼來說,做的都是一些準備工作,因此為了提高效率,這段代碼通常都是使用匯編語言來完成的。下面我將結合具體的代碼來分析一下Stage1的啟動過程。
1)設置中斷向量
設置S3C44B0處理器定義的8種系統中斷的中斷向量地址。這八種系統中斷分別是復位中斷、未定義指令中斷、軟件中斷、指令預取異常中斷、數據異常中斷、地址異常中斷、IRQ中斷和FIQ中斷。這8個中斷通常是通過無條件跳轉的方式來實現的。具體的代碼如下。
__entry :
b ResetHandler /* Reset vector */
b HandlerUndef /* Undefined instruction */
b HandlerSWI /* SWI */
b HandlerPabort /* Prefetch abort */
b HandlerDabort /* Data abort */
b 。 /* Address exception */
b HandlerIRQ /* IRQ */
b HandlerFIQ /* FIQ */
2)初始化微處理器內部寄存器
這段代碼主要是要完成硬件部分的初始化,包括關閉中斷響應、初始化微處理器通用端口、設置CPU頻率等操作。不過需要注意的是,在進行硬件初始化之前需要將微處理器的運行狀態轉換到SVC模式下。
MRS a1,CPSR /*; Pickup current CPSR*/
BIC a1,a1,#MODE_MASK /*; Clear the mode bits*/
ORR a1,a1,#SUP_MODE /*; Set the supervisor mode bits*/
ORR a1,a1,#LOCKOUT /*; Insure IRQ and FIQ intr are locked out*/
MSR CPSR_cxsf,a1 /*; Setup the new CPSR*/
3)初始化系統RAM空間
這個部分的工作主要是為之后啟動代碼和內核映像的拷貝操作做準備,并且也為之后的C代碼的執行初始化堆棧。這部分的工作主要可以分成兩個部分來處理。首先,根據系統配置的存儲器特性來初始化相關的存儲器控制寄存器。在我們使用的S3C44B0處理器中,存儲空間被分成了BANK0-BANK7一共8個塊,分別由BANKCON0-BANKCON7控制各個塊存儲器的讀寫時鐘和片選時鐘等信號參數。具體代碼如下:
ldr r0,=rBANKCON0
ldr r1,=0x700
str r1,[r0]
ldr r0,=rBANKCON1
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON2
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON3
ldr r1,=0x7568
str r1,[r0]
ldr r0,=rBANKCON4
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON5
ldr r1,=0x700 /* 0x7ffc */
str r1,[r0]
ldr r0,=rBANKCON6
ldr r1,=0x18008
str r1,[r0]
ldr r0,=rBANKCON7
ldr r1,=0x18000
str r1,[r0]
ldr r0,=rREFRESH
ldr r1,=0xac03e1
str r1,[r0]
ldr r0,=rBANKSIZE
ldr r1,=0x16
str r1,[r0]
ldr r0,=rMRSRB6
ldr r1,=0x020
str r1,[r0]
ldr r0,=rMRSRB7
ldr r1,=0x020
str r1,[r0]
初始化RAM空間的第二個部分就是初始化連接腳本文件中指定的需要清0的地址空間,將該斷地址空間的內容清0。該部分地址空間主要是用來存放C語言代碼中的全局變量等內容的。實現代碼如下:
LDR a1,=Image_ZI_Base /* Pickup the start of the BSS area */
MOV a3,#0 /* Clear value in a3 */
LDR a2,=Image_ZI_Limit /* Pickup the end of the BSS area */
CMP a1,a2
BEQ move_data
clear_loop :
STR a3,[a1],#4 /* Clear a word, a1 += 4 */
CMP a1,a2 /* end of ZI ? */
BNE clear_loop
4)為Stage2的C語言代碼的執行準備必要的堆棧
因為在Stage2階段一般都是采用C語言代碼來完成的,因此必須在使用C語言代碼之前先建立起必要的堆棧信息。通常為了避免堆棧數據被執行代碼破壞,通常都是放在RAM的高端地址,并且使得堆棧指針的增長方向是向下增長的。
5)將初始化代碼拷貝到RAM中,并且跳轉到RAM中執行。因為在我們采用的S3C44B0微處理器里對于FLASH和RAM地址空間是使用的統一編址的,因此我們可以直接使用一個簡單循環來完成拷貝。
ldr r3, =0x10000 /* 64K Bytes */
ldr r2, =0xc700000
ldr r1, =0
next :
ldr r0,[r1],#4
str r0,[r2],#4
cmp r1,r3
bne next
6)跳轉到C代碼執行(即Stage2階段)
這個過程是直接給指令指針賦值于跳轉的C代碼的入口地址,在我們的試驗中該入口地址是Main。
LDR pc,=Main
2.4 Stage2階段
該階段的代碼主要使用C語言來實現的。該階段的工作主要是建立開發板與宿主機之間的通信,加載uClinux內核映像文件和配置內核啟動參數,并且啟動內核。
嵌入式設備與宿主機的通訊方式有多種,最常用的是使用串口方式進行數據交換。本試驗采用的S3C44B0微處理器提供了兩個UART口,因此我們可以任選其中一個來初始化并且使用它來與宿主機交互。對于串口的初始化主要是波特率、奇偶校驗、停止位、數據位等內容。
對于串口的波特率和波特因子的計算采用如下公式
Iubrd =((int(mclk/16 / baud + 0.5) – 1)
mclk是頻率、baud為波特率
2.4.1 檢測內存
該部分的功能主要是檢測系統在進行硬件初始化的時候是否發生了內存映射錯誤,即是否物理地址是否被映射到不存在的地址空間。通常是使用讀寫方式來檢測的,即以內存頁為單位,在每個頁頭進行讀寫操作,比較讀寫結果。因為S3C44B0處理器并不支持內存映射,因此我們在Stage2過程中并沒有包含該部分功能函數。
2.4.2 加載uClinux內核映像
該過程其實只是一個從Flash的指定位置(該位置是uClinux燒寫的起始地址)拷貝到RAM中指定的地址空間里。在拷貝之前必須要為uClinux的全局變量結構,即啟動參數、內核頁表、RAM的頁目錄等信息預留一定的空間。如果我們將FLASH和RAM看成連在一起的線性地址,則系統的空間分配會如下圖:
。..。..
Boot
初始化代碼
uClinux
未用
中斷向量表
初始化映像代碼
啟動參數
內核映像
未用
堆棧
2.4.3 配置內核啟動參數
我們采用的uClinux是2.4.x內核版本,該版本的內核支持參數啟動過程。在嵌入式系統中,啟動參數的傳入主要是依靠bootloader程序向標記列表(tagged list)的相關域中填寫相應的值來完成的。
2.5 uClinux內核引導
當我們初始化完畢uClinux的啟動參數之后,控制權就可以交給uClinux內核了,uClinux系統調用內核解壓函數(decompress_kernel)來對上一個階段拷貝的uClinux內核在RAM空間里進行解壓(當然如果系統內核在建立的時候沒有配置成壓縮格式,則解壓過程略去)。在解壓完畢后,跳轉到內核調用函數(call_kernel),該函數實際上執行的是start_kernel(),這個函數包含了有關處理器初始化、中斷初始化、進程初始化等操作。最后,將控制權完全的交與uClinux操作系統來執行。
偽處理過程如下:
IF(啟動參數正確)
CALL decmporess_kernel()
CALL call_kernel()
ELSE
啟動失敗
decompress_kernel()
{
解壓內核映像
}
call_kernel()
{
。..
start_kernel()
。..。
}
3 總結
本文是對S3C44B0的啟動過程進行了一次分析,啟動部分的代碼可以說是嵌入式設備開發比較重要的部分。而且該部分的處理工作往往又比較麻煩,因此在這里我只是想起到拋磚引玉的作用。因為成文時間比較倉促,難免有錯誤,請大家批評指正。
==========================================================
《ARM7 uClinux開發實驗與實踐》P130
Bootloader完成系統初始化工作后,將運行控制權交給uClinux內核。根據內核是否壓縮以及內核是否在本地執行,uClinux通常有以下兩種可選的啟動方式:
(1)Flash本地運行方式。內核中未經壓縮的可執行映像固化在Flash中,系統啟動時,內核在Flash中開始逐句執行。
(2)壓縮內核加載方式。內核的壓縮映像固化在Flash上,系統啟動時,由附加在壓縮映像前的解壓復制程序讀取壓縮映像,并在內存中解壓后執行。這種方式相對復雜,但是運行速度更快。
首先介紹內核的Flash本地運行方式。
本地運行時,內核的啟動包括特定體系結構設置和uClinux系統初始化兩步,內核啟動的入口文件是head-armv.s。
評論
查看更多