bootloader是嵌入式系统上电后第一段执行的代码,它先初始化CPU和相关的硬件,建立内存空间映射,把内核或应用程序加载到相应的内存执行位置,最后调用内核或应用程序,释放CPU控制权,完成整个bootloader的流程。

bootloader以及应用程序开发往往是离不开C库的,笔者此处就s3c2416基于newlib的bootloader以及裸机应用开发作一个简单的介绍。

1、bootloader概述

一般对于一个bootloader应实现两种不同的操作模式:启动加载模式和下载模式。

  • 启动加载模式就是bootloader从设备系统的某个固态储存器中将操作系统或应用程序加载进RAM,整个过程无需用户的介入,这种模式是设备成型后bootloader的正常工作模式。
  • 下载模式是bootloader可以通过串口、USB或网络等通信手段从主机下载文件,并最终写入到到设备系统的某个固态储存器中,这种模式用在设备开发调试中,用于刷机升级。

此处笔者从自己的角度简单介绍s3c2416 bootloader实现的一般流程。

与bootloader相关的代码文件放在System目录文件夹中,目录架构如下:

s3c2416.S,启动代码文件,代码执行时的入口,控制了整个bootloader的执行流程,最终建立起c运行环境后,进入到应用程序main入口。
LowLevelInit.S,板级初始化代码,包括CPU时钟的初始化、DDR2内存控制器初始化、代码加载实现(sd/mmc卡启动、Nand flash启动)。
Coprocessor.c,协处理器相关代码,主要是CP15协处理器I/D  Cache、MMU内存映射、中断向量基址等操作代码。
NandBoot.c,实现Nand flash启动相关Nand驱动代码,实现相应Nand代码下载、Nand代码启动的接口实现,支持IROM Nand、Nand Boot启动。
Download.c,实现代码下载调试功能,支持代码直接下载内存调试运行,支持代码下载进Nand flash以及Nand下载固化。
Exception.c,异常处理相关代码,支持中断处理的完整架构,如中断嵌套处理、中断注册、中断开启等接口实现,为系统应用架构实现。
Retarge.c,所有newlib操作系统移植层的重定向实现,如标准输入、输出流重定向到串口、文件操作重定向到相应的文件系统接口实现等,大部分为空实现。
s3c2416.lds,gcc下链接文件,指定板载内存范围,代码以及RAM等布局。

1.1 异常向量表

ARM核在异常发生时,会从异常向量表中取得相应的异常向量地址执行,发生IRQ中断则从异常向量表0x18偏移处取得IRQ异常向量地址。异常向量表就是用来记录各个异常进入时的代码处理位置。除了Cortex-M处理器有嵌套向量中断控制器(NVIC),以下的异常向量处理对所有系列的ARM核均是成立的。

    .text
    .arm
    .align  2
    .globl  _start

_start:
    B   Reset_Handler
    LDR PC, Undef_Addr
    LDR PC, SWI_Addr
    LDR PC, PAbt_Addr
    LDR PC, DAbt_Addr
    LDR PC, Notuse_Addr
    LDR PC, IRQ_Addr
    LDR PC, FIQ_Addr

    .word   0x55aa55aa // 0x20位置用来判断代码的运行区域
    .extern Undef_Handler
    .extern SWI_Handler
    .extern PAbt_Handler
    .extern DAbt_Handler
    .extern IRQ_Handler
    .extern FIQ_Handler

Reset_Addr:
    .word   Reset_Handler
Undef_Addr:
    .word   Undef_Handler
SWI_Addr:
    .word   SWI_Handler
PAbt_Addr:
    .word   PAbt_Handler
DAbt_Addr:
    .word   DAbt_Handler
Notuse_Addr:
    .word   0          // Reserved Address
IRQ_Addr:
    .word   IRQ_SaveContext
FIQ_Addr:
    .word   FIQ_Handler

