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一般来说有两个作用:另外注意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 .endmdo_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,r0CPSR 是当前的程序状态寄存器(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 开始的位置搬移到内存中的适当的位置继续执行。为啥要搬移代码?原因可能如下:
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启动:
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
#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就是内存上的地址。只有栈设置好了之后,才可以调用c函数了,如下图所示:
用于栈顶sp就指向了栈的地址。下面的空间就可以用于运行uboot.bin所使用的栈
参考文档:
1.Uboot中start.S源码的指令级的详尽解析_v1.6
2.天嵌TQ2440 uboot代码start.S