arm 嵌入式LINUX启动过程(1)[转]

一位大师级的人物写的,不看要后悔的哟!!
                             LINUX启动过程



首先,porting linux的时候要规划内存影像,如小弟的系统有64m SDRAM, 
地址从0x 0800 0000 -0x0bff ffff,32m flash,地址从0x0c00 0000-0x0dff ffff. 
规划如下:bootloader, linux kernel, rootdisk放在flash里。 
具体从 0x0c00 0000开始的第一个1M放bootloader, 
0x0c10 0000开始的2m放linux kernel,从 0x0c30 0000开始都给rootdisk。 



启动: 
首先,启动后arm920T将地址0x0c00 0000映射到0(可通过跳线设置), 
实际上从0x0c00 0000启动,进入我们的bootloader,但由于flash速度慢, 
所以bootloader前面有一小段程序把bootloader拷贝到SDRAM 中的0x0AFE0100, 
再从0x 0800 0000 运行bootloader,我们叫这段小程序为flashloader, 
flashloader必须要首先初始化SDRAM,不然往那放那些东东: 





.equ SOURCE, 0x0C000100 bootloader的存放地址 
.equ TARGET, 0x0AFE0100 目标地址 
.equ SDCTL0, 0x221000 SDRAM控制器寄存器 
// size is stored in location 0x0C0000FC 


.global _start 
_start: //入口点 


//;*************************************** 
//;* Init SDRAM 
//;*************************************** 



// *************** 
// * SDRAM 
// *************** 


LDR r1, =SDCTL0 // 


//  Set Precharge Command 
LDR r3, =0x92120200 
//ldr r3,=0x92120251 
STR r3, [r1] 


//  Issue Precharge All Commad 
LDR r3, =0x8200000 
LDR r2, [r3] 


//  Set AutoRefresh Command 
LDR r3, =0xA2120200 
STR r3, [r1] 


//  Issue AutoRefresh Command 
LDR r3, =0x8000000 
LDR r2, [r3] 
LDR r2, [r3] 
LDR r2, [r3] 
LDR r2, [r3] 
LDR r2, [r3] 
LDR r2, [r3] 
LDR r2, [r3] 
LDR r2, [r3] 


//  Set Mode Register 
LDR r3, =0xB2120200 
STR r3, [r1] 


//  Issue Mode Register Command 
LDR r3, =0x08111800 //; Mode Register value 
LDR r2, [r3] 


//  Set Normal Mode 
LDR r3, =0x82124200 
STR r3, [r1] 


//;*************************************** 
//;* End of SDRAM and SyncFlash Init * 
//;*************************************** 



// copy code from FLASH to SRAM 


_CopyCodes: 
ldr r0,=SOURCE 
ldr r1,=TARGET 
sub r3,r0,#4 
ldr r2,[r3] 


_CopyLoop: 
ldr r3,[r0] 
str r3,[r1] 
add r0,r0,#4 
add r1,r1,#4 
sub r2,r2,#4 
teq r2,#0 
beq _EndCopy 
b _CopyLoop 


_EndCopy: 
ldr r0,=TARGET 
mov pc,r0 



上回书说到flashloader把bootloader load到0x0AFE0100, 然回跳了过去, 
其实0x0AFE0100 就是烧在flash 0x0C000100中的真正的bootloader: 


bootloader 有几个文件组成,先是START.s,也是唯一的一个汇编程序,其余的都是C写成的,START.s主要初始化堆栈: 


_start: 
ldr r1,=StackInit 
ldr sp,[r1] 
b main 
//此处我们跳到了C代码的main函数,当C代码执行完后,还要调用 
//下面的JumpToKernel0x跳到LINXU kernel运行 



.equ StackInitvalue, __end_data+0x1000 // 4K __end_data在连结脚本中指定 


StackInit: 
.long StackInitvalue 


.global JumpToKernel 


JumpToKernel: 
// jump to the copy code (get the arguments right) 
mov pc, r0 


.global JumpToKernel0x 
// r0 = jump address 
// r1-r4 = arguments to use (these get shifted) 
JumpToKernel0x: 
// jump to the copy code (get the arguments right) 
mov r8, r0 
mov r0, r1 
mov r1, r2 
mov r2, r3 
mov r3, r4 
mov pc, r8 
.section ".data.boot" 
.section ".bss.boot" 


下面让我们看看bootloader的c代码干了些什么。main函数比较长,让我们分段慢慢看。 


int main() 

U32 *pSource, *pDestin, count; 
U8 countDown, bootOption; 
U32 delayCount; 
U32 fileSize, i; 
char c; 
char *pCmdLine; 
char *pMem; 


