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.mk、Makefile、rules.mk。
rules.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,工具简单易用。
6. bootloader下载功能
bootloader通过串口实现主机代码文件的下载,主机端代码下载需要三星专用的工具dnw.exe工具,连接好串口后,按住空格键后,目标板上电会进入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