嵌入式Linux——分析u-boot运行过程(1):u-boot第一阶段代码

2019-07-12 18:18发布

简介:

        本文主要介绍在u-boot-1.1.6中代码的运行过程,以此来了解在u-boot中如何实现引导并启动内核。这里我们先介绍u-boot的第一阶段代码:单板各个硬件的初始化,我们只有做好了硬件基础才能为下一阶段的引导和启动内核做准备。同时我们也会在第二部分介绍u-boot的一些强大功能。

声明:

        本文主要是看了韦东山老师的视频后所写,希望对你有所帮助。

u-boot版本 : u-boot-1.1.6

开发板 : JZ2440

Linux内核 : Linux-2.6.22.6

 

u-boot第一阶段代码:

        原想的是写完BootLoader就可以对u-boot这部分的内容告一段落了,但是仔细查看u-boot代码后发现如果只写了BootLoader而不去完成u-boot的功能描述总还是有些遗憾的。毕竟u-boot无论在代码规范性,功能复杂性上都要远超于BootLoader。同时只有当你可以真正看懂u-boot的时候你才有能力去移植一个现有的u-boot到你想要的开发板。而不是去使用BootLoader去完成一些简单的操作。而如果你想真正的了解u-boot,那你就要按着u-boot的代码一步一步的看下去。同时我希望大家可以在看u-boot的时候结合我上一篇文章:嵌入式Linux——写jz2440BootLoader的第一阶段代码一起看,因为u-boot和BootLoader其实在硬件初始化这方面做得还是很相似的。而真正区分u-boot和BootLoader的是后面的第二阶段代码。下面我们就按着u-boot的代码来了解u-boot的实现。

标识开始以及各个偏移向量:

        当我们做代码重定位即从nand中将u-boot代码转移到SDRAM的时候,我们要知道代码在nand中的地址,但是这时我们的当前程序并不在代码开始位置。所以我们要在代码的开始位置用一个标识来标志代码开始的位置。这个标识我们在代码中用_start表示。而对于其他的突发情况或者错误的情况我们就要用不同的偏移向量引导到对应的操作函数处。这里的开始标识和各个偏移向量的代码为: .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         而其中reset就是当程序开始执行后所要跳转到的地址,这里其实就是我们u-boot代码真正开始的位置。

设置CPU为管理模式:

        这里一开始就设置CPU为管理模式,我想也就是在这里直接给了CPU特权以便让他处理以后的事情更加方便。同时我们想说最开始就从CPU设置,然后慢慢从内向外这样的设计也是不错的。而设置CPU为管理模式的代码为; /* * the actual reset code */ reset: /* * set the cpu to SVC32 mode */ mrs r0,cpsr bic r0,r0,#0x1f orr r0,r0,#0xd3 msr cpsr,r0         我多截了一些代码,其中包括了reset的部分,从上面的注释我们可以知道,这才是u-boot真正开始位置的代码。而关于设置CPU为管理模式的汇编语句就是操作程序状态寄存器中的0~4位为0b10011。