init(); //初始化FLASH控制器和CPU时钟 


EUARTinit(); //串口初始化 
EUARTputString("/n/nDBMX1 Linux Bootloader ver 0.2.0/n"); 
EUARTputString("Copyright (C) 2002 Motorola Ltd./n/n"); 
EUARTputString((U8 *)cmdLine); 
EUARTputString("/n/n"); 


EUARTputString("Press any key for alternate boot-up options ... "); 





小弟的bootloader主要干这么几件事:init(); 初始化硬件,打印一些信息和提供一些操作选项: 
0. Program bootloader image 
1. Program kernel image 
2. Program root-disk image 
3. Download kernel and boot from RAM 
4. Download kernel and boot with ver 0.1.x bootloader format 
5. Boot a ver0.1.x kernel 
6. Boot with a different command line 


也就是说,可以在bootloader里选择重新下载kernel,rootdisk并写入flash, 
下载的方法是用usb连接,10m的rootdisk也就刷的一下。关于usb下载的讨论请参看先前的贴子“为arm开发平台增加usb下载接口“。 
如果不选,直接回车,就开始把整个linux的内核拷贝到SDRAM中运行。 


列位看官,可能有人要问,在flashloader中不是已经初始化过sdram控制器了吗?怎么init(); 中还要初始化呢,各位有所不知,小弟用的是syncflash, 
可以直接使用sdram控制器的接口,切记:在flash中运行的代码是不能初始化连接flash的sdram控制器的,不然绝对死掉了。所以,当程序在flash中运行的时候,去初始化sdram,而现在在sdram中运行,可放心大胆地初始化flash了,主要是设定字宽,行列延时,因为缺省都是最大的。 


另外,如果列位看官的cpu有足够的片内ram,完全可以先把bootloader放在片内ram,干完一切后再跳到LINUX,小弟着也是不得已而为之啊。 



如果直接输入回车,进入kernel拷贝工作: 



EUARTputString("Copying kernel from Flash to RAM .../n"); 
count = 0x200000; // 2 Mbytes 
pSource = (U32 *)0x0C100000; 
pDestin = (U32 *)0x08008000; 
do 

*(pDestin++) = *(pSource++); 
count -= 4; 
} while (count > 0); 



EUARTputString("Booting kernel .../n/n"); 


这一段没有什么可说的,运行完后kernel就在0x08008000了,至于为什么要 
空出0x8000的一段,主要是放kelnel的一些全局数据结构,如内核页表,arm的页目录要有16k大。 


我们知道,linux内核启动的时候可以传入参数,如在PC上,如果使用LILO, 
当出现LILO:,我们可以输入root=/dev/hda1.或mem=128M等指定文件系统的设备或内存大小,在嵌入式系统上,参数的传入是要靠bootloader完成的, 


pMem = (char *)0x083FF000; //参数字符串的目标存放地址 
pCmdLine = (char *)&cmdLine; //定义的静态字符串 
while ((*(pMem++)=*(pCmdLine++)) != 0);//拷贝 


JumpToKernel((void *)0x8008000, 0x083FF000) //跳转到内核 


return (0); 
JumpToKernel在前文中的start.S定义过: 


JumpToKernel: 
// jump to the copy code (get the arguments right) 
mov pc, r0 


.global JumpToKernel0x 
// r0 = jump address 
// r1 = arguments to use (these get shifted) 


由于arm-GCC的c参数调用的顺序是从左到右R0开始,所以R0是KERNKEL的地址, 
r1是参数字符串的地址: 



到此为止,为linux引导做的准备工作就结束了,下一回我们就正式进入linux的代码。 



好,从本节开始,我们走过了bootloader的漫长征途,开始进入linux的内核: 
说实话,linux宝典的确高深莫测,洋人花了十几年修炼,各种内功心法层处不穷。有些地方反复推敲也领悟不了其中奥妙,炼不到第九重啊。。 


linux的入口是一段汇编代码,用于基本的硬件设置和建立临时页表,对于 
ARM LINUX是 linux/arch/arm/kernle/head-armv.S, 走! 


#if defined(CONFIG_MX1) 
mov r1, #MACH_TYPE_MX1 
#endif 


这第一句话好像就让人看不懂,好像葵花宝典开头的八个字:欲练神功。。。。 


那来的MACH_TYPE_MX1?其实,在head-armv.S 
中的一项重要工作就是设置内核的临时页表,不然mmu开起来也玩不转,但是内核怎么知道如何映射内存呢?linux的内核将映射到虚地址0xCxxx xxxx处,但他怎么知道把哪一片ram映射过去呢? 


