嵌入式Linux开发之uboot启动Linux整体流程分析
Uboot全称Universal Boot Loader,一个遵循GPL协议的的开源项目,其作用是引导操作系统,支持引导linux、VxWorks、Solaris等操作系统;其源码组织形式和linux源码很相似,编译也可参照linux源码编译,且包含许多linux源码中的驱动源码,所以uboot实际上可以算作一个微型的操作系统,可以做一些简单工作。
本文的分析对象是u-boot-2012.10版本,以下内容将根据此版本源码和特定的board展开分析,从设备上电运行的第一行程序开始到引导linux系统正常启动。
一、uboot 目录组织形式:
1、../u-boot-2012.10 //一级目录:
├── api
├── arch
├── board
├── common
├── disk
├── doc
├── drivers
├── dts
├── examples
├── fs
├── include
├── lib
├── nand_spl
├── net
├── post
├── spl
├── test
└── tools
可见其目录组织样式和linux是极其相似的。
2、arch为所支持的体系结构,内容如下:
├── arch
│ ├── arm
│ ├── avr32
│ ├── blackfin
│ ├── m68k
│ ├── microblaze
│ ├── mips
│ ├── nds32
│ ├── nios2
│ ├── openrisc
│ ├── powerpc
│ ├── sandbox
│ ├── sh
│ ├── sparc
│ └── x86
看得出来uboot支持的硬件体系是很全面的。
3、board目录是目前已适配的板子:
├── board
│ ├── a3000
│ ├── a4m072
│ ├── actux1
│ ├── actux2
│ ├── actux3
│ ├── actux4
│ ├── adder
│ ├── afeb9260
│ ├── … //太多,此处不再列举
其他目录就不一一列举,和linux相比,还是熟悉的味道,还是熟悉的配方。
好了,进入正题,uboot的启动本身分为两个大的阶段,第一阶段是从存储介质中读取小部分程序到cpu中,这部分程序要完成引导linux所用的硬件的初始化,以及加载uboot其余程序到RAM中;第二阶段是继续初始化必备硬件,加载linux镜像到RAM中,把执行权限交给linux,完成使命。
二、Uboot启动第一阶段:
主脉络:部分硬件初始化——>加载完整uboot到RAM——>跳转到第二阶段入口开始执行
整个过程最重要的两个文件:
start.S,汇编语言文件,涉及到特定硬件设备的读写寄存器操作以及特定体系结构的汇编语言;
lowlevel_init.S,设计的操作和文件名吻合,即完成底层的初始化。
1、执行流程分析:
①、中断向量表
1 .globl _start //定义一个全局标号_star 2 _start: b reset //标号_star处的内容为跳转到reset标号开始执行 3 //将_undefined_instruction标号表示的内容作为地址,加载到PC中, 4 //这种用法可以实现执行流程的跳转 5 ldr pc, _undefined_instruction 6 ldr pc, _software_interrupt 7 ldr pc, _prefetch_abort 8 ldr pc, _data_abort 9 ldr pc, _not_used 10 ldr pc, _irq 11 ldr pc, _fiq 12 //以上七条ldr pc,x加上b reset共八条指令组成中断向量表 13 … 14 //_undefined_instruction标号表示定义了一个word类型的变量undefined_instruction 15 _undefined_instruction: .word undefined_instruction 16 … 17 //exception handlers //异常处理 18 .align 5 //5字节对齐 19 20 //可知undefined_instruction的真正用途是指向此处代码,即异常处理的具体实现 21 undefined_instruction: 22 get_bad_stack 23 bad_save_user_regs 24 bl do_undefined_instruction
由以上内容可知,除第一行代码外,其余代码都是跳转到特定位置去执行中断服务子程序。
由b reset可知程序正常的流程并不会走到中断处理流程中去(正常情况下当然不应该执行中断子程序,只有发生中断时才去执行),而是直接跳转到reset标号处开始执行。
②、reset:
1 reset: 2 /* 3 * set the cpu to SVC32 mode 设置CPU为SVC32模式 4 */ 5 mrs r0, cpsr //读出 6 bic r0, r0, #0x1f //低五位清0 7 orr r0, r0, #0xd3 //与D3做与操作 8 msr cpsr,r0 //写回
CPSR是ARM体系结构中的程序状态寄存器,其结构如下:
M[4:0] 处理器模式 可访问的寄存器
ob10000 user pc,r14~r0,CPSR
0b10001 FIQ PC,R14_FIQ-R8_FIQ,R7~R0,CPSR,SPSR_FIQ
0b10010 IRQ PC,R14_IRQ-R13_IRQ,R12~R0,CPSR,SPSR_IRQ
0B10011 SUPERVISOR PC,R14_SVC-R13_SVC,R12~R0,CPSR,SPSR_SVC
0b10111 ABORT PC,R14_ABT-R13_ABT,R12~R0,CPSR,SPSR_ABT
0b11011 UNDEFINEED PC,R14_UND-R8_UND,R12~R0,CPSR,SPSR_UND
0b11111 SYSTEM PC,R14-R0,CPSR(ARM V4以及更高版本)
I、F、T三位如果写1即禁用,所以以上四句操作的结果为设置CPU为SUPERVISOR模式且禁用中断,至于为什么选择当前模式而不是其他模式?
首先可以排除的是,中止abt和未定义und模式,那都是不太正常的模式;
其次,对于快中断fiq和中断irq来说,此处uboot初始化的时候,也还没啥中断要处理和能够处理,而且即使是注册了终端服务程序后,能够处理中断,那么这两种模式,也是自动切换过去的,所以,此处也不应该设置为其中任何一种模式。
至于usr模式,由于此模式无法直接访问很多的硬件资源,而uboot初始化,就必须要去访问这类资源,所以此处可以排除,不能设置为用户usr模式。
而svc模式本身就属于特权模式,本身就可以访问那些受控资源,而且,比sys模式还多了些自己模式下的影子寄存器,所以,相对sys模式来说,可以访问资源的能力相同,但是拥有更多的硬件资源。
好了,接着上面的源码继续向下分析:
1 #ifndef CONFIG_SKIP_LOWLEVEL_INIT 2 bl cpu_init_cp15 3 bl cpu_init_crit 4 #endif
③、cpu_init_cp15:
CP15是协处理器,uboot引导时不需要这部分功能,所以相关的寄存器都配置为不工作状态。
1 /************************************************************************* 2 * Setup CP15 registers (cache, MMU, TLBs). The I-cache is turned on unless 3 * CONFIG_SYS_ICACHE_OFF is defined. 4 *************************************************************************/ 5 ENTRY(cpu_init_cp15) 6 /* 7 * Invalidate L1 I/D 8 */ 9 mov r0, #0 @ set up for MCR 10 mcr p15, 0, r0, c8, c7, 0 @ invalidate TLBs 11 mcr p15, 0, r0, c7, c5, 0 @ invalidate icache 12 mcr p15, 0, r0, c7, c5, 6 @ invalidate BP array 13 mcr p15, 0, r0, c7, c10, 4 @ DSB 14 mcr p15, 0, r0, c7, c5, 4 @ ISB 15 /* 16 * disable MMU stuff and caches 17 */ 18 mrc p15, 0, r0, c1, c0, 0 19 bic r0, r0, #0x00002000 @ clear bits 13 (--V-) 20 bic r0, r0, #0x00000007 @ clear bits 2:0 (-CAM) 21 orr r0, r0, #0x00000002 @ set bit 1 (--A-) Align 22 orr r0, r0, #0x00000800 @ set bit 11 (Z---) BTB 23 24 #ifdef CONFIG_SYS_ICACHE_OFF 25 bic r0, r0, #0x00001000 @ clear bit 12 (I) I-cache 26 #else 27 orr r0, r0, #0x00001000 @ set bit 12 (I) I-cache 28 #endif 29 30 mcr p15, 0, r0, c1, c0, 0 31 mov pc, lr @ back to my caller 32 33 ENDPROC(cpu_init_cp15)
执行完这部分代码后返回跳转点继续向下执行,即cpu_init_crit。
④、cpu_init_crit:
1 ENTRY(cpu_init_crit) 2 /* 3 * Jump to board specific initialization... 4 * The Mask ROM will have already initialized 5 * basic memory. Go here to bump up clock rate and handle 6 * wake up conditions. 7 */ 8 b lowlevel_init @ go setup pll,mux,memory 9 ENDPROC(cpu_init_crit)
接下来会跳转到lowlevel_init去执行,所做工作即注释所提及的,初始化PLL\MUX\MEM等
⑤、lowlevel_init:
1 ENTRY(lowlevel_init) 2 // Setup a temporary stack 3 ldr sp, =CONFIG_SYS_INIT_SP_ADDR 4 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */ 5 6 // Save the old lr(passed in ip) and the current lr to stack 7 push {ip, lr} 8 9 // go setup pll, mux, memory 10 bl s_init 11 pop {ip, pc} 12 ENDPROC(lowlevel_init)
而s_init是个C函数,所以在调用之前需要准备堆栈。在这个函数中做了一系列的初始化操作,其实就是禁用看门狗等,可以看到很多板子的初始化操作会直接忽略这一部分。
1 void s_init(void) 2 { 3 … 4 /* Watchdog init */ 5 writew(0xA500, &rwdt0->rwtcsra0); 6 writew(0xA500, &rwdt1->rwtcsra0); 7 … 8 /* CPG */ 9 writel(0xFF800080, &cpg->rmstpcr4); 10 writel(0xFF800080, &cpg->smstpcr4); 11 … 12 }
执行完这个函数后执行流程返回到lowlevel_init,再返回到调用cpu_init_crit的地方,流程往下执行到call_board_init_f。
⑥、call_board_init_f:
1 void board_init_f(ulong bootflag) 2 { 3 bd_t *bd; 4 init_fnc_t **init_fnc_ptr; 5 gd_t *id; 6 ulong addr, addr_sp; 7 8 bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_F, "board_init_f"); 9 10 gd = (gd_t *) ((CONFIG_SYS_INIT_SP_ADDR) & ~0x07); 11 12 memset((void *)gd, 0, sizeof(gd_t)); 13 gd->mon_len = _bss_end_ofs; 14 ..//gd全局结构体成员复制 15 16 for (init_fnc_ptr = init_sequence; *init_fnc_ptr; ++init_fnc_ptr) { 17 if ((*init_fnc_ptr)() != 0) { 18 hang (); 19 } 20 } 21 ... ..//gd全局结构体成员复制 22 gd->bd->bi_baudrate = gd->baudrate; 23 /* Ram ist board specific, so move it to board code ... */ 24 dram_init_banksize(); 25 display_dram_config(); /* and display it */ 26 ... 27 gd->relocaddr = addr; 28 gd->start_addr_sp = addr_sp; 29 gd->reloc_off = addr - _TEXT_BASE; 30 debug("relocation Offset is: %08lx\n", gd->reloc_off); 31 memcpy(id, (void *)gd, sizeof(gd_t)); 32 33 relocate_code(addr_sp, id, addr); //调用start.s中的汇编函数 34 }
以上C函数完成了gt结构体部分成员赋值,这个结构体将会在之后的流程中用到;此函数最后调用汇编函数,执行流程再次跳转。
⑦、relocate_code:
1 ENTRY(relocate_code) 2 mov r4, r0 /* save addr_sp */ 3 mov r5, r1 /* save addr of gd */ 4 mov r6, r2 /* save addr of destination */ 5 6 /* Set up the stack */ 7 stack_setup: 8 … //堆栈处理 9 copy_loop: 10 … //循环拷贝uboot到RAM 11 //这个过程只能用到ARM的通用寄存器,所以每次只能拷贝几字节,循环 12 clear_bss: 13 …//bss段,由于是未初始化数据,没什么用,无需拷贝,直接留出空间,并初始化为0即可 14 jump_2_ram: //调到第二阶段,即调到RAM中去执行 15 … 16 ldr r0, _board_init_r_ofs 17 adr r1, _start 18 add lr, r0, r1 19 add lr, lr, r9 20 /* 上面下面都是调用C函数的准备工作 */ 21 mov r0, r5 /* gd_t */ 22 mov r1, r6 /* dest_addr */ 23 /* jump to it ... */ 24 mov pc, lr 25 ENDPROC(relocate_code)
经过以上诸多过程,uboot已经把自己从flash中拷贝到RAM中,并且为之后的执行准备好了各种参数,最终跳转到第二部分的入口call_board_init_r。
⑧、call_board_init_r:
1 { 2 gd = id; 3 4 gd->flags |= GD_FLG_RELOC; /* tell others: relocation done */ 5 bootstage_mark_name(BOOTSTAGE_ID_START_UBOOT_R, "board_init_r"); 6 board_init(); /* Setup chipselects */ 7 mem_malloc_init (malloc_start, TOTAL_MALLOC_LEN); 8 9 env_relocate(); //环境变量初始化 10 stdio_init(); //标准IO初始化 11 jumptable_init(); 12 console_init_r(); //终端初始化 13 load_addr = getenv_ulong("loadaddr", 16, load_addr); 14 15 for (;;)
{ 16 //一切准备就绪后进入死循环,此循环用于判断是否有用户输入,并且随之做出相应 17 main_loop(); 18 } 19 }
此函数执行的操作是后一阶段的各种初始化,为引导linux kernel做好准备,接下来分析一下main_loop()的大致过程。
三、uboot流程第二阶段:
①、Main_loop经过简化后如下所示,看见,这是个很清晰很简单的流程,检测是否有按键按下,没有则倒计时,有则开始处理命令相关的内容。
1 void main_loop (void) 2 { 3 s = getenv ("bootcmd"); //获取引导命令 4 5 //检测是否有按键按下,有则执行下面的死循环 6 if (bootdelay != -1 && s && !abortboot(bootdelay)) 7 { 8 run_command_list(s, -1, 0); 9 } 10 for (;;) 11 { 12 len = readline (CONFIG_SYS_PROMPT); 13 flag = 0; /* assume no special flags for now */ 14 if (len > 0) 15 strcpy (lastcommand, console_buffer); 16 else if (len == 0) 17 flag |= CMD_FLAG_REPEAT; 18 19 //输入命令的的合法性验证 20 if (len == -1) 21 puts ("<INTERRUPT>\n"); 22 else 23 rc = run_command(lastcommand, flag); //执行命令 24 25 if (rc <= 0) { 26 /* invalid command or not repeatable, forget it */ 27 lastcommand[0] = 0; 28 } 29 }
}
②、流程转到run_command,经简化可得:这部分是对函数的进一步封装,这里其实是有两个流程的,一个是有关哈希查找命令的,另一个就是下面这个,简单粗暴的。
↓
1 int run_command(const char *cmd, int flag) 2 { 3 if (builtin_run_command(cmd, flag) == -1) 4 return 1; 5 return 0; 6 }
③、流程转到r builtin_run_command,经简化可得:这里所做的各种为完整解析命令,并调用函数去进一步执行。
↓
1 static int builtin_run_command(const char *cmd, int flag) 2 { 3 //合法性校验 4 while (*str) { 5 //特殊字符解析 6 } 7 process_macros (token, finaltoken); //宏展开,即完全解析命令 8 9 //命令执行过程 10 if (cmd_process(flag, argc, argv, &repeatable)) 11 rc = -1; 12 return rc ? rc : repeatable; 13 }
④、流程转到cmd_process,经简化可得:得到完整的命令和参数,执行命令。
↓
1 cmd_process(int flag, int argc, char * const argv[], 2 int *repeatable) 3 { 4 cmd_tbl_t *cmdtp; 5 6 cmdtp = find_cmd(argv[0]); //查找命令 7 if (cmdtp == NULL) { 8 printf("Unknown command '%s' - try 'help'\n", argv[0]); 9 return 1; 10 } 11 12 if (argc > cmdtp->maxargs) 13 rc = CMD_RET_USAGE; 14 15 /* If OK so far, then do the command */ 16 if (!rc) { 17 rc = cmd_call(cmdtp, flag, argc, argv); //真正的执行命令 18 *repeatable &= cmdtp->repeatable; 19 } 20 return rc; 21 }
至此,uboot的使命便完成了,将舞台交给linux。