再造STM32---第十二部分:启动文件详解

发布者:云自南国来最新更新时间:2019-09-28 来源: eefocus关键字:stm32  启动文件  编译器 手机看文章 扫描二维码
随时随地手机看文章

       本章参考资料《STM32F4xx 中文参考手册》第十章-中断和事件:表 46.STM32F42xxx 和 STM32F43xxx 的向量表; MDK 中的帮助手册—ARM Development Tools:用来查询 ARM 的汇编指令和编译器相关的指令。


12.1 启动文件简介:

启动文件由汇编编写,是系统上电复位后第一个执行的程序。主要做了以下工作:

              1、 初始化堆栈指针 SP=_initial_sp

              2、 初始化 PC 指针=Reset_Handler

              3、 初始化中断向量表

              4、 配置系统时钟

              5、 调用 C 库函数_main 初始化用户堆栈,从而最终调用 main 函数去到 C 的世界


12.2 查找 ARM 汇编指令:

       在讲解启动代码的时候,会涉及到 ARM 的汇编指令和 Cortex 内核的指令,有关Cortex 内核的指令我们可以参考《CM3 权威指南 CnR2》第四章:指令集。剩下的 ARM 的汇编指令我们可以在 MDK->Help->Uvision Help 中搜索到,以 EQU 为例,检索如下:

       检索出来的结果会有很多,我们只需要看 Assembler User Guide 这部分即可。下面列出了启动文件中使用到的 ARM 汇编指令,该列表的指令全部从 ARM Development Tools这个帮助文档里面检索而来。其中编译器相关的指令 WEAK 和 ALIGN 为了方便也放在同一个表格了。

表格 10 启动文件使用的 ARM 汇编指令汇总

image.png

12.3 启动文件代码讲解:

1. Stack—栈:


Stack_Size EQU 0x00000400

AREA STACK, NOINIT, READWRITE, ALIGN=3

Stack_Mem SPACE Stack_Size

__initial_sp

       开辟栈的大小为 0X00000400(1KB),名字为 STACK, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。

       栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了硬 fault 的时候,这时你就要考虑下是不是栈不够大,溢出了。

       EQU:宏定义的伪指令,相当于等于,类似与 C 中的 define。

       AREA:告诉汇编器汇编一个新的代码段或者数据段。 STACK 表示段名,这个可以任意命名; NOINIT 表示不初始化;READWRITE 表示可读可写, ALIGN=3,表示按照 2^3对齐,即 8 字节对齐。

       SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。

       标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的。

2. Heap 堆:


Heap_Size EQU 0x00000200

AREA HEAP, NOINIT, READWRITE, ALIGN=3

__heap_base

Heap_Mem SPACE Heap_Size

__heap_limit

       开辟堆的大小为 0X00000200(512 字节),名字为 HEAP, NOINIT 即不初始化,可读可写, 8(2^3)字节对齐。 __heap_base 表示对的起始地址, __heap_limit 表示堆的结束地址。堆是由低向高生长的,跟栈的生长方向相反。

       堆主要用来动态内存的分配,像 malloc()函数申请的内存就在堆上面。这个在 STM32里面用的比较少。


PRESERVE8

THUMB

       PRESERVE8: 指定当前文件的堆栈按照 8 字节对齐。

       THUMB: 表示后面指令兼容 THUMB 指令。 THUBM 是 ARM 以前的指令集, 16bit,现在 Cortex-M 系列的都使用 THUMB-2 指令集, THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超级。

3. 向量表:


AREA RESET, DATA, READONLY

EXPORT __Vectors

EXPORT __Vectors_End

EXPORT __Vectors_Size

       定义一个数据段,名字为 RESET,可读。并声明 __Vectors、 __Vectors_End 和Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。


       EXPORT: 声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。


       当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了―向量表查表机制‖。这里使用一张向量表。向量表其实是一个WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类: 0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。

表格 11 F429 向量表

image.png

image.png

代码 12 向量表

 


__Vectors DCD __initial_sp ;栈顶地址

DCD Reset_Handler ;复位程序地址

DCD NMI_Handler

DCD HardFault_Handler

DCD MemManage_Handler

DCD BusFault_Handler

DCD UsageFault_Handler

DCD 0 ; 0 表示保留

DCD 0

DCD 0

DCD 0

DCD SVC_Handler

DCD DebugMon_Handler

DCD 0

DCD PendSV_Handler

DCD SysTick_Handler

;外部中断开始

DCD WWDG_IRQHandler

DCD PVD_IRQHandler

DCD TAMP_STAMP_IRQHandler

;限于篇幅,中间代码省略

DCD LTDC_IRQHandler