IRQ_SaveContext:
// 保存中断上下文,支持中断嵌套            
    SUB LR, LR, #4 // 计算返回地址
    STMFD SP!, {R0-R12, LR} // 所有寄存器压栈保存
    MRS     R0, SPSR // 保存中断前的CPSR(即现在的SPSR)
    STMFD   SP!, {R0}
    MSR CPSR_cxsf, #(Mode_SYS |I_Bit) // 切换到系统模式   
    STMFD   SP!, {LR} // 压栈系统模式LR
    LDR     R0, =IRQ_Handler // 系统模式下进行IRQ代码处理
    MOV LR, PC // 准备函数的返回地址
    BX  R0 // 调用中断处理函数
    LDMFD   SP!, {LR} // 出栈系统模式LR
    MSR CPSR_cxsf, #(Mode_IRQ |I_Bit) // 切换到IRQ模式
    LDMFD   SP!, {R0} // 返回中断前的CPSR
    MSR SPSR_cxsf, R0
    LDMFD  SP!, {R0-R12, PC}^// ^表同时从spsr恢复给cpsr

1.2 关看门狗

复位代码最先做的事应该是关看门狗,因为如果看门狗打开的话,在启动代码进行初始化过程中是无法喂狗的,可能造成处理器一直不停复位。

// 看门狗关闭
    LDR R0, =WT_BASE
    LDR R1, =0
    STR R1, [R0]

1.3 关闭所有中断

启动代码未完成时,各个状态都还不是确定的,如果有中断打开并引起中断异常,可能造成代码跑飞。

