STM32启动文件简介
STM32启动文件由ST官方提供,在官方的固件包里。启动文件由汇编编写,是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作:
初始化堆栈指针 SP = _initial_sp
初始化程序计数器指针 PC = Reset_Handler
设置堆和栈的大小
初始化中断向量表
配置外部SRAM作为数据存储器(可选)
配置系统时钟,通过调用SystemInit函数(可选)
调用C库中的 _main 函数初始化用户堆栈,最终调用 main 函数
ARM指针寄存器 —— 堆栈指针寄存器SP、程序计数器PC、连接寄存器LR
堆栈指针R13(SP):每一种异常模式都有其自己独立的R13,它通常指向异常模式所专用的堆栈,也就是说五种异常模式、非异常模式(用户模式和系统模式),都有各自独立的堆栈,用不同的堆栈指针来索引。这样当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性;
连接寄存器R14(LR):每种模式下R14都有自身版组,它有两个特殊功能;
保存子程序返回地址;使用BL或BLX时,跳转指令自动把返回地址放入R14中;子程序通过把R14复制到PC来实现返回,通常用下列指令之一:
MOV PC, LR
BX LR
通常子程序这样写,保证了子程序中还可以调用子程序:
stmfd sp!, {lr}
...
ldmfd sp!, {pc}
当异常发生时,异常模式的R14用来保存异常返回地址,将R14:如栈可以处理嵌套中断
程序计数器R15(PC):PC是有读写限制的,当没有超过读取限制的时候,读取的值是指令的地址加上8个字节,由于ARM指令总是以字对齐的,故bit[1:0]总是00。当用str或stm存储PC的时候,偏移量有可能是8或12等其它值。
ARM处理器使用流水线来增加处理器指令流的速度,这样可使几个操作同时进行,并使处理与存储器系统之间的操作更加流畅,连续,能提供0.9MIPS/MHZ的指令执行速度。
在随机存储器区划出一块区域作为堆栈区,数据可以一个个顺序地存入(压入)到这个区域之中,这个过程称为压栈(push )。通常用一个指针(堆栈指针 SP—StackPointer)实现做一次调整,SP总指向最后一个压入堆栈的数据所在的数据单元(栈顶)。从堆栈中读取数据时,按照堆栈 指针指向的堆栈单元读取堆栈数据,这个过程叫做弹出(pop ),每弹出一个数据,SP 即向相反方向做一次调整,如此就实现了后进先出的原则。
堆栈是计算机中广泛应用的技术,基于堆栈具有的数据进出LIFO特性,常应用于:
保存中断断点;
保存子程序调用返回点;
保存CPU现场数据等;
也用于程序间传递参数;
ARM处理器中通常将寄存器R13作为堆栈指针(SP)。ARM处理器针对不同的模式,共有 6 个堆栈指针(SP),其中用户模式和系统模式共用一个SP,每种异常模式都有各自专用的R13寄存器(SP)。它们通常指向各模式所对应的专用堆栈,也就是ARM处理器允许用户程序有六个不同的堆栈空间。这些堆栈指针分别为R13、R13_svc、R13_abt、R13_und、R13_irq、R13_fiq,如下表堆栈指针寄存器所示。
为了更准确地描述堆栈,根据“压栈”操作时堆栈指针的增减方向,将堆栈区分为‘递增堆栈’(SP 向大数值方向变化)和‘递减堆栈’(SP 向小数值方向变化);又根据SP 指针指向的存储单元是否含有堆栈数据,又将堆栈区分为‘满堆栈’(SP 指向单元含有堆栈有效数据)和‘空堆栈’(SP 指向单元不含有堆栈有效数据)。
这样两两组合共有四种堆栈方式——满递增、空递增、满递减和空递减。
ARM处理器的堆栈操作具有非常大的灵活性,对这四种类型的堆栈都支持。
ARM处理器中的R13被用作SP。当不使用堆栈时,R13 也可以用做通用数据寄存器。
栈的整体作用
保护现场;
传递参数;
临时变量保存在栈中;
深入理解ARM三个寄存器
PC 代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:
取指:从存储器装载一条指令;
译码:识别将要被执行的指令;
执行:处理指令并将结果写回寄存器;
R15(PC)总是指向 正在取指 的指令:ARM指令是三级流水线,取指、译指、执行是同时进行的,现在PC指向的是正在取指的地址,那么cpu正在译指的指令地址是PC-4(假设在ARM状态 下,一个指令占4个字节),cpu正在执行的指令地址是PC-8,也就是说PC所指向的地址和现在所执行的指令地址相差8。当突然发生中断的时候,保存的是PC的地址这样你就知道了,如果返回的时候返回PC,那么中间就有一个指令没有执行,所以用 SUB pc lr -lrq #4
启动文件中的一些指令
启动文件代码详解
下面,我们以 STM32F103 的启动代码为例讲解,版本是:STM32Cube_FW_F1_V1.8.0,
启动文件名称是:startup_stm32f103xe.s。把启动代码分成几个功能段进行详细的讲解,详
情如下
栈空间的开辟
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
**EQU:**宏定义的伪指令,给数字常量取一个符号名,类似与 C 中的 define。定义栈大小为 0x00000400 字节,即 1024B(1KB),常量的符号是 Stack_Size。
AREA 汇编一个新的代码段或者数据段。段名为 STACK,段名可以任意命名;NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。
SPACE 分配内存指令,分配大小为 Stack_Size 字节连续的存储单元给栈空间。
__initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。
栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部 SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改 Stack_Size 的值。如果程序出现了莫名其妙的错误,并进入了 HardFault 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。
堆空间的开辟
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
堆空间开辟代码跟栈空间开辟代码是类似的了。这部分代码的意思就是:开辟堆的大小为 0x00000200(512 字节),段名为 HEAP,不初始化,可读可写,(2^3 对齐)8 字节对齐。__heap_base表示堆的起始地址,__heap_limit 表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。
堆主要用于动态内存的分配,像 malloc()、calloc()和 realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
PRESERVE8
THUMB
PRESERVE8:指示编译器按照 8 字节对齐。
THUMB:指示编译器之后的指令为 THUMB 指令。
中断向量表定义(简称:向量表)
中断向量表定义代码
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
定义一个数据段,名字为RESET, READONLY表示只读;EXPORT表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End 和 __Vectors_Size 三个标号具有全局性,可被外部的文件使用
当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了向量表查表机制。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。
中间部分省略,详情请参考《STM32中文参考手册》第九章 中断和事件 中断和异常向量
举个例子,如果发生了异常 SVCall,则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类:地址 0x0000 0000 并不是什么入口地址,而是给出了复位后 MSP 的初值。更详细的向量表,可以参考《STM32中文参考手册》第九章-中断和事件-中断和异常向量
F103 的向量表格中灰色部分是系统内核异常。表格中位置 0 到 59 是外部中断,CM3内核的芯片最大支持 240 个外部中断,具体使用多少个由芯片厂家设计决定。如这个表格中的 103 芯片只是使用了 60 个。这里说的外部中断是相对内核而言
__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 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 ; SysTick Handler
; External Interrupts(外部中断)
DCD WWDG_IRQHandler ; Window Watchdog
DCD PVD_IRQHandler ; PVD through EXTI Line detect
DCD TAMPER_IRQHandler ; Tamper
DCD RTC_IRQHandler ; RTC
DCD FLASH_IRQHandler ; Flash
; 中间篇幅太长, 省略掉, 代码向量表与STM32F103的向量表对应
DCD DMA2_Channel1_IRQHandler ; DMA2 Channel1
DCD DMA2_Channel2_IRQHandler ; DMA2 Channel2
DCD DMA2_Channel3_IRQHandler ; DMA2 Channel3
DCD DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
__Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
中断向量表被放置在代码段的最前面。例如:当我们的程序在 FLASH 运行时,那么向量表的起始地址是:0x0800 0000。结合图 2.3.2 可以知道,地址 0x0800 0000 存放的是栈顶地址。DCD:以四字节对齐分配内存,也就是下个地址是0x0800 0004,存放的是Reset_Handler中断函数入口地址。
从代码上看,向量表中存放的都是中断服务函数的函数名,所以 C 语言中的函数名对芯片来说实际上就是一个地址。
复位程序
接下来是定义只读代码段
AREA |.text|, CORE, READONLY
定义一个段命为.text,只读的代码段,在CODE区;复位子程序代码
; Reset handler
Reset_Handler PROC // 子程序开始
EXPORT Reset_Handler [WEAK] // 声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务;WEAK: 表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的
IMPORT __main // IMPORT 表示该标号来自外部文件
IMPORT SystemInit // 这里表示 SystemInit 和__main 这两个函数均来自外部的文件
LDR R0 ,= SystemInit // LDR 表示从存储器中加载字到一个存储器中; SystemInit 是一个标准的库函数,在 system_stm32f1xx.c 文件中定义,主要作用是配置系统时钟、还有就是初始化 FSMC/FMC总线上外挂的 SRAM(可选),前面说配置外部 SRAM 作为数据存储器(可选)就是这个
BLX R0 // BLX 表示跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR
LDR R0 ,= __main // 把__main 的地址给 R0。__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈和变量等,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因,如果不调用__main,那么程序最终就不会调用我们 C 文件里面的main,也就无法正常运行
BX R0 // BX 表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main地址,最终调用 main 函数,不返回,进入 C 的世界
ENDP // 子程序结束
利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰;
LDR、BLX、BX 是内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到
对于weak的理解
weak 顾名思义是“弱”的意思,在汇编中,在函数名称后面加[WEAK]来表示,而在 C语言中,在函数名称前面加上 __weak 修饰符来表示,这样的函数我们称为“弱函数”;
被 [WEAK] 或 __weak 声明的函数,我们可以在自己的文件中重新定义一个同名函数,最终编译器编译的时候,会选择我们定义的函数,如果我们没有重新定义这个函数,那么编译器就会执行[WEAK]或__weak 声明的函数,并且编译器不会报错;
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
同样我们打开stm32f1xx_it.c文件中也定义了HardFault_Handler中断函数
void HardFault_Handler(void)
{
/* Go to infinite loop when Hard Fault exception occurs */
while(1)
{
}
}
在 stm32f1xx_it.c 文件定义了 HardFault_Handler 中断函数的情况下,当HardFault_Handler 中断来到的时候,代码会运行到 stm32f1xx_it.c 文件的 HardFault_Handler中断函数,且进入 while(1);
下面,我们注释掉 stm32f1xx_it.c 的 HardFault_Handler 中断函数,然后进行编译,发现不会报错。这时候当 HardFault_Handler 中断来到的时候,代码会运行到启动文件的“弱函数”中,即在启动文件中 164 行代码,进行原地跳转(即无限循环);
对于_main函数的分析
当看到__main 函数时,估计有不少人认为这个是 main 函数的别名或是编译之后的名字,否则在启动代码中再也无法找到和 main 相关的字眼了。可事实是,_main 和 main 是两个完全不同的函数。_main 代码是编译器自动创建的,因此无法找到_main 代码。MDK 文档中有一句说明:it is automatically craated by the linker when it sees a definition of main() 。大体意思可以理解为:当编译器发现定义了 main 函数,那么就会自动创建_main;
程序经过汇编启动代码,执行到__main()后,可以看出有两个大的函数:
__scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了 ZI运行域的初始化工作;
__rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数;
中断服务程序
接下来就是中断服务程序了
; 系统异常中断
NMI_Handler PROC
EXPORT NMI_Handler [WEAK]
B . ;原地跳转(即无限循环)
ENDP
HardFault_Handler\
PROC
EXPORT HardFault_Handler [WEAK]
B .
ENDP
;中间代码太长, 已经省略掉
SysTick_Handler PROC
EXPORT SysTick_Handler [WEAK]
B .
ENDP
;外部中断
Default_Handler PROC
EXPORT WWDG_IRQHandler [WEAK]
EXPORT PVD_IRQHandler [WEAK]
EXPORT TAMPER_IRQHandler [WEAK]
EXPORT RTC_IRQHandler [WEAK]
EXPORT FLASH_IRQHandler [WEAK]
;中间代码太长, 已经省略掉
DMA2_Channel1_IRQHandler
DMA2_Channel2_IRQHandler
DMA2_Channel3_IRQHandler
DMA2_Channel4_5_IRQHandler
B .
ENDP
可以看到这些中断服务函数都被[WEAK]声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错;
这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环;
在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现;
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个‘.’中,无限循环;
这里的系统异常中断是内核的,外部中断是外设的;
用户堆栈初始化
ALIGN指令
ALIGN
ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。要注意的是,这个不是 ARM 的指令,是编译器的;
接下就是启动文件最后一部分代码,用户堆栈初始化代码
IF :DEF:__MICROLIB // 判断是否定义了__MICROLIB。关于__MICROLIB 这个宏定义
// 如果定义__MICROLIB,声明__initial_sp、__heap_base 和__heap_limit这三个标号具有全局属性,可被外部的文件使用。
//__initial_sp 表示栈顶地址,__heap_base表示堆起始地址,__heap_limit 表示堆结束地址
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
ELSE // 没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的 C 库运行。堆栈的初始化由 C 库函数__main 来完成
IMPORT __use_two_region_memory // IMPORT 声明__use_two_region_memory 标号来自外部文件
EXPORT __user_initial_stackheap // EXPORT 声明__user_initial_stackheap 具有全局属性,可被外部的文件使用
__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 // 跳转到 LR 标号给出的地址,不用返回
ALIGN
ENDIF
END // END 表示到达文件的末尾,文件结束
IF, ELSE, ENDIF 是汇编的条件分支语句
系统启动流程
在以前 ARM7/ARM9 内核的控制器在复位后,CPU 会从存储空间的绝对地址0x00000000 取出第一条指令执行复位中断服务程序的方式启动,即固定了复位后的起始地址为 0x00000000(PC = 0x00000000),同时中断向量表的位置也是固定的。而 Cortex-M3内核复位后的起始地址和中断向量表的位置可以被重映射。充映射的方法是通过启动模式的选择,有以下 3 种情况:
通过 boot 引脚设置可以将中断向量表定位于 SRAM 区,即起始地址为 0x2000000,同时复位后 PC 指针位于
0x2000000 处;
通过 boot 引脚设置可以将中断向量表定位于 FLASH 区,即起始地址为 0x8000000,同时复位后 PC 指针位于
0x8000000 处;
通过 boot 引脚设置可以将中断向量表定位于内置 Bootloader 区,本文不对这种情况做论述;
Cortex-M3 内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在 Cortex-M3 内核复位后,会自动从起始地址的下一个 32 位空间取出复位中断入口向量,跳转执行复位中断服务程序。
下面将结合《Cortex-M3 权威指南(中文)》chpt03-复位序列的内容进行讲解。
启动模式不同,启动的起始地址是不一样的,下面我们以代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行。
我们知道的复位方式有三种:上电复位,硬件复位和软件复位。当产生复位,并且离开复位状态后,CM3 内核做的第一件事就是读取下列两个 32 位整数的值:
从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址;
从地址 0x0800 0004 处取出程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令;
下面用示意图表示
请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。而 在 CM3 内核中,0 地址处提供 MSP 的初始值,然后就是向量表(向量表在以后还可以被移至其它位置)。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是 Reset_Handler 这个函数。下面继续以战舰开发板 HAL库例程的实验 1 跑马灯实验为例,代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、MSP 和 PC 后的内存情况
因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加 1。举例来说,如果你的栈区域在 0x20000388‐0x20000787 之间,那么 MSP 的初始值就必须是 0x20000788。
向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。
R15 是程序计数器,在汇编代码中,可以使用名字“PC”来访问它。ARM 规定:PC最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令(0)还是 Thumb 指令(1),因为 CM3 主要执行 Thumb 指令,所以这些指令的最低位都是 1(都是奇数)。因为 CM3 内
部使用了指令流水线,读 PC 时返回的值是当前指令的地址+4。比如说:
0x1000: MOV R0, PC ; R0 = 0x1004
如果向 PC 写数据,就会引起一次程序的分支(但是不更新 LR 寄存器)。CM3 中的指令至少是半字对齐的,所以 PC 的 LSB 总是读回 0。然而,在分支时,无论是直接写 PC 的值还是使用分支指令,都必须保证加载到 PC 的数值是奇数(即 LSB=1),表明是在Thumb 状态下执行。倘若写了 0,则视为转入 ARM 模式,CM3 将产生一个 fault 异常。
正因为 上 述 原 因 , 图 3.3 中使用 0x080001CD 来表达地址 0x080001CC 。 当0x080001CD 处的指令得到执行后,就正式开始了程序的执行(即去到 C 的世界)。所以在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没执行就会被 NMI 或是其它 fault 打断。MSP 初始化好后就已经为它们的服务例程准备好了堆栈。
————————————————
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/xuexiwd/article/details/143113275
|