STM32启动代码原理分析(底层技术);

简述

ARM Cortex-M系列MCU的启动代码(使用汇编语言编程则不需要)主要做3件事情:

  • 初始化并正确放置异常/中断矢量表;

  • 分散加载;

  • 初始化C语言运行环境(初始化堆栈以及C Library、浮点等)。

Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口矢量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口矢量,跳转执行复位中断服务程序。对比ARM7/ARM9内核,Cortex-M3内核则是固定了中断矢量表的位置而起始地址是可变化的。

源码分析

基于STM32F103C6T6的启动文件startup_stm32f103x6.s的简要说明如下:

;******************** (C) COPYRIGHT 2017 STMicroelectronics ********************
;* File Name          : startup_stm32f103x6.s
;* Author             : MCD Application Team
;* Description        : STM32F103x6 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.
;******************************************************************************
;* @attention
;*
;* Copyright (c) 2017 STMicroelectronics.
;* All rights reserved.
;*
;* This software component is licensed by ST under BSD 3-Clause license,
;* the "License"; You may not use this file except in compliance with the
;* License. You may obtain a copy of the License at:
;*                        opensource.org/licenses/BSD-3-Clause
;*
;******************************************************************************

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
;  Stack Configuration
;    Stack Size (in Bytes)
;

Stack_Size		EQU     0x400       ;声明栈的大小为0x400字节

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size  ;开辟一段大小为Stack_Size的内存空间作为栈
__initial_sp                        ;标号__initial_sp,表示栈空间顶地址。

;  Heap Configuration
;     Heap Size (in Bytes)
;

Heap_Size       EQU     0x200     ;声明栈的大小为0x200字节

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base                       ;标号__heap_base,表示堆空间起始地址。
Heap_Mem        SPACE   Heap_Size ;开辟一段大小为Heap_Size的内存空间作为堆。
__heap_limit                      ;标号__heap_limit,表示堆空间结束地址。

;--------------------------------------------
;第一部分:
;启动代码最重要的工作是把异常中断向量表放到正确的Flash地址上
;把向量表定义为只读数据段,并导出向量表标号(Symbol),让链接器识别此标号并根据分散加载文件正确的放置向量表
;__Vectors标号需要与分散加载文件合起来看,才会明白其真正的功能
;--------------------------------------------
                PRESERVE8         ;告诉编译器以8字节对齐。
                THUMB             ;告诉编译器使用THUMB指令集。

; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY   ;声明权限为“READONLY”的名称为“RESET”的数据段
                                       ;(假设STM32从FLASH启动,则此中断矢量表起始地址即为0x8000000)
                EXPORT  __Vectors      ;将标号__Vectors声明为全局标号,这样外部文档就可以使用这个标号。
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size
;标号__Vectors,表示中断矢量表入口地址
;创建中断矢量表
;---------------------------------------
;第二部分:
;__initial_sp
;   1、栈顶指针地址,此语法跟MDK编译器的底层相关,是ARMCC编译器才能识别的语法
;      GCC与IAR的底层编译器ICCARM编译器不能识别;
;   2、__initial_sp 是一个链接器Image Symbol;
;   3、此处__initial_sp相当于是顶地址,或者此处直接把顶地址写到此处也行(如:0x20004000);
;   4、__initial_sp具体是多少,在此种写法下,是由分散加载文件决定的,下文会有详细论述;
;
;Reset_Handler:
;   1、Reset_Handler函数地址,此处相当于把Reset_Handler函数地址赋值给PC,即调用Reset_Handler函数;
;   2、此处也可以是其他函数,只是把复位函数放于此处最符合实际应用场景。
;      重要关键节点:
;   1、绝大多数cortex-M微控制器(M0、M3、M4都是这样)复位后先进入厂商BOOTROM,此时所有用户行为均无法介入处理器;
;   2、厂商BOOTROM(有些厂商会有其他名称来称呼此功能) 主要负责处理一些芯片最初级初始化、
;      加密以及一些对MCU的差异化设置等工作:
;   3、BOOTROM顺利完成后,MCu控制权会交给用户,即启动代码;
;   4、启动代码(运行汇编语言则不需要此启动代码),最重要的工作在于设置MSP (主堆栈指针)以及PC(程序计数器)的值;
;   5、Cortex-M微控制器会默认把0x00000000地址里面的值设置为MSP的值,0x00000004地址里面的值设置为PC的值;
;   6、5中的默认地址可以通过修改Cortex-M中的VTOR寄存器来重新映射,比如改到0x20000000地址或其他;
;
;关于Exception(异常) 与Interrupt (中断) 的区别说明
;   1、Exception(异常)与Interrupt(中断)是不同的,是两个不同的概念,很多人会混淆两者,
;      把他们都按照中断来看待,这是错误的;
;   2、Exception(异常)是向量表的前16个向量,其优先级为负数,高于所有中断,而且不可调整优先级也不可关闭,
;      可以打断正常程序与Interrupt(中断) 的运行:
;   3、从第16个向量以后才是Interrupt(中断),可以设置优先级,不用时可以关闭,但优先级永远低于Exception(异常);
;   4、从这里大家就可以理解Pendsy Handler与svsTick Handler为什么会被用于嵌入式操作系统,
;      因为其优先级高于所有中断,可以确保操作系统拥有高于普通用户程序执行的超级权限,
;      SVC_Handler有时也会用于操作系统,原理相同;
;   5、用户应用程序应尽量避免使用Exception(异常);
;---------------------------------------
__Vectors       DCD     __initial_sp               ; Top of Stack  栈顶地址
                DCD     Reset_Handler              ; Reset Handler 复位向量
                DCD     NMI_Handler                ; NMI Handler
                DCD     HardFault_Handler          ; Hard Fault Handler
                DCD     MemManage_Handler          ; MPU Fault Handler
                DCD     BusFault_Handler           ; Bus Fault Handler
                DCD     UsageFault_Handler         ; Usage Fault Handler
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SVC_Handler                ; SVCall Handler
                DCD     DebugMon_Handler           ; Debug Monitor Handler
                DCD     0                          ; Reserved
                DCD     PendSV_Handler             ; PendSV Handler 操作系统会用到的异常向量
                DCD     SysTick_Handler            ; SysTick Handler 操作系统会用到的心跳定时器异常向量(没有操作系统时可以用作普通定时器中断)
