uboot第一阶段分析(目前还有很多不懂的地方)

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

三、上电后设置CPU( arm920t,只要这个cpu都需要这样设置)为SVC 模式
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(程序状态寄存器的位分布)

uboot第一阶段分析(目前还有很多不懂的地方)_第1张图片

从上图可以看到,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的空间分布:

uboot第一阶段分析(目前还有很多不懂的地方)_第2张图片

七、对时钟进行初始化,修改时钟除数寄存器,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函数了,如下图所示:

uboot第一阶段分析(目前还有很多不懂的地方)_第3张图片


用于栈顶sp就指向了栈的地址。下面的空间就可以用于运行uboot.bin所使用的栈

参考文档:

1.Uboot中start.S源码的指令级的详尽解析_v1.6

2.天嵌TQ2440 uboot代码start.S

你可能感兴趣的:(平台)