0x00 为啥浅析这个文件呢
在使用Keil做STM32的开发时,比较沉迷于一键编译和一键下载,所以开发时关注点直接聚焦在了main()函数里,但其实单片机从上电到真正开始运行main()函数,中间还做了很多的工作,分析startup_stm32f103xe.s
这个文件(以下称为启动文件),有利于理解这一过程。
0x01 什么是启动文件,有什么用?
- 什么是启动过程:MCU从复位到开始执行main()函数之间的过程。
- 什么是启动文件:在启动过程中,正确配置MCU,使其准备好运行C程序。
- 谁提供启动文件:谁生产的芯片,谁就来提供启动文件。
- 为什么芯片厂商要提供启动文件:向用户屏蔽启动过程,专注于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行:从外部文件导入__main
和SystemInit
这两个标号。
第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()函数。