;---------------------------------------
;第三部分:
;   1、这里开始是中断向量表;
;   2、各个向量的顺序是芯片设计的时候就定义好的,不能更改;
;---------------------------------------
                ; External Interrupts
                DCD     WWDG_IRQHandler            ; Window Watchdog
                DCD     PVD_IRQHandler             ; PVD through EXTI Line detect
                DCD     TAMPER_IRQHandler          ; Tamper
                DCD     RTC_IRQHandler             ; RTC
                DCD     FLASH_IRQHandler           ; Flash
                DCD     RCC_IRQHandler             ; RCC
                DCD     EXTI0_IRQHandler           ; EXTI Line 0
                DCD     EXTI1_IRQHandler           ; EXTI Line 1
                DCD     EXTI2_IRQHandler           ; EXTI Line 2
                DCD     EXTI3_IRQHandler           ; EXTI Line 3
                DCD     EXTI4_IRQHandler           ; EXTI Line 4
                DCD     DMA1_Channel1_IRQHandler   ; DMA1 Channel 1
                DCD     DMA1_Channel2_IRQHandler   ; DMA1 Channel 2
                DCD     DMA1_Channel3_IRQHandler   ; DMA1 Channel 3
                DCD     DMA1_Channel4_IRQHandler   ; DMA1 Channel 4
                DCD     DMA1_Channel5_IRQHandler   ; DMA1 Channel 5
                DCD     DMA1_Channel6_IRQHandler   ; DMA1 Channel 6
                DCD     DMA1_Channel7_IRQHandler   ; DMA1 Channel 7
                DCD     ADC1_2_IRQHandler          ; ADC1_2
                DCD     USB_HP_CAN1_TX_IRQHandler  ; USB High Priority or CAN1 TX
                DCD     USB_LP_CAN1_RX0_IRQHandler ; USB Low  Priority or CAN1 RX0
                DCD     CAN1_RX1_IRQHandler        ; CAN1 RX1
                DCD     CAN1_SCE_IRQHandler        ; CAN1 SCE
                DCD     EXTI9_5_IRQHandler         ; EXTI Line 9..5
                DCD     TIM1_BRK_IRQHandler        ; TIM1 Break
                DCD     TIM1_UP_IRQHandler         ; TIM1 Update
                DCD     TIM1_TRG_COM_IRQHandler    ; TIM1 Trigger and Commutation
                DCD     TIM1_CC_IRQHandler         ; TIM1 Capture Compare
                DCD     TIM2_IRQHandler            ; TIM2
                DCD     TIM3_IRQHandler            ; TIM3
                DCD     0                          ; Reserved
                DCD     I2C1_EV_IRQHandler         ; I2C1 Event
                DCD     I2C1_ER_IRQHandler         ; I2C1 Error
                DCD     0                          ; Reserved
                DCD     0                          ; Reserved
                DCD     SPI1_IRQHandler            ; SPI1
                DCD     0                          ; Reserved
                DCD     USART1_IRQHandler          ; USART1
                DCD     USART2_IRQHandler          ; USART2
                DCD     0                          ; Reserved
                DCD     EXTI15_10_IRQHandler       ; EXTI Line 15..10
                DCD     RTC_Alarm_IRQHandler        ; RTC Alarm through EXTI Line
                DCD     USBWakeUp_IRQHandler       ; USB Wakeup from suspend
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors
;---------------------------------------
;第四部分:
;这部分开始可以称作Reset Handler实体,芯片上电后,经过BOOTROM后进入的用户可控最开始处的地方;
;   1、如果想让Mcu正常使用c语言,务必在此处调用 main函数;
;   2、__main() 不是main() 两者有着本质性的区别;
;   3、__main()是c Library中的函数,Kei1开发环境中自带的c Library中的函数;
;   4、main()是被 __main()调用的,__main()工作完成后最后一步就是调用main();
;   5、__main()被调用之前,可以根据需要插入一个或多个其他功能函数;
;---------------------------------------
                AREA    |.text|, CODE, READONLY             ;定义只读的代码段

