第14章 启动文件详解

第14章     启动文件详解

全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn

野火视频教程优酷观看网址:http://i.youku.com/firege

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

14.1 启动文件简介

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

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

2、初始化PC指针=Reset_Handler

3、初始化中断向量表

4、配置系统时钟

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

14.2 查找ARM汇编指令

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

图 141 ARM 汇编指令索引

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

表格 10 启动文件使用的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类似

14.3 启动文件代码讲解

1.    Stack—栈
 1 Stack_Size      EQU
									0x00000400
				
 2
			
 3                 AREA    STACK, NOINIT, READWRITE, ALIGN=3
				
 4 Stack_Mem       SPACE   Stack_Size

			
 5 __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堆
 1 Heap_Size     EQU
									0x00000200
				
 2
			
 3               AREA    HEAP, NOINIT, READWRITE, ALIGN=3
				
 4 __heap_base
				
 5 Heap_Mem      SPACE   Heap_Size
				
 6 __heap_limit

			

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

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

 1 PRESERVE8
				
 2 THUMB

			

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

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

3.    向量表
 1 AREA    RESET, DATA, READONLY
				
 2 EXPORT  __Vectors
				
 3 EXPORT  __Vectors_End
				
 4 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向量表


编号


优先级


优先级类型


名称


说明


地址

 
-


-


-


保留(实际存的是MSP地址)


0X0000 0000

 
-3


固定


Reset


复位


0X0000 0004

 
-2


固定


NMI


不可屏蔽中断。 RCC 时钟安全系统
(CSS) 连接到 NMI 向量


0X0000 0008

 
-1


固定


HardFault


所有类型的错误


0X0000 000C

 
0


可编程


MemManage


存储器管理


0X0000 0010

 
1


可编程


BusFault


预取指失败,存储器访问失败


0X0000 0014

 
2


可编程


UsageFault


未定义的指令或非法状态


0X0000 0018

 
-


-


-


保留


0X0000 001C-

0X0000 002B

 
3


可编程


SVCall


通过 SWI 指令调用的系统服务


0X0000 002C

 
4


可编程


Debug Monitor


调试监控器


0X0000 0030

 
-


-


-


保留


0X0000 0034

 
5


可编程


PendSV


可挂起的系统服务


0X0000 0038

 
6


可编程


SysTick


系统嘀嗒定时器


0X0000 003C


0


7


可编程


-


窗口看门狗中断


0X0000 0040


1


8


可编程


PVD


连接EXTI 线的可编程电压检测中断


0X0000 0044


2


9


可编程


TAMP_STAMP


连接EXTI 线的入侵和时间戳中断


0X0000 0048


中间部分省略,详情请参考STM32F4xx 中文参考手册》第十章-中断和事件-向量表部分


84


91


可编程


SPI4


SPI4全局中断


0X0000 0190


85


92


可编程


SPI5


SPI5全局中断


0X0000 0194


86


93


可编程


SPI6


SPI6全局中断


0X0000 0198


87


94


可编程


SAI1


SAI1全局中断


0X0000 019C


88


95


可编程


LTDC


LTDC全局中断


0X0000 01A0


89


96


可编程


LTDC_ER


LTDC_ER全局中断


0X0000 01A4


90


97


可编程


DMA2D


DMA2D全局中断


0X0000 01A8

代码 12 向量表

 1 __Vectors  DCD   __initial_sp        ;栈顶地址
				
 2            DCD   Reset_Handler       ;复位程序地址
			
 3            DCD   NMI_Handler
				
 4            DCD   HardFault_Handler
				
 5            DCD   MemManage_Handler
				
 6            DCD   BusFault_Handler
				
 7            DCD   UsageFault_Handler
				
 8            DCD
								0                    ; 0 表示保留
			
 9            DCD
								0
				
10            DCD
								0
				
11            DCD
								0
				
12            DCD   SVC_Handler
				
13            DCD   DebugMon_Handler
				
14            DCD
								0
				
15            DCD   PendSV_Handler
				
16            DCD   SysTick_Handler
				
17
			
18
			
19 ;外部中断开始
					
20            DCD   WWDG_IRQHandler
				
21            DCD   PVD_IRQHandler
				
22            DCD   TAMP_STAMP_IRQHandler
				
23
			
24 ;限于篇幅,中间代码省略
25            DCD   LTDC_IRQHandler
				
26            DCD   LTDC_ER_IRQHandler
				
27            DCD   DMA2D_IRQHandler
				
