新闻中心

EEPW首页>嵌入式系统>设计应用> arm 嵌入式LINUX启动过程(1)

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

作者: 时间:2016-11-09 来源:网络 收藏
一位大师级的人物写的,不看要后悔的哟!!
LINUX启动过程

首先,portinglinux的时候要规划内存影像,如小弟的系统有64mSDRAM,
地址从0x08000000-0x0bffffff,32mflash,地址从0x0c000000-0x0dffffff.
规划如下:bootloader,linuxkernel,rootdisk放在flash里。
具体从0x0c000000开始的第一个1M放bootloader,
0x0c100000开始的2m放linuxkernel,从0x0c300000开始都给rootdisk。

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

.equSOURCE,0x0C000100bootloader的存放地址
.equTARGET,0x0AFE0100目标地址
.equSDCTL0,0x221000SDRAM控制器寄存器
//sizeisstoredinlocation0x0C0000FC

.global_start
_start://入口点

//;*
//;*InitSDRAM
//;*

//*
//*SDRAM
//*

LDRr1,=SDCTL0//

//SetPrechargeCommand
LDRr3,=0x92120200
//ldrr3,=0x92120251
STRr3,[r1]

//IssuePrechargeAllCommad
LDRr3,=0x8200000
LDRr2,[r3]

//SetAutoRefreshCommand
LDRr3,=0xA2120200
STRr3,[r1]

//IssueAutoRefreshCommand
LDRr3,=0x8000000
LDRr2,[r3]
LDRr2,[r3]
LDRr2,[r3]
LDRr2,[r3]
LDRr2,[r3]
LDRr2,[r3]
LDRr2,[r3]
LDRr2,[r3]

//SetModeRegister
LDRr3,=0xB2120200
STRr3,[r1]

//IssueModeRegisterCommand
LDRr3,=0x08111800//;ModeRegistervalue
LDRr2,[r3]

//SetNormalMode
LDRr3,=0x82124200
STRr3,[r1]

//;*
//;*EndofSDRAMandSyncFlashInit*
//;*

//copycodefromFLASHtoSRAM

_CopyCodes:
ldrr0,=SOURCE
ldrr1,=TARGET
subr3,r0,#4
ldrr2,[r3]

_CopyLoop:
ldrr3,[r0]
strr3,[r1]
addr0,r0,#4
addr1,r1,#4
subr2,r2,#4
teqr2,#0
beq_EndCopy
b_CopyLoop

_EndCopy:
ldrr0,=TARGET
movpc,r0

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

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

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

.equStackInitvalue,__end_data+0x1000//4K__end_data在连结脚本中指定

StackInit:
.longStackInitvalue

.globalJumpToKernel

JumpToKernel:
//jumptothecopycode(gettheargumentsright)
movpc,r0

.globalJumpToKernel0x
//r0=jumpaddress
//r1-r4=argumentstouse(thesegetshifted)
JumpToKernel0x:
//jumptothecopycode(gettheargumentsright)
movr8,r0
movr0,r1
movr1,r2
movr2,r3
movr3,r4
movpc,r8
.section".data.boot"
.section".bss.boot"

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

intmain()
{
U32*pSource,*pDestin,count;
U8countDown,bootOption;
U32delayCount;
U32fileSize,i;
charc;
char*pCmdLine;
char*pMem;

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

EUARTinit();//串口初始化
EUARTputString("/n/nDBMX1LinuxBootloaderver0.2.0/n");
EUARTputString("Copyright(C)2002MotorolaLtd./n/n");
EUARTputString((U8*)cmdLine);
EUARTputString("/n/n");

EUARTputString("Pressanykeyforalternateboot-upoptions...");

小弟的bootloader主要干这么几件事:init();初始化硬件,打印一些信息和提供一些操作选项:
0.Programbootloaderimage
1.Programkernelimage
2.Programroot-diskimage
3.DownloadkernelandbootfromRAM
4.Downloadkernelandbootwithver0.1.xbootloaderformat
5.Bootaver0.1.xkernel
6.Bootwithadifferentcommandline

也就是说,可以在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("CopyingkernelfromFlashtoRAM.../n");
count=0x200000;//2Mbytes
pSource=(U32*)0x0C100000;
pDestin=(U32*)0x08008000;
do
{
*(pDestin++)=*(pSource++);
count-=4;
}while(count>0);
}

