浅析启动文件startup_stm32f103xe.s

0x00 为啥浅析这个文件呢

在使用Keil做STM32的开发时,比较沉迷于一键编译和一键下载,所以开发时关注点直接聚焦在了main()函数里,但其实单片机从上电到真正开始运行main()函数,中间还做了很多的工作,分析startup_stm32f103xe.s这个文件(以下称为启动文件),有利于理解这一过程。

0x01 什么是启动文件,有什么用?

  1. 什么是启动过程:MCU从复位到开始执行main()函数之间的过程。
  2. 什么是启动文件:在启动过程中,正确配置MCU,使其准备好运行C程序。
  3. 谁提供启动文件:谁生产的芯片,谁就来提供启动文件。
  4. 为什么芯片厂商要提供启动文件:向用户屏蔽启动过程,专注于main()函数中功能和逻辑的开发。(开发的门槛低了,不就有更多用户选择自家产品了嘛)

0x02 分析启动文件

文件最上方的注释,有芯片厂商对该文件的概括,值得一看。

;******************** (C) COPYRIGHT 2017 STMicroelectronics ********************
;* File Name          : startup_stm32f103xe.s
;* Author             : MCD Application Team
;* Description        : STM32F103xE Devices vector table for MDK-ARM toolchain. 
;*                      This module performs:
;*                      - Set the initial SP
;*                      - Set the initial PC == Reset_Handler
;*                      - Set the vector table entries with the exceptions ISR address
;*                      - Configure the clock system
;*                      - Branches to __main in the C library (which eventually
;*                        calls main()).
;*                      After Reset the Cortex-M3 processor is in Thread mode,
;*                      priority is Privileged, and the Stack is set to Main.
;******************************************************************************

看第4行:这个文件是给MDK-ARM这个工具链使用的,描述STM32F103XE系列芯片的设备向量表。
6~10行告诉我们这个文件里都做了些什么工作呀。
第6行:设置初始SP(Stack Pointer),也就是栈顶指针。
第7行:设置初始PC(Program Counter),程序计数器。
第8行:设置向量表入口为中断服务程序的地址。
第9行:配置时钟系统。
第10行:跳转到C语言库的__main处,最终调用main()函数。这里告诉我们一个信息:__main和main()不是同一个东西来的!

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size		EQU     0x400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp

注释:根据应用程序的需要给栈分配大小。
第7行:Stack_Size是一个符号(汇编中的符号可以表示变量、数字常量);EQU是等于的意思;0x400的十进制是1024,单位是字节。
第9行:AREA伪指令用于定义一个代码段或数据段;STACK是段的名字;NOINIT是赋于该段的属性,表示不对内存单元做初始化;READWRITE是属性,表示可读可写;ALIGN表示对齐方式,2ALIGN对齐,此处为3,也就是23=8字节对齐。
第10行:Stack_Mem是标号表示地址;SPACE申请一篇内存空间但不赋值;Stack_Size是前面定义的符号,1024字节,也就是申请内存空间的大小。
第11行:__initial_sp标号,紧随上一条指令,表示栈结束的地址,也就是栈顶地址。

; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

堆的初始化代码,__heap_base__heap_limit分别表示堆的起始和结束地址,其他的跟栈差不多,就不解释了。

                PRESERVE8
                THUMB

PRESERVE8意思是堆栈按8字节对齐;THUMB意思是接下来的就是Thumb指令了。

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

第2行:定义名为RESET的只读数据段。
第3、4、5行:声明3个标号可供外部文件调用。

__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                ;;;;;;;;;此处省略部分代码;;;;;;;;;;;
                DCD     PendSV_Handler             ; PendSV Handler
                DCD     SysTick_Handler            ; SysTick Handler

                ; External Interrupts
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                ;;;;;;;;;此处省略部分代码;;;;;;;;;;;
                DCD     DMA2_Channel4_5_IRQHandler ; DMA2 Channel4 & Channel5
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

第1行开头的标号、第14行和第16行的标号就是前面声明的3个可供外部文件调用的标号。
__Vectors保存了向量表起始地址;__Vectors_End保存了向量表结束地址;二者想减即得到__Vectors_Size,即向量表的大小。
DCD分配一个字(4字节)的内存空间,并初始化内存,该指令后面跟随的标号就是要初始化的值。(这些标号在下文有定义,往下看就知道了。)

                AREA    |.text|, CODE, READONLY

定义一个名为.text的只读代码段。

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP

第2行:PROC表示定义子程序,与第10行ENDP成对出现。那现在看看这个子程序里做了什么。
第3行:声明Reset_Handler标号可被外部调用;[WEAK]是编译器的指令,表示若定义,优先使用外部文件的标号,如果外部文件没有定义也不出错。
第4、5行:从外部文件导入__mainSystemInit这两个标号。
第6、7行:执行SystemInit函数。(这个函数定义在system_stm32f1xx.c中)
第8、9行:执行__main函数。(标准C库函数,主要功能是初始化用户堆栈,跟我们自己写的main()不是同一个东西)

NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler          [WEAK]
                B       .
                ENDP
; 省略部分代码......
Default_Handler PROC

                EXPORT  WWDG_IRQHandler            [WEAK]
                ;省略部分代码......
                EXPORT  DMA2_Channel4_5_IRQHandler [WEAK]

WWDG_IRQHandler
;  省略部分代码......
DMA2_Channel4_5_IRQHandler
                B       .

                ENDP

之后是依次跳转到每一个中断服务函数的入口地址,这些中断服务函数都被[WEAK]修饰,也就意味着这些函数可以不定义,用户如果需要使用的话,自己去实现就好了。

                ALIGN

接下来跟着的是一个对齐的指令,后面会跟一个立即数,表示多少个字节对齐,此处缺省,表示4字节对齐。

;*******************************************************************************
;         用户堆栈初始化
;*******************************************************************************
                 IF      :DEF:__MICROLIB
                
                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
                
                 ELSE
                
                 IMPORT  __use_two_region_memory
                 EXPORT  __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

                 ALIGN

                 ENDIF

                 END

第4行:如果定义了__MICROLIB,则把6、7、8行的标号赋于全局属性。之后用户堆栈的初始化工作由C库函数的__main接管。
第10行:没有定义,则执行下面代码(直到25行的ENDIF)
第12、13行:导入一个标号,给__user_initial_stackheap这个标号全局属性。
第15行:实现__user_initial_stackheap这个标号。
17~20行:R0存放堆地址,R1存放栈结束地址,R2存放堆结束地址,R3存放栈地址。
21行:跳转到LR寄存器(链接寄存器)里的地址处。
25行:完结撒花。

0x03 总结

启动文件主要干了啥:定义堆栈大小;定义中断向量表,从栈顶指针开始,依次接中断服务函数入口地址;实现复位中断函数,先初始化时钟系统,然后跳转到__main完成用户堆栈的初始化,最后进入main()函数。

  • 7
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值