DCD LTDC_ER_IRQHandler

DCD DMA2D_IRQHandler

__Vectors_End

__Vectors_Size EQU __Vectors_End - __Vectors

       __Vectors 为向量表起始地址, __Vectors_End 为向量表结束地址,两个相减即可算出向量表大小。

       向量表从 FLASH 的 0 地址开始放置,以 4 个字节为一个单位,地址 0 存放的是栈顶地址, 0X04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。

       DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中, DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。

4. 复位程序:


AREA |.text|, CODE, READONLY

       定义一个名称为.text 的代码段,可读。


Reset_Handler PROC

EXPORT Reset_Handler [WEAK]

IMPORT SystemInit

IMPORT __main

LDR R0, =SystemInit

BLX R0

LDR R0, =__main

BX R0

ENDP

       复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数_mian,最终调用 main 函数去到 C 的世界。

       WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。

       IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表示 SystemInit 和__main 这两个函数均来自外部的文件。

       SystemInit()是一个标准的库函数,在 system_stm32f4xx.c 这个库文件总定义。主要作用是配置系统时钟,这里调用这个函数之后, F429 的系统时钟配被配置为 180M。

       __main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因。如果我们在这里不调用__main,那么程序最终就不会调用我们 C 文件里面的 main,如果是调皮的用户就可以修改主函数的名称,然后在这里面 IMPORT 你写的主函数名称即可。


Reset_Handler PROC

EXPORT Reset_Handler [WEAK]

IMPORT SystemInit

IMPORT user_main

LDR R0, =SystemInit

BLX R0

LDR R0, =user_main

BX R0

ENDP

       这个时候你在 C 文件里面写的主函数名称就不是 main 了,而是 user_main 了。

       LDR、 BLX、 BX 是 CM4 内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到,具体作用见下表:

image.png

5. 中断服务程序:


       在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断复服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。

       如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无线循环,即程序就死在这里。


NMI_Handler PROC ;系统异常

EXPORT NMI_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 TAMP_STAMP_IRQHandler [WEAK]

;限于篇幅,中间代码省略

LTDC_IRQHandler

LTDC_ER_IRQHandler

DMA2D_IRQHandler

B .

ENDP

B:跳转到一个标号。这里跳转到一个‘.’,即表示无线循环。

6. 用户堆栈初始化:


ALIGN

       ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。


;用户栈和堆初始化

IF :DEF:__MICROLIB

EXPORT __initial_sp

EXPORT __heap_base

EXPORT __heap_limit

ELSE

IMPORT __use_two_region_memory

EXPORT __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

ALIGN

ENDIF

END

       判断是否定义了__MICROLIB ,如果定义了则赋予标号__initial_sp(栈顶地址)、__heap_base(堆起始地址)、 __heap_limit(堆结束地址)全局属性,可供外部文件调用。如果没有定义(实际的情况就是我们没定义__MICROLIB)则使用默认的 C 库,然后初始化用户堆栈大小,这部分有 C 库函数__main 来完成,当初始化完堆栈之后,就调用 main函数去到 C 的世界。

       IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似

       END:文件结束


12.4 系统启动流程:

       下面这段话引用自《CM3 权威指南 CnR2》 3.8—复位序列, CM4 的复位序列跟 CM3 一样。 

       在离开复位状态后, CM3 做的第一件事就是读取下列两个 32 位整数的值:

1、 从地址 0x0000,0000 处取出 MSP 的初始值。

2、 从地址 0x0000,0004 处取出 PC 的初始值——这个值是复位向量, LSB 必须是1。 然后从这个值所对应的地址处取指。


       请注意,这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。 在CM3 中,在 0 地址处提供 MSP 的初始值,然后紧跟着就是向量表。 向量表中的数值是 32位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是我们刚刚分析的 Reset_Handler 这个函数。

       因为 CM3 使用的是向下生长的满栈,所以 MSP 的初始值必须是堆栈内存的末地址加1。举例 来说,如果我们的堆栈区域在 0x20007C00-0x20007FFF 之间,那么 MSP 的初始值就必须是 0x20008000。

       向量表跟随在 MSP 的初始值之后——也就是第 2 个表目。要注意因为 CM3 是在Thumb 态下执行,所以向量表中的每个数值都必须把 LSB 置 1(也就是奇数)。正是因为这个原因, 图 12-3 中使用 0x101 来表达地址 0x100。当 0x100 处的指令得到执行后,就正式开始了程序的执行(即去到 C 的世界) 。在此之前初始化 MSP 是必需的,因为可能第 1条指令还没来得及执行,就发生了 NMI 或是其它 fault。 MSP 初始化好后就已经为它们的服务例程准备好了堆栈。

       现在,程序就进入了我们熟悉的 C 世界,现在我们也应该明白 main 并不是系统执行的第一个程序了。


