————————————————————————
小编写在前面的话:
这篇博文是小编在学习的过程中不懂就查,查完整理再加上自己的理解后的结果。涉及的内容广度会比较大,包括ARM和汇编科普、堆栈概念、汇编代码解读、程序运行。希望能够基于startup_stm32l071xx.s代码将上述几个方面的知识平铺开来,让大家能够读懂ARM启动文件。
预备知识
此处针对ARM小白的快速科普喔~
ARM的科普
相信大家都听说过CPU,CPU的学名叫中央处理器,它是一个机器的大脑,可以用于计算和控制机器等。世界上又各种大大小小的机器,有的复杂有的简单,就产生了不同的架构需求。ARM和x86就是我们比较熟悉的CPU的两个不同的架构。ARM是RISC(精简指令集)CPU,而x86是Intel公司出品的CISC(复杂指令集)CPU。ARM架构偏向于处理简单任务,应用在如智能传感终端上。而Intel公司出品的CPU(x86,x64等)偏向于处理更复杂的任务,应用在如台式机、服务器处理器上。
ARM汇编
如果你有一个ARM的工程文件,必不可少的就是它的启动文件,就那stm32l071这款芯片举例,它的启动文件就叫做startup_stm32l071xx.s,打开它,你就看到了ARM汇编代码。汇编是低级语言(不是说语言很简单,而是更贴近计算机底层的语言。小编觉得会这个低级语言的人十分高级),它直接描述/控制着CPU的运行。
堆和栈
主要说说两个非常基本的内存分配区:堆和栈。
栈偏高效,堆偏灵活。为什么这么说呢?
栈(stack)
- 系统自动分配释放:栈的地址都有专门的寄存器提供存放,也有专门的指令实现进栈、出栈操作,这就决定了栈的效率比较高。
- 所分配的内存是连续的,从高地址向低地址分配(低地址方向是出栈入栈的口)。栈的最大容量是系统预先规定好的
- 栈区存放的内容与函数有关(有函数就有栈):断点地址(返回地址)、函数参数、函数内部动态局部变量、函数返回的数据。
堆(heap)
- 由程序员malloc分配,free释放:堆是由用户高级语言提供的,指令的封装比栈复杂,所以效率没有栈高。如果程序员没有free,则分配的内存一直存在直到程序结束后被os回收。
- 所提供的内存不连续,从低地址向高地址分配。每一次malloc得到的内存块连续,但上一次malloc和下一次malloc得到的内存块不连续,需要用链表串起来。堆的大小受限于系统中有效的虚拟内存。
- 堆区存放的数据内容由用户决定:数据、数组、结构体、字符串。
在内存分配上还有:
guard栈保护区:用来检测栈是否溢出。当栈发生溢出时,操作系统可以很容易地检测到这种情况。对于不具备MMU(内存管理单元)的小型嵌入式系统,栈保护区也可以用来存储写入到里面的数据。
global/static variables全局区:全局变量和静态变量存放区,程序结束后由系统释放。
注:频繁使用heap会产生内存碎片,应该遵循先申请后释放的原则来避免在堆中产生碎片。
代码解读(KEIL)
如果我在使用C语言开发智能终端,却不知道程序到底是从哪开始,要往哪里去的,我就还是不能说我明白这个程序,所以,今天我们就一起来看看运行ARM工程最最最开始的代码——启动文件startup_stm32l071xx.s。
注:汇编代码中的分号;表示注释。
//声明一个栈
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
EQU 等值命令 EQU 0x00000400 相当于#define Stack_Size 0x00000400,只是一个声明,并未分配地址。
AREA:告诉编译器一个新的代码段或者数据段。STACK表示段名(可任意命名);NOINIT表示不初始化;READWRITE表示可读可写;ALIGN=3表示按照2^3对齐,即8字节对齐。
SPACE:用于分配大小为Stack_Size的内存空间
__initial_sp表示栈的结束地址(栈是由高向低地址生长的)
//声明一个堆
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
PRESERVE8
THUMB
对于堆来说,有一些和栈类似就不再赘述。说说不一样的地方:
__heap_base表示堆的起始地址。
__heap_limit表示堆的结束地址。(堆是由低向高地址生长的)
PRESERVE8:指定当前文件的堆按照8字节对齐,这是keil编译器的一个编程要求。
THUMB:表示后面指令兼容THUMB指令(THUMB指令是ARM以前的指令集)
//向量声明
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
定义一个名为RESET的数据段,权限为仅可读。
声明的三个symbol可供外部文件调用。
(EXPORT使symbol有全局属性,如果是IAR编译器,则使用的是GLOBAL这个指令。)
//向量表
Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
...
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY //定义一个名为.text的代码段,可读。
注:DCD相当于C语言当中的&,定义地址。
向量表从FLASH的0地址开始放置,以4个字节为一个单位,地址0存放的是栈顶地址。0x04存放的是复位程序的地址,以此类推。
; Reset handler routine
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]//弱定义
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit //调用SystemInit初始化系统时钟
BLX R0
LDR R0, =__main //调用main初始化用户堆栈,引导程序进入__main
BX R0
ENDP
接下来有很多中断服务函数。但是这些函数在这里是空的,只是占了个位置。真正的中断服务函数程序需要我们在外部c文件里面重新实现。
如果我们在使用某个外设的时候开启了某个中断,却忘记写中断服务程序,那么当中断被触发,程序就会跳到这里的启动文件里空的中断并无限循环,程序就死了。
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . //跳转到. 表示无限循环
ENDP
HardFault_Handler PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
……
ALIGN //对指令或者数据存放的地址进行对齐,后面会跟立即数,若缺省则表示4字节对齐。
用户堆栈初始化,在keil的option for target中的target标签可设置。
汇编代码如下:
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
//判断是否定义了__MICROLIB
IF :DEF:__MICROLIB //若已经定义,则赋予下面三个symblos全局属性
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE//若没有定义,则使用双段存储器模式。
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__user_initial_stackheap //让用户自来初始化堆栈。
LDR R0, = Heap_Mem
LDR R1, =(Stack_Mem + Stack_Size)
LDR R2, = (Heap_Mem + Heap_Size)
LDR R3, = Stack_Mem
BX LR
ALIGN
ENDIF
END
代码解读(IAR)
在小编的IAR工程中,引用的启动文件和keil又不相同了。在IAR的库中提供了cstartup.s, cmain.s, cexit.s文件。即使你用的是KEIL,也不妨看看IAR这部分,也许有些知识点是上面没有出现的。
//MODULE...END表示开始和结束,?cstartup是模块的名字
MODULE ?cstartup
...
END
代码的启动函数在cstartup.s中
SECTION(声明段)。
语法格式:SECTION section:type [flag] [(align)]
section是段的名字;
type是memory的类型,取值是CODE, CONST,DATA。
flag取值有:NOROOT, ROOT, REORDER, NOREORDER。默认是ROOT,表示不可被优化。NOROOT表示如果这个段中的符号没有被引用,将会被连接器舍弃。REORDER表示开始一个新的名字是section的段(section)。NOREORDER表示开始一个新名字为section的片段(fragment),多个fragment组成一个section。
align,用于指定地址对齐到2^align(align取值范围在0到30)
;; Forward declaration of sections.
//分了两个段:数据段和代码段
SECTION CSTACK:DATA:NOROOT(3)
SECTION .intvec:CODE:NOROOT(2)
//表示导入其它的模块
EXTERN __iar_program_start
EXTERN SystemInit
PUBLIC __vector_table
//PUBLIC说明当前的模块所引用的标识符(__vector_table)可以被其它的模块引用。
启动文件的引导地址可以自己定义,如果使用默认的配置,IAR在编译时,将会使用IAR自己的系统库作为引导。
DATA表示以下的标签是32位的标签,THUME表示以下的标签是16位的标签。(标签是地址的别名,不占用代码空间,给编译器看的。)
DATA
__vector_table
DCD sfe(CSTACK)
DCD Reset_Handler ; Reset Handler
...
中断向量表的顺序不能变化,此部分在flash的最开始部分。当系统启动的时候会加载前两个地址,第一个地址是c程序栈的栈顶地址,第二个地址是向量表的开始地址。中断发生时会根据向量表的首地址和偏移量来找到程序的入口。
sfe指令的作用:返回栈的结尾。(因为栈的增长方向是反方向的)
ARM核分为两个状态:ARM和THUMB。两者的区别是:指令的长度不同。
Cortex-M3只有Thumb-2状态和调试状态。Thumb-2状态时arm和thumb状态的结合和优化。
ARM的M系列主要用Thumb指令,ARM9和A系列主要用ARM指令。
两状态的切换:
ARM->THUMB 执行BX指令后,R0[0]=1,
THUMB->ARM 执行BX指令后,R0[0]=0,
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; Default interrupt handlers.
;;
THUMB
PUBWEAK Reset_Handler
SECTION .text:CODE:NOROOT:REORDER(2)
Reset_Handler
LDR R0, =SystemInit//将SystemInit寄存器地址加载到R0
BLX R0 //程序跳转到R0中的地址执行,并将处理器切换为THUMB状态,将PC地址保存到R14。
LDR R0, =__iar_program_start//把__iar_program_start地址加载到寄存器R0。__iar_programe_start就是程序的入口函数。
BX R0 //程序跳转到R0中的地址执行,并将处理器切换为THUMB状态/
B{条件} 目标地址 //最简单的跳转指令,立即跳转。
BL{条件} 目标地址 //带链接的跳转。先将当前指令的下一条指令地址(PC)保存在LR寄存器(R14),然后跳转到目标地址。
BX{条件} 目标地址 //带状态切换的跳转。跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM指令(最低位为0),也可以是Thumb指令(最低为为1)。
BLX{条件} 目标地址 //带链接和状态切换的跳转(ARM/Thumb指令的跳转)。结合BX与BL功能。
程序到底如何运行
首先,在汇编器中编写有一个短模块Reset_Handler()(在代码解读里描述过),系统已启动就立即执行。主要是为应用程序的运行模式初始化堆栈指针还有一些必须的配置。
简单看一下函数实现,不做详解。
void Reset_Handler(void)
{
uint32_t *pSrc, *pDest;
/* Initialize the relocate segment */
pSrc = &_etext;
pDest = &_srelocate;
if (pSrc != pDest) {
for (; pDest < &_erelocate;) {
*pDest++ = *pSrc++;
}
}
/* Clear the zero segment */
for (pDest = &_szero; pDest < &_ezero;) {
*pDest++ = 0;
}
/* Set the vector table base address */
pSrc = (uint32_t *) & _sfixed;
SCB->VTOR = ((uint32_t) pSrc & SCB_VTOR_TBLOFF_Msk);
/* Overwriting the default value of the NVMCTRL.CTRLB.MANW bit (errata reference 13134) */
NVMCTRL->CTRLB.bit.MANW = 1;
/* Initialize the C library */
__libc_init_array();
/* Branch to main function */
main();
/* Infinite loop */
while (1);
}
所以在main()函数之前还有一些初始化处理,然后才进入我们编写的应用程序main()。
——————————————
小编写在结尾的话:
了解ARM的启动文件不仅更深入了解了嵌入式的工作原理,更为嵌入式终端的远程升级的开发打下基础,如果你也在做类似的事情,欢迎给小编发私信交流!若上文小编有理解不对的地方,也欢迎指正!