STM32F40x的启动代码汇编分析

STM32F40x的启动代码汇编分析_第1张图片
启动代码界面

博客主页,欢迎访问:blog.spursgo.com

之前接触较多的是stm32F103单片机,在开始学习的时候,为了深入地学习,在这款单片机的启动部分也花了不少时间。但是,当时作为初学者,还没有养成学习之后做笔记的习惯。以至于,过了一段时间后,对这部分又有所忘却,刚好这次电子设计竞赛,又要使用stm32的另外一种型号的单片机stm32F407,借此机会,对之前的知识做一个简单的复习与总结。并且作为笔记记录下来,当再次来看这些知识的时候,会轻松很多,也希望能够给正在学习stm32的朋友带来一点帮助。

对于stm32系列的单片机,就启动代码来说,基本上没有什么区别。在这里,我就以stm32F407型号的单片机为例,来讲解一下启动问题。

1.首先,我们来讲解一下stm32的启动方式。

stm32的启动方式我一直觉得是一个比较有趣的地方:通过BOOT0和BOOT1两个管脚的不同电平状态来决定单片机从何处启动运行代码,这给我们的代码调试以及芯片的升级带来了很大的便利。


STM32F40x的启动代码汇编分析_第2张图片
启动方式

从这张图片中我们可以看到,stm32一共有3种不同的启动方式。

在讲解具体的启动方式之前,可能之前没有接触过stm32的朋友会不理解0x00000000和0x00000004地址被映射到不同的地址是什么意思。

下面,我们通过讲解stm32的复位过程来理解上面所提到的几个地址。


STM32F40x的启动代码汇编分析_第3张图片
复位

对所有单片机来说 ,当它获取到复位REESET信号后,首先它会做两个事情:取出栈指针SP的初始值和取出程序指针PC的初始值。对于不同位数的单片机来说,取出这些值的地址表示是不同的。还是以stm32来说,作为32位的处理器,那么以16进制来表示地址的话,就应该是8位数字。所以,栈指针SP的初始值就是从地址0x00000000处取出,程序指针PC的初始值从地址0x00000004处取出。而且可以看出,我们取出的这些值正好也是32位的,占用4个字节,因为我们存储的是地址嘛,32位的,没毛病。

了解了0x00000000和0x00000004是怎么回事了之后,就来继续说启动方式的问题吧。

既然stm32要想实现多种方式的启动,那么我们的SP和PC就应该从不同的位置取呀!而前面所提到的将0x00000000地址映射到0x08000000地址做的正是这个是呀,懂了映射是怎么回事了吗?

好!正式讲解具体的启动方式:

(1)内部FLASH启动方式

当芯片上电后采样到BOOT0引脚为低电平时,0x00000000和0x00000004地址被映射到内部FLASH的首地址0x08000000和0x08000004。因此,内核离开复位状态后,读取内部FLASH的0x08000000地址空间存储的内容,赋值给栈指针SP,作为栈顶地址,再读取内部FLASH的0x08000004地址空间存储的内容,赋值给程序指针PC,作为将要执行的第一条指令所在的地址。具备这两个条件后,内核就可以开始从PC指向的地址中读取指令执行了。

(2)内部SRAM启动方式

类似地,当芯片上电后采样到BOOT0和BOOT1引脚均为高电平时,0x00000000和0x00000004地址被映射到内部SRAM的首地址0x20000000和0x20000004,内核从SRAM空间获取内容进行自举。其实自举就是设置运行环境并执行主体程序,只是听起来高大上罢了。

(3)系统存储器启动方式

当芯片上电后采样到BOOT0引脚为高电平,BOOT1为低电平时,内核将从系统存储器的0x1FFF0000及0x1FFF0004获取MSP及PC值进行自举。系统存储器是一段特殊的空间,用户不能访问,ST公司在芯片出厂前就在系统存储器中固化了一段代码。因而使用系统存储器启动方式时,内核会执行该代码,该代码运行时,会为ISP提供支持(In System Program),如检测USART1/3、CAN2及USB通讯接口传输过来的信息,并根据这些信息更新自己内部FLASH的内容,达到升级产品应用程序的目的,因此这种启动方式也称为ISP启动方式。