; Reset handler routine
;复位中断服务程序,PROC…ENDP结构表示程序的开始和结束。
Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]   ;声明复位中断矢量Reset_Handler为全局属性
                                                            ;这样外部文档就可以调用此复位中断服务。
     IMPORT  __main                                         ;声明__main标号。
     IMPORT  SystemInit                                     ;声明SystemInit标号。
                 LDR     R0, =SystemInit                    ;跳转SystemInit地址执行
                 BLX     R0
                 LDR     R0, =__main                        ;跳转__main地址执行
                 BX      R0
                 ENDP

; Dummy Exception Handlers (infinite loops which can be modified)
;---------------------------------------------------------------
;第五部分:
;   1、[weak]指定了一个这个函数为"弱函数”;
;   2、这些中断服务函教定义成弱函数的意义是,当中断出现时,需要有一个中断服务函数予以响应,但真实的
;      用户程序往往只会使用一部分中断,甚至不使用中断,所以以下这些函数给出了异常/中断服务函数的
;      默认实现,很简单,默认实现就是死循环汇编中的"B."语句,相当于while(1);因为不知道用户是否会
;      用到多少中断,但这些服务函数又很重要,所以就把这些函数都"实现"并声明为弱函数;
;   3、弱函数的意思是如果用户定义了同样名称的另一个函数,那么默认实现的弱函数就会被覆盖,比如
;      HardFault_Handler异常在下面有一个默认的实现,但这种默认的实现不能满足我的需要的时候,我可以
;      再重新定义一个HardEault Handler函数这个新定义的HardFault_Handler函数会覆盖原有的被声明
;      为[WEAK]的弱函数;
;   4、有很多种适合使用弱函数的场合,默认的异常/中断服务函数只是一种应用场景;
;   5、在C语言中声明弱函数是在函数后加“__attribute((weak))”;
;----------------------------------------------------------------
NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler          [WEAK]
                B       .
                ENDP
MemManage_Handler\
                PROC
                EXPORT  MemManage_Handler          [WEAK]
                B       .
                ENDP
BusFault_Handler\
                PROC
                EXPORT  BusFault_Handler           [WEAK]
                B       .
                ENDP
UsageFault_Handler\
                PROC
                EXPORT  UsageFault_Handler         [WEAK]
                B       .
                ENDP
SVC_Handler     PROC
                EXPORT  SVC_Handler                [WEAK]
                B       .
                ENDP
DebugMon_Handler\
                PROC
                EXPORT  DebugMon_Handler           [WEAK]
                B       .
                ENDP
PendSV_Handler  PROC
                EXPORT  PendSV_Handler             [WEAK]
                B       .
                ENDP
SysTick_Handler PROC
                EXPORT  SysTick_Handler            [WEAK]
                B       .
                ENDP