// 关闭所有外设中断
    LDR R0, =INT_BASE
    LDR R1, =0xFFFFFFFF
    STR R1, [R0, #INTMSK1_OFS]
    STR R1, [R0, #INTMSK2_OFS]
    STR R1, [R0, #INTSUBMSK_OFS]

1.4 初始化系统时钟

一般来说,处理器复位后都是运行在一个较低速的时钟下,为加快启动,通常尽可能快地设置处理器的各个时钟。

// 系统时钟设置
    .extern Clock_Init
    BL  Clock_Init

1.5 初始化外部内存

除了Nor flash可以直接执行代码外,其它的代码存储器如nand flash、sd/mmc都是不能直接执行代码的。一般对于应用处理器,代码都是加载进RAM后执行。

// 外部内存控制设置
    .extern ERAM_Init
    BL  ERAM_Init ;// 外部RAM初始化
    LDR SP, =__StackTop

1.6 代码加载

对于代码存储在Nand flash、sd/mmc等不能直接执行代码的存储器,bootloader是一定需要把用户代码从这些设备读入到特定的内存中执行的。而对于Nor flash可直接执行代码的存储器,通常为了提高性能,也是会把代码从Nor flash读出,在内存中执行的。

// 拷贝用户代码到RAM
    .extern CopyCodeToRAM
    BL  CopyCodeToRAM

1.7 下载模式

在调试开发阶段,可以通过直接下载代码到RAM调试执行,避免重复地烧写固态存储器,加快调试开发,在确定代码OK后,再把代码下载进固态存储器(Nandflash)即可。在开发完成后,下载模式代码可注释掉。

// 检查是否进入串口下载模式,开机时按住空格键进入串口下载模式
    .extern DownloadCheck
    BL  DownloadCheck

1.8 MMU内存映射

为了提高CPU的处理性能,MMU必须开启,在开启之前,需先建立相应的内存空间映射表,设置相应的内存区域访问权限等。开启MMU后,也必须同时开启I/D Cache,不然CPU的性能将极其低下,有几十倍的性能差异。

// MMU初始化   
    LDR SP, =__StackTop
    .extern MMU_Init
    BL  MMU_Init

1.9 初始化栈

ARM核有多种工作模式,每种模式的栈是一定要分配及初始化的,这样才能在相应模式下正确地执行代码。

// 堆栈初始化
    LDR R0, =__StackTop
//  EnterUndefined Instruction Mode and set its Stack Pointer
    MSR CPSR_c, #(Mode_UND | I_Bit | F_Bit)
    MOV SP, R0
    SUB R0, R0, #UND_Stack_Size
//  Enter AbortMode and set its Stack Pointer
    MSR CPSR_c, #(Mode_ABT | I_Bit | F_Bit)
    MOV SP, R0
    SUB R0, R0, #ABT_Stack_Size
//  Enter FIQMode and set its Stack Pointer
    MSR CPSR_c, #(Mode_FIQ | I_Bit | F_Bit)
    MOV SP, R0
    SUB R0, R0, #FIQ_Stack_Size
//  Enter IRQMode and set its Stack Pointer
    MSR CPSR_c, #(Mode_IRQ | I_Bit | F_Bit)
    MOV SP, R0
    SUB R0, R0, #IRQ_Stack_Size
//  EnterSupervisor Mode and set its Stack Pointer
    MSR CPSR_c, #(Mode_SVC | I_Bit | F_Bit)
    MOV SP, R0
    SUB R0, R0, #SVC_Stack_Size
//  Enter SystemMode and set its Stack Pointer
    MSR CPSR_c, #Mode_SYS
    MOV SP, R0
    SUB SL, SP, #USR_Stack_Size

1.10 初始化C运行环境

进入c入口之前是需要初始化c环境(如:清0全局变量、静态变量区);对于加载域与运行域不一致的段,需从加载域加载进运行域;对于c++,可能需执行构造函数等。此处用c开发,并且已经由bootloader把整个代码段、数据段等加载进RAM运行域了。

Clear_bss:
    LDR R0, =_sbss
    LDR R1, =_ebss
    MOV R2, #0
Clear_bss_loop:
    CMP R0, R1
    BGE Clear_bss_done
    STR R2, [R0], #4
    B   Clear_bss_loop
Clear_bss_done:

1.11 跳转到main

整个c环境构架起来后,即可跳转到应用程序入口main函数,用绝对地址跳转到c入口main,bootloader释放CPU控制权,完成整个流程。

// 绝对地址跳转到c入口
    .extern  main
    LDR R0, =main
    BX  R0

2. Makefile

Makefile用来构建和管理工程,工程顶级目录下有三个make文件config.mkMakefilerules.mkrules.mk,用来解决依赖关系,修改了某一文件,将会重新编译所有依赖于该文件的源文件。
Makefile,控制各个目录的编译、链接生成相应的目标对象。
config.mk,不同的项目,只需改config.mk,该文件定义了交叉工具链、项目名、需编译的目录、编译选项、链接文件、库链接等。其中在这个文件指定c库链接为newlib

3. 链接文件

s3c2416.lds为项目链接文件,一般无需更改。bootloader相关的代码链接在最开始的8k范围内,链接文件提供相应的段位置信息来界定堆位置、.bss区等。其中.mem_uninit为未初始化段,定义在这段的内存变量在软复位后也不会被改变,可以保存复位信息等;.mem_ncnb为cache、写缓存关闭,定义在这段的内存变量不会有cache数据一致性的问题;.mem_cnb为内存写通模式,定义在这段的内存变量数据可cache,写数据即使命中,也将数据更新到主存中;.mem_ncb为cache不会命中、开启写缓存,定义在这段的内存变量数据每次访问均访问主存,写数据时先写入写缓存;默认的内存模式为写回模式。

SECTIONS
{
    . =0x00000000;
    . = ALIGN(4);
    .text   :
    {
      _scode = .;
      ./System/s3c2416.o (.text* .rodata*)
      ./System/LowLevelInit.o (.text* .rodata*)
      ./System/NandBoot.o (.text* .rodata*)
      ./System/Coprocessor.o (.text* .rodata*)
      ./System/Download.o (.text* .rodata*)
      *(.text*)
    }
    . = ALIGN(4);
    .rodata : {*(.rodata*) }
    . = ALIGN(4);
    .data :
    {
      _sdata = .;
      *(.data*)
      _edata = .;
    }
    _ecode = .;
    . = ALIGN(4);
    .bss :
    {
      _sbss = .;
      *(.bss*)
      *(COMMON*)
      _ebss = .;
    }
    . = ALIGN(4);
    .heap(NOLOAD):
    {
      PROVIDE(end = .);
      *(.heap*)
    }
    . = ALIGN(4);
    .stack(NOLOAD):
    {
      *(.stack*)
    }
    . = ALIGN(4);
    .mem_uninit(NOLOAD):
    {
      *(.mem_uninit)
    }
    /* cache off,write buffer off */
    . = ALIGN(0x100000);
    .mem_ncnb(NOLOAD):
    {
      _smem_ncnb = .;
      *(.mem_ncnb)
      _emed_ncnb = .;
    }
    /* cache off,write buffer on */
    . =ALIGN(0x100000);
    .mem_ncb(NOLOAD):
    {
      _smem_ncb = .;
      *(.mem_ncb)
      _emem_ncb = .;
    }  
    /* cache on,write through */
    . =ALIGN(0x100000);
    .mem_cnb(NOLOAD):
    {
      _smem_cnb = .;
      *(.mem_cnb)
      _emem_cnb = .;
    }
}

4.应用编程

在main中实现用户功能函数,可以任意使用newlib中的c库函数。

#include "ProjectConfig.h"
#include "Uart.h"
#include "Speed.h"

int main(void)
{  
    int i;
    Uart_Init();
    printf("CPU:  S3C2416@%dMHz\n", get_ARMCLK()/1000000);
    printf("      Fclk = %dMHz, Hclk = %dMHz, Pclk = %dMHz\n",
            get_FCLK()/1000000,get_HCLK()/1000000, get_PCLK()/1000000);
    i = 0;
    while (1) 
    {
        Delay_ms(1000);
        printf("i= %d\n", i);
        i++;
    }
}

打开shell终端,执行make,编译完成后即可在顶层目录生成Bootloader.elf、Bootloader.map、Bootloader.srec、Bootloader.bin、Bootloader.dis这五个文件。其中,Bootloader.map为链接Mapping文件,Bootloader.dis为工程的汇编生成文件,这是编译器经过编译所有的源码并进行链接最终给出的汇编文件,Bootloader.bin即为我们用来烧录进sd卡或nand等存储器的二进制代码文件。

5. 代码烧写

代码可以存储在Nand flash或sd/mmc卡中,一般先把代码烧写进sd/mmc卡中,这样无需复杂的操作以及昂贵的烧录器等。sd/mmc卡启动后通过里面的Bootloader下载功能把主机上的相应代码下载进设备的Nand flash上,之后无需再用sd/mmc卡,设置Nand flash启动,代码更新也通过Nandflash上的Bootloader下载更新即可。

sd/mmc卡启动需要专用的烧录软件SdBoot,SdBoot.exe为笔者在windows下针对三星S3C2416和S5PV210这两个平台开发的sd/mmc启动烧录工具。sd/mmc启动需要相应的代码格式以及需烧录进sd/mmc卡指定的位置,SdBoot.exe工具集代码格式转换以及sd/mmc卡烧录于一体,可以烧录S3C2416和S5PV210这两个平台下笔者编写的裸机bootloader,wince bootloader,uboot,工具简单易用。

03_基于newlib的bootloader - 图1

图5-1 SdBoot.exe工具

6. bootloader下载功能

bootloader通过串口实现主机代码文件的下载,主机端代码下载需要三星专用的工具dnw.exe工具,连接好串口后,按住空格键后,目标板上电会进入Bootloader的下载模式。

03_基于newlib的bootloader - 图2

图6-1 Bootloader下载模式

其中选项1可以基于笔者Bootloader开发的应用,下载进RAM,并直接调试运行。下载进RAM的代码均是编译器直接编译出来的代码,未经过SdBoot工具格式转换。
选项2用于代码烧录进Nand flash,把编译器直接编译出来的代码下载进Nand flash,之后设置Nand flash启动即可。

7. 附录

附录为arm交叉编译工具链下基于newlib的s3c2416 bootloader工程,附带了newlib库,sd卡windows下烧录工具,工程直接make即可。
http://pan.baidu.com/s/1qYvyqmC
arm交叉编译工具链下基于newlib的s3c2416 uCGUI工程,基于bootloader的uCGUI协议栈、定时器、电容屏、IIC、RGB屏等驱动。
http://pan.baidu.com/s/1bpkZy1L