JNH官网

【ARM】启动文件(startup)-STM32F1X系列

一、文档背景

分析以STM321x系列为例的启动文件解析,了解相关的代码原理和启动文件的配置内容。对MDK的售前和售后培训有一定的积累帮助,以及客户对相关汇编语言的咨询,同时JNH官网可以更加深入了解到功能更复杂的CPU启动文件。


二、 启动文件简介

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

1. 初始化堆栈指针SP(__initial_sp)

2. 初始化PC指针(Reset_Handler)

3. 初始化中断向量表(__Vectors)

4. 配置系统时钟(SystemInit)

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


三、查找ARM汇编指令

在MDK内可以搜索到ARM的汇编指令,以EQU为例,检索步骤如下:

打开MDK软件界面,点击“Help”->“Uvision Help“后进入”ARM Development Tools“界面进入搜索界面,输入检索名称,选中”只搜索标题“后回车搜索。

图3-1

下面列出了启动文件中使用到的ARM汇编指令,该列表的指令全部从ARM Development Tools这个帮助文档里面检索而来。其中编译器相关的指令WEAK和ALIGN为了方便也放在同一个表格内。

表 2‑1 启动文件使用的ARM汇编指令汇总

指令名称作用
EQU给数字常量取一个符号名,相当于C语言中的define
AREA汇编一个新的代码段或者数据段
SPACE分配内存空间
PRESERVE8当前文件堆栈需按照8字节对齐
EXPORT声明一个标号具有全局属性,可被外部的文件使用
DCD以字为单位分配内存,要求4字节对齐,并要求初始化这些内存
PROC定义子程序,与ENDP成对使用,表示子程序结束
WEAK弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号, 如果外部文件没有定义也不出错。要注意的是:这个不是ARM的指令,是编译器的,这里放在一起只是为了方便。
IMPORT声明标号来自外部文件,跟C语言中的EXTERN关键字类似
B跳转到一个标号
ALIGN编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐 要注意的是:这个不是ARM的指令,是编译器的,这里放在一起只是为了方便。
END到达文件的末尾,文件结束
IF,ELSE,ENDIF汇编条件分支语句,跟C语言的if else类似

注意:经测试MDK5.36及以前版本搜索出来标题为”Assember User Guide:“


表 2‑1 启动文件使用的ARM汇编指令汇总


图3-2



四、启动文件代码解析

1. 注释说明


图4-1

该启动代码适用的芯片系列、代码版本、日期、版权所有者等相关信息。


2. Stack栈

图4-2

EQU:给数字常量取一个符号名,相当于C语言中的define。

本例中使用EQU命名 Stack_Size为0x00000400。

AREA:汇编一个新的代码段或者数据段。

STACK:命名为(HEAP)栈,

NOINIT:不进行初始化

READWRITE:可读可写

ALIGN=3 :8(2^3)字节对齐,为8字节对齐。

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

本例中使用SPACE分配 Stack_Mem大小为 Stack_Size的内存空间,即为0x00000400(1024个字节)(1K)

标号__initial_sp紧挨着SPACE语句放置,表示栈的结束地址,即栈顶地址(栈的增长方向是从高地址到低地址)


注:

栈存储函数的形参、以及函数里定义的局部变量,所以在本例中函数的局部变量、数组这些不能超过1K(含嵌套的函数),否则程序就会崩溃进入hardfaul.

除去这些局部变量以外,还有一些实时操作系统的现场保护、返回地址都是存储在栈里面。



3. Heap堆

图4-3

EQU:给数字常量取一个符号名,相当于C语言中的define。

本例中使用EQU命名 Heap_Size为0x00000200。

AREA:汇编一个新的代码段或者数据段。

HEAP:命名为(HEAP)栈,

NOINIT:不进行初始化

READWRITE:可读可写

ALIGN=3 :8(2^3)字节对齐,为8字节对齐。

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

本例中使用SPACE分配 Heap_Mem大小为 Heap_Size的内存空间,即为0x00000200(512个字节)

标号_heap_limit紧挨着SPACE语句放置,表示堆的结束地址(堆的增长方向是从低地址到高地址)


注:

堆主要用来动态内存的分配,意味着如果你用malloc()函数,那么最大分配的内存不能大于512字节,否则程序会崩溃。


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

THUMB:THUMB指令指示汇编程序使用UAL语法将后续指令解释为T32指令。


4. 向量表



图4-4-1

AREA:汇编一个新的代码段或者数据段。

RESET:命名为RESET(复位),

DATA:包含数据,而不是指令。默认为READWRITE

READONLY:只可读不可写

EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。