Default_Handler PROC

                EXPORT  WWDG_IRQHandler            [WEAK]
                EXPORT  PVD_IRQHandler             [WEAK]
                EXPORT  TAMPER_IRQHandler          [WEAK]
                EXPORT  RTC_IRQHandler             [WEAK]
                EXPORT  FLASH_IRQHandler           [WEAK]
                EXPORT  RCC_IRQHandler             [WEAK]
                EXPORT  EXTI0_IRQHandler           [WEAK]
                EXPORT  EXTI1_IRQHandler           [WEAK]
                EXPORT  EXTI2_IRQHandler           [WEAK]
                EXPORT  EXTI3_IRQHandler           [WEAK]
                EXPORT  EXTI4_IRQHandler           [WEAK]
                EXPORT  DMA1_Channel1_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel2_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel3_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel4_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel5_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel6_IRQHandler   [WEAK]
                EXPORT  DMA1_Channel7_IRQHandler   [WEAK]
                EXPORT  ADC1_2_IRQHandler          [WEAK]
                EXPORT  USB_HP_CAN1_TX_IRQHandler  [WEAK]
                EXPORT  USB_LP_CAN1_RX0_IRQHandler [WEAK]
                EXPORT  CAN1_RX1_IRQHandler        [WEAK]
                EXPORT  CAN1_SCE_IRQHandler        [WEAK]
                EXPORT  EXTI9_5_IRQHandler         [WEAK]
                EXPORT  TIM1_BRK_IRQHandler        [WEAK]
                EXPORT  TIM1_UP_IRQHandler         [WEAK]
                EXPORT  TIM1_TRG_COM_IRQHandler    [WEAK]
                EXPORT  TIM1_CC_IRQHandler         [WEAK]
                EXPORT  TIM2_IRQHandler            [WEAK]
                EXPORT  TIM3_IRQHandler            [WEAK]
                EXPORT  I2C1_EV_IRQHandler         [WEAK]
                EXPORT  I2C1_ER_IRQHandler         [WEAK]
                EXPORT  SPI1_IRQHandler            [WEAK]
                EXPORT  USART1_IRQHandler          [WEAK]
                EXPORT  USART2_IRQHandler          [WEAK]
                EXPORT  EXTI15_10_IRQHandler       [WEAK]
                EXPORT  RTC_Alarm_IRQHandler        [WEAK]
                EXPORT  USBWakeUp_IRQHandler       [WEAK]
;---------------------------------------------------
;第六部分
;   这部分比较简单,看一下第五部分的代码就可以理解下面的部分;
;---------------------------------------------------
WWDG_IRQHandler
PVD_IRQHandler
TAMPER_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_IRQHandler
EXTI1_IRQHandler
EXTI2_IRQHandler
EXTI3_IRQHandler
EXTI4_IRQHandler
DMA1_Channel1_IRQHandler
DMA1_Channel2_IRQHandler
DMA1_Channel3_IRQHandler
DMA1_Channel4_IRQHandler
DMA1_Channel5_IRQHandler
DMA1_Channel6_IRQHandler
DMA1_Channel7_IRQHandler
ADC1_2_IRQHandler
USB_HP_CAN1_TX_IRQHandler
USB_LP_CAN1_RX0_IRQHandler
CAN1_RX1_IRQHandler
CAN1_SCE_IRQHandler
EXTI9_5_IRQHandler
TIM1_BRK_IRQHandler
TIM1_UP_IRQHandler
TIM1_TRG_COM_IRQHandler
TIM1_CC_IRQHandler
TIM2_IRQHandler
TIM3_IRQHandler
I2C1_EV_IRQHandler
I2C1_ER_IRQHandler
SPI1_IRQHandler
USART1_IRQHandler
USART2_IRQHandler
EXTI15_10_IRQHandler
RTC_Alarm_IRQHandler
USBWakeUp_IRQHandler

                B       .

                ENDP

                ALIGN

;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
;IF…ELSE…ENDIF结构,判断是否使用DEF:__MICROLIB(此处为不使用)。
;若使用DEF:__MICROLIB,则将__initial_sp,__heap_base,__heap_limit
;亦即栈顶地址,堆始末地址赋予全局属性,使外部程序可以使用。
                 IF      :DEF:__MICROLIB

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit

                 ELSE

                 IMPORT  __use_two_region_memory    ;定义全局标号__use_two_region_memory。
                 EXPORT  __user_initial_stackheap   ;声明全局标号__user_initial_stackheap,
                                                    ;这样外程序也可调用此标号

__user_initial_stackheap                    ;标号__user_initial_stackheap,表示用户堆栈初始化程序入口
                 ;分别保存栈顶指针和栈大小,堆始地址和堆大小至R0,R1,R2,R3寄存器。
                 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;程序完毕
