编写2440bootloader

来源:互联网 发布:js 对象转数组 编辑:程序博客网 时间:2024/04/30 02:19

bootloader介绍

CPU上电后会从IO空间的某地址取第一条指令。 但此时:PLL没有启动,CPU工作频率为外部输入晶振频率,非常低;CPU工作模式、中断设置等不确定;存储空间的各个BANK(包括内存)都没有驱动, 内存不能使用。 在这种情况下必须在第一条指令处做一些初始化工作,这段初始化程序与操作系统独立分开,称之为bootloader


实际上,很少有必要自己写一个Bootloader,因为U-Boot已经强大到能够满足各种需要。但是强大必然复杂,一个初学者想要分析U-Boot的源代码,还是有些难度的。出于学习的目的, 我写了这个史上最简单的启动加载器, 它只包含最基本的功能, 却囊括了一个嵌入式Bootloader应该有的核心和精华。


第一阶段的汇编代码:start.S

一个嵌入式Bootloader最初始部分的代码几乎必须是用汇编语言写成的,因为开发板刚上电后没有准备好C程序运行环境, 比如堆栈指针SP没有指到正确的位置。 汇编代码应该完成最原始的硬件设备初始化, 并准备好C运行环境, 这样后面的功能就可以用 C语言来写了。


异常向量:

//global 声明一个符号可被其他文档引用,相当于声明了一个全局变量, .globl 和.global 相同。
//该部分为处理器的异常处理向量表。地址范围为 0x0000 0000 ~ 0x0000 0020,刚好 8 条指令。


text
.global _start
_start:
Reset @ 0x00: 发生复位异常时从地址零处开始运行
b HandleUndef @ 0x04: 未定义指令中止模式的向量地址
b HandleSWI @ 0x08: 管理模式的向量地址,通过SWI指令进入此模式
b HandlePrefetchAbort @ 0x0C: 指令预取终止导致的异常的向量地址
b HandleDataAbort @ 0x10: 数据访问终止导致的异常的向量地址
b HandleNotUsed @ 0x14: 保留
b HandleIRQ @ 0x18: 中断模式的向量地址
b HandleFIQ @ 0x1C: 快中断模式的向量地址


ARM920T的规定,从地址0x000x1C放置异常向量表, 向量表每个条目占四个字节, 正好可以放置一条跳转指令, 跳转到相应异常的服务程序中去。在S-Boot中没有使用中断,所以除Reset异常外,其它异常的服务程序都可简单地写个死循环。Reset异常是系统上电后自动触发的,所以我们的代码都写在Reset服务程序里面。
实际上,异常向量表不一定非要位于地址0x00处,CP15协处理器中的c1寄存器的第13位用来控制异常向量表的起始地址。该位为0时,异常向量表位于低地址0x00处;该位为1时,异常向量表位于高地址0xFFFF0000处。 我们没有必要改变这个位的值, 使用默认的低地址就行了。


设置操作模式

Reset:
mrs r0,cpsr @set cpu to SVC32 mode
bic r0,r0,#0x1F @xxx0 0000
orr r0,r0,#0xD3 @1101 0011

msr cpsr,r0 @cpsr=11x10011, IRQ/FIQ disabled


OPERATING MODES
ARM920T supports seven modes of operation:
·User (usr):The normal ARM program execution state
· FIQ (fiq): Designed to support a data transfer or channel process
· IRQ (irq): Used for general-purpose interrupt handling
· Supervisor (svc):Protected mode for the operating system
· Abort mode (abt):Entered after a data or instruction prefetch abort
· System (sys): A privileged user mode for the operating system
· Undefined (und):Entered when an undefined instruction is executed

代码最初始的任务是设置CPU工作在SVC32模式(管理模式),关闭所有中断,禁用看门狗。实际上,即使不设置工作模式,CPU在复位之后将自动工作在管理模式。在整个S-Boot运行期间,我们没有使用中断,也没有改变CPU工作模式,它将一直工作在SVC32模式。


MMUICacheDCache的打开和关闭都是由CP15协处理器的c1寄存器控制的。实际上在复位之后这三者都是自动关闭的,所以省略了关闭它们的代码。