本例中:声明 __Vectors、__Vectors_End和__Vectors_Size这三个标号具有全局属性,可供外部的文件调用。

图4-4-2

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


__initial_sp:栈顶地址 0x0000 0000

Reset_Handler:复位程序 0x0000 0004

NMI_Handler:不可屏蔽中断,RCC时钟安全程序连接到NMI向量0x0000 0008

HardFault_Handler: 所有类型的错误 0x0000 000C

MemManage_Handler:存储器管理 0x0000 0010

BusFault_Handler:预取指失败,存储器访问失败 0x0000 0014

UsageFault_Handler:未定义的指令或非法状态 0x0000 0018

0:保留函数当前状态,0x0000 001C - 0x0000 002B

SVC_Handler:通过SWI指令的系统服务调用 0x0000 002C

DebugMon_Handler :调试监控器 0x0000 0030

0:0:保留函数当前状态 0x0000 0030 - 0x0000 0034

PendSV_Handler :可挂起的系统服务 :0x0000 0034

SysTick_Handler:系统嘀嗒定时器:0x0000 0038


外部中断:

WWDG_IRQHandler :窗口定时器中断 0x0000 0040

PVD_IRQHandler:连到EXTI的电源电压检测(PVD)中断 0x0000 0044

TAMPER_IRQHandler:侵入检测中断 0x0000 0048

RTC_IRQHandler:实时时钟(RTC)全局中断 0x0000 004C

FLASH_IRQHandler:闪存全局中断 0x0000 0050

RCC_IRQHandler:复位和时钟控制(RCC)中断 0x0000 0054


根据芯片手册,在起始文件内进行地址的设置。


__Vectors为向量表起始地址,__Vectors_End 为向量表结束地址,两者标号的差值即可算出向量表大小。

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



5. 复位程序



图4-5

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

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

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

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

SystemInit()是一个标准的库函数,在system_stm32f103xe.c这个库文件中定义。主要作用是配置系统时钟,这里调用这个函数之后,单片机的系统时钟被配置为72M。

指令名称作用
LDR从存储器中加载字到一个寄存器中
BL跳转到由寄存器/标号给出的地址,并把跳转前的下条指令地址保存到LR
BLX跳转到由寄存器给出的地址,并根据寄存器的LSE确定处理器的状态,还要把跳转前的下一条指令地址保存到LR
BX跳转到由寄存器/标号给出的地址,不用返回__main是一个标准的C库函数,主要作用是初始化用户堆栈,并在函数的最后调用main函数去到C的世界。这就是为什么JNH官网写的程序都有一个main函数的原因。


6. 中断服务程序

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

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

里。


图4-6

例如:NMI_Handler

PROC/ENDP:定义子程序,PROC与ENDP成对使用,表示子程序结束

EXPORT:声明一个标号具有全局属性,可被外部的文件使用

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



7. 用户堆栈初始化

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

图4-7-1

首先判断是否定义了__MICROLIB,如果定义了这个宏则赋予标号__initial_sp(栈顶地址)、 __heap_base(堆起始地址)、__heap_limit(堆结束地址)全局属性, 可供外部文件调用。有关这个宏JNH官网在KEIL里面配置,具体见下图。然后堆栈的初始化就由C库函数_main来完成。

图4-7-2

如果没有定义__MICROLIB,则插入标号__use_two_region_memory,这个函数需要用户自己实现,具体要实现成什么样, 可在KEIL的帮助文档里面查询到。

图4-7-3

然后声明标号__user_initial_stackheap具有全局属性,可供外部文件调用,并实现这个标号的内容。

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

END :程序结束。


五、讨论分析

1. 如何快速修改启动文件的值?

在编辑界面将”Text Editor“选择为”Configuration Wizard“,在编辑器中打开文件。大多数启动文件都包含对配置向导提供类似 GUI 的控件来设置值。在下方”Value“内修改相关数据即可。

图5-1


六、总结

无论是何种MCU 都必须有启动文件,因为对于嵌入式开发,绝大部分情况都是使用C语言,而C语言一般都是从main 函数开始,但是对于MCU来说,它是如何找到并执行main函数的,就需要用到“启动文件”,就是各种 startup_xxxx.s 文件。

启动文件是使用机器可以理解的汇编语言,经过一些必要的配置,最终能够调用 main 函数,使得用户程序能够在 MCU上正常运行起来的必备文件。

本文对较为简易的STM32F1x系列的启动文件进行分析,对内部的汇编语言进行剖析,进行逐行分析和说明注释。


jnh官网 jnh官网 jnh官网 jnh官网 金年会 金年会 金年会 金年会