宋宝华谈 ARM 的嵌入式 Linux 移植体验之二:BootLoader

本文介绍ARM的BootLoader概念及其在嵌入式Linux移植中的作用,详细讲解了BootLoader的两个阶段、开发环境搭建过程及调试方法,并探讨了ARM汇编语言的基本指令。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

宋宝华谈 ARM 的嵌入式 Linux 移植体验之二:BootLoader

BootLoader 指系统启动后,在操作系统内核运行之前运行的一段小程序。通过 BootLoader,我们可以初始化硬件设备、建立内存空间的映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。通常,BootLoader 是严重地依赖于硬件而实现的,特别是在嵌入式世界。因此,在嵌入式世界里建立一个通用的 BootLoader 几乎是不可能的。尽管如此,我们仍然可以对 BootLoader 归纳出一些通用的概念来,以指导用户特定的 BootLoader 设计与实现。
BootLoader 的实现依赖于 CPU 的体系结构,因此大多数 BootLoader 都分为 stage1 和 stage2 两大部分。依赖于 CPU 体系结构的代码,比如设备初始化代码等,通常都放在 stage1 中,而且通常都用汇编语言来实现,以达到短小精悍的目的。而 stage2 则通常用 C 语言来实现,这样可以实现更复杂的功能,而且代码会具有更好的可读性和可移植性。
BootLoader 的 stage1 通常包括以下步骤:
·硬件设备初始化;
·为加载 Boot Loader 的 stage2 准备 RAM 空间;
·拷贝 Boot Loader 的 stage2 到 RAM 空间中;
·设置好堆栈;
·跳转到 stage2 的 C 入口点。
Boot Loader 的 stage2 通常包括以下步骤:
·初始化本阶段要使用到的硬件设备;
·检测系统内存映射(memory map);
·将 kernel 映像和根文件系统映像从 flash 上读到 RAM 空间中;
·为内核设置启动参数;
·调用内核。
本系统中的 BootLoader 参照韩国 mizi 公司的 vivi 进行修改。
1.开发环境
我们购买了武汉创维特信息技术有限公司开发的具有自主知识产权的应用于嵌入式软件开发的集成软、硬件开发平台 ADT(ARM Development Tools)它为基于 ARM 核的嵌入式应用提供了一整套完备的开发方案,包括程序编辑、工程管理和设置、程序编译、程序调试等。
ADT 嵌入式开发环境由 ADT Emulator for ARM 和ADT IDE for ARM 组成。ADT Emulator for ARM 通过 JTAG 实现主机和目标机之间的调试支持功能。它无需目标存储器,不占用目标系统的任何端口资源。目标程序直接在目标板上运行,通过 ARM 芯片的 JTAG 边界扫描口进行调试,属于完全非插入式调试,其仿真效果接近真实系统。
ADT IDE for ARM 为用户提供高效明晰的图形化嵌入式应用软件开发环境,包括一整套完备的面向嵌入式系统的开发和调试工具:源码编辑器、工程管理器、工程编译器(编译器、汇编器和连接器)、集成调试环境、ADT Emulator for ARM 调试接口等。其界面同 Microsoft Visual Studio 环境相似,用户可以在 ADT IDE for ARM 集成开发环境中创建工程、打开工程,建立、打开和编辑文件,编译、连接、设置、运行、调试嵌入式应用程序。
ADT 嵌入式软件开发环境采用主机-目标机交叉开发模型。ADT IDE for ARM 运行于主机端,而 ADT Emulator for ARM 实现 ADT IDE for ARM 与目标机之间的连接。开发时,首先由 ADT IDE for ARM 编译连接生成目标代码,然后建立与 ADT Emulator for ARM 之间的调试通道,调试通道建立成功后,就可以在 ADT IDE for ARM 中通过 ADT Emulator for ARM 控制目标板实现目标程序的调试,包括将目标代码下载到目标机中,控制程序运行,调试信息观察等等。