很好,方式这个问题我们已经讲完了,那现在来讲讲难啃的启动代码吧。

我们在这里以内部FLASH的启动过程为例,来讲解启动汇编代码。

2.启动代码汇编分析



STM32F40x的启动代码汇编分析_第4张图片
启动代码位置

注意到图片中左边的红色框了吗?startup_stm32f40xx.s文件就是我们的启动代码所在的文件,很奇怪吧!怎么是.s后缀呢?没啥奇怪的,因为里面是汇编代码呀,不是c语言写的哦!启动代码可是要求运行速度非常快的呀,虽然c语言速度也很快了,但是和汇编比起来,呵呵,你懂得。

右边是官方给出的英文注释,本人英语渣渣,在这里我也不去献丑翻译啦,想了解的朋友可以自行翻译,但是个人觉得没有必要去看懂。

下面不废话了,直接来干货,讲解汇编代码。

STM32F40x的启动代码汇编分析_第5张图片
汇编代码

下面对汇编代码进行逐一解释,涉及到相关汇编指令和伪指令,想详细了解的朋友自行百度,这里只是简单的介绍一下,不做深入探讨。

1)堆和栈的初始化

Stack_Size      EQU    0x00000400

这段代码很简单,就是给Stack_Size赋一个值而已,用来定义栈区大小。

栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

AREA    STACK,  NOINIT,  READWRITE,  ALIGN=3

AREA 伪指令用于定义一个段,如代码段、数据段或者堆栈段。(注意:段是汇编语言中非常重要的一个概念,可以去详细了解一下) ;STACK表示我们定义的是栈,其实这里仅仅是一个便于人理解的一个单词啦;NOINIT指定此数据段仅仅保留了内存单元,而没有将各初始值写入内存单元,或者将各个内存单元值初始化为0;READWRITE属性,指定本段为可读可写;ALIGN属性,用来指定数据对齐的方式,为2的ALIGN次方,这是ALIGN=3,也就是按照字节对齐。

Stack_Mem    SPACE  Stack_Size 

SPACE用来分配一片连续的存储区域并初始化为0。这里也就是分配一篇大小为0x400的连续存储区域,并初始化为0,并且该区域的起始地址为Stack_Mem。

__initial_sp 

是汇编代码地址标号,在这里我们用来表示栈空间顶地址。  

Heap_Size      EQU    0x00000200 

用来定义堆区大小。

堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

AREA    HEAP,  NOINIT,  READWRITE,  ALIGN=3

STACK表示我们定义的是堆,其他见上文

Heap_Mem        SPACE  Heap_Size

这里也就是分配一篇大小为0x200的连续存储区域,并初始化为0,并且该区域的起始地址为Stack_Mem。

__heap_limit  

是汇编代码地址标号,在这里我们用来表示堆空间结束地址。

2)中断向量表定义

PRESERVE8

THUMB

PRESERVE8指定当前文件要求堆栈8位对齐。

THUMB指定所用的指令集为thumb。

其实这两条代码我也不是很懂,希望懂得朋友可以指点一下。

这里我们需要注意一下这条注释:

; Vector Table Mapped to Address 0 at Reset

它指的Address 0并不是真正意义上的0x00000000。在我们之前的假设下(假设STM32从FLASH启动)则此中断向量表起始地址为0x8000000,实际上是在CODE区。

AREA    RESET,  DATA,  READONLY 

AREA定义一块数据段,只可读,段名字是RESET;DATA属性:用于定义数据段,默认为READWRITE。指定本段为可读可写。

EXPORT  __Vectors     

EXPORT伪指令用于在程序中声明一个全局的标号,该标号可在其他的文件中引用。EXPORT可用GLOBAL代替。标号在程序中区分大小写。在程序中声明一个全局的标号__Vectors,该标号可在其他的文件中引用

