STM32 启动文件简介
STM32 启动文件由 ST 官方提供,在官方的固件包里。startup_stm32f40_41xxx.s启动文件由汇编编写,是系统上电复位后第一个执行的程序。启动文件主要做了以下工作:1 、初始化堆栈指针 SP = _initial_sp2 、初始化程序计数器指针 PC = Reset_Handler3 、设置堆和栈的大小4 、初始化中断向量表5 、配置外部 SRAM 作为数据存储器(可选)6 、配置系统时钟,通过调用 SystemInit 函数(可选)7 、调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数启动文件中的一些指令接下来结合这些指令说明对启动文件进行注释
栈空间的开辟
栈空间的开辟,源码如图 2.1.1 所示:
33 行 EQU :宏定义的伪指令, 给数字常量取一个符号名, 类似与 C 中的 define 。 定义栈大小为 0x00000400 字节,即 1024B ( 1KB ),常量的符号是 Stack_Size 。35 行 AREA 汇编一个新的代码段或者数据段。段名为 STACK ,段名可以任意命名; NOINIT 表示不初始化; READWRITE 表示可读可写; ALIGN=3 ,表示按照 2^3 对齐,即 8 字节对齐。36 行 SPACE 分配内存指令,分配大小为 Stack_Size 字节连续的存储单元给栈空间。37 行 __initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。栈主要用于存放局部变量,函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部 SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改 Stack_Size 的值。如果程序出现了莫名其妙的错误,并进入了 HardFault 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。关于ARM的栈设计
ARM使用的是满减栈
点击以上链接可查看详情。
关于栈顶地址,我们 可以通过.map 文件查看,方法如图 2.1.2 所示。关于 .map 文件的详细介绍,大家可以查看正点原子团队编写的:《MAP 文件浅析 .pdf 》这个文档 。图 2.1.2 通过 .map 文件查看栈顶地址我们定义 Stack_Size 的大小是 0x00000400 ,图 2.1.2 中可以看到栈顶地址 __initial_sp 的 地址是 0x20000788 ,那栈底地址是多少呢?从图中可以知道是 0x20000388 。所以栈顶地址0x20000788 到栈底地址 0x20000388 的内存大小刚好就是 Stack_Size 的大小。栈是从高往低生长,所以每使用一个栈空间地址,栈顶地址__initial_sp 就减一。
堆空间的开辟
堆空间的开辟,源码如图 2.2.1 所示:堆空间开辟代码跟栈空间开辟代码是类似的了。这部分代码的意思就是:开辟堆的大小为 0x00000200 ( 512 字节),段名为 HEAP , 不初始化,可读可写, 8 字节对齐。 __heap_base表示堆的起始地址,__heap_limit 表示堆的结束地址。堆和栈的生长方向相反的,堆是由低向高生长,而 栈是从高往低生长 。堆主要用于动态内存的分配,像 malloc() 、 calloc() 和 realloc() 等函数申请的内存就在堆上。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
接下来是 PRESERVE8 和 THUMB 指令两行代码。如图 2.2.2 所示。PRESERVE8 :指示编译器按照 8 字节对齐。THUMB :指示编译器之后的指令为 THUMB 指令。
中断向量表定义(简称:向量表)
中断向量表定义代码,如图 2.3.1 所示:
定义一个数据段,名字为 RESET, READONLY 表示只读。EXPORT 表示声明一个标号具有全局属性,可被外部的文件使用。这里是声明了__Vectors、__Vectors_End 和__Vectors_Size 三个标号具有全局性,可被外部的文件使用。
当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址,内核使用了向量表查表机制。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。
注意,这里是逻辑位置,就是说,如果Flash中代码段的真实起始地址为A,则实际的向量表地址为A+相对偏移。
__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 语言中的函数名对芯片来说实际上就是一个地址。举个例子,如果发生了异常 SVCall ,则 NVIC 会计算出地址偏移量(注意,是偏移量,不是实际地址)是 11x4=0x2C ,然后从那里取出服务例程的入口地址并跳入。 要注意的是这里有个另类:地址 0x0000 0000 并不是什么入口地址,而是给出了复位后 MSP 的初值(这个指针里存的是栈顶地址)。
复位程序
接下来是定义只读代码段,如图 2.4.1 所示:
定义一个段,命名为 .text ,只读的代码段, 在 CODE 区。 复位子程序代码,如图 2.4.2 所示:复位子程序代码,如图 2.4.2 所示:利用 PROC 、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。145 行子程序开始146 行 声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务。WEAK :表示弱定义,如果外部文件优先定义了该标号则首先引用 外部定义的标号 ,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。147 行和 148 行 IMPORT 表示该标号来自外部文件。这里表示 SystemInit 和 __main 这两个函数均来自外部的文件。149 行 LDR 表示从存储器中加载字到一个存储器中。 SystemInit 是一个标准的库函数,在 system_stm32f1xx.c 文件中定义,主要作用是配置系统时钟、还有就是初始化 FSMC/FMC总线上外挂的 SRAM( 可选 ) ,前面说配置外部 SRAM 作为数据存储器(可选)就是这个。150 行 BLX 表示 跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR 。151 行把 __main 的地址给 R0 。 __main 是一个标准的 C 库函数,主要作用是初始化用户堆栈和变量等,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因,如果不调用 __main ,那么程序最终就不会调用我们 C 文件里面的main,也就无法正常运行。152 行 BX 表示 跳转到由寄存器 / 标号给出的地址,不用返回。这里表示切换到 __main地址,最终调用 main 函数,不返回,进入 C 的世界。153 行 ENDP 表示 子程序结束。LDR、 BLX 、 BX 是内核的指令,可在《 CM3 权威指南 CnR2 》第四章 - 指令集里面查询到。
中断服务程序
接下来就是中断服务程序了,如图 2.5.1 所示:
可以看到这些中断服务函数都被 [WEAK] 声明为弱定义函数,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不会出错。这些中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。 B 指令是跳转到一个标号,这里跳转到一个‘. ’,表示无限循环。在启动文件代码中,已经把我们所有中断的中断服务函数写好了,但都是声明为弱定义,所以真正的中断服务函数需要我们在外部实现。如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个‘ .’中,无限循环。这里的系统异常中断是内核的,外部中断是外设的。
用户堆栈初始化
ALIGN 指令,如图 2.6.1 所示:
ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。要注意的是,这个不是 ARM 的指令,是编译器的。接下就是启动文件最后一部分代码,用户堆栈初始化代码,如图 2.6.2 所示:331 行判断是否定义了 __MICROLIB 。关于 __MICROLIB 这个宏定义,我们是在 KEIL里面配置勾选了 Use MicroLIB 就代表定义了 __MICROLIB 这个宏。333 行到 335 行如果定义 __MICROLIB ,声明 __initial_sp 、 __heap_base 和 __heap_limit,这三个标号具有全局属性,可被外部的文件使用。__initial_sp 表示栈顶地址, __heap_base表示堆起始地址,__heap_limit 表示堆结束地址。337 行没有定义 __MICROLIB ,实际的情况就是我们没有定义 __MICROLIB ,所以使用默认的 C 库运行。堆栈的初始化由 C 库函数 __main 来完成。339 行 IMPORT 声明 __use_two_region_memory 标号来自外部文件。340 行 EXPORT 声明 __user_initial_stackheap 具有全局属性,可被外部的文件使用。342 行标号 __user_initial_stackheap ,表示用户堆栈初始化程序入口。接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。344 行保存堆起始地址。345 行保存栈大小。346 行保存堆大小。347 行保存栈顶指针。348 行 跳转到 LR 标号给出的地址,不用返回。354 行 END 表示到达文件的末尾,文件结束。Use MicroLIBMicroLIB 是 MDK 自带的微库,是缺省 C 库的备选库, MicroLIB 进行了高度优化使得其代码变得很小,功能比缺省 C 库少。 MicroLIB 是没有源码的,只有库。
系统启动过程
可参考:关于ARM架构和cortexM内核的知识总结_路溪非溪的博客-CSDN博客