关看门狗,关中断:

        这里我们将关看门狗和关中断放到一起是因为在代码中他们几乎是在一起的。同时我个人认为关看门狗和关中断同样都是准备工作。都是为了防止CPU在执行代码时出现突然重启或者突发中断,进而打断原有程序的进程。而关于关看门狗,关中断的代码为: /* 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 INTMOD 0X4A000004 # 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] #endif         从上面的代码中我们可以看出u-boot有很大的兼容性,如上面的代码他既可以让2400用也可以让2410用,只要你定义你所用单板的宏,他就会对你所选的单板进行设置。而我们的程序只定义了2410而没有定义2400,所以我们只用2410的代码就可以了而不用去看2400相关的代码。

底层相关初始化:

        而在初始化底层硬件之前,我们会看到下面的代码: adr r0, _start /* r0 <- current position of code */ ldr r1, _TEXT_BASE /* test if we run from flash or RAM */ cmp r0, r1         他的意思是通过比较当前地址和连接地址来确定当前是否已经在RAM上运行,如果是在RAM中运行则不做底层相关的初始化,如果不在RAM上运行,则做底层相关的初始化,这里我们的程序还在nand的4Kstepstoneing中运行,并没有在RAM中运行,所以这时我们要做底层相关的初始化,即调用  cpu_init_crit : blne cpu_init_crit         下面我们跳到cpu_init_crit处,看他做了什么工作: 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         从上面看这个底层相关的初始化做了三件事情: 1. 关cache,关TLB 2. 关MMU,并开I-cache 3. 跳转到lowlevel_init处,做SDRAM的初始化         第一和第二件事都是对协处理器的操作,我想大家看看2440的芯片手册或者百度一下就可以很清楚了。这里我为大家推荐一个网址,大家如果不想自己动手去查可以直接点开这个博客:ARM协处理器CP15寄存器详解。下面我们就要讲跳转lowlevel_init执行了,但是我想在讲之前说一下下面的代码: mov ip, lr bl lowlevel_init mov lr, ip mov pc, lr         想问一下大家知不知道我们为什么要有上面这样的代码?这是因为lr寄存器中保存的是我们这个程序的返回地址,而当我们要去调用其他的子程序的时候要先将这个寄存器中的值存到其他的寄存器中,不然当我们调用子函数的时候lr寄存器中会自动放入子程序的返回值,如果我们没有将当前程序的lr值保存到其他寄存器中,那么我们执行完子程序后再执行当前程序,但是执行完当前程序后我们却不能跳回到调用当前函数的函数了,这样函数就会跑飞了。所以当发生多次函数调用的时候我们要记得及时将lr的值保存到其他的寄存器中。         讲完上面的知识,下面我们跳到lowlevel_init来看看这个函数中做了什么事: .globl lowlevel_init lowlevel_init: /* memory control configuration */ /* make r0 relative the current location so that it */ /* reads SMRDATA out of FLASH rather than memory ! */ ldr r0, =SMRDATA ldr r1, _TEXT_BASE sub r0, r0, r1 ldr r1, =BWSCON /* Bus Width Status Controller */ add r2, r0, #13*4 0: ldr r3, [r0], #4 str r3, [r1], #4 cmp r2, r0 bne 0b /* everything is fine now */ mov pc, lr .ltorg /* the literal pools origin */ SMRDATA: .word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28)) .word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC)) .word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC)) .word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC)) .word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC)) .word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC)) .word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC)) .word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN)) .word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN)) .word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT) .word 0xb1 .word 0x30 .word 0x30         这里你会发现上面的代码其实就是对SDRAM的初始化,而他与我们在BootLoader中不一样的部分就是SMRDATA这部分的数据形式不一样而已。在这里他详细的描述了在各个寄存器中各个参数的设置。

设置栈空间:

        讲完底层相关的初始化,我们现在就要讲栈的设置了。前面的代码都是用汇编语言写的,所以可以不用设置栈空间,但是下面的程序中要调用C函数了,所以这里设置栈是非常有必要的。而栈相关的代码为: /* Set up the stack */ 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 */         这里的设置栈不像我们设置BootLoader时那么简单。因为u-boot中添加了一些功能,相应的就会有更多的数据,同样也就需要更过的栈空间来存放这些数据。所以我们要在这里为他们留出空间。而这些数据有: CFG_MALLOC_LEN : 为堆预留的空间 CFG_GBL_DATA_SIZE : 为全局变量预留的空间 CONFIG_STACKSIZE_IRQ : 为中断预留的空间 CONFIG_STACKSIZE_FIQ :为快中断预留的空间          为上述数据留好空间后地址指针再减12字节的空间,我们就可以设置当前的栈顶了。而设置好栈之后我们就可以调用C函数了,而这之后栈空间向下生长。

