Start.S
uboot的第一阶段大致可以分别下面几个步骤:
(1)设置CPU模式
(2)关闭看门狗
(3)关闭中断
(4)设置堆栈sp指针
(5)清除bss段
(6)异常中断处理
那么先分析下代码
.globl _start
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq
.balignl 16,0xdeadbeef
解析:
Start.s文件一开始,就定义了_start 的全局变量(
.global)。也即,在别的文件,照样能引用这个_start 变量。
这段代码验证了我们之前学过的 arm 体系的理论知识:中断向量表放在从0x0 开始的地方。其中,每个异常中断的摆放次序,是事先规定的。比如第一个必须是 reset 异常,第二个必须是未定义的指令异常等等(位置都是事先写死的)。
分析下面跳转命令之前先分析下ldr是什么:
LDR指令的格式为:
LDR {条件} 目的寄存器,<存储器地址>
LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。
“ARM是RISC结构,数据从内存到CPU之间的移动只能通过L/S指令来完成,也就是ldr/str指令。
比如想把数据从内存中某处读取到寄存器中,只能使用ldr
比如:
ldr r0, 0x12345678
就是把0x12345678这个地址中的值存放到r0中。”
那么就分析一个未定义异常(其他类似)
过程如下:
ldr pc, _undefined_instruction //表示如果板子出现了未定义的异常,就跳转到_undefined_instruction地址去执行相关的操作
_undefined_instruction:
.word undefined_instruction
undefined_instruction:
get_bad_stack
bad_save_user_regs
bl do_undefined_instruction
.word的作用就是分配一个32bit的空间,里面存放的内容就是undefined_instruction,
以_undefined_instruction为例,就是,此处分配了一个word=32bit=4字节的地址空间,里面存放的值是undefined_instruction。
而此处_undefined_instruction也就是该地址空间的地址了。用C语言来表达就是:
_undefined_instruction = &undefined_instruction
或
*_undefined_instruction = undefined_instruction
undefined_instruction也是下面执行代码的地址。所以到最后ldr pc, _undefined_instruction执行的操作是:
get_bad_stack
bad_save_user_regs
bl do_undefined_instruction
get_bad_stack(对栈的操作)和bad_save_user_regs(保存用户寄存器)=====》保存现场的作用
get_bad_stack的实现:
.macro get_bad_stack
ldr r13, _armboot_start @ setup our mode stack
sub r13, r13, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
sub r13, r13, #(CFG_GBL_DATA_SIZE+8) @ reserved a couple spots in abort stack
/*设置栈的地址为:r13 = _armboot_start - CONFIG_STACKSIZE - CFG_MALLOC_LEN - CFG_GNL_DATA_SIZE -8的地方*/
str lr, [r13] @ save caller lr / spsr
mrs lr, spsr
str lr, [r13, #4]
/*保存调用者的lr到r13的地址(lr是当前模式下一条指令的地址),将保存程序状态寄存器(spsr保存的是前一个工作模式的状态,从而可以回到之前的模式)的值保存到r13 + 4的地址那里,保存现场*/
mov r13, #MODE_SVC @ prepare SVC-Mode
@ msr spsr_c, r13
msr spsr, r13
mov lr, pc
movs pc, lr
/*设置svc模式*/
.endm
对于lr的解析(转载:
http://hi.baidu.com/a843538946/item/4e2a34fe48b6e5be31c199ec):
1.SP(R13) LR(R14)PC(R15)
2.lr(r14)的作用问题,这个lr一般来说有两个作用:1》. 当使用bl或者blx跳转到子过程的时候,r14保存了返回地址,可以在调用过程结尾恢复。
2》 .异常中断发生时,这个异常模式特定的物理R14被设置成该异常模式将要返回的地址。
另外注意pc,在调试的时候显示的是当前指令地址,而用mov lr,pc的时候lr保存的是此指令向后数两条指令的地址,大家可以试一下用mov pc,pc,结果得到的是跳转两条指令,这个原因是由于arm的流水线造成的,预取两条指令的结果.
bad_save_user_regs保存用户寄存器:
.macro bad_save_user_regs
sub sp, sp, #S_FRAME_SIZE //sp指向 sp - 72 的地方
stmia sp, {r0 - r12} @ Calling r0-r12,//每4个字节依次存放一个寄存器,*sp = r0 *(sp + 1) = r1 ....
ldr r2, _armboot_start
sub r2, r2, #(CONFIG_STACKSIZE+CFG_MALLOC_LEN)
sub r2, r2, #(CFG_GBL_DATA_SIZE+8) @ set base 2 words into abort stack
ldmia r2, {r2 - r3} @ get pc, cpsr
add r0, sp, #S_FRAME_SIZE @ restore sp_SVC
add r5, sp, #S_SP
mov r1, lr
stmia r5, {r0 - r3} @ save sp_SVC, lr_SVC, pc, cpsr
mov r0, sp
.endm
do_undefined_instruction跳转到中断服务子程序中执行。
do_undefined_instruction
show_regs (pt_regs); //打印一些寄存器的信息
bad_mode ();
reset_cpu (0);//重启CPU
值得一提的是,当发生异常时,都将执行 \cpu\arm920t\interrupts.c 中定义的中断函数。比如:show_regs (pt_regs);和bad_mode (); reset_cpu (0)
重启机器的代码reset_cpu
void reset_cpu (ulong ignored)
{
volatile S3C24X0_WATCHDOG * watchdog;
#ifdef CONFIG_TRAB
extern void disable_vfd (void);
disable_vfd();
#endif
watchdog = S3C24X0_GetBase_WATCHDOG();//获取cpu看门狗的基地址
/* Disable watchdog */
watchdog->WTCON = 0x0000;//关闭看门狗
/* Initialize watchdog timer count register */
watchdog->WTCNT = 0x0001;//在使能看门狗之前需要设置计数值。在这里设置为1,所以立刻就会重启
/* Enable watchdog timer; assert reset at timer timeout */
watchdog->WTCON = 0x0021;使能看门狗
while(1); /* loop forever and wait for reset to happen */
/*NOTREACHED*/
}
二、Uboot存储器映射的定义
_TEXT_BASE:
.word TEXT_BASE //基地址
.globl _armboot_start
_armboot_start:
.word _start //uboot.bin存放的地址
/*
* These are defined in the board-specific linker script.
*/
.globl _bss_start
_bss_start:
.word __bss_start //定义于连接脚本中
.globl _bss_end
_bss_end:
.word _end
#ifdef CONFIG_USE_IRQ
/* IRQ stack memory (calculated at run-time) */
.globl IRQ_STACK_START
IRQ_STACK_START:
.word 0x0badc0de
/* IRQ stack memory (calculated at run-time) */
.globl FIQ_STACK_START
FIQ_STACK_START:
.word 0x0badc0de
#endif
上面的代码格式都差不多,截取一段分析:
.globl _armboot_start
_armboot_start:
.word _start
上面的代码就相当于:
*(_armboot_start) = _start
如果有:ldr pc _armboot_start 则会去执行_start
reset:
/*
* set the cpu to SVC32 mode
*/
mrs r0,cpsr
bic r0,r0,#0x1f
orr r0,r0,#0xd3
msr cpsr,r0
CPSR 是当前的程序状态寄存器(Current Program Status Register),
保存的是当前的工作模式的值 。
SPSR 是保存的程序状态寄存器(Saved Program Status Register):保存的是前一工作模式的CPSR的值。
下面就是PSR(程序状态寄存器的位分布)
从上图可以看到,PSR的低五位就是设置相关的模式的。
bic r0,r0,#0x1f //清除低五位
orr r0,r0,#0xd3 //d3 == 10011(SVC)
msr cpsr,r0 //重新写回cpsr,这样当前cpu就处于了SVC模式了。
几个汇编指令:
mrs :从状态寄存器中传到通用寄存器(status -> register)
bic : 清除操作数1(r0)的某些位,幵把结果放置到目的寄存器(r0)中
orr : 或运算
msr :和mrs相反的动作
问题:为什么要设置SVC模式??
四、关闭看门狗并且关闭中断
/* turn off the watchdog */
#if defined(CONFIG_S3C2400)
# define pWTCON 0x15300000
# define INTMSK 0x14400008 /* Interupt-Controller base addresses */
# define CLKDIVN 0x14800014 /* clock divisor register */
#elif defined(CONFIG_S3C2410)
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
#endif
#if defined(CONFIG_S3C2400) || defined(CONFIG_S3C2410)
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0]
/*
* mask all IRQs by setting all bits in the INTMR - default
*/
mov r1, #0xffffffff
ldr r0, =INTMSK
str r1, [r0]
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff
ldr r0, =INTSUBMSK
str r1, [r0] //str 是将r1寄存器中的值写到r0地址所指的空间内
# endif
根据S3C2440 的datasheet 文档(别的CPU要进行具体设置),系统启动后,看门狗寄存器是被使能的,所以,如果不在预计的时间内“喂狗”,就有“被狗咬”的可能。直接把看门狗关闭即可。
ldr r0, =pWTCON
这个指令和前面说的
ldr pc, _undefined_instruction
是不一样的。这是一条伪指令。
伪指令,就是“伪”的指令,是针对“真”的指令而言的。
真的指令就是那些常见的指令,比如上面说的arm的ldr,bic,msr等等指令,是arm体系架构中真正存在的指令,你在arm汇编指令集中找得到对应的含义。 而伪指令是写出来给汇编程序看的,汇编程序能看懂伪指令具体表示的是啥意思,然后将其翻译成真正的指令或者进行相应的处理。
ldr伪指令可以在立即数前加上=,以表示把一个地址写到某寄存器中,比如:
ldr r0, =0x12345678
就把0x12345678这个地址写到r0中了。所以,ldr 伪指令和mov是比较相似的。mov指令后面的立即数是有限制的,这个立即数,能够必须由一个8位的二进制数,即在0x00-0xFF内的某个值,经过偶数次右移后得到,这样才是合法数据,而ldr 伪指令没有这个限制。
问题:什么才是合法立即数??
五、调用cpu_init_crit
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
#endif
分析:
cpu_init_crit:
/*
* flush v4 I/D caches
*/
mov r0, #0
mcr p15, 0, r0, c7, c7, 0 /* flush v3/v4 cache */
mcr p15, 0, r0, c8, c7, 0 /* flush v4 TLB */
/*
* disable MMU stuff and caches
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
orr r0, r0, #0x00000002 @ set bit 2 (A) Align
orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
mcr p15, 0, r0, c1, c0, 0
/*
* before relocating, we have to setup RAM timing
* because memory timing is board-dependend, you will
* find a lowlevel_init.S in your board directory.
*/
mov ip, lr
bl lowlevel_init //对内存进行初始化,没有看懂!!!
mov lr, ip
mov pc, lr
#endif
注意:移植的时候好像只需要修改lowlevel_init即可。
六、栈的设置:
stack_setup:
ldr r0, _TEXT_BASE /* upper 128 KiB: relocated uboot */
sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* leave 3 words for abort-stack *
这段代码是用来分配各个栈空间的。包括分配动态内存区,全局数据区,IRQ和FIQ 的栈空间等
下面是uboot的空间分布:
七、对时钟进行初始化,修改时钟除数寄存器,FCLK:HCLK:PCLK = 1:4:8(原因:2440的原始时钟只有12MHZ,为了让CPU处于400MHZ的工作频率下)
栈设置好了,就可以调用c函数了。(问题:调用c函数为什么要设置栈???)
bl clock_init
对于时钟的设置以后再分析
八、代码搬移
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
adr r0, _start /*这里使用的是位置无关码,用于读取程序下载的地址 r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
beq stack_setup
ldr r2, _armboot_start //定义于上面,就是_start
ldr r3, _bss_start //定义于连接脚本中
sub r2, r3, r2 /* r2 <- size of armboot */
add r2, r0, r2 /* r2 <- source end address */
/*copy_loop:
ldmia r0!, {r3-r10} /* copy from source address [r0] */
stmia r1!, {r3-r10} /* copy to target address [r1] */
cmp r0, r2 /* until source end addreee [r2] */
ble copy_loop
#endif /* CONFIG_SKIP_RELOCATE_UBOOT */
*/
#if 1
bl CopyCode2Ram /* r0: source, r1: dest, r2: size */
#else
在SDRAM 初始化完毕后,我们开始搬移代码,把代码从原先的 0x0 开始的位置搬移到内存中的适当的位置继续执行。为啥要搬移代码?原因可能如下:
1、运行速度的考虑。
flash 的读写速度远小于SDRAM 的读写速度,搬移到 SDRAM 后,可提高运行效率。
2、空间的考虑。
如果是nandflash 启动模式,那么只有 4KB的空间(仅仅用于arm920t CPU)供用户使用,实际的代码是永远大于4KB的,因此需要重新开辟空间来进行代码的运行工作。
CopyCode2Ram 就是对代码进行搬移的操作。下面对这个函数具体分析一下:
CopyCode2Ram代码搬移分析:
CopyCode2Ram
首先会对bBootFrmNORFlash函数的返回值进行判断,
如果是NOR启动,直接进行搬移操作:
for (i = 0; i < size / 4; i++)
{
pdwDest[i] = pdwSrc[i];
}
如果是nandflash启动,调用:
nand_init_ll(); /* 初始化NAND Flash ,要根据具体的nand进行初始化操作*/
nand_read_ll_lp(buf, start_addr, (size + NAND_BLOCK_MASK_LP)&~(NAND_BLOCK_MASK_LP)); /* 从 NAND Flash启动 *
分析下如何判断是从nand启动还是从nor启动:
原理:
Norflash上是不可写的,nandflash上可写(因为对 NOR FLASH 的写操作需要遵循特定的命令序列,最终由芯片内部的控制单元完成写操作。)。从而可以通过在0地址上写一段数据,再从上面读一下,看看读出的数据是否和写的数据一样。一样的话表示是nandflash启动,不一样的话表示是norflash启动。
代码如下:
bBootFrmNORFlash
volatile unsigned int *pdw = (volatile unsigned int *)0;
unsigned int dwVal;
dwVal = *pdw;
*pdw = 0x12345678;
if (*pdw != 0x12345678) //说明是nor启动
{
return 1;
}
else
{
*pdw = dwVal; //说明是nand启动
return 0;
}
对于nor启动,直接进行赋值即可,对于nandflash启动,需要先对nandflash进行初始化,然后再做拷贝工作,对于nandflash的分析。以后再分一章讨论
九、清bss段
clear_bss:
ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* stop here */
mov r2, #0x00000000 /* clear */、
clbss_l:str r2, [r0] /* clear loop... */
add r0, r0, #4
cmp r0, r1
ble clbss_l
本段代码先设置了BSS段的起始地址与结束地址,然后循环清除所有的BSS段。至此,所有的 cpu 初始化工作(stage1 阶段)已经全部结束了。后面的代码,将通过ldr pc, _start_armboot ,进入C 代码执行。这个C 入口的函数,是在u-boot-1.1.6\lib_arm\board.c 文件中。它标志着后续将全面启动C 语言程序,同时它也是整个u-boot的主函数。
十、运行uboot的第一个c函数start_armboot,从而进入uboot的第二阶段:
ldr pc, _start_armboot
_start_armboot:
.word start_armboot
FAQ:
1._start,与_TEXT_BASE的区别:
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
adr r0, _start /* r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
blne cpu_init_crit
#endif
这段代码就显示了他们之间的区别:
1.如果uboot启动是从nandflash启动的,这个_start就为0地址,_TEXT_BASE就是内存上的地址。2.如果uboot是从仿真器上直接烧写到内存上的话,这个_start就和_TEXT_BASE的地址是相同的。
3.如果r0和r1不相等的话,就说明SDRAM还没有进行初始化,如果r0和r1相等的话,说明uboot已经跑在了SDRAM上了,就不需要进入cpu_init_crit来进行SDRAM的初始化了
2.为什么要设置栈:
只有栈设置好了之后,才可以调用c函数了,如下图所示:
用于栈顶sp就指向了栈的地址。下面的空间就可以用于运行uboot.bin所使用的栈
参考文档:
1.Uboot中start.S源码的指令级的详尽解析_v1.6
2.天嵌TQ2440 uboot代码start.S