12.5 总结:

1、启动文件的讲解 — 开始

1-注释的讲解

2-程序的讲解

3-如何查找资料( ARM的汇编指令)


2、启动文件的作用


1-初始化堆栈指针SP

2-初始化PC指针,指向复位程序

3-初始化中断向量表

4-配置系统时钟

5-调用C库函数_main,最终进入C的世界

3、汇编程序如何注释


1-汇编注释用“;”

2-C语言注释用“//”戒者“/**/”

4、启动文件详解


① -Stack—栈

用于局部变量、函数调用、函数形参的开销

EQU: 宏定义的伪指令,相当于等于,类似与 C 中的 define

AREA:告诉汇编器汇编一个新的代码段戒者数据段

SPACE : 用于分配一定大小的内存空间,单位为字节

标号__initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的。


② -Heap—堆

堆用于动态内存的分配, malloc函数

PRESERVE8: 指定当前文件的堆栈按照 8 字节对齐

THUMB: 表示后面指令为 THUMB 指令。 THUBM 是ARM 以前的指令集, 16bit,现在 Cortex-M 系列的都使用THUMB-2 指令集, THUMB-2 是 32 位的,兼容 16 位和 32位的指令,是 THUMB 的超级。

EXPORT: 声明一个标号具有全局属性,可被外部的文件使用。 如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。

DCD: 分配一个戒者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中, DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。

③ -向量表

1-向量表实际上是一个32位的整型数组,一个元素对应一个异常( ESR),数组元素存的就是ESR的入口地址。

2-向量表在复位后从FLASH的0地址开始,具体的初始化值请查询参考手册的中断章节。

从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C语言中的函数名就是一个地址。

④ -复位程序

1-复位程序是上电后单片机执行的第一个程序

2-调用SystemInit函数配置系统时钟;调用C库函数_main,并最终进入C的世界

WEAK: 表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。

IMPORT: 表示该标号来自外部文件,跟 C 语言中的EXTERN 关键字类似。这里表示 SystemInit 和__main 这两个函数均来自外部的文件。

Cortex内核的指令

⑤ -中断服务程序

1-启动文件为我们写好了全部的中断服务程序,函数的名称必须与向量表里面初始化的名称一样。

2-这些程序都是空的,需要我们在C文件里面重新实现。如果我们写的中断服务程序的函数名写错了,程序也不会报错,而是会进入一个死循环。

⑥ -用户堆栈初始化

由标准的C库函数_main来完成。

IF,ELSE,ENDIF: 汇编的条件分支语句,跟 C 语言的if ,else 类似。

END: 文件结束。

ALIGN: 对指令戒者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。


启动文件里面涉及到的ARM指令

关键字:stm32  启动文件  编译器 引用地址:再造STM32---第十二部分:启动文件详解

上一篇:再造STM32---第十三部分:RCC—使用 HSE/HSI 配置时钟
下一篇:再造STM32---第十一部分:GPIO—位带操作

推荐阅读最新更新时间:2024-10-28 08:14