S3C2440APSR寄存器(ProgramStatusReguster)中每个Bit位的含义如图所示。Bit4~Bit0为模式位,用来设置CPU工作模式,M[4:0]= 10011 表示 SVC32模式。 Bit5为状态位, T=0表示工作在ARM状态,T=1表示工作在Thumb状态,默认为0,不需要改变。Bit6为快速中断禁止位,F=1为禁止快速中断,F=0为使能快速中断。Bit7为中断禁止位,I=1为禁止中断,F=0为使能中断。其它Bit位暂时可以不必理会。


mrsmsr是在PSR寄存器和其它寄存器间传递数据的指令。如:mrsr0,cpsrcpsr的值传送到r0中,msrcpsr,r0r0的值传送到cpsr中。bic是位清零(BitClear)指令,bicr0,r0,#0x1F意思是把r0Bit[4:0]位清零(由0x1F指示),然后把结果写入r0中。orr是按位求或指令,orrr0,r0,#0xD3表示把r0Bit7,Bit6,Bit4,Bit1,Bit0置为1,其它位保持不变。执行完上述操作后,cpsr中的I=1,F=1,T保持不变(默认为0),M[4:0]=10011,意思是禁止IRQ,禁止FIQ,工作在ARM状态,工作在SVC32模式Bit4~Bit0为模式位,用来设置CPU工作模式,现在只要知道M[4:0]= 10011 表示 SVC32模式就行了。Bit5为状态位,T=0表示工作在ARM状态,T=1表示工作在Thumb状态,默认为0,不需要改变。Bit6为快速中断禁止位,F=1为禁止快速中断,F=0为使能快速中断。Bit7为中断禁止位,I=1为禁止中断,F=0为使能中断。其它Bit位暂时可以不必理会。





关闭看门狗定时器:

关闭看门狗很简单,因为WTCON寄存器的地址为0x53000000,直接向该寄存器写0即可。


#define pWTCON 0x53000000
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0]


屏蔽所有中断:

ARM中断寄存器主要包括:

  ·中断模式寄存器可以设置2个中断源为IRQ或FIQ方式。

  ·中断挂起寄存器,当有中断请求产生时,相应的位会被硬件置1,处于挂起状态。当进入中断处理程序时,必须通过软件清除这个标志位,以标志响应中断请求。

  ·中断屏蔽寄存器,当需要屏蔽某些中断源时,可以设置相对应的位。

  ·中断优先级寄存器可以设置21个中断源优先级的高低。

  ·中断偏移寄存器,中断响应时通过读这个寄存器可以查到当前的中断源。


@ mask all IRQs by setting all bits in the INTMR - default


mvn r1,#0x0 @mvn传送取反的值
ldr r0,=0x4a000008
str r1,[r0]  @ 屏蔽所有中断



禁止 MMU:

/** disable MMU stuff and caches*/

mcr p15,0,r0,c7,c7,0
mrc p15,0,r0,c1,c0,0
bic r0,r0,#0x00000007
mcr p15,0,r0,c1,c0,0
mov pc,lr

为什么要关闭 catch 和 MMU 呢? catch 和 MMU 是做什么用的?
Catch 是 cpu 内部的一个 2 级缓存,她的作用是将常用的数据和指令放在 cpu 内部, MMU是用来做虚实地址转换用的,我们的目的是设置控制的寄存器,寄存器都是实地址,如果既要开启 MMU 又要做虚实地址转换的话,中间还多一步,先要把实地址转换成虚地址,然后再做设置,但对 uboot 而言就是起到一个简单的初始化的作用和引导操作系统,如果开启 MMU 的话,很麻烦,也没必要,所以关闭 MMU.

说道 catch 就必须提到一个关键字 Volatile,以后在设置寄存器时会经常遇到,他的本质是告诉编译器不要对我的代码进行优化,优化的过程是将常用的代码取出来放到 catch 中,它没有从实际的物理地址去取,它直接从 cpu 的缓存中去取,但常用的代码就是为了感知一些常用变量的变化,如果正在取数据的时候发生跳变,那么就感觉不到变量的变化了,所以在这种情况下要用 Volatile 关键字告诉编译器不要做优化,每次从实际的物理地址中去取指令,这就是为什么关闭 catch 关闭 MMU。但在 C 语言中是不会关闭 catch 和 MMU 的,会打
开,如果编写者要感知外界变化,或变化太快,从 catch 中取数据会有误差,就加一个关键字 Volatile。


更改CPU频率:


详情链接:    时钟初始化

