系统启动流程
- 前言
- 一、系统启动方式
- 二、启动汇编文件分析
- 1. 初始化堆栈指针
- 2. 初始化中断向量表
- 3. 在Reset_Handler中调用 SystemInit
- 4.进入main函数前,初始化用户堆栈
- 总结
前言
本章介绍STM32系统启动流程的相关内容,包括对系统启动方式,启动汇编文件的分析,启动原理的介绍等。
一、系统启动方式
在介绍系统启动流程之前,我们先来了解下启动模式,就像我们对电脑进行刷机一样,我们从硬盘或是U盘去引导系统启动是不同的,STM32有三种启动模式,即从FLASH启动,从SRAM启动,从系统存储启动。
系统是从 0x0000 0000 和 0x0000 0004 两个的地址获取堆栈指针 SP 和程序计数器指针 PC。而 0x0000 0000 和 0x0000 0004 两个的地址可以被重映射到其他的地址空间,映射的空间地址由BOOT0和BOOT1共同决定。如果内部 FLASH 启动:会将 0x0800 0000 映射到 0x0000 0000,会将 0x0800 0004 映射到 0x0000 0004。如果内部 SRAM 启动:会将 0x0200 0000 映射到 0x0000 0000,会将 0x0200 0004 映射到 0x0000 0004。如果用系统存储器启动:会将 0x01FF FF00 映射到 0x0000 0000,会将 0x01FF FF04 映射到 0x0000 0004
随后从地址 0x0800 0000/ 0x0200 0000 /0x01FF FF00 处取出堆栈指针 MSP 的初始值,从地址 0x0800 0004/ 0x0200 0004 /0x01FF FF04处取出程序计数器指针PC 的初始值。CPU 会从 PC 寄存器指向的地址空间取出的第 1 条指令开始执行程序,就是开始执行复位中断服务程序 Reset_Handler。
其中,我们最常用的启动方式是从FLASH启动。当然选择了启动模式后也要将固件下载到对应的地址空间才能引导成功,
在keil进行烧写固件时可以看到对固件烧写起始地址的设置,如下图所示:
二、启动汇编文件分析
前面介绍过通过STM32CubeMX生成了一个简单的工程,如下图,生成的项目中是一些官方提供的基本库和文件:
- 用户程序:应用程序,中断配置,用户配置文件等
- 外设驱动,这STM32CubeMX根据你配置的引脚功能涉及的功能库进行自动添加的
- 标准接口文件:ARM Cortex 微控制器软件接口标准文件
- 芯片的启动文件
启动文件主要做了以下工作:
- 初始化堆栈指针
- 初始化中断向量表
- 在Reset_Handler中调用 SystemInit
- 调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数进入C程序
下面我们对stm32f10系列芯片的启动文件startup_stm32f10x_ld.s分段进行分析,在分析的过程中我们不对汇编指令进行介绍,因为很少用到,都是简写,也不容易记忆,所以我们仅仅对大致功能做一个说明。
1. 初始化堆栈指针
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>Stack_Size EQU 0x00000400AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
这段代码的目的是初始化栈空间:开辟一段大小为 0x0000 0400(1KB)的栈空间,其中 ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐,栈是从高到低排布的。
我们知道,栈由编译器自动分配和释放的内存,不需要用户参与管理,主要作用是存放局部变量,函数形参等,比如在程序中定义局部变量较多或者较大,占用占空间较多时,可能会导致栈空间溢出报 HardFault错误。这时,我们就需要修改启动代码中修改栈的大小Stack_Size,但是栈空间Stack_Size的大小不能大于内存SRAM的大小。
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>Heap_Size EQU 0x00000200AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
这段代码的目的是初始化堆空间:开辟一段大小为 0x0000 0200(512 字节)的堆空间,其中 ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐,__heap_base 表示堆的起始地址, __heap_limit 表示堆的结束地址,堆是由低到高排布的。
我们知道,堆由程序员自行管理,动态分配和释放,如使用 malloc()、 calloc()和 realloc()等函数申请的内存就在堆上面。如果程序员没有释放,程序结束时可能由操作系统回收。
2. 初始化中断向量表
; Vector Table Mapped to Address 0 at ResetAREA RESET, DATA, READONLYEXPORT __VectorsEXPORT __Vectors_EndEXPORT __Vectors_Size__Vectors DCD __initial_sp ; Top of StackDCD Reset_Handler ; Reset HandlerDCD NMI_Handler ; NMI HandlerDCD HardFault_Handler ; Hard Fault Handler......略DCD PendSV_Handler ; PendSV HandlerDCD SysTick_Handler ; SysTick Handler; External InterruptsDCD WWDG_IRQHandler ; Window WatchdogDCD PVD_IRQHandler ; PVD through EXTI Line detectDCD TAMPER_IRQHandler ; Tamper.......略DCD EXTI15_10_IRQHandler ; EXTI Line 15..10DCD RTCAlarm_IRQHandler ; RTC Alarm through EXTI LineDCD USBWakeUp_IRQHandler ; USB Wakeup from suspend
__Vectors_End__Vectors_Size EQU __Vectors_End - __VectorsAREA |.text|, CODE, READONLY
这段代码的目的是初始化中断向量表: __Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小。
__Vectors DCD __initial_sp ; Top of StackDCD Reset_Handler ; Reset Handler
中断向量表被放置在代码段的最前面。起始地址是: 0x0800 0000。如上以DCD开头的为中断向量表中的内容,后面跟着的是中断服务函数的函数名,DCD 以四字节对齐分配内存,所以 Reset_Handler 中断函数入口地址为 0x0800 0004。
3. 在Reset_Handler中调用 SystemInit
; Reset handler routine
Reset_Handler PROCEXPORT Reset_Handler [WEAK]IMPORT __mainIMPORT SystemInitLDR R0, =SystemInitBLX R0LDR R0, =__mainBX R0ENDP; Dummy Exception Handlers (infinite loops which can be modified)
这段代码的目的是定义复位子程序:从中断向量表中我们可以看到,Reset_Handler 是初始化栈空间后的第一个子程序,也是复位后第一个要执行的子程序,因此非常重要。子程序中首先调用SystemInit 函数对系统进行初始化的配置,然后在调用 C 库函数__main,这里最终会调用到 main 函数,至此,将进入我们用c语言写的程序代码中执行。
NMI_Handler PROCEXPORT NMI_Handler [WEAK]B .ENDP
HardFault_Handler\PROCEXPORT HardFault_Handler [WEAK]B .ENDP
MemManage_Handler\PROCEXPORT MemManage_Handler [WEAK]B .ENDP
BusFault_Handler\PROCEXPORT BusFault_Handler [WEAK]B .ENDP
UsageFault_Handler\PROCEXPORT UsageFault_Handler [WEAK]B .ENDP
SVC_Handler PROCEXPORT SVC_Handler [WEAK]B .ENDP
DebugMon_Handler\PROCEXPORT DebugMon_Handler [WEAK]B .ENDP
PendSV_Handler PROCEXPORT PendSV_Handler [WEAK]B .ENDP
SysTick_Handler PROCEXPORT SysTick_Handler [WEAK]B .ENDP......
这段代码的目的是声明各个中断服务子程序,只有函数名,具体的实现会在stm32f10x_it.c中,如下图所示
4.进入main函数前,初始化用户堆栈
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************IF :DEF:__MICROLIBEXPORT __initial_spEXPORT __heap_baseEXPORT __heap_limitELSEIMPORT __use_two_region_memoryEXPORT __user_initial_stackheap__user_initial_stackheapLDR R0, = Heap_MemLDR R1, =(Stack_Mem + Stack_Size)LDR R2, = (Heap_Mem + Heap_Size)LDR R3, = Stack_MemBX LRALIGNENDIFEND
这段代码的目的是初始化用户堆栈空间,里面值得注意的是 IF :DEF:__MICROLIB(即判断是否使用MICROLIB库)。
MicroLib是一个高度优化的库,适用于用C语言编写的基于ARM的嵌入式应用程序。与ARM编译器工具链中包含的标准C库相比,MicroLib提供了许多嵌入式系统所需的代码是其显著优势。
MicroLib与标准C库的主要区别在于:
- MicroLib专为深度嵌入式应用程序设计。
- MicroLib经过优化,比使用ARM标准库使用更少的代码和数据内存。
- MicroLib被设计为在没有操作系统的情况下工作,但这并不妨碍它与任何OS或RTOS(如Keil RTX)一起使用。
- MicroLib不包含文件I/O或宽字符支持。
- 由于MicroLib已被优化以最小化代码大小,一些函数的执行速度将比ARM编译工具中的标准C库例程慢。
要在嵌入式应用程序中使用MicroLib,请选中keil5中的MicroLib复选框并编译应用程序。keil5将您的程序与MicroLib连接起来,快速轻松地缩小程序大小。
总结
本章内容介绍了STM32三种常用的启动方式和系统启动引导的原理,然后介绍了启动文件的启动流程, 启动文件起到的作用:初始化堆栈指针,初始化中断向量表 ,在Reset_Handler中调用 SystemInit ,调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数进入C程序。
至此,STM32的开发前的一些储备知识到此结束,下面我们通过STM32CubeMX配置功能引脚,进行开发逐一介绍。