STM32 DMA彻底研究
typedef struct { u32 DMA_PeripheralBaseAddr; u32 DMA_MemoryBaseAddr; u32 DMA_DIR; u32 DMA_BufferSize; u32 DMA_PeripheralInc; u32 DMA_MemoryInc; u32 DMA_PeripheralDataSize; u32 DMA_MemoryDataSize; u32 DMA_Mode; u32 DMA_Priority; u32 DMA_M2M; } DMA_InitTypeDef; DMA_InitTypeDef 定义于文件“stm32f10x_dma.h” DMA_PeripheralBaseA
[单片机]
stm32ADC校准和连续单次转换的理解
校准 STM32的ADC分为三种状态:掉电状态、上电状态、工作状态。 当芯片启动运行时,ADC处于掉电状态。当第一次将ADON位设定为1时,ADC从掉电状态进入上电状态,也就是手册里说的“从掉电状态下唤醒”,这时ADON位已经为1。当我们再次设定ADON位等于1时,这时ADC会按照此时的ADC_CR1、ADC_CR2等寄存器的设置开始转换工作,也就进入了工作状态。这也就是下图红框中话的含义。 接下来再分析HAL_ADCEx_Calibration_Start这个函数,通过一层一层查找,方知这个函数之所以要放在HAL_ADC_Start()之前或HAL_ADC_Stop()之后是因为这个函数本身就会执行将ADON置1这么一条语句
[单片机]
stm32ADC校准和连续单次转换的理解
STM32 ST-LINK Utility解决错误
使用ST-LINK V2下载程序时,出现以下错误,并且连接线无出错 解决方法: Target --- Settings--- Mode更改为Connect Under Reset
[单片机]
<font color='red'>STM32</font> ST-LINK Utility解决错误
C51编译器-语言扩展(4)-函数
Function Declarations函数声明 Cx51对标准C进行了一定的扩展,使用这些扩展可以: (1)把一个函数声明为一个中断过程 (2)选择使用的寄存器组 (3)选择存储器模式 (4)声时可重入 (5)声明外部函数 在函数声中包含这些扩展或属性,使用下面的方法声明Cx51函数 funcname ( ) 这里 return_type 返回值类型 funcname 函数名称 args 参数 small, compact, or large 存储器模式 reentrant 是否可重入 interrupt 是否是中断函数
[单片机]
STM32 外部中断配置
1配置中断 1、 分配中断向量表: /* Set the Vector Table base location at 0x20000000 */ NVIC_SetVectorTable(NVIC_VectTab_RAM, 0x0); 2、 设置中断优先级: NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0); //设置中断优先级 3、 初始化外部中断: /*允许EXTI4中断 */ NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQChannel; //中断通道 NVIC_InitS
[单片机]
嵌入式系统学习——STM32之按键输入
之前写了两篇关于STM32 GPIO的介绍和运用,跑马灯用到了GPIO的推挽输出,但是对于输入还是没有用到,这次就运用一下GPIO的上拉输入。实验还是和以前51做的实验一样,就是判断按键的输入,然后控制LED灯。这次没有直接配置寄存器,而是调用库函数和位操作结合。 注:每一块开发板对应电路都不相同,编写代码需要对应自己的板子,本人两个LED灯对应的GPIO为:GPIOD13和GPIOD14,并且是共阴极。两个按键对应的GPIO为:GPIOC13和GPIOE0,并且共阴极。 首先,LED初始化和上一篇博客中的跑马灯初始化一样,只需拷贝就可以了。 LED初始化函数: #include sys.h
[单片机]
嵌入式系统学习——<font color='red'>STM32</font>之按键输入
STM32 之 UART1(2)
(2)Init_External_Device.c C语言: Codee#14663 #include includes.h /******************************************************************************* == 全局变量 == *******************************************************************************/ //=== UART1_RX变量,TX变量 ======================================
[单片机]
使用STM32解析Rd-03E的串口数据
前言 安信可新款雷达模组Rd-03E已经上市,该雷达采用高性能一发一收微带天线,包含极简化24GHz雷达传感器硬件Rd-03E和智能算法固件RM01,而智能算法固件RM01采用FMCW波形和S3系列芯片专有的先进信号处理技术,可以实现精准的人体测距和运动/微动人体感应。 本应用示例使用STM32解析Rd-03E的串口数据,检测人体距离雷达的距离,根据距离不同来点亮不同的灯珠,0~2米内点亮 LED1,2~4米内点亮 LED2,4~6米内点亮 LED3。 01Rd-03E引脚说明 J2引脚说明: J3引脚说明: 02软件设计框架 03STM32F103C8T6使用CubeMX搭配HAL库配置 打开CubeMX,选
[单片机]
使用<font color='red'>STM32</font>解析Rd-03E的串口数据
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
  • 学习ARM开发(16)
    ARM有很多东西要学习,那么中断,就肯定是需要学习的东西。自从CPU引入中断以来,才真正地进入多任务系统工作,并且大大提高了工作效率。采 ...
  • 学习ARM开发(17)
    因为嵌入式系统里全部要使用中断的,那么我的S3C44B0怎么样中断流程呢?那我就需要了解整个流程了。要深入了解,最好的方法,就是去写程序 ...
  • 学习ARM开发(18)
    上一次已经了解ARM的中断处理过程,并且可以设置中断函数,那么它这样就可以工作了吗?答案是否定的。因为S3C44B0还有好几个寄存器是控制中 ...
  • 嵌入式系统调试仿真工具
    嵌入式硬件系统设计出来后就要进行调试,不管是硬件调试还是软件调试或者程序固化,都需要用到调试仿真工具。 随着处理器新品种、新 ...
  • 最近困扰在心中的一个小疑问终于解惑了~~
    最近在驱动方面一直在概念上不能很好的理解 有时候结合别人写的一点usb的例子能有点感觉,但是因为arm体系里面没有像单片机那样直接讲解引脚 ...
  • 学习ARM开发(1)
  • 学习ARM开发(2)
  • 学习ARM开发(4)
  • 学习ARM开发(6)
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved