F407/103启动文件and启动过程

STM32 启动文件简介

STM32 启动文件由 ST 官方提供,在官方的固件包里。
startup_stm32f40_41xxx.s
启动文件由汇编编写,是系统上电复位后第一个执行的程序。
启动文件主要做了以下工作:
1 、初始化堆栈指针 SP = _initial_sp
2 、初始化程序计数器指针 PC = Reset_Handler
3 、设置堆和栈的大小
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的入口地址,内核使用了向量表查表机制。向量表其实是一个 WORD32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 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 MicroLIB
MicroLIB MDK 自带的微库,是缺省 C 库的备选库, MicroLIB 进行了高度优化使得其代码变得很小,功能比缺省 C 库少。 MicroLIB 是没有源码的,只有库。

系统启动过程

可参考:关于ARM架构和cortexM内核的知识总结_路溪非溪的博客-CSDN博客

STM32复位的全过程

其实看懂启动文件就知道了。

This module performs:

;*                      - Set the initial SP

;*                      - Set the initial PC == Reset_Handler

;*                      - Set the vector table entries with the exceptions ISR address

;*                      - Configure the system clock and the external SRAM mounted on

;*                        STM324xG-EVAL board to be used as data memory (optional,

;*                        to be enabled by user)

;*                      - Branches to __main in the C library (which eventually

;*                        calls main()).

;*                      After Reset the CortexM4 processor is in Thread mode,

;*                      priority is Privileged, and the Stack is set to Main.

主要就是干以下三件事情:

  1. 为堆栈分配大小并进行初始化;
  2. 在0地址处定义中断向量表,并声明中断服务函数;
  3. 执行复位的中断服务程序,在该程序里执行系统初始化,并设置时钟,系统初始化结束后跳转main函数开始执行用户程序;
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值