STM32启动文件浅析
目录
- STM32启动文件简介
- 启动文件中的一些指令
- 启动文件代码详解
- 栈空间的开辟
- 堆空间的开辟
- 中断向量表定义(简称:向量表)
- 复位程序
- 对于weak的理解
- 对于_main函数的分析
- 中断服务程序
- 用户堆栈初始化
- 系统启动流程
- 其他
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,如下表堆栈指针寄存器所示。
堆栈指针 | 用户 | 系统 | 管理 | 中止 | 未定义 | 中断 | 快速中断 |
---|---|---|---|---|---|---|---|
R13 (SP) | R13 | 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
启动文件中的一些指令
指令名称 | 作用 |
---|---|
EQU | 给数字常量取一个符号名,相当于C语言中的define |
AREA | 汇编一个新的代码段或者数据段 |
ALIGN | 编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐。要注意的是,这个不是ARM的指令,是编译器的,这里放到一起为了方便 |
SPACE | 分配空间 |
PRESERVE8 | 当前文件堆栈需要按照8字节对齐 |
THUMB | 表示后面指令兼容 THUMB 指令。在 ARM 以前的指令集中有 16 位的THUMBM 指令,现在 Cortex-M 系列使用的都是 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超级版 |
EXPORT | 声明一个标号具有全局属性,可被外部的文件使用 |
DCD | 以字节为单位分配内存,要求 4 字节对齐,并要求初始化这些内存 |
PROC | 定义子程序,与 ENDP 成对使用,表示子程序结束 |
WEAK | 弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。要注意的是,这个不是 ARM 的指令,是编译器的,这里放到一起为了方便 |
IMPORT | 声明标号来自外部文件,跟 C 语言中的 extern 关键字类似 |
LDR | 从存储器中加载字到一个存储器中 |
BLX | 跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR |
BX | 跳转到由寄存器/标号给出的地址,不用返回 |
B | 跳转到一个标号 |
IF,ELSE,ENDIF | 汇编条件分支语句,跟 C 语言的类似 |
END | 到达文件的末尾,文件结束 |
启动文件代码详解
下面,我们以 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) 处必须包含一张向量表,用于初始时的异常分配。
复位程序
位置 | 优先级 | 优先级类型 | 名称 | 说明 | 地址 |
---|---|---|---|---|---|
- | - | - | 保留 | 0x0000_0000 | |
-3 | 固定 | Reset | 复位 | 0x0000_0004 | |
-2 | 固定 | NMI | 不可屏蔽中断;RCC时钟安全系统(CSS)联接到NMI向量 | 0x0000_0008 | |
-1 | 固定 | 硬件失效(HardFault) | 所有类型的失效 | 0x0000_000C | |
0 | 可设置 | 存储管理(MemManage) | 存储器管理 | 0x0000_0010 | |
1 | 可设置 | 总线错误(BusFault) | 预取值失败,存储器访问失败 | 0x0000_0014 | |
2 | 可设置 | 错误应用(UsageFault) | 未定义的指令或非法状态 | 0x0000_0018 | |
- | - | - | 保留 | 0x0000_001C ~ 0x0000_002B | |
3 | 可设置 | SVCall | 通过SWI指令的系统服务调用 | 0x0000_002C | |
4 | 可设置 | 调式监控(DebugMonitor) | 调试监控器 | 0x0000_0030 | |
- | - | - | 保留 | 0x0000_0034 | |
5 | 可设置 | PendSV | 可挂起的系统服务 | 0x0000_0038 | |
6 | 可设置 | SysTick | 系统嘀嗒定时器 | 0x0000_003C | |
0 | 7 | 可设置 | WWDG | 窗口定时器中断 | 0x0000_0040 |
1 | 8 | 可设置 | PVD | 连到EXTI的电源电压检测(PVD)中断 | 0x0000_0044 |
2 | 9 | 可设置 | TAMPER | 侵入检测中断 | 0x0000_0048 |
3 | 10 | 可设置 | RTC | 实时时钟(RTC)全局中断 | 0x0000_004C |
4 | 11 | 可设置 | FLASH | 内存全局中断 | 0x0000_0050 |
中间部分省略,详情请参考《STM32中文参考手册》第九章 中断和事件 中断和异常向量
56 | 63 | 可设置 | DMA2通道1 | DMA2通道1全局中断 | 0x0000_0120 |
---|---|---|---|---|---|
57 | 64 | 可设置 | DMA2通道2 | DMA2通道2全局中断 | 0x0000_0124 |
58 | 65 | 可设置 | DMA2通道3 | DMA2通道3全局中断 | 0x0000_0128 |
59 | 66 | 可设置 | DMA2通道4_5 | DMA2通道4和DMA2通道5全局中断 | 0x0000_012C |
举个例子,如果发生了异常 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 MemManage_Handler ; H