STM32F40x的启动代码汇编分析

启动代码界面

博客主页,欢迎访问: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应用程序。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,340评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,762评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,329评论 0 329
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,678评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,583评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,995评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,493评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,145评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,293评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,250评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,267评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,973评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,556评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,648评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,873评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,257评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,809评论 2 339

推荐阅读更多精彩内容

  • 8086汇编 本笔记是笔者观看小甲鱼老师(鱼C论坛)《零基础入门学习汇编语言》系列视频的笔记,在此感谢他和像他一样...
    Gibbs基阅读 37,076评论 8 114
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,258评论 25 707
  • 这篇文章是我之前翻阅了不少的书籍以及从网络上收集的一些资料的整理,因此不免有一些不准确的地方,同时不同JDK版本的...
    高广超阅读 15,507评论 3 83
  • 若说到儿童文学界的泰斗,恐怕就是曹文轩了。 他的文章自然而幽默,精彩处会让人忍俊不禁。最经典的是他的《草房子》了。...
    慢煮生活阅读 775评论 3 2
  • 每个男人心中都住着一位不可比拟的董小姐,亦会遇见乘风而来的赵小姐,无论心中是否存在草原,与你牵手的,一定是能让你温...
    慕与阅读 4,507评论 75 93