目标程序的调试
2.ARM 汇编
ARM 本身属于 RISC 指令系统,指令条数就很少,而其编程又以 C 等高级语言为主,我们仅需要在 Bootloader 的第一阶段用到少量汇编指令:
(1)+-运算

ADD r0, r1, r2 ―― r0 := r1 + r2 SUB r0, r1, r2 ―― r0 := r1 - r2
其中的第二个操作数可以是一个立即数:

ADD r3, r3, #1 ―― r3 := r3 + 1
第二个操作数还可以是位移操作后的结果:

ADD r3, r2, r1, LSL #3 ―― r3 := r2 + 8.r1
(2)位运算

AND r0, r1, r2 ―― r0 := r1 and r2 ORR r0, r1, r2 ―― r0 := r1 or r2 EOR r0, r1, r2 ―― r0 := r1 xor r2 BIC r0, r1, r2 ―― r0 := r1 and not r2
(3)寄存器搬移

MOV r0, r2 ―― r0 := r2 MVN r0, r2 ―― r0 := not r2
(4)比较

CMP r1, r2 ―― set cc on r1 - r2 CMN r1, r2 ―― set cc on r1 + r2 TST r1, r2 ―― set cc on r1 and r2 TEQ r1, r2 ―― set cc on r1 or r2
这些指令影响 CPSR 寄存器中的 (N, Z, C, V) 位
(5)内存操作