EUARTputString("Bootingkernel.../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:
//jumptothecopycode(gettheargumentsright)
movpc,r0

.globalJumpToKernel0x
//r0=jumpaddress
//r1=argumentstouse(thesegetshifted)

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

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

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

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

#ifdefined(CONFIG_MX1)
movr1,#MACH_TYPE_MX1
#endif

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

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

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

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

MACHINE_START(MX1ADS,"MotorolaMX1ADS")
MAINTAINER("SPSMotorola")

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和内存是统一编址的。
其他的项,在下面的初始化过程中会逐个介绍到。

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

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

msrcpsr_c,r0@andallirqsdiabled
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
.long0x41009200//CPUid
.long0xff00fff0//cpumask
.long0x00000c1e@mmuflags
b__arm920_setup
.longcpu_arch_name
.longcpu_elf_name
.longHWCAP_SWP|HWCAP_HALF|HWCAP_26BIT
.longcpu_arm920_info
.longarm920_processor_functions

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

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

//r5=08000000ram起始地址r6=00200000io地址,r7=f0200000虚io
teqr7,#0@invalidarchitecture?
moveqr0,#a@yes,errora
beq__error
bl__create_page_tables

其中__create_page_tables为:
__create_page_tables:
pgtblr4
//r4=08004000临时页表的起始地址
//r5=08000000,ram的起始地址
//r6=00200000,i/o寄存器空间的起始地址
//r7=00003c08
//r8=00000c1e

//thepagetablein08004000isjusttempbasepage,wheninit_taskssweaper_page_dirready,
//thetemppagewillbeuseless
//thehigh12bitofvirtualaddressisbasetableindex,soweneed4kx4=16ktempbasepage,

movr0,r4
movr3,#0
addr2,r0,#0x4000@16kofpagetable
1:strr3,[r0],#4@Clearpagetable
strr3,[r0],#4
strr3,[r0],#4
strr3,[r0],#4
teqr0,r2
bne1b
/*
*CreateidentitymappingforfirstMBofkernel.
*Thisismarkedcacheableandbufferable.
*
*Theidentitymappingwillberemovedby
*/

//由于linux编译的地址是0xC0008000,load的地址是0x08008000,我们需要将虚地址0xC0008000映射到0800800一段
//同时,由于部分代码也要直接访问0x08008000,所以0x08008000对应的表项也要填充
//页表中的表象为section,AP=11表示任何模式下可访问,domain为0。
addr3,r8,r5@mmuflags+startofRAM
//r3=08000c1e
addr0,r4,r5,lsr#18
//r0=08004200
strr3,[r0]@identitymapping
//*08004200=08000c1e0x200表象对应的是08000000的1m
/*
*Nowsetupthepagetablesforourkerneldirect
*mappedregion.WeroundTEXTADDRdowntothe
*nearestmegabyteboundary.
*/
//下面是映射4M

addr0,r4,#(TEXTADDR&0xfff00000)>>18@startofkernel
//r0=r4+0x3000=08004000+3000=08007000
strr3,[r0],#4@PAGE_OFFSET+0MB
//*08007004=08000c1e
addr3,r3,#1<<20
//r3=08100c1e
strr3,[r0],#4@PAGE_OFFSET+1MB
//*08007008=08100c1e
addr3,r3,#1<<20
strr3,[r0],#4
//*0800700c=08200c1e@PAGE_OFFSET+2MB
addr3,r3,#1<<20
strr3,[r0],#4@PAGE_OFFSET+3MB
//*08007010=08300c1e

bicr8,r8,#0x0c@turnoffcacheable
//r8=00000c12@andbufferablebits
movpc,lr//子程序返回。
下一回就要开始打开mmu的操作了

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

__arm920_setup:
movr0,#0
mcrp15,0,r0,c7,c7@invalidateI,Dcachesonv4
mcrp15,0,r0,c7,c10,4@drainwritebufferonv4
mcrp15,0,r0,c8,c7@invalidateI,DTLBsonv4
mcrp15,0,r4,c2,c0@loadpagetablepointer
movr0,#0x1f@Domains0,1=client
mcrp15,0,r0,c3,c0@loaddomainaccessregister
mrcp15,0,r0,c1,c0@getcontrolregisterv4
/*
*Clearoutunwantedbits(thenputtheminifweneedthem)
*/
@VIZFRSBLDPWCAM
bicr0,r0,#0x0e00
bicr0,r0,#0x0002
bicr0,r0,#0x000c
bicr0,r0,#0x1000@...0000.....000.
/*
*Turnonwhatwewant
*/
orrr0,r0,#0x0031
orrr0,r0,#0x2100@..1....1..11...1

#ifdefCONFIG_CPU_ARM920_D_CACHE_ON
orrr0,r0,#0x0004@.............1..
#endif
#ifdefCONFIG_CPU_ARM920_I_CACHE_ON
orrr0,r0,#0x1000@...1............
#endif
movpc,lr

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

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

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

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

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

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

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

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

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


评论


技术专区

关闭