28 __Vectors_End

			

1 __Vectors_Size EQU __Vectors_End - __Vectors

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

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

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

4.    复位程序
 1 AREA    |.text|, CODE, READONLY

			
定义一个名称为.text的代码段,可读。
 1 Reset_Handler PROC
				
 2               EXPORT  Reset_Handler    [WEAK]
				
 3               IMPORT  SystemInit
				
 4               IMPORT  __main
				
 5
			
 6               LDR
								R0, =SystemInit
				
 7
							BLX
									R0
				
 8
							LDR
									R0, =__main
				
 9
							BX
									R0
				
10
							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你写的主函数名称即可。

1 Reset_Handler PROC
				
 2               EXPORT  Reset_Handler    [WEAK]
				
 3               IMPORT  SystemInit
				
 4               IMPORT  user_main
				
 5
			
 6               LDR
								R0, =SystemInit
				
 7
							BLX
									R0
				
 8
							LDR
									R0, =user_main
				
 9
						BX
								R0
10
						ENDP

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

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


指令名称


作用


LDR


从存储器中加载字到一个寄存器中


BL


跳转到由寄存器/标号给出的地址,并把跳转前的下条指令地址保存到LR


BLX


跳转到由寄存器给出的地址,并根据寄存器的LSE确定处理器的状态,还要把跳转前的下条指令地址保存到LR


BX


跳转到由寄存器/标号给出的地址,不用返回

5.    中断服务程序

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

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

 1 NMI_Handler     PROC    ;系统异常
			
 2                 EXPORT  NMI_Handler           [WEAK]
				
 3                 B       .
				
 4                 ENDP
				
 5
			
 6 ;限于篇幅,中间代码省略
			
 7 SysTick_Handler PROC
				
 8                 EXPORT  SysTick_Handler       [WEAK]
				
 9                 B       .
				
10                 ENDP
				
11
			
12 Default_Handler PROC    ;外部中断
			
13                 EXPORT  WWDG_IRQHandler       [WEAK]
				
14                 EXPORT  PVD_IRQHandler        [WEAK]
				
15                 EXPORT  TAMP_STAMP_IRQHandler [WEAK]
				
16
			
17 ;限于篇幅,中间代码省略
			
18 LTDC_IRQHandler
				
19 LTDC_ER_IRQHandler
				
20 DMA2D_IRQHandler
				
21                 B       .
				
22                 ENDP

			

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

6.    用户堆栈初始化
 1 ALIGN

			

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

;用户栈和堆初始化

 2    IF      :DEF:__MICROLIB
				
 3
			
 4    EXPORT  __initial_sp
				
 5    EXPORT  __heap_base
				
 6    EXPORT  __heap_limit
				
 7
			
8     ELSE
				
9
			
10    IMPORT  __use_two_region_memory
				
11    EXPORT  __user_initial_stackheap
				
12
			
13 __user_initial_stackheap
				
14
			
15    LDR     R0, =  Heap_Mem
				
16    LDR     R1, =(Stack_Mem + Stack_Size)
				
17    LDR     R2, = (Heap_Mem +  Heap_Size)
				
18    LDR     R3, = Stack_Mem
				
19
							BX      LR
				
20
			
21
							ALIGN
				
22
			
23
							ENDIF

			
24
				END
			

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

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

END:文件结束

14.4 系统启动流程

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

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

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

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

图 142 复位序列

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

图 143 初始化MSP和PC的一个范例

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

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

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

14.5 每课一问

1、启动文件的主要作用是什么?

2、FLASH地址0存放的是什么?

3、熟悉启动文件里面的ARM汇编指令

原文地址:https://www.cnblogs.com/tianqiang/p/8414464.html

时间: 2024-08-22 04:06:05

第14章 启动文件详解的相关文章

第14章 启动文件详解—零死角玩转STM32-F429系列

第14章     启动文件详解 全套200集视频教程和1000页PDF教程请到秉火论坛下载:www.firebbs.cn 野火视频教程优酷观看网址:http://i.youku.com/firege 本章参考资料<STM32F4xx 中文参考手册>第十章-中断和事件:表 46. STM32F42xxx 和 STM32F43xxx 的向量表:MDK中的帮助手册—ARM Development Tools:用来查询ARM的汇编指令和编译器相关的指令. 14.1 启动文件简介 启动文件由汇编编写,是

KEIL中启动文件详解(汇编语言)

