博客主页,欢迎访问:blog.spursgo.com
之前接触较多的是stm32F103单片机,在开始学习的时候,为了深入地学习,在这款单片机的启动部分也花了不少时间。但是,当时作为初学者,还没有养成学习之后做笔记的习惯。以至于,过了一段时间后,对这部分又有所忘却,刚好这次电子设计竞赛,又要使用stm32的另外一种型号的单片机stm32F407,借此机会,对之前的知识做一个简单的复习与总结。并且作为笔记记录下来,当再次来看这些知识的时候,会轻松很多,也希望能够给正在学习stm32的朋友带来一点帮助。
对于stm32系列的单片机,就启动代码来说,基本上没有什么区别。在这里,我就以stm32F407型号的单片机为例,来讲解一下启动问题。
1.首先,我们来讲解一下stm32的启动方式。
stm32的启动方式我一直觉得是一个比较有趣的地方:通过BOOT0和BOOT1两个管脚的不同电平状态来决定单片机从何处启动运行代码,这给我们的代码调试以及芯片的升级带来了很大的便利。
从这张图片中我们可以看到,stm32一共有3种不同的启动方式。
在讲解具体的启动方式之前,可能之前没有接触过stm32的朋友会不理解0x00000000和0x00000004地址被映射到不同的地址是什么意思。
下面,我们通过讲解stm32的复位过程来理解上面所提到的几个地址。
对所有单片机来说 ,当它获取到复位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.启动代码汇编分析
注意到图片中左边的红色框了吗?startup_stm32f40xx.s文件就是我们的启动代码所在的文件,很奇怪吧!怎么是.s后缀呢?没啥奇怪的,因为里面是汇编代码呀,不是c语言写的哦!启动代码可是要求运行速度非常快的呀,虽然c语言速度也很快了,但是和汇编比起来,呵呵,你懂得。
右边是官方给出的英文注释,本人英语渣渣,在这里我也不去献丑翻译啦,想了解的朋友可以自行翻译,但是个人觉得没有必要去看懂。
下面不废话了,直接来干货,讲解汇编代码。
下面对汇编代码进行逐一解释,涉及到相关汇编指令和伪指令,想详细了解的朋友自行百度,这里只是简单的介绍一下,不做深入探讨。
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应用程序。
我废话了这么多,还是希望各位要好好消化一下哦!