因为不通的系统有不通的内存影像,所以,LINUX约定,内核代码开始的时候, 
R1放的是系统目标平台的代号,对于一些常见的,标准的平台,内核已经提供了支持,只要在编译的时候选中就行了,例如对X86平台,内核是从物理地址1M开始映射的。如果老兄是自己攒的平台,只好麻烦你自己写了。 


小弟拿人钱财,与人消灾,用的是摩托的MX1,只好自己写了,定义了#MACH_TYPE_MX1,当然,还要写一个描述平台的数据结构: 


MACHINE_START(MX1ADS, "Motorola MX1ADS") 
MAINTAINER("SPS Motorola") 


BOOT_MEM(0x08000000, 0x00200000, 0xf0200000) 


FIXUP(mx1ads_fixup) 
MAPIO(mx1ads_map_io) 
INITIRQ(mx1ads_init_irq) 
MACHINE_END 


看起来怪怪的,但现在大家只要知道他定义了基本的内存映象:RAM从0x08000000开始,i/o空间从0x00200000开始,i/o空间映射到虚拟地址空间 
0xf0200000开始处。摩托的芯片i/o和内存是统一编址的。 
其他的项,在下面的初始化过程中会逐个介绍到。 


好了好了,再看下面的指令: 



mov r0, #F_BIT | I_BIT | MODE_SVC @ make sure svc mode //设置为SVC模式,允许中断和快速中断 
//此处设定系统的工作状态,arm有7种状态 
//每种状态有自己的堆栈 


msr cpsr_c, r0 @ and all irqs diabled 
bl __lookup_processor_type 


//定义处理器相关信息,如value, mask, mmuflags, 
//放在proc.info段中 
//__lookup_processor_type 取得这些信息,在下面 
//__lookup_architecture_type 中用 


这一段是查询处理器的种类,大家知道arm有arm7, arm9等类型,如何区分呢? 
在arm协处理器中有一个只读寄存器,存放处理器相关信息。__lookup_processor_type将返回如下的结构: 


__arm920_proc_inf 
.long 0x41009200 //CPU id 
.long 0xff00fff0 //cpu mask 
.long 0x00000c1e @ mmuflags 
b __arm920_setup 
.long cpu_arch_name 
.long cpu_elf_name 
.long HWCAP_SWP | HWCAP_HALF | HWCAP_26BIT 
.long cpu_arm920_info 
.long arm920_processor_functions 



第一项是CPU id,将与协处理器中读出的id作比较,其余的都是与处理器相关的 
信息,到下面初始化的过程中自然会用到。。 


查询到了处理器类型和系统的内存映像后就要进入初始化过程中比较关键的一步了,开始设置mmu,但首先要设置一个临时的内核页表,映射4m的内存,这在初始化过程中是足够了: 


//r5=0800 0000 ram起始地址 r6=0020 0000 io地址,r7=f020 0000 虚io 
teq r7, #0 @ invalid architecture? 
moveq r0, #'a' @ yes, error 'a' 
beq __error 
bl __create_page_tables 


其中__create_page_tables为: 
__create_page_tables: 
pgtbl r4 
//r4=0800 4000 临时页表的起始地址 
//r5=0800 0000, ram的起始地址 
//r6=0020 0000, i/o寄存器空间的起始地址 
//r7=0000 3c08 
//r8=0000 0c1e 


//the page table in 0800 4000 is just temp base page, when init_task's sweaper_page_dir ready, 
// the temp page will be useless 
// the high 12 bit of virtual address is base table index, so we need 4kx4 = 16k temp base page, 


mov r0, r4 
mov r3, #0 
add r2, r0, #0x4000 @ 16k of page table 
1: str r3, [r0], #4 @ Clear page table 
str r3, [r0], #4 
str r3, [r0], #4 
str r3, [r0], #4 
teq r0, r2 
bne 1b 
/* 
* Create identity mapping for first MB of kernel. 
* This is marked cacheable and bufferable. 

* The identity mapping will be removed by 
*/ 


// 由于linux编译的地址是0xC0008000,load的地址是0x08008000,我们需要将虚地址0xC0008000映射到0800800一段 
//同时,由于部分代码也要直接访问0x08008000,所以0x08008000对应的表项也要填充 
// 页表中的表象为section,AP=11表示任何模式下可访问,domain为0。 
add r3, r8, r5 @ mmuflags + start of RAM 
//r3=0800 0c1e 
add r0, r4, r5, lsr #18 
//r0=0800 4200 
str r3, [r0] @ identity mapping 
//*0800 4200 = 0800 0c1e 0x200表象 对应的是0800 0000 的1m 
/* 
* Now setup the pagetables for our kernel direct 
* mapped region. We round TEXTADDR down to the 
* nearest megabyte boundary. 
*/ 
//下面是映射4M 