原文在此:http://www.cnblogs.com/mddblog/p/4920063.html 概述 在嵌入式系统中,启动文件是整个系统非常关键的部分,它会进行一些底层的初始化,构建程序运行必要的环境,比如堆栈初始化,变量初始化等.如果启动文件出现错误,则整个系统就跑不起来,因此研究启动文件非常必要. 在keil中,启动文件由汇编代码编写,一般命名为startup_xxx.s,xxx为支持的某种芯片,比如可以是lpc15xx(NXP的LPC15xx系列).MK60D10(飞思卡尔).stm

STM32启动文件详解及SystemInit函数分析(转)

;先在RAM中分配系统使用的栈,RAM的起始地址为0x2000_0000 ;然后在RAM中分配变量使用的堆 ;然后在CODE区(flash)分配中断向量表,flash的起始地址为0x0800_0000,该中断向量表就从这个起始地址开始分配 ;分配完成后,再定义和实现相应的中断函数, ;所有的中断函数全部带有[weak]特性,即弱定义,如果编译器发现在别处文件中定义了同名函数,在链接时用别处的地址进行链接. ;中断函数仅仅实现了Reset_Handler,其他要么是死循环,要么仅仅定义了函数名称

[转]AndroidManifest.xml文件详解

转自:http://www.cnblogs.com/greatverve/archive/2012/05/08/AndroidManifest-xml.html AndroidManifest.xml文件详解(一) 每个应用程序在它的根目录中都必须要有一个AndroidManifest.xml文件.这个清单把应用程序的基本信息提交给Android系统,在应用程序的代码能够运行之前,这个信息系统必须建立.以下是清单文件要做的一些事情: 1. 用Java包给应用程序命名.这个包名是应用程序的唯一标识

【转】linux中inittab文件详解

原文网址:http://www.2cto.com/os/201108/98426.html linux中inittab文件详解 init的进程号是1(ps -aux | less),从这一点就能看出,init进程是系统所有进程的起点,Linux在完成核内引导以后,就开始运行init程序. init程序需要读取配置文件/etc/inittab.inittab是一个不可执行的文本文件,它有若干行指令所组成. 理解Runlevel: runlevel用来表示在init进程结束之后的系统状态,在系统的硬

Linux下inittab文件详解

/etc/inittab文件详解 Linux系统的启动过程为:加电自检-->根据BIOS中的设置从指定的设备启动-->找到设备MBR中的bootloader引导启动系统-->启动kernel-->启动init进程 我们看到,Linux系统启动的最后是启动init进程,而init进程是怎么工作的呢?init进程就是根据/etc/inittab这个文件来在不同的运行级别启动相应的进程或执行相应的操作. /etc/inittab这个文件中定义的登记项都是以:隔开的四个段,即: id:ru

ARM Cortex-M底层技术(2)—启动代码详解

杂谈 工作了一天,脑袋比较乱.一直想把底层的知识写成一个系列,希望可以坚持下去.为什么要写底层的东西呢?首先,工作用到了这部分内容,最近和内部Flash打交道比较多,自然而然会接触到一些底层的东西:第二,近些年来Cortex-M阵营各厂商(ST.Nordic.ATMEL……)对新产品的迭代速度越来越快,以及微控制器应用普及程度的加深,越来越多的开发者把更多精力投注在应用层开发上,花在对底层技术上的时间越来越少,更深层次的原因是走嵌入式底层没有做互联网上层赚钱.希望自己可以把嵌入式ARM Cort

Linux开机启动程序详解

我们假设大家已经熟悉其它操作系统的引导过程,了解硬件的自检引导步骤,就只从Linux操作系统的引导加载程序(对个人电脑而言通常是LILO)开始,介绍Linux开机引导的步骤. 加载内核LILO启动之后,如果你选择了Linux作为准备引导的操作系统,第一个被加载的东西就是内核.请记住此时的计算机内存中还不存在任何操作系统,PC(因为它们天然的设计缺陷)也还没有办法存取机器上全部的内存.因此,内核就必须完整地加载到可用RAM的第一个兆字节之内.为了实现这个目的,内核是被压缩了的.这个文件的头部包含着

T-SQL 操作文件 详解

/*******  导出到excel EXEC master..xp_cmdshell 'bcp SettleDB.dbo.shanghu out c:\temp1.xls -c -q -S"GNETDATA/GNETDATA" -U"sa" -P""' /***********  导入Excel SELECT * FROM OpenDataSource( 'Microsoft.Jet.OLEDB.4.0', 'Data Source="