设置时钟:

        设置好栈空间后就可以调用C函数来实现更加复杂的功能了。而我们首先要做的就是提速,因为只有CPU的运行速率提上来了,我们再运行其他的代码时才能更加的快速,便捷。而设置时钟的代码为: void clock_init(void) { S3C24X0_CLOCK_POWER *clk_power = (S3C24X0_CLOCK_POWER *)0x4C000000; /* support both of S3C2410 and S3C2440, by www.100ask.net */ if (isS3C2410) { /* FCLK:HCLK:PCLK = 1:2:4 */ clk_power->CLKDIVN = S3C2410_CLKDIV; /* change to asynchronous bus mod */ __asm__( "mrc p15, 0, r1, c1, c0, 0 " /* read ctrl register */ "orr r1, r1, #0xc0000000 " /* Asynchronous */ "mcr p15, 0, r1, c1, c0, 0 " /* write ctrl register */ :::"r1" ); /* to reduce PLL lock time, adjust the LOCKTIME register */ clk_power->LOCKTIME = 0xFFFFFFFF; /* configure UPLL */ clk_power->UPLLCON = S3C2410_UPLL_48MHZ; /* some delay between MPLL and UPLL */ delay (4000); /* configure MPLL */ clk_power->MPLLCON = S3C2410_MPLL_200MHZ; /* some delay between MPLL and UPLL */ delay (8000); } else { /* FCLK:HCLK:PCLK = 1:4:8 */ clk_power->CLKDIVN = S3C2440_CLKDIV; /* change to asynchronous bus mod */ __asm__( "mrc p15, 0, r1, c1, c0, 0 " /* read ctrl register */ "orr r1, r1, #0xc0000000 " /* Asynchronous */ "mcr p15, 0, r1, c1, c0, 0 " /* write ctrl register */ :::"r1" ); /* to reduce PLL lock time, adjust the LOCKTIME register */ clk_power->LOCKTIME = 0xFFFFFFFF; /* configure UPLL */ clk_power->UPLLCON = S3C2440_UPLL_48MHZ; /* some delay between MPLL and UPLL */ delay (4000); /* configure MPLL */ clk_power->MPLLCON = S3C2440_MPLL_400MHZ; /* some delay between MPLL and UPLL */ delay (8000); } }         从上面的代码中我们知道这个函数是兼容2410和2440的,当在2410中运行时,走if语句后到else语句前的代码,而在2440中运行时,则走else语句中的代码。在2410中CPU被提速到200MHz,而在2440中CPU被提速到400MHz。而具体的操作就是对寄存器的设置了大家可以去看一下2440的芯片手册。

代码的重定位:

        这里重定位代码就非常的重要了,因为u-boot的体积远远大于4k,所以我们必须将nand中的u-boot代码复制到SDRAM中。下面就是我们重定位的代码了: 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 clear_bss ldr r2, _armboot_start ldr r3, _bss_start sub r2, r3, r2 /* r2 <- size of armboot */ #if 1 bl CopyCode2Ram /* r0: source, r1: dest, r2: size */         这里有测试当前程序是否在RAM中的语句,如果当前程序在RAM中,那么我们将跳过重定位代码而去做清bss的任务。如果没有在RAM中我们就要调用C函数CopyCode2Ram来将nand中的代码复制到SDRAM中。而这里数据传输的三要素:源,目的和长度,分别为: 源 : _start (这里的_start就是我们在u-boot代码开始位置标识的代码开始地址) 目的: _TEXT_BASE (这里_TEXT_BASE就是我们u-boot代码所以复制到的SDRAM中的目的地址) 长度 : _bss_start - _armboot_start (这里的长度就是从代码开始的位置到bss开始的位置)         而CopyCode2Ram函数其实和我们在BootLoader中所用的函数相同,这里就不再细说,我们看函数代码: int CopyCode2Ram(unsigned long start_addr, unsigned char *buf, int size) { unsigned int *pdwDest; unsigned int *pdwSrc; int i; if (bBootFrmNORFlash()) { pdwDest = (unsigned int *)buf; pdwSrc = (unsigned int *)start_addr; /* 从 NOR Flash启动 */ for (i = 0; i < size / 4; i++) { pdwDest[i] = pdwSrc[i]; } return 0; } else { /* 初始化NAND Flash */ nand_init_ll(); /* 从 NAND Flash启动 */ nand_read_ll_lp(buf, start_addr, (size + NAND_BLOCK_MASK_LP)&~(NAND_BLOCK_MASK_LP)); return 0; } }         而从上面的函数我们可以看出这个u-boot不仅可以在nand上使用,同时还可以在nor上使用。

清bss:

        做完上面的代码重定位,我们就要清bss来做u-boot第一阶段代码的扫尾工作了,而这里的清bss的代码使用汇编所写,但是也不难看懂,其主要的意思是使用一个循环语句来将bss段中的各个参数初始化为0 。而清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         做完上面的工作我们就可以跳转到u-boot的第二阶段了。在看完这章的讲解后,大家是不是觉得其实u-boot中的第一阶段代码和BootLoader中的第一阶段代码十分相似啊。确实他们都是做的单板硬件的初始化,而基本的硬件就那么几个,所以两个的第一阶段代码十分相似。但是到了第二阶段就不一样了。第二阶段u-boot的功能比BootLoader的强大很多,故代码量和复杂性更是多很多,所以到时候分析u-boot第二阶段代码就不会像分析BootLoader第二阶段代码那么简单了。同时又由于这部分功能的增加,代码量的增加将会对我们学习u-boot提供更多的帮助。希望我有耐心有毅力写完,写详细第二阶段代码以不辜负我这么长时间的学习。