本文以STM32F429的启动文件为例去做介绍,其他系列芯片甚至其他厂家芯片的启动文件也类似,至少我见过的是这样的。
启动文件的作用:
1.初始化SP。
2.初始化PC 等于复位中断函数,上电首先执行复位中断函数。
3.用中断地址初始化中断向量表。
4.配置系统时钟
5.跳转到C库函数的_main()函数,_main()函数会调用main函数。运行用户编写的应用程序。
这五点是ST官网标准启动文件所做的事,其实这五点不是必须的,有的可以省去,比如初始化时钟,初始化中断向量表(假设我们不需要使用芯片的中断),调用__main 函数等等。只有初始化堆栈指针SP才是必须的,因为初始化完SP就可以执行C函数了,可以去C的世界里嗨了。
下面开始按照文件内容由上至下介绍启动文件。
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
初始化栈,为什么要有栈?栈用于局部变量,函数调用,函数形参的开销。这个大家可以做实验验证,搞一个递归。看他调用到多少层崩溃,然后将栈改大,观察可调用的层数是不是变大了?
EQU:类似C语言中的define宏定义。
Stack_Size EQU 0x00000400
表示Stack_Size
的大小是0x400
AREA :告诉汇编器汇编一个新的代码段或数据段
AREA STACK, NOINIT, READWRITE, ALIGN=3
汇编一段内存空间名字叫STACK,不初始化,可读可写,8字节对齐
SPACE:用于分配一定大小的内存空间,单位为字节。
Stack_Mem SPACE Stack_Size
分配 Stack_Size
大小的内存空间名字叫Stack_Mem
,
标号__initial_sp
紧挨着SPACE语句之后放置,表示栈的结束地址,即栈顶地址,因为STM32的栈是向下生长的,所以初始化以后栈指针要指向栈的末尾地址(高地址)。
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
初始化堆,为什么要有堆?堆用于动态内存的分配。大家可以做一个实验,用malloc函数申请内存,将堆空间改大,测试可以申请到的空间是否变大?
前两行和初始化栈部分代码一样的意义。
__heap_base
放置在SPACE
之前表示堆的起始地址。
Heap_Mem SPACE Heap_Size
分配Heap_Size
大小的空间名字叫Heap_Mem
__heap_limit
放置在SPACE
之后表示堆的结束地址。
PRESERVE8
THUMB
PRESERVE8:指定当前文件的堆栈按照8字节对齐。
THUMB:表示后面的指令为THUMB指令。
AREA RESET, DATA, READONLY;汇编一个数据段 名字是REST 只读
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 0 ; Reserved
DCD 0 ; Reserved
DCD 0 ; Reserved
DCD SVC_Handler ; SVCall Handler
DCD DebugMon_Handler ; Debug Monitor Handler
DCD 0 ; Reserved
DCD PendSV_Handler ; PendSV Handler
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog
DCD PVD_IRQHandler ; PVD through EXTI Line detection
DCD TAMP_STAMP_IRQHandler ; Tamper and TimeStamps through the EXTI line
;省略一部分内容
DCD SAI1_IRQHandler ; SAI1
DCD LTDC_IRQHandler ; LTDC
DCD LTDC_ER_IRQHandler ; LTDC error
DCD DMA2D_IRQHandler ; DMA2D
__Vectors_End
中断向量表映射到0地址,也就是说中断向量表从flash起始地址处开始存储。
DCD 分配以字为单位的空间,并要求初始化他们,在向量表中,DCD分配了一堆内存,并且以ISR的入口地址初始化它们。
说白了,中断向量表实际上是一个32位的整形数组,一个元素对应一个中断/异常,数组存放在FLASH的起始地址。数组元素的值就是中断/异常服务函数的入口地址。那我们在MDK中debug一下,观察以下是不是这样子。
发现并不一样,其实是一样的。最低位表示的不是地址是处理器的状态。CM4指令至少是半字对齐的,所以最低位总是0,这时候,最低位用来标记处理器是ARM状态还是Thumb状态。cortex-M3权威指南有如下解释。
下面是复位中断函数
; Reset handler 复位中断函数
Reset_Handler PROC ;PROC表示子程序的开始
EXPORT Reset_Handler [WEAK]
IMPORT SystemInit
IMPORT __main
LDR R0, =SystemInit
BLX R0
LDR R0, =__main;__main是C库函数 想要使用__main这个C库函数 必须勾选微库选项
BX R0
ENDP
在复位中断函数中都做了哪些事?
将SystemInit
函数的地址加载到R0中,执行SystemInit
函数
将__main
函数的地址加载到R0中,执行__main
函数。
__main
函数是标准C库函数,__main
函数会调用main
函数。
;初始化堆和栈,文件开始只是开辟了堆和栈 并没有初始化 此处初始化堆和栈 此部分代码由C库__main调用
;*******************************************************************************
IF :DEF:__MICROLIB
; 如果定义了微库 执行这一部分
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE
; 如果没定义微库执行这一部分
IMPORT __use_two_region_memory
EXPORT __user_initial_stackheap
__main函数就很有意思了,他是一个库函数,我们看不到源码,他里边替我们做了很多工作。
关于__main的思考
我们做实验并思考如下几个问题:
1.在程序中定义一个变量 int a = 3;
这是一个初始值不为0 的变量,程序编译后a变量被编译到RW-data段。我们可以在程序中打印出a的值是3,a的地址是芯片RAM空间的地址。问题来了,a的地址是ram地址,a的初始值又是3,我们都知道RAM掉电数据丢失,上电数据随机,那为什么程序一上电能准确无误的输出a的变量呢?
其实这个3是保存在flash区域的,只是在进入C函数之前,将这个3从flash拷贝到了RAM区域。
谁来拷贝的?
毫无疑问 __main拷贝的,为什么这么肯定,从汇编到C所有的代码都被我们拿捏的死死地,只有这个__main函数对我们来说是一个“黑盒子”,因此推断出就是__main干的。
如果在汇编中不调用__main,在汇编中直接跳入到自己写的C函数中,发现a的变量真就是随机值了。
2.在程序中定义一个全局变量 int b;
我们可以在程序中打印出b的值是0,b的地址是芯片的RAM地址,
问题又来了,b的地址是ram空间的地址,b没有赋初始值,而RAM的特性是掉电数据丢失,上电数据随机,那为什么程序一上电a的值总是0,而非随机值?
b是无初始值的变量,被编译到了bss段。程序启动在进入C函数之前,bss段会被清0。是谁来清bss段呢?还是__main。
如果在汇编中不调用__main
,在汇编中直接跳入到自己写的C函数中,发现b的变量就是随机值了。
3.在程序中定义一个常量 const int c = 4;
被const 修饰以后就是常量了,也就是说不能对c赋值了,我们在程序中打印出c的值是4,c的地址是flash空间内的地址。
如果在汇编中不调用__main
,在汇编中直接跳入到自己写的C函数中,发现c的值依然是4。
为什么常量就不依赖__main
了呢?因为我们不会写常量,一直呆在flash空间就行了,访问c的时候去flash空间访问,所以有没有__main
都无所谓。
总结:RW-data和bss都由__main
来处理。
自己动手写拷贝RW-data段代码和清除BSS段代码
从flash拷贝数据到RAM和清除bss段,这些工作我们自己能做吗?答案是能做,但是一般不用我们做,除非想装个X或其他原因。
接下来看我装X表演。
由于我对汇编不是手到擒来,所以拷贝工作和清除工作我用C函数实现。
1.先写一个内存拷贝函数
/**
* @brief 内存拷贝函数
* @param 目的地址
* @param 源地址
* @param 长度
* @retval 无
*/
void user_memcpy(void * dest,void *src,uint32_t len)
{
uint8_t * pdest = dest;
uint8_t * psrc = src;
while(len--)
*pdest++ = *psrc++;
}
2.再写一个内存设置函数
/**
* @brief 内存设置函数
* @param 地址
* @param 值
* @param 长度
* @retval 无
*/
void user_memset(void * dest,int val,unsigned int len)
{
unsigned char *pcDest = dest;
while(len--)
*pcDest++ = val;
}
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT user_main ;导入user_main函数
IMPORT user_memcpy ;导入user_memcpy函数
IMPORT user_memset ;导入user_memset函数
IMPORT |Image$$RW_IRAM1$$Base| ; RW-data段的运行域
IMPORT |Load$$RW_IRAM1$$Base| ; RW-data段的加载域
IMPORT |Image$$RW_IRAM1$$Length| ; RW-data段的长度
IMPORT |Image$$RW_IRAM1$$ZI$$Base| ; bss段的运行域
IMPORT |Image$$RW_IRAM1$$ZI$$Length| ; bss段的长度
LDR R0,= |Image$$RW_IRAM1$$Base| ; RW-data段的运行域 赋值给R0寄存器
LDR R1,= |Load$$RW_IRAM1$$Base| ; RW-data段的加载域 赋值给R1寄存器
LDR R2,= |Image$$RW_IRAM1$$Length| ; RW-data段的长度赋值给R2寄存器
BL user_memcpy ;执行user_memcpy函数 拷贝RW-data段
LDR R0,= |Image$$RW_IRAM1$$ZI$$Base| ; bss段的运行域赋值给R0寄存器
LDR R1,= 0 ; R1寄存器赋值为0
LDR R2,= |Image$$RW_IRAM1$$ZI$$Length| ; bss段的长度赋值给R2寄存器
BL user_memset ;执行user_memset函数
LDR R0,=user_main ;将自己写的C程序入口地址赋值给R0寄存器
BLX R0 ;跳转到user_main函数
ENDP