STM32F429启动文件详解

本文以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
  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值