先看一下STM32启动的总体过程:
1、设置堆栈
2、跳转到Reset_Handler
3、Reset_Handler调用SystemInit完成时钟、中断向量偏移的初始工作。然后跳转到__main,__main函数会完成RW、ZI数据段的重定位工作,即将ROM中的RW数据拷贝到RAM文件中,然后将ZI段清零。
4、跳转到真正的main函数
然后我们来具体看一下ST官方提供的启动文件,即startup_stm32fxxx.s。我们以stm32F767为例,分析这个文件的具体内容。
首先来看一下启动文件设计的几个汇编指令。
然后再来看这个文件
一、栈(Stack)的设置
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
首先使用EQU给0x00000400取名称为Stack_Size,然后汇编一个数据段,名称为STACK,不初始化,可读可写,8(2^3)字节对齐,然后将Stack_Size大小字节的内存分配给Stakce_Mem,最后_initial_sp为栈顶地址。
这段代码的含义就是开辟大小为0x00000400(1KB)的空间,名称为STACK,不初始化,可读可写,8字节对齐。
二、堆(Heap)的设置
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
和上面一样,首先使用EQU给0x00000200取名称为Heap_Size,然后汇编一个数据段,名称为HEAP,不初始化,可读可写,8字节对齐,然后_heap_base为堆的起始地址,_heap_limit为堆的终止地址。中间那个为将上面设置的内存大小分配出来。
这段代码的含义是开辟大小为0x00000200(512B)的空间为堆,名称为HEAP,不初始化,可读可写,8字节对齐。并且提供了堆起始和结束位置的标签。
三、中断向量表
PRESERVE8
THUMB
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
DCD HardFault_Handler ; Hard Fault Handler
DCD MemManage_Handler ; MPU Fault Handler
DCD BusFault_Handler ; Bus Fault Handler
DCD UsageFault_Handler ; Usage Fault Handler
DCD 0 ; Reserved
略
DCD CAN3_TX_IRQHandler ; CAN3 TX
DCD CAN3_RX0_IRQHandler ; CAN3 RX0
DCD CAN3_RX1_IRQHandler ; CAN3 RX1
DCD CAN3_SCE_IRQHandler ; CAN3 SCE
DCD JPEG_IRQHandler ; JPEG
DCD MDIOS_IRQHandler ; MDIOS
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
PRESERVE8:指定当前文件的堆栈按照8字节对齐
THUMB:表示后面的指令兼容THUMB指令。THUMB是以前的ARM的指令集,16bit,现在Cotex-M系列都使用THUMB-2指令集,是32位的,兼容THUMB指令集。
AREA RESET, DATA, READONLY
定义了一个名为“RESET”的内存区域,并且只读。
DATA:指示该内存区域中包含数据。这表示该区域中存储的内容是在程序运行时可以读取的数据,而不是程序的指令代码。
READONLY:指示该内存区域中的数据是只读的,不能被修改。这意味着在程序运行时,不能对该区域中的数据进行写入操作,只能进行读取操作。
然后声明了几个全局属性的标号。
__Vectors:向量表起始地址
__Vectors_End:向量表终止地址
__Vectors_Size EQU __Vectors_End - __Vectors:向量表大小
对于Cotex-M4内核,ARM规定向量表的起始地址存放的是栈的入口地址(这里为__initial_sp),然后存放复位中断的入口地址,上电之后,硬件会根据向量表的地址找到向量表的具体位置(可以在NVIC寄存器中设置),根据这两个地址设置SP,PC的值,之后CPU就可以从复位中断的入口函数中去指令运行。
中间那一大坨就是各个中断的入口地址,最开始的两个就是堆栈指针和复位中断。
四、复位中断函数
AREA |.text|, CODE, READONLY
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP
; Dummy Exception Handlers (infinite loops which can be modified)
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B .
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
省略
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMP_STAMP_IRQHandler [WEAK]
EXPORT RTC_WKUP_IRQHandler [WEAK]
省略
SDMMC2_IRQHandler
CAN3_TX_IRQHandler
CAN3_RX0_IRQHandler
CAN3_RX1_IRQHandler
CAN3_SCE_IRQHandler
JPEG_IRQHandler
MDIOS_IRQHandler
B .
ENDP
ALIGN
AREA |.text|, CODE, READONLY:定义了一个只读的.text的代码段。
然后实现了Reset_Handler函数,PROC到ENDP为函数内容,声明一个弱定义函数Reset_Handler,然后导入SystemInit函数和__main函数,在SystemInit中完成时钟和中断向量偏移的工作,在__mian中完成RW和ZI的重定位工作,这个__mian最后跳到main函数中。
后面是很长的中断函数的编写,都是弱定义。
五、堆栈的初始化
IF :DEF:__MICROLIB
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
对堆栈做一些设置,主要是清零。
IF,ELSE和C语言的一样。
有两条分支,首先判断是否定义了_MICROLIB,如果定义了,就调用C库完成堆栈的初始化,赋予__initial_sp,__heap_base,__heap_limit全局属性。可以在Keil中设置。
否则就采用双段存储的模式,即堆区和栈区是分开的(如果不采用双段模式,因为堆和栈的增长方向是相反的,如果装上了会导致程序崩溃),并且声明_user_initial_stackheap具有全局属性,让用户自己初始化堆栈。