LDR r0, [r1] ―― r0 := mem [r1] STR r0, [r1] ―― mem [r1] := r0 LDR r0, [r1, #4] ―― r0 := mem [r1+4] LDR r0, [r1, #4] ! ―― r0 := mem [r1+4] r1 := r1 + 4 LDR r0, [r1], #4 ―― r0 := mem [r1] r1 := r1 +4 LDRB r0 , [r1] ―― r0 := mem8 [r1] LDMIA r1, {r0, r2, r5} ―― r0 := mem [r1] r2 := mem [r1+4] r5 := mem [r1+8]
{..} 可以包括 r0~r15 中的所有寄存器,若包括 r15 (PC)将导致程序的跳转。
(6)控制流
例1:

MOV r0, #0 ; initialize counter LOOP: ADD r0, r0, #1 ; increment counter CMP r0, #10 ; compare with limit BNE LOOP ; repeat if not equal
例2:

CMP r0, #5 ADDNE r1, r1, r0 SUBNE r1, r1, r2 ―― if (r0 != 5) { r1 := r1 + r0 - r2 }
3.BootLoader 第一阶段
3.1硬件设备初始化
基本的硬件初始化工作包括:
·屏蔽所有的中断;
·设置 CPU 的速度和时钟频率;
·RAM 初始化;
·初始化 LED
ARM 的中断向量表设置在 0 地址开始的 8 个字空间中,如下表:

ARM的中断向量表
每当其中的某个异常发生后即将 PC 值置到相应的中断向量处,每个中断向量处放置一个跳转指令到相应的中断服务程序去进行处理,中断向量表的程序如下:

@ 0x00: Reset b Reset @ 0x04: Undefined instruction exception UndefEntryPoint: b HandleUndef @ 0x08: Software interrupt exception SWIEntryPoint: b HandleSWI @ 0x0c: Prefetch Abort (Instruction Fetch Memory Abort) PrefetchAbortEnteryPoint: b HandlePrefetchAbort @ 0x10: Data Access Memory Abort DataAbortEntryPoint: b HandleDataAbort @ 0x14: Not used NotUsedEntryPoint: b HandleNotUsed @ 0x18: IRQ(Interrupt Request) exception IRQEntryPoint: b HandleIRQ @ 0x1c: FIQ(Fast Interrupt Request) exception FIQEntryPoint: b HandleFIQ
复位时关闭看门狗定时器、屏蔽所有中断:

Reset: @ disable watch dog timer mov r1, #0x53000000 mov r2, #0x0 str r2, [r1] @ disable all interrupts mov r1, #INT_CTL_BASE mov r2, #0xffffffff str r2, [r1, #oINTMSK] ldr r2, =0x7ff str r2, [r1, #oINTSUBMSK]
设置系统时钟:

@init clk @ 1:2:4 mov r1, #CLK_CTL_BASE mov r2, #0x3 str r2, [r1, #oCLKDIVN] 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 @ now, CPU clock is 200 Mhz mov r1, #CLK_CTL_BASE ldr r2, mpll_200mhz str r2, [r1, #oMPLLCON]
点亮所有的用户 LED:

@ All LED on mov r1, #GPIO_CTL_BASE add r1, r1, #oGPIO_F ldr r2,=0x55aa str r2, [r1, #oGPIO_CON] mov r2, #0xff str r2, [r1, #oGPIO_UP] mov r2, #0x00 str r2, [r1, #oGPIO_DAT]
设置(初始化)内存映射:

ENTRY(memsetup) @ initialise the static memory @ set memory control registers mov r1, #MEM_CTL_BASE adrl r2, mem_cfg_val add r3, r1, #52 1: ldr r4, [r2], #4 str r4, [r1], #4 cmp r1, r3 bne 1b mov pc, lr
设置(初始化)UART:

@ set GPIO for UART mov r1, #GPIO_CTL_BASE add r1, r1, #oGPIO_H ldr r2, gpio_con_uart str r2, [r1, #oGPIO_CON] ldr r2, gpio_up_uart str r2, [r1, #oGPIO_UP] bl InitUART @ Initialize UART @ @ r0 = number of UART port InitUART: ldr r1, SerBase mov r2, #0x0 str r2, [r1, #oUFCON] str r2, [r1, #oUMCON] mov r2, #0x3 str r2, [r1, #oULCON] ldr r2, =0x245 str r2, [r1, #oUCON] #define UART_BRD ((50000000 / (UART_BAUD_RATE * 16)) - 1) mov r2, #UART_BRD str r2, [r1, #oUBRDIV] mov r3, #100 mov r2, #0x0 1: sub r3, r3, #0x1 tst r2, r3 bne 1b #if 0 mov r2, #'U' str r2, [r1, #oUTXHL] 1: ldr r3, [r1, #oUTRSTAT] and r3, r3, #UTRSTAT_TX_EMPTY tst r3, #UTRSTAT_TX_EMPTY bne 1b mov r2, #'0' str r2, [r1, #oUTXHL] 1: ldr r3, [r1, #oUTRSTAT] and r3, r3, #UTRSTAT_TX_EMPTY tst r3, #UTRSTAT_TX_EMPTY bne 1b #endif mov pc, lr
此外,vivi 还提供了几个汇编情况下通过串口打印字符的函数 PrintChar、PrintWord 和 PrintHexWord:

@ PrintChar : prints the character in R0 @ r0 contains the character @ r1 contains base of serial port @ writes ro with XXX, modifies r0,r1,r2 @ TODO : write ro with XXX reg to error handling PrintChar: TXBusy: ldr r2, [r1, #oUTRSTAT] and r2, r2, #UTRSTAT_TX_EMPTY tst r2, #UTRSTAT_TX_EMPTY beq TXBusy str r0, [r1, #oUTXHL] mov pc, lr @ PrintWord : prints the 4 characters in R0 @ r0 contains the binary word @ r1 contains the base of the serial port @ writes ro with XXX, modifies r0,r1,r2 @ TODO : write ro with XXX reg to error handling PrintWord: mov r3, r0 mov r4, lr bl PrintChar mov r0, r3, LSR #8 /* shift word right 8 bits */ bl PrintChar mov r0, r3, LSR #16 /* shift word right 16 bits */ bl PrintChar mov r0, r3, LSR #24 /* shift word right 24 bits */ bl PrintChar mov r0, #'/r' bl PrintChar mov r0, #'/n' bl PrintChar mov pc, r4 @ PrintHexWord : prints the 4 bytes in R0 as 8 hex ascii characters @ followed by a newline @ r0 contains the binary word @ r1 contains the base of the serial port @ writes ro with XXX, modifies r0,r1,r2 @ TODO : write ro with XXX reg to error handling PrintHexWord: mov r4, lr mov r3, r0 mov r0, r3, LSR #28 bl PrintHexNibble mov r0, r3, LSR #24 bl PrintHexNibble mov r0, r3, LSR #20 bl PrintHexNibble mov r0, r3, LSR #16 bl PrintHexNibble mov r0, r3, LSR #12 bl PrintHexNibble mov r0, r3, LSR #8 bl PrintHexNibble mov r0, r3, LSR #4 bl PrintHexNibble mov r0, r3 bl PrintHexNibble mov r0, #'/r' bl PrintChar mov r0, #'/n' bl PrintChar mov pc, r4
3.2Bootloader 拷贝
配置为从 NAND FLASH 启动,需要将 NAND FLASH 中的 vivi 代码 copy 到 RAM 中:

#ifdef CONFIG_S3C2410_NAND_BOOT bl copy_myself @ jump to ram ldr r1, =on_the_ram add pc, r1, #0 nop nop 1: b 1b @ infinite loop #ifdef CONFIG_S3C2410_NAND_BOOT @ @ copy_myself: copy vivi to ram @ copy_myself: mov r10, lr @ reset NAND mov r1, #NAND_CTL_BASE ldr r2, =0xf830 @ initial value str r2, [r1, #oNFCONF] ldr r2, [r1, #oNFCONF] bic r2, r2, #0x800 @ enable chip str r2, [r1, #oNFCONF] mov r2, #0xff @ RESET command strb r2, [r1, #oNFCMD] mov r3, #0 @ wait 1: add r3, r3, #0x1 cmp r3, #0xa blt 1b 2: ldr r2, [r1, #oNFSTAT] @ wait ready tst r2, #0x1 beq 2b ldr r2, [r1, #oNFCONF] orr r2, r2, #0x800 @ disable chip str r2, [r1, #oNFCONF] @ get read to call C functions (for nand_read()) ldr sp, DW_STACK_START @ setup stack pointer mov fp, #0 @ no previous frame, so fp=0 @ copy vivi to RAM ldr r0, =VIVI_RAM_BASE mov r1, #0x0 mov r2, #0x20000 bl nand_read_ll tst r0, #0x0 beq ok_nand_read #ifdef CONFIG_DEBUG_LL bad_nand_read: ldr r0, STR_FAIL ldr r1, SerBase bl PrintWord 1: b 1b @ infinite loop #endif ok_nand_read: #ifdef CONFIG_DEBUG_LL ldr r0, STR_OK ldr r1, SerBase bl PrintWord #endif @ verify mov r0, #0 ldr r1, =0x33f00000 mov r2, #0x400 @ 4 bytes * 1024 = 4K-bytes go_next: ldr r3, [r0], #4 ldr r4, [r1], #4 teq r3, r4 bne notmatch subs r2, r2, #4 beq done_nand_read bne go_next notmatch: #ifdef CONFIG_DEBUG_LL sub r0, r0, #4 ldr r1, SerBase bl PrintHexWord ldr r0, STR_FAIL ldr r1, SerBase bl PrintWord #endif 1: b 1b done_nand_read: #ifdef CONFIG_DEBUG_LL ldr r0, STR_OK ldr r1, SerBase bl PrintWord #endif mov pc, r10 @ clear memory @ r0: start address @ r1: length mem_clear: mov r2, #0 mov r3, r2 mov r4, r2 mov r5, r2 mov r6, r2 mov r7, r2 mov r8, r2 mov r9, r2 clear_loop: stmia r0!, {r2-r9} subs r1, r1, #(8 * 4) bne clear_loop mov pc, lr #endif @ CONFIG_S3C2410_NAND_BOOT
3.3进入 C 代码
首先要设置堆栈指针 sp,堆栈指针的设置是为了执行 C 语言代码作好准备。设置好堆栈后,调用 C 语言的 main 函数:

@ get read to call C functions ldr sp, DW_STACK_START @ setup stack pointer mov fp, #0 @ no previous frame, so fp=0 mov a2, #0 @ set argv to NULL bl main @ call main mov pc, #FLASH_BASE @ otherwise, reboot
4. BootLoader 第二阶段
vivi Bootloader 的第二阶段又分成了八个小阶段,在 main 函数中分别调用这几个小阶段的相关函数:

int main(int argc, char *argv[]) { int ret; /* * Step 1: */ putstr("/r/n"); putstr(vivi_banner); reset_handler(); /* * Step 2: */ ret = board_init(); if (ret) { putstr("Failed a board_init() procedure/r/n"); error(); } /* * Step 3: */ mem_map_init(); mmu_init(); putstr("Succeed memory mapping./r/n"); /* * Now, vivi is running on the ram. MMU is enabled. */ /* * Step 4: */ /* initialize the heap area*/ ret = heap_init(); if (ret) { putstr("Failed initailizing heap region/r/n"); error(); } /* Step 5: */ ret = mtd_dev_init(); /* Step 6: */ init_priv_data(); /* Step 7: */ misc(); init_builtin_cmds(); /* Step 8: */ boot_or_vivi(); return 0; }
STEP1 的 putstr(vivi_banner)语句在串口输出一段字符说明 vivi 的版本、作者等信息,vivi_banner 定义为:

const char *vivi_banner = "VIVI version " VIVI_RELEASE " (" VIVI_COMPILE_BY "@" VIVI_COMPILE_HOST ") (" VIVI_COMPILER ") " UTS_VERSION "/r/n";
reset_handler 进行相应的复位处理:

void reset_handler(void) {  int pressed;  pressed = is_pressed_pw_btn();  if (pressed == PWBT_PRESS_LEVEL) {   DPRINTK("HARD RESET/r/n");   hard_reset_handle();  } else {   DPRINTK("SOFT RESET/r/n");   soft_reset_handle();  } }
hard_reset_handle 会 clear 内存,而软件复位处理则什么都不做:

static void hard_reset_handle(void) {  clear_mem((unsigned long)USER_RAM_BASE, (unsigned long)USER_RAM_SIZE); }
STEP2 进行板初始化,设置时间和可编程 I/O 口:

int board_init(void) {  init_time();  set_gpios();  return 0; }
STEP3 进行内存映射及 MMU 初始化:

void mem_map_init(void) {  #ifdef CONFIG_S3C2410_NAND_BOOT   mem_map_nand_boot();  #else   mem_map_nor();  #endif  cache_clean_invalidate();  tlb_invalidate(); }
S3C2410A 的 MMU 初始化只需要调用通用的 arm920 MMU 初始化函数:

static inline void arm920_setup(void) { unsigned long ttb = MMU_TABLE_BASE; __asm__( /* Invalidate caches */ "mov r0, #0/n" "mcr p15, 0, r0, c7, c7, 0/n" /* invalidate I,D caches on v4 */ "mcr p15, 0, r0, c7, c10, 4/n" /* drain write buffer on v4 */ "mcr p15, 0, r0, c8, c7, 0/n" /* invalidate I,D TLBs on v4 */ /* Load page table pointer */ "mov r4, %0/n" "mcr p15, 0, r4, c2, c0, 0/n" /* load page table pointer */ /* Write domain id (cp15_r3) */ "mvn r0, #0/n" /* Domains 0, 1 = client */ "mcr p15, 0, r0, c3, c0, 0/n" /* load domain access register */ /* Set control register v4 */ "mrc p15, 0, r0, c1, c0, 0/n" /* get control register v4 */ /* Clear out 'unwanted' bits (then put them in if we need them) */ /* .RVI ..RS B... .CAM */ "bic r0, r0, #0x3000/n" /* ..11 .... .... .... */ "bic r0, r0, #0x0300/n" /* .... ..11 .... .... */ "bic r0, r0, #0x0087/n" /* .... .... 1... .111 */ /* Turn on what we want */ /* Fault checking enabled */ "orr r0, r0, #0x0002/n" /* .... .... .... ..1. */ #ifdef CONFIG_CPU_D_CACHE_ON "orr r0, r0, #0x0004/n" /* .... .... .... .1.. */ #endif #ifdef CONFIG_CPU_I_CACHE_ON "orr r0, r0, #0x1000/n" /* ...1 .... .... .... */ #endif /* MMU enabled */ "orr r0, r0, #0x0001/n" /* .... .... .... ...1 */ "mcr p15, 0, r0, c1, c0, 0/n" /* write control register */ : /* no outputs */ : "r" (ttb) ); }
STEP4 设置堆栈;STEP5 进行 mtd 设备的初始化,记录 MTD 分区信息;STEP6 设置私有数据;STEP7 初始化内建命令。
STEP8 启动一个 SHELL,等待用户输出命令并进行相应处理。在 SHELL 退出的情况下,启动操作系统:

#define DEFAULT_BOOT_DELAY 0x30000000 void boot_or_vivi(void) { char c; int ret; ulong boot_delay; boot_delay = get_param_value("boot_delay", &ret); if (ret) boot_delay = DEFAULT_BOOT_DELAY; /* If a value of boot_delay is zero, * unconditionally call vivi shell */ if (boot_delay == 0) vivi_shell(); /* * wait for a keystroke (or a button press if you want.) */ printk("Press Return to start the LINUX now, any other key for vivi/n"); c = awaitkey(boot_delay, NULL); if (((c != '/r') && (c != '/n') && (c != '/0'))) { printk("type /"help/" for help./n"); vivi_shell(); } run_autoboot(); return; }
SHELL 中读取用户从串口输出的命令字符串,执行该命令:

void vivi_shell(void) {  #ifdef CONFIG_SERIAL_TERM   serial_term();  #else   #error there is no terminal.  #endif } void serial_term(void) {  char cmd_buf[MAX_CMDBUF_SIZE];  for (;;) {   printk("%s> ", prompt);   getcmd(cmd_buf, MAX_CMDBUF_SIZE);   /* execute a user command */   if (cmd_buf[0])    exec_string(cmd_buf);  } }
5.电路板调试
在电路板的调试过程中,我们首先要在 ADT 新建的工程中添加第一阶段的汇编代码 head.S 文件,修改 Link 脚本,将代码和数据映射到 S3C2410A 自带的 0x40000000 开始的 4KB 内存空间内:

SECTIONS { . = 0x40000000; .text : { *(.text) } Image_RO_Limit = .; Image_RW_Base = .; .data : { *(.data) } .rodata : { *(.rodata) } Image_ZI_Base = .; .bss : { *(.bss) } Image_ZI_Limit = .; __bss_start__ = .; __bss_end__ = .; __EH_FRAME_BEGIN__ = .; __EH_FRAME_END__ = .; PROVIDE (__stack = .); end = .; _end = .; .debug_info 0 : { *(.debug_info) } .debug_line 0 : { *(.debug_line) } .debug_abbrev 0 : { *(.debug_abbrev)} .debug_frame 0 : { *(.debug_frame) } }
借助万用表、示波器等仪器仪表,调通 SDRAM,并将 vivi 中自带的串口、NAND FLASH 驱动添加到工程中,调试通过板上的串口和 FLASH。如果板电路的原理与三星公司 DEMO 板有差距,则 vivi 中硬件的操作要进行相应的修改。全部调试通过后,修改 vivi 源代码,重新编译 vivi,将其烧录入 NAND FLASH 就可以在复位后启动这个 Bootloader 了。
调试板上的新增硬件时,宜在 ADT 中添加相应的代码,在不加载操作系统的情况下,单纯地操作这些硬件。如果电路板设计有误,要进行飞线和割线等处理。
6.小结
本章讲解了 ARM 汇编、Bootloader 的功能,Bootloader 的调试环境及 ARM 电路板的调试方法。
原文链接:http://dev.yesky.com/6/2527006.shtml

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值