到现在为止,CPU工作在外接晶振12MHz频率之下。现在使用以下代码设置PLL,提升工作频率。

/* FCLK:HCLK:PCLK = 1:4:8 */
/* default FCLK is 120 MHz ! */
#define CLKDIVN 0x4c000014
#define PLLCON 0x4c000008
#define PLL_405MHZ ((127<<12)|(2<<4)|(1))

init_clock:
ldr r0, =CLKDIVN
mov r1, #0x5
str r1, [r0]
mov pc,lr

mrc p15,0,r0,c1,c0,0
orr r0,r0,#0xc000000
mcr p15,0,r0,c1,c0,0

ldr r0, =PLLCON
ldr r1, =PLL_405MHZ
str r1, [r0]



配置存储控制器:


#define mem_contrl 0x48000000   @第一个存储控制寄存器地址


init_sdram:
ldr r0, =mem_contrl@将第一个存储控制寄存器地址存入r0
add r3, r0,#4*13        @计算最后一个控制寄存器地址存入r3
adrl r1, mem_data@将 将要写入控制寄存器数据的地址写入r1

0:
ldr r2, [r1], #4@将数据载入r2
str r2, [r0], #4@将数据载入控制寄存器
cmp r0, r3
bne 0b
 
mov pc, lr

mem_data:
.long 0x22000000
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00000700
.long 0x00018001
.long 0x00018001
.long 0x008c04f5
.long 0x000000b1
.long 0x00000030
.long 0x00000030




拷贝bootloader代码to RAM:


copy_to_ram:

ldr r0, =0x00000000@bootloader代码起始地址
ldr r1, =0x30008000@要存入RAM中的地址
add r3, r0, #1024*4@拷贝代码大小  (这里只考4K,我们自己写的 并不多大, 注意:小于4K可直接在SteppingStone中执行 大于4K 则要拷贝到RAM中去了。 )

copy_loop:
ldr r2, [r0], #4
str r2, [r1], #4@拷贝
cmp r0, r3@判断是否拷贝完。
bne copy_loop



码足够小(小于4KB)的话,那只在SteppingStone中运行,加载Linux内核到内存即可。但通常代码肯定会大于4KB。所以Bootloader一般分为两部分,Stage1的代码在SteppingStone中运行, 它会把Stage2的代码拷贝到RAM中, 并跳转到RAM中执行;Stage2的代码在RAM中执行, 它可以完成加载内核及其它任何复杂的功能。 因为Stage2的起始位置不好确定,为了方便,我们把所有的代码都拷贝到RAM中了.


初始化堆栈段,BSS数据段:

init_stack:
ldr sp, =0x32FFF000    @初始化堆栈指针


clean_bss:
ldr r0, =bss_start
ldr r1, =bss_end@设置bss段起始终止地址
cmp r0, r1 @若bss段无任何数据,则直接返回。
moveq pc, lr

clean_loop:
mov r2, #0
str r2, [r0], #4@赋初值0
cmp r0, r1
bne clean_loop@判断是否赋完值
mov pc,lr

这里先来规划一下内存空间的分配。RAM的地址范围是从0x300000000x3400000064MByte。把S-BootKernel放在高地址处,S-Boot0x33000000开始,预留8MByte空间,内核从0x33800000开始,可供使用的空间也是8MByte。因栈空间是向下生长的,我们在S-Boot下面预留4096Byte的空闲区域,然后向下为栈空间,故栈指针SP初始化为0x32FFF000。其实留不留空闲区域是无所谓的,这里只是为了把二者更明显地区分开。

们只设置SVC模式下的SP,不使用CPU的其它工作模式,所以也没必要设置其它模式下的栈指针。另外,程序中不使用动态内存分配,故而也不必分配堆空间




1)数据存放位置:
初始化的全局变量 : 数据段
局部变量 : 栈
mallco  : 堆
未初始化全局变量 :BSS段


2) BSS段 作用 :
使从BSS段取出的初始值为 0  。(未初始化的随机值可能会 造成不明错误的原因吗? 所以要初始化为0?)


跳转至C语言环镜

ldr pc, =my_main     @(ldr绝对跳转 b ,bl 相对跳转)



此时Makefile内容 要添加main.o :


all: start.o my_main.o 

arm-linux-ld -TMyboot.lds -o Myboot.elf $^
arm-linux-objcopy -O binary Myboot.elf Myboot.bin