;************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE*****

部分解释

以上便是STM32的启动代码的完整解析,接下来对几个小地方做解释:

1、AREA指令:伪指令,用于定义代码段或数据段,后跟属性标号。其中比较重要的一个标号为“READONLY”或者“READWRITE”,其中“READONLY”表示该段为只读属性,联系到STM32的内部存储介质,可知具有只读属性的段保存于FLASH区,即0x8000000地址后。而“READONLY”表示该段为“可读写”属性,可知“可读写”段保存于SRAM区,即0x2000000地址后。由此可以从栈堆定义的代码知道,堆栈段位于SRAM空间。从AREA RESET, DATA, READONLY可知,中断矢量表放置于FLASH区,而这也是整片启动代码中最先被放进FLASH区的数据。因此可以得到一条重要的信息:0x8000000地址存放的是栈顶地址__initial_sp,0x8000004地址存放的是复位中断矢量Reset_Handler(STM32使用32位总线,因此存储空间为4字节对齐)。

2、 DCD指令:作用是开辟一段空间,其意义等价于C语言中的地址符“&”。因此从__Vectors行开始创建的中断矢量表则类似于使用C语言定义了一个指针数组,其每一个成员都是一个函数指针,分别指向各个中断服务函数。

3、 标号:前文多处使用了“标号”一词。标号主要用于表示一片内存空间的某个位置,等价于C语言中的“地址”概念。地址仅仅表示存储空间的一个位置,从C语言的角度来看,变量的地址,数组的地址或是函数的入口地址在本质上并无区别。

4、 IMPORT __main的__main标号并不表示C程序中的main函数入口地址,因此接下来的BX R0也并不是跳转至main函数开始执行C程序。__main标号表示C/C++标准实时库函数里的一个初始化子程序__main的入口地址。该程序的一个主要作用是初始化堆栈(对于程序清单一来说则是跳转__user_initial_stackheap标号进行初始化堆栈的),并初始化映像文档,最后跳转C程序中的main函数。这就解释了为何所有的C程序必须有一个main函数作为程序的起点——因为这是由C/C++标准实时库所规定的——并且不能更改,因为C/C++标准实时库并不对外界开发源代码。因此,实际上在用户可见的前提下,程序在BX R0后就跳转至.c文档中的main函数,开始执行C程序了。

至此可以总结一下STM32的启动文档和启动过程。首先对栈和堆的大小进行定义,并在代码区的起始处创建中断矢量表,其第一个表项是栈顶地址,第二个表项是复位中断服务入口地址。然后在复位中断服务程序中跳转C/C++标准实时库的__main函数,完成用户堆栈等的初始化后,跳转.c文档中的main函数开始执行C程序。

假设STM32被设置为从内部FLASH启动(这也是最常见的一种情况),中断矢量表起始地位为0x8000000,则栈顶地址存放于0x8000000处,而复位中断服务入口地址存放于0x8000004处。当STM32遇到复位信号后,则从0x80000004处取出复位中断服务入口地址,继而执行复位中断服务程序,然后跳转__main函数,最后进入mian函数,来到C的世界。

图片示意

函数的调用过程:

img

启动流程1(使用标准库,不使用Microlib)

如下图:

img

启动流程2(使用Microlib)

microlib 是缺省 C 库的备选库。它旨在与需要装入到极少量内存中的深层嵌入式应用程序配合使用。这些应用程序不在操作系统中运行。 microlib 进行了高度优化以使代码变得很小。它的功能比缺省 C 库少,并且根本不具备某些 ISOC 特性。某些库函数的运行速度也比较慢,例如, memcpy() 。 microlib与缺省C库之间的主要差异是: microlib不符合ISO C库标准。不支持某些ISO特性,并且其他特性具有的功能也较少; microlib不符合IEEE 754二进制浮点算法标准; microlib进行了高度优化以使代码变得很小; 无法对区域设置进行配置。缺省C区域设置是唯一可用的区域设置; 不能将main()声明为使用参数,并且不能返回内容; 不支持stdio,但未缓冲的stdin、stdout和stderr除外; microlib对C99函数提供有限的支持; microlib不支持操作系统函数; microlib不支持与位置无关的代码; microlib不提供互斥锁来防止非线程安全的代码; microlib不支持宽字符或多字节字符串; 与stdlib不同,microlib不支持可选择的单或双区内存模型。microlib只提供双区内存模型,即单独的堆栈和堆区。

启动流程如下图:

img