首先,portinglinux的时候要规划内存影像,如小弟的系统有64m SDRAM,
地址从0x0800 0000 -0x0bff ffff,32m flash,地址从0x0c00 0000-0x0dff ffff.
规划如下:bootloader,linux kernel, rootdisk放在flash里。
具体从0x0c00 0000开始的第一个1M放bootloader,
0x0c100000开始的2m放linux kernel,从 0x0c30 0000开始都给rootdisk。
启动:
首先,启动后arm920T将地址0x0c000000映射到0(可通过跳线设置),
实际上从0x0c000000启动,进入我们的bootloader,但由于flash速度慢,
所以bootloader前面有一小段程序把bootloader拷贝到SDRAM中的0x0AFE0100,
再从0x0800 0000 运行bootloader,我们叫这段小程序为flashloader,
flashloader必须要首先初始化SDRAM,不然往那放那些东东:
.equSOURCE, 0x0C000100 bootloader的存放地址
.equTARGET, 0x0AFE0100 目标地址
.equSDCTL0, 0x221000 SDRAM控制器寄存器
//size is stored in location 0x0C0000FC
.global_start
_start://入口点
//;***************************************
//;*Init SDRAM
//;***************************************
//;***************
//;* SDRAM
//;***************
LDRr1, =SDCTL0 //
//; Set Precharge Command
LDRr3, =0x92120200
//ldrr3,=0x92120251
STRr3, [r1]
//; Issue Precharge All Commad
LDRr3, =0x8200000
LDRr2, [r3]
//; Set AutoRefresh Command
LDRr3, =0xA2120200
STRr3, [r1]
//; Issue AutoRefresh Command
LDRr3, =0x8000000
LDRr2, [r3]
LDRr2, [r3]
LDRr2, [r3]
LDRr2, [r3]
LDRr2, [r3]
LDRr2, [r3]
LDRr2, [r3]
LDRr2, [r3]
//; Set Mode Register
LDRr3, =0xB2120200
STRr3, [r1]
//; Issue Mode Register Command
LDRr3, =0x08111800 //; Mode Register Value
LDRr2, [r3]
//; Set Normal Mode
LDRr3, =0x82124200
STRr3, [r1]
//;***************************************
//;*End of SDRAM and SyncFlash Init *
//;***************************************
//copy code from FLASH to SRAM
_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就是烧在flash 0x0C000100中的真正的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:
//jump to the copy code (get the arguments right)
movpc, r0
.globalJumpToKernel0x
//r0 = jump address
//r1-r4 = arguments to use (these get shifted)
JumpToKernel0x:
//jump to the copy code (get the arguments right)
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\nDBMX1Linux Bootloader ver 0.2.0\n");
EUARTputString("Copyright(C) 2002 Motorola Ltd.\n\n");
EUARTputString((U8*)cmdLine);
EUARTputString("\n\n");
EUARTputString("Pressany 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("Copyingkernel from Flash to RAM ...\n");
count= 0x200000; // 2 Mbytes
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:
//jump to the copy code (get the arguments right)
movpc, r0
.globalJumpToKernel0x
//r0 = jump address
//r1 = arguments to use (these get shifted)
由于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,"Motorola MX1ADS")
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 @ make sure svc mode//设置为SVC模式,允许中断和快速中断
//此处设定系统的工作状态,arm有7种状态
//每种状态有自己的堆栈
msrcpsr_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_info:
.long0x41009200 //CPU id
.long0xff00fff0 //cpu mask
.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=08000000 ram起始地址 r6=0020 0000 io地址,r7=f020 0000 虚io
teqr7, #0 @ invalid architecture?
moveqr0, #'a' @ yes, error 'a'
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
//thepage table in 0800 4000 is just temp base page, when init_task'ssweaper_page_dir ready,
//the temp page will be useless
//the high 12 bit of virtual address is base table index, so we need4kx4 = 16k temp base page,
movr0, r4
movr3, #0
addr2, r0, #0x4000 @ 16k of page table
1:str r3, [r0], #4 @ Clear page table
strr3, [r0], #4
strr3, [r0], #4
strr3, [r0], #4
teqr0, r2
bne1b
//由于linux编译的地址是0xC0008000,load的地址是0x08008000,我们需要将虚地址0xC0008000映射到0800800一段
//同时,由于部分代码也要直接访问0x08008000,所以0x08008000对应的表项也要填充
//页表中的表象为section,AP=11表示任何模式下可访问,domain为0。
addr3, r8, r5 @ mmuflags + start of RAM
//r3=08000c1e
addr0, r4, r5, lsr #18
//r0=08004200
strr3, [r0] @ identity mapping
/
//下面是映射4M
addr0, r4, #(TEXTADDR & 0xfff00000)>> 18 @ start of kernel
//r0= r4+ 0x3000 = 0800 4000 + 3000 = 0800 7000
strr3, [r0], #4 @ PAGE_OFFSET + 0MB
/
#defineNR_BANKS 4
//definethe systen mem region, not consistent
structmeminfo {
intnr_banks;
unsignedlong end;
struct{
unsignedlong start;
unsignedlong size;
intnode;
}bank[NR_BANKS];
};
下面是:ROOT_DEV= MKDEV(0, 255);
ROOT_DEV是宏,指明启动的设备,嵌入式系统中通常是flashdisk.
这里面有一个有趣的悖论:linux的设备都是在/dev/下,访问这些设备文件需要设备驱动程序支持,而访问设备文件才能取得设备号,才能加载驱动程序,那么第一个设备驱动程序是怎么加载呢?就是ROOT_DEV,不需要访问设备文件,直接指定设备号。
下面我们准备初始化真正的内核页表,而不再是临时的了。
首先还是取得当前系统的内存映像:
mdesc= setup_architecture(machine_arch_type);
//findthe machine type in mach-integrator/arch.c
//theads name, mem map, io map
返回如下结构:
mach-integrator/arch.c
MACHINE_START(INTEGRATOR,"Motorola MX1ADS")
MAINTAINER("ARMLtd/Deep Blue Solutions Ltd")
BOOT_MEM(0x08000000,0x00200000, 0xf0200000)
FIXUP(integrator_fixup)
MAPIO(integrator_map_io)
INITIRQ(integrator_init_irq)
MACHINE_END
我们在前面介绍过这个结构,不过这次用它可是玩真的了。
书接上回,
下面是init_mm的初始化,init_mm定义在/arch/arm/kernel/init_task.c:
structmm_struct init_mm = INIT_MM(init_mm);
从本回开始的相当一部分内容是和内存管理相关的,凭心而论,操作系统的
内存管理是很复杂的,牵扯到处理器的硬件细节和软件算法,
限于篇幅所限制,请大家先仔细读一读armmmu的部分,
中文参考资料:linux内核源代码情景对话,
linux2.4.18原代码分析。
init_mm.start_code= (unsigned long) &_text;
内核代码段开始
init_mm.end_code= (unsigned long) &_etext;
内核代码段结束
init_mm.end_data= (unsigned long) &_edata;
内核数据段开始
init_mm.brk= (unsigned long) &_end;
内核数据段结束
每一个任务都有一个mm_struct结构管理任务内存空间,init_mm
是内核的mm_struct,其中设置成员变量*mmap指向自己,
意味着内核只有一个内存管理结构,设置*pgd=swapper_pg_dir,
swapper_pg_dir是内核的页目录,在arm体系结构有16k,
所以init_mm定义了整个kernel的内存空间,下面我们会碰到内核
线程,所有的内核线程都使用内核空间,拥有和内核同样的访问
权限。
memcpy(saved_command_line,from, COMMAND_LINE_SIZE);
//clearcommand array
saved_command_line[COMMAND_LINE_SIZE-1]= '\0';
//setthe end flag
parse_cmdline(&meminfo,cmdline_p, from);
//将bootloader的参数拷贝到cmdline_p,
bootmem_init(&meminfo);
定义在arm/mm/init.c
这个函数在内核结尾分一页出来作位图,根据具体系统的内存大小
映射整个ram
下面是一个非常重要的函数
paging_init(&meminfo,mdesc);
定义在arm/mm/init.c
创建内核页表,映射所有物理内存和io空间,
对于不同的处理器,这个函数差别很大,
void__init paging_init(struct meminfo *mi, struct machine_desc*mdesc)
{
void*zero_page, *bad_page, *bad_table;
intnode;
//staticstruct meminfo meminfo __initdata = { 0, };
memcpy(&meminfo,mi, sizeof(meminfo));
zero_page= alloc_bootmem_low_pages(PAGE_SIZE);
bad_page= alloc_bootmem_low_pages(PAGE_SIZE);
bad_table= alloc_bootmem_low_pages(TABLE_SIZE);
分配三个页出来,用于处理异常过程,在armlinux中,得到如下
地址:
zero_page=0xc0000000
badpage=0xc0001000
bad_table=0xc0002000
上回我们说到在paging_init中分配了三个页:
zero_page=0xc0000000
badpage=0xc0001000
bad_table=0xc0002000
但是奇怪的很,在更新的linux代码中只分配了一个
zero_page,而且在源代码中找不到zero_page
用在什么地方了,大家讨论讨论吧。
paging_init的主要工作是在
void__init memtable_init(struct meminfo *mi)
中完成的,为系统内存创建页表:
meminfo结构如下:
structmeminfo {
intnr_banks;
unsignedlong end;
struct{
unsignedlong start;
unsignedlong size;
intnode;
}bank[NR_BANKS];
};
是用来纪录系统中的内存区段的,因为在嵌入式
系统中并不是所有的内存都能映射,例如sdram只有
64m,flash32m,而且不见得是连续的,所以用
meminfo纪录这些区段。
void__init memtable_init(struct meminfo *mi)
{
structmap_desc *init_maps, *p, *q;
unsignedlong address = 0;
inti;
init_maps= p = alloc_bootmem_low_pages(PAGE_SIZE);
其中map_desc定义为:
structmap_desc {
unsignedlong virtual;
unsignedlong physical;
unsignedlong length;
intdomain:4, //页表的domain
prot_read:1,//保护标志
prot_write:1,//写保护标志
cacheable:1,//是否cache
bufferable:1,//是否用write buffer
last:1;//空
};init_maps
map_desc是区段及其属性的定义,属性位的意义请
参考ARMMMU的介绍。
下面对meminfo的区段进行遍历,同时填写init_maps
中的各项内容:
for(i = 0; i nr_banks; i++) {
if(mi->bank.size == 0)
continue;
p->physical= mi->bank.start;
p->virtual= __phys_to_virt(p->physical);
p->length= mi->bank.size;
p->domain= DOMAIN_KERNEL;
p->prot_read= 0;
p->prot_write= 1;
p->cacheable= 1; //可以CACHE
p->bufferable= 1; //使用write buffer
p++; //下一个区段
}
如果系统有flash,
#ifdefFLUSH_BASE
p->physical= FLUSH_BASE_PHYS;
p->virtual= FLUSH_BASE;
p->length= PGDIR_SIZE;
p->domain= DOMAIN_KERNEL;
p->prot_read= 1;
p->prot_write= 0;
p->cacheable= 1;
p->bufferable= 1;
p++;
#endif
其中的prot_read和prot_write是用来设置页表的domain的,
下面就是逐个区段建立页表:
q= init_maps;
do{
if(address virtual || q == p) {
clear_mapping(address);
address+= PGDIR_SIZE;
}else {
create_mapping(q);
address= q->virtual + q->length;
address= (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q++;
}
}while (address != 0);
上次说到memtable_init中初始化页表的循环,
这个过程比较重要,我们看仔细些:
q= init_maps;
do{
if(address virtual || q == p) {
//由于内核空间是从c0000000开始,所以c000 0000
//以前的页表项全部清空
clear_mapping(address);
address+= PGDIR_SIZE;
//每个表项增加1m,这里感到了section的好处
}
其中clear_mapping()是个宏,根据处理器的
不同,在920下被展开为
cpu_arm920_set_pmd(((pmd_t*)(((&init_mm )->pgd+
((virt) >> 20 )))),((pmd_t){( 0)}));
其中init_mm为内核的mm_struct,pgd指向
swapper_pg_dir,在arch/arm/kernel/init_task.c中定义
ENTRY(cpu_arm920_set_pmd)
#ifdefCONFIG_CPU_ARM920_WRITETHROUGH
eorr2, r1, #0x0a
tstr2, #0x0b
biceqr1, r1, #4
#endif
strr1, [r0]
把pmd_t填写到页表项中,由于pmd_t=0,
实际等于清除了这一项,由于dcache打开,
这一条指令实际并没有写回内存,而是写到cache中
mcrp15, 0, r0, c7, c10, 1
把cache中地址r0对应的内容写回内存中,
这一条语句实际是写到了writebuffer中,
还没有真正写回内存。
mcrp15, 0, r0, c7, c10, 4
等待把writebuffer中的内容写回内存。在这之前core等待
movpc, lr
在这里我们看到,由于页表的内容十分关键,为了确保写回内存,
采用了直接操作cache的方法。由于在armcore中,打开了d cache
则必定要用writebuffer.所以还有wb的回写问题。
由于考虑到效率,我们使用了cache和buffer,
所以在某些地方要用指令保证数据被及时写回。
下面映射c0000000后面的页表
else{
create_mapping(q);
address= q->virtual + q->length;
address= (address + PGDIR_SIZE - 1) & PGDIR_MASK;
q++;
}
}while (address != 0);
create_mapping也在mm-armv.c中定义;
staticvoid __init create_mapping(struct map_desc *md)
{
unsignedlong virt, length;
intprot_sect, prot_pte;
longoff;
prot_pte= L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
(md->prot_read? L_PTE_USER : 0) |
(md->prot_write? L_PTE_WRITE : 0) |
(md->cacheable? L_PTE_CACHEABLE : 0) |
(md->bufferable? L_PTE_BUFFERABLE : 0);
prot_sect= PMD_TYPE_SECT | PMD_DOMAIN(md->domain) |
(md->prot_read? PMD_SECT_AP_READ : 0) |
(md->prot_write? PMD_SECT_AP_WRITE : 0) |
(md->cacheable? PMD_SECT_CACHEABLE : 0) |
(md->bufferable? PMD_SECT_BUFFERABLE : 0);
由于arm中section表项的权限位和page表项的位置不同,
所以根据structmap_desc 中的保护标志,分别计算页表项
中的AP,domain,CB标志位。
有一段时间没有写了,道歉先,前一段时间在做armlinux的xip,终于找到了
在flash中运行kernel的方法,同时对系统的存储管理
的理解更深了一层,我们继续从上回的create_mapping往下看:
while((virt & 0xfffff || (virt + off) &0xfffff) && length>= PAGE_SIZE) {
alloc_init_page(virt,virt + off, md->domain, prot_pte);
virt+= PAGE_SIZE;
length-= PAGE_SIZE;
}
while(length >= PGDIR_SIZE) {
alloc_init_section(virt,virt + off, prot_sect);
virt+= PGDIR_SIZE;
length-= PGDIR_SIZE;
}
while(length >= PAGE_SIZE) {
alloc_init_page(virt,virt + off, md->domain, prot_pte);
virt+= PAGE_SIZE;
length-= PAGE_SIZE;
}
这3个循环的设计还是很巧妙的,create_mapping的作用是设置虚地址virt
到物理地址virt+ off的映射页目录和页表。arm提供了4种尺寸的页表:
1M,4K,16K,64K,armlinux只用到了1M和4K两种。
这3个while的作用分别是“掐头“,“去尾“,“砍中间“。
第一个while是判断要映射的地址长度是否大于1m,且是不是1m对齐,
如果不是,则需要创建页表,例如,如果要映射的长度为1m零4k,则先要将“零头“
去掉,4k的一段需要中间页表,通过第一个while创建中间页表,
而剩下的1M则交给第二个while循环。最后剩下的交给第三个while循环。
alloc_init_page分配并填充中间页表项
staticinline void
alloc_init_page(unsignedlong virt, unsigned long phys, int domain, int prot)
{
pmd_t*pmdp;