嵌入式开发底层——KEIL中启动文件详解(汇编语言)
概述
在嵌入式系统中,启动文件是整个系统非常关键的部分,它会进行一些底层的初始化,构建程序运行必要的环境,比如堆栈初始化,变量初始化等。如果启动文件出现错误,则整个系统就跑不起来,因此研究启动文件非常必要。
在keil中,启动文件由汇编代码编写,一般命名为startup_xxx.s,xxx为支持的某种芯片,比如可以是lpc15xx(NXP的LPC15xx系列)、MK60D10(飞思卡尔)、stm32f10x(意法半导体stm32f10x系列)等Cortext-M0/M3/M4内核芯片。它们的代码格式非常相近,根据启动文件代码由上到下的编写顺序.
可以将其分为以下5个典型部分:
- 堆栈空间定义;
- 存放中断向量表;
- 复位中断函数(Reset_Handler);
- 其它中断异常服务函数,以及弱[WEAK]声明;
- 将堆栈地址传递给库函数,利用库函数初始化堆栈,和库函数自身初始化。
5个部分具体说明如下:
1 堆栈空间定义
如下图所示,定义了栈大小Stack_Size = 0X200
,即512字节;堆大小Heap_Size = 0X100
,256字节。还定义了三个标号:__initial_sp(栈顶)
、__heap_base(堆起始地址)
和__heap_limit(堆终止地址)
,它们的空间由SPACE
关键字来申请,并记作Stack_Mem
和Heap_Mem
。
通过这些我们可以很容易的知道堆栈的大小,但是它们的绝对地址或者说基地址仅仅从这里是得不到的。编译器编译完工程后,根据生成.bss段(比如未初始化的全局变量)和.data段(比如初始化的全局变量)的大小以及RAM的起始地址,来计算堆栈的基地址。
举个例子:
一个芯片的RAM起始地址为0x0200_0000,RAM大小为0x500字节,程序编译后.bss段为0x100个字节,.data段为0x100个字节。堆栈大小定义如上图。则:
-
A:堆起始地址
__heap_base==Heap_Mem==0x0200_0200;
-
B:堆终止地址即栈底
__heap_limit==Stack_Mem==0x0200_0300;
-
C:栈顶地址
__initial_sp==0x0200_0500(栈是向下生长,栈顶处于RAM最大地址处)。
其实,我可以在.map文件中查看堆栈的大小和基地址,如下图所示:
.map文件2 存放中断向量表
在启动代码中,会见到许多由DCD申请空间存放的一个个函数入口,即中断向量表,如下图所示,只列出了部分。
中断向量表关键字DCD代表申请一个字的空间,后面的函数名即为中断服务函数入口地址。另外中断向量表一般存放在Flash
0地址。
另外,对于NXP微控制器,均实现了芯片的加密,加密的设置在向量表的结尾处,具体地址为0x02FC处。通过在此地址存放不同的值实现是否加密或者加密的等级。加密分为三个等级,CRP1:0x12345678;CRP2:0x87654321;CRP3:0x43218765。
至于每个等级的具体说明请参考芯片用户手册。下面说一下加密步骤,以CRP1为例:
首先将上图中0xFFFFFFFF,修改为0x12345678。
其次,图中IF :LNOT::DEF:NOCRP
表示如果没有定义宏NOCRP
则执行下面的代码,那么必须保证汇编中没有定义NOCRP
宏。即保证下图中Define:
一栏中没有定义NOCRP
即可。
3 复位中断函数(Reset_Handler)
程序上电后,首先加载SP和PC,ARM规定从0地址处加载SP,从偏移为4的地址(0x00000004)处加载PC。然后将程序控制权交给程序。我们知道0地址处存放__initial_sp
,0x00000004地址处存放Reset_Handler
,加载PC后,程序跳转到Reset_Handler
开始运行。Reset_Handler
函数体如下图所示:
首先调用SystemInit
函数来初始化系统的各种时钟,然后调用__main
函数(由KEIL微库或者C库实现),在__main
函数中:.data
段数据的初始化->.bss
段变量清零->设置堆栈指针->库函数初始化(比如常用的malloc
函数)->如果必要会设置main函数的argc和argv两个参数->调用用户main函数->退出。
4 其它中断异常服务函数,以及弱[WEAK]声明
WEAK弱声明如上图所示,这里的中断服务函数是弱声明的(由[WEAK]关键字标注)。所谓弱声明,即:如果用户定义了相同的函数则此处函数失效而使用用户定义的中断服务函数。这样是为了防止用户使能了中断而没有中断服务函数,从而造成程序崩溃。假设使能了中断,而用户又没有定义中断服务函数则会进入默认中断,如下图所示,默认中断为死循环(死循环与程序崩溃不是一个概念)。
默认中断5 将堆栈地址传递给库函数
第三步骤中,调用__main
函数,然后__main
调用库函数初始化堆栈,但库函数并不知道堆栈的大小,因此我们需要告诉它,具体做法就是传递参数或声明标号。
下图为具体做法,可以看到第一行为:
IF :DEF:__MICROLIB
是条件编译选项,如果定义__MICROLIB
,则编译图中红线上面部分,否则编译红线下面部分。那么就分2种情况。
2种情况的选择可以如下实现:
如果勾选【Options for Target】
->【Target】
->【Use MicroLIB】
,如下图所示。即使用微库,则__MICROLIB
会被定义,编译器编译红线以上代码。用EXPORT声明 __initial_sp
、__heap_base
和__heap_limit
。
如果不勾选【Use MicroLIB】,则缺省使用KEIL C库,上图红线以下会参与编译,KEIL C库函数会调用__user_initial_stackheap
,通过R0~R3将堆栈以参数形式传递给KEIL C库。