EXPORT  __Vectors_End 

在程序中声明一个全局的标号__Vectors_End

EXPORT  __Vectors_Size 

在程序中声明一个全局的标号__Vectors_Size

__Vectors      DCD    __initial_sp              

DCD  用于分配一片连续的字存储单元并用指定的数据初始化。向量表第一个表项是栈顶地址,该处物理地址值即为 __Vetors 标号所表示的值,该地址中存储__initial_sp所表示的地址值,大小为一个字(32bit)。

DCD    Reset_Handler          

向量表第二个表项是复位中断服务函数Reset Handler入口地址

DCD    NMI_Handler

...

...

...

DCD    OTG_FS_IRQHandler        

这些都是在中断向量表中注册函数入口地址,就不在这里挨个解释了。

__Vectors_End  

表示中断向量表结束

__Vectors_Size   EQU   __Vectors_End  -  __Vectors  

得到向量表的大小

3)地址重映射及中断向量表的转移

AREA    |.text|,  CODE,  READONLY   

定义一个代码段,可读,段名字是.text  段名若以数字或者标点开头,则该段名需用"|"括起来,如|1_test|。

Reset_Handler    PROC 

标记一个函数的开始。利用PROC、ENDP这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。

EXPORT  Reset_Handler            [WEAK]  

EXPORT伪指令用于在程序中声明一个全局的标号,[WEAK]选项声明其他的同名标号优先于该标号被引用。在外部没有定义该符号时导出该符号Reset_Handler。

IMPORT  SystemInit

IMPORT  __main 

 IMPORT伪指令用于通知编译器要使用的标号在其他的源文件中定义

LDR    R0, =SystemInit

BLX    R0

把SystemInit函数的地址装载到R0,BLX为跳转指令,跳转到R0中的地址处,换句话说,就是执行SystemInit()这个函数啦。函数SystemInit()主要作用是设置系统时钟频率和中断寄存器的初始化。

LDR    R0, =__main 

BX      R0  

把main()函数的地址装载到R0,BLX为跳转指令,跳转到R0中的地址处,换句话说,就是执行main()这个函数啦,进入C的世界。

ENDP

函数结束

然后由很长的代码都是用来定义相关函数的,这里就不解释了。

B

B本来也是跳转指令。它的作用相当于 MOV PC, 目标地址。但是这里B后面没有接上目标地址,我也不知道为什么要用上这么一句。

ENDP

ALIGN

ENDP不多说。ALIGN属性:使用方式为ALIGN 表达式。在默认时,ELF(可执行连接文件)的代码段和数据段是按字对齐的,表达式的取值范围为0~31,相应的对齐方式为2表达式次方。

4)堆和栈的初始化

IF      :DEF:__MICROLIB  

判断是否使用DEF:__MICROLIB(micro lib)

EXPORT  __initial_sp     


EXPORT  __heap_base  ;使外部程序可以使用

EXPORT  __heap_limit

将__initial_sp、__heap_base、__heap_limit赋予全局属性、

ELSE   

IMPORT  __use_two_region_memory  

定义全局标号 __use_two_region_memory

EXPORT  __user_initial_stackheap   

声明全局标号__user_initial_stackheap,这样外程序也可调用此标号。允许进行栈和堆的赋值,在__main函数执行过程中调用。

__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

相当于MOV  PC, LR

LR就是连接寄存器(Link Register,LR)在ARM体系结构中LR的特殊用途有两种:一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。在这里是第一种功能,那么结合BX的用法,就是回到之前保存的返回地址处。

ALIGN

同上一个ALIGN

ENDIF

END

over喏!

最后我们来总结一下启动代码的作用:

(1)堆和栈的初始化;

(2)向量表定义;

(3)地址重映射及中断向量表的转移;

(4)设置系统时钟频率;

(5)中断寄存器的初始化;

(6)进入C应用程序。

我废话了这么多,还是希望各位要好好消化一下哦!

你可能感兴趣的:(STM32F40x的启动代码汇编分析)