add r0, r4, #(TEXTADDR & 0xfff00000) >> 18 @ start of kernel 
//r0 = r4+ 0x3000 = 0800 4000 + 3000 = 0800 7000 
str r3, [r0], #4 @ PAGE_OFFSET + 0MB 
//*0800 7004 = 0800 0c1e 
add r3, r3, #1 << 20 
//r3=0810 0c1e 
str r3, [r0], #4 @ PAGE_OFFSET + 1MB 
//*0800 7008 = 0810 0c1e 
add r3, r3, #1 << 20 
str r3, [r0], #4 
//*0800 700c = 0820 0c1e @ PAGE_OFFSET + 2MB 
add r3, r3, #1 << 20 
str r3, [r0], #4 @ PAGE_OFFSET + 3MB 
//*0800 7010 = 0830 0c1e 





bic r8, r8, #0x0c @ turn off cacheable 
//r8=0000 0c12 @ and bufferable bits 
mov pc, lr //子程序返回。 
下一回就要开始打开mmu的操作了 


上回书讲到已经设置好了内核的页表,然后要跳转到__arm920_setup, 
这个函数在arch/arm/mm/proc-arm929.s 


__arm920_setup: 
mov r0, #0 
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4 
mcr p15, 0, r0, c7, c10, 4@ drain write buffer on v4 
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4 
mcr p15, 0, r4, c2, c0 @ load page table pointer 
mov r0, #0x1f @ Domains 0, 1 = client 
mcr p15, 0, r0, c3, c0 @ load domain access register 
mrc p15, 0, r0, c1, c0 @ get control register v4 
/* 
* Clear out 'unwanted' bits (then put them in if we need them) 
*/ 
@ VI ZFRS BLDP WCAM 
bic r0, r0, #0x0e00 
bic r0, r0, #0x0002 
bic r0, r0, #0x000c 
bic r0, r0, #0x1000 @ ...0 000. .... 000. 
/* 
* Turn on what we want 
*/ 
orr r0, r0, #0x0031 
orr r0, r0, #0x2100 @ ..1. ...1 ..11 ...1 


#ifdef CONFIG_CPU_ARM920_D_CACHE_ON 
orr r0, r0, #0x0004 @ .... .... .... .1.. 
#endif 
#ifdef CONFIG_CPU_ARM920_I_CACHE_ON 
orr r0, r0, #0x1000 @ ...1 .... .... .... 
#endif 
mov pc, lr 


这一段首先关闭i,d cache,清除write buffer ,然后设置页目录地址,设置 
domain的保护,在上节中,注意到页目录项的domain都是0,domain寄存器中 
的domain 0 对应的是0b11,表示访问模式为manager,不受限制。 


接下来设置控制寄存器,打开d,i cache和mmu 
注意arm的d cache必须和mmu一起打开,而i cache可以单独打开 


其实,cache和mmu的关系实在是紧密,每一个页表项都有标志标示是否是 
cacheable的,可以说本来就是设计一起使用的 


最后,自函数返回后,有一句 
mcr p15, 0, r0, c1, c0 
使设置生效。 



上回我们讲到arm靠初始化完成了,打开了cache, 
到此为止,汇编部分的初始化代码就差不多了,最后还有几件事情做: 


1。初始化BSS段,全部清零,BSS是全局变量区域。 
2。保存与系统相关的信息:如 
.long SYMBOL_NAME(compat) 
.long SYMBOL_NAME(__bss_start) 
.long SYMBOL_NAME(_end) 
.long SYMBOL_NAME(processor_id) 
.long SYMBOL_NAME(__machine_arch_type) 
.long SYMBOL_NAME(cr_alignment) 
.long SYMBOL_NAME(init_task_union)+8192 
不用讲,大家一看就明白意思 


3。重新设置堆栈指针,指向init_task的堆栈。init_task是系统的第一个任务,init_task的堆栈在task structure的后8K,我们后面会看到。 


4。最后就要跳到C代码的start_kernel。 
b SYMBOL_NAME(start_kernel) 


现在让我们来回忆一下目前的系统状态: 
临时页表已经建立,在0X08004000处,映射了4M,虚地址0XC000000被映射到0X08000000. 
CACHE,MMU都已经打开。 
堆栈用的是任务init_task的堆栈。

你可能感兴趣的:(arm 嵌入式LINUX启动过程(1)[转])