STM32-启动文件 startup_stm32f103xe.s
- ■ STM32-启动文件
- ■ STM32-启动文件主要做了以下工作:
- ■ STM32-启动文件指令
- ■ STM32-启动文件代码详解
- ■ 栈空间的开辟
- ■ 栈空间大小 Stack_Size
- ■ .map 文件的详细介绍
- ■ 打开map文件
- ■ 堆空间
- ■ PRESERVE8 和 THUMB 指令
- ■ 中断向量表定义(简称:向量表)???????
- ■ 复位程序
- ■ weak
- ■ _main 函数的分析
- ■ __scatterload()函数
- ■ __rt_entry()函数
- ■ 中断服务程序
- ■ ALIGN指令
- ■ 用户堆栈初始化
- ■ Use MicroLIB
- ■ 系统启动流程
- ■ 代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行分析
- ■ 1.CM3 内核做的第一件事就是读取下列两个 32 位整数的值:
- ■ 2.MSP 和 PC
- ■ 3.代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、 MSP 和 PC 后的内存情况。
■ STM32-启动文件
STM32 启动文件由 ST 官方提供
启动文件由汇编编写,是系统上电复位后第一个执行的程序。
■ STM32-启动文件主要做了以下工作:
1、初始化堆栈指针 SP = _initial_sp
2、初始化程序计数器指针 PC = Reset_Handler
3、设置堆和栈的大小
4、初始化中断向量表
5、配置外部 SRAM 作为数据存储器(可选)
6、配置系统时钟,通过调用 SystemInit 函数(可选)
7、调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数
■ STM32-启动文件指令
关于其他更多的 ARM 汇编,我们可以通过 MDK 的索引搜索工具中搜索找到。打开索引搜索工具的方法:
MDK->Help->uVision Help,
■ STM32-启动文件代码详解
例如:startup_stm32f103xe.s 把启动代码分成几个功能段进行详细的讲解
■ 栈空间的开辟
栈是从高往低生长,所以每使用一个栈空间地址,栈顶地址__initial_sp 就减一。
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 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。
■ 栈空间大小 Stack_Size
- 打开.map文件 搜索__initial_sp
栈顶地址 __initial_sp 的地址是 0x20000538
栈低地址 STACK 0x20000138
Stack_Size = 栈顶地址-栈低地址 Stack_Size 的大小是 0x00000400
■ .map 文件的详细介绍
■ 打开map文件
■ 堆空间
这部分代码的意思就是:开辟堆的大小为 0x00000200(512 字节),
段名为 HEAP,NOINIT不初始化,READWRITE可读可写, ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。
__heap_base表示堆的起始地址,
__heap_limit 表示堆的结束地址。
堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。
注意点:
由于不需要使用 C 库的 malloc 和 free 等函数,也就用不到堆空间,因此我们可以设置 Heap_Size 的大小为 0,以节省内存空间
■ PRESERVE8 和 THUMB 指令
PRESERVE8: 指示编译器按照 8 字节对齐。
THUMB: 指示编译器之后的指令为 THUMB 指令。
■ 中断向量表定义(简称:向量表)???
定义一个数据段,
RESET, READONLY 表示只读。
EXPORT 表示声明一个标号具有全局属性,可被外部的文件使用。
这里是声明了__Vectors、 __Vectors_End 和 __Vectors_Size 三个标号具有全局性,可被外部的文件使用。
__Vectors 为向量表起始地址,
__Vectors_End 为向量表结束地址,
__Vectors_Size 为向量表大小, __Vectors_Size = __Vectors_End - __Vectors。
向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。
向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。
在复位后,该寄存器的值为 0。
因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。如下:
要注意的是这里有个另类: 地址 0x0000 0000 并不是什么入口地址,而是给出了复位后 MSP 的初值。
向量表格中灰色部分是系统内核异常
表格中位置 0 到 59 是外部中断
DCD: 分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
???????????
■ 复位程序
定义一个段命为.text, 只读的代码段, 在 CODE 区。
利用 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》第四章-指令集里面查询到。
■ weak
weak 顾名思义是“弱”的意思,
在汇编中, 在函数名称后面加[WEAK]来表示
在 C语言中,在函数名称前面加上__weak 修饰符来表示, 这样的函数我们称为“弱函数”。
■ _main 函数的分析
_main 和 main 是两个完全不同的函数。
_main 代码是编译器自动创建的,因此无法找到_main 代码。
当编译器发现定义了 main 函数,那么就会自动创建_main。
程序经过汇编启动代码,执行到__main()后,可以看出有两个大的函数:
__scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了 ZI运行域的初始化工作。
__rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数。
■ __scatterload()函数
■ __rt_entry()函数
■ 中断服务程序
这些中断服务函数都被[WEAK]声明为弱定义函数
中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。
B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。
中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个‘.’中,无限循环。
■ ALIGN指令
ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。
要注意的是,这个不是 ARM 的指令,是编译器的。
■ 用户堆栈初始化
== IF, ELSE, ENDIF 是汇编的条件分支语句。==
关键词 | 描述 |
---|---|
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 是没有源码的,只有库。
关于 MicroLIB 更多知识可以看官方介绍 http://www.keil.com/arm/microlib.asp 。
■ 系统启动流程
Cortex-M3内核复位后的起始地址和中断向量表的位置可以被重映射。充映射的方法是通过启动模式的
选择, 有以下 3 种情况:
1、 通过 boot 引脚设置可以将中断向量表定位于 SRAM 区,即起始地址为 0x2000000,同时复位后 PC 指针位于 0x2000000 处;
2、 通过 boot 引脚设置可以将中断向量表定位于 FLASH 区,即起始地址为 0x8000000,同时复位后 PC 指针位于 0x8000000 处;
3、 通过 boot 引脚设置可以将中断向量表定位于内置 Bootloader 区,本文不对这种情况做论述。
Cortex-M3 内核规定,**起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,**这样在 Cortex-M3 内核复位后,会自动从起始地址的下一个 32 位空间取出复位中断入口向量,跳转执行复位中断服务程序。
Cortex-M3 权威指南(中文)
■ 代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行分析
复位方式有三种:上电复位,硬件复位和软件复位当产生复位,并且离开 复位状态后,
■ 1.CM3 内核做的第一件事就是读取下列两个 32 位整数的值:
(1)从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址。
(2)从地址 0x0800 0004 处取出程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令。 下面用示意图表示
■ 2.MSP 和 PC
例如:
CM3内核是小端模式,所以倒着读。
0x08000000 的值是 0x20000788 堆栈指针 SP = 0x20000788
0x08000004 的值是 0x080001CD 程序计数器指针 PC = 0x080001CD
■ 3.代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、 MSP 和 PC 后的内存情况。
ARM 规定: PC最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令( 0)还是 Thumb 指令( 1)