一、文档背景
分析以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系列的启动文件进行分析,对内部的汇编语言进行剖析,进行逐行分析和说明注释。