%.o : %.S
arm-linux-gcc -g -c $^

%.o : %.c
arm-linux-gcc -g -c $^


到此就完成了bootloader第一阶段的任务了 。可在主函数my_main()中写个点亮LED程序测试前面代码是否正常执行,下面给个代码:

#define GPFCON (volatile unsigned long*)0x56000050#define GPFDAT (volatile unsigned long*)0x56000054void delay(volatile unsigned long time){unsigned long i;while(time--){i=50000;while(i--) ;}}int my_main(){unsigned long i,j,k;unsigned long flag = 0;*(GPFCON) = 5376; i = ~(1<<4);j = ~(2<<4);k = ~(4<<4);for(;;flag++){if(flag%2==0) {delay(1);  *(GPFDAT) = i;}else{delay(1);  *(GPFDAT) = k;} delay(1); *(GPFDAT) = j;}return 0;}



第二阶段的C代码:


第二阶段(stage 2)通常用C语言来实现。

首先以start_armboot()函数为入口点,主要进行如下操作:

     初始化(cpu, 板卡,中断,串口,控制台等),开启I/D cache。初始化FLASH,根据系统配置执行其他初始化操作。打印LOG,使能中断,获取环境变量,初始化网卡。最后进入main_loop()函数。在main_loop函数中会检查BOOTDELAY和BOOTCMD环境变量,如果BOOTCMD已经设置过,则在等待BOOTDELAY个毫秒后会自动执行BOOTCMD。如果等待过程中被用户中断(ctl+c)或者BOOTCMD没有设置,则会等待用户输入命令。


start_armboot将完成以下工作。


1.全局数据结构的初始化

比如 gd_t 结构的初始化:
251 gd = (gd_t*)(_armboot_start – CFG_MALLOC_LEN – sizeof(gd_t));
_armboot_start 是 u-boot 在 RAM 中的开始地址(对于 u-boot 最终搬移到 RAM 中运行的情况) ,CFG_MALLOC_LEN 在 include/configs/<board name>.h 中定义。


bd_t 结构的初始化:
272 gd->bd = (bd_t*)((char*)gd-sizeof(bd_t));
u-boot 把 bd_t 结构紧接着 gd_t 结构存放。


内存分配的初始化
316 mem_malloc_init(_armboot_start-CFG_MALLOC_LEN);
经过以上的初始化后, u-boot 在内存中的布局为(在底端为低地址)
-----------------------------
BSS
-----------------------------
U-BOOT TEXT/DATA
-----------------------------
CFG_MALLOC_LEN
-----------------------------
gd_t
-----------------------------
bd_t
-----------------------------
STACK
-----------------------------


2.调用通用初始化函数
for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) {
if ((*init_fnc_ptr)() != 0) {
hang ();
}
}
init_sequence[]是 init_fnc_t 函数指针数组,这个数组包含了众多初始化函数,比如 cpu_init,board_init 等。
//init_fnc_t *init_sequence[] = {
// cpu_init, /* basic cpu dependent setup */
// board_init, /* basic board dependent setup */
// interrupt_init, /* set up exceptions */
// env_init, /* initialize environment */
// init_baudrate, /* initialze baudrate settings */
// serial_init, /* serial communications setup */
// display_banner,
// dram_init, /* configure available RAM banks */
// display_dram_config,
// NULL,
// };

3.初始化具体设备

这一部分包括对 Flash, LCD,网络的初始化等,例如
318 #if (CONFIG_COMMANDS & CFG_CMD_NAND)
puts ("NAND: ");
nand_init(); /* go init the NAND */
#endif
367 devices_init();
386 #ifdef CONFIG_DRIVER_CS8900
cs8900_get_enetaddr (gd->bd->bi_enetaddr);
#endif


4.初始化环境变量
环境变量在通用初始化函数里面,已经初始化一次( env_init),这里调用 env_relocate 对环境变量进行重新定位。在我的另一篇文章”U-BOOT ENV 实现”中有对环境变量实现的讨论。

5.进入主循环
当然 start_armboot 除了以上工作外,还完成其它的初始化工作,具体参考 lib_arm/board.c,
在一切准备就绪之后,就进入 u-boot 的主循环:
416 for (;;) {
main_loop ();
}
main_loop 的代码比较长,基本是就是执行用户的输入命令。


0 0