STM32 MCU的启动过程与分散加载分析

预备知识

ARM Cortex系列的MCU在复位、开机的时候, 都会从 0x00000000的内存地址上去获取向量表,特权等级的软件也可以通过写入VTOR寄存器来修改向量表存储地址, 这个值是相当对于Flash_Base地址的偏移量, 这样就可以让程序从不同的存储位置去获取向量表,注意: VTOR寄存器存储 向量表的基地址相对于0x000000地址偏移值。
完整的向量表形式类似下图:
STM32 MCU的启动过程与分散加载分析_第1张图片
首先0x00的地址存储的是栈指针,即内核用于设置MSP寄存器的初始值, 而0x04的地址存储的是所有异常向量的起始地址, 也就是内核用于设置PC寄存器的初始值。这两个内存地址和大小在stm32芯片中, 可以通过MDK->TARGET中配置。 注意: MSP寄存器最先被设置是很有必要的, 这是因为MCU在复位的时候,在很短的时间内很有可能产生NMI和HardFault的异常, 而在异常处理前,将处理器压栈便需要先进行栈存储的操作。

STM32烧写配置

在使用MDK编写stm32的程序的时候,我们的设置一般类似于下图, 配置了只读内存空间的起始地址和大小,以及可读写内存空间的起始地址和大小, 一般这个配置都是根据MDK-->Device中选择的文件来的, 每个型号的这个地址不一定相同, 所以型号要选对, 才能够确保程序被正确加载和启动。 注意: 可以设置mcu内置的内存空间, 也可以配置mcu外接的内存空间。
STM32 MCU的启动过程与分散加载分析_第2张图片
这个配置最终会输出到sct文件中, 通过sct文件将会指定只读内存空间的地址和大小(从0x800000开始),然后指定可读写内存空间的地址和大小(从0x20000000开始)。

分散加载文件是一个文本文件,通过编写一个分散加载文件来指定ARM连接器在生成映像文件时如何分配RO,RW,ZI等数据的存放地址。
其中, RESET,+First表示会启动代码里的创建的RESET数据段放置在内存最前面, 分散加载文件中的ER_IROM会将RO数据存储于0x8000000这个区域, RW_IRAM1会将 RW+ZI数据存储于0x20000000这个区域。
而这其中和汇编代码的关系就在于, 启动代码会去开辟RW内存区域作为栈, 开辟RI内存区域存放向量表, 详情参考最下方的启动程序。
这块知识内容, 可以参考《 CM3启动分析及文件分散加载》。

STM32 MCU的启动过程与分散加载分析_第3张图片

STM32 启动设置

既然arm都规定了上电后, MCU从0x0000000地址开始取值执行, 为何stm32在烧写的时候, 配置RO内存的位置是从0x08000000的地址开始呢? 这个问题是因为ST公司通过了地址映射来设计了三种启动模式。如下图所示:

  1. 当设置了从flash启动, stm32 mcu就会将0x0000000地址映射到0x08000000,那么mcu从0x00000000启动时,读到的值和0x08000000的值时一样的。
  2. 当设置了从SRAM启动,stm32 mcu就会将0x20000000地址映射到0x080000000。
  3. 当设置了从system memory启动时,system memory的地址(0x1FF00000)将会被映射到0x00000000。其实该部分内存存储的正是ST官方在生产芯片时写入的bootloader, 方便用户进行ISP下载。
    如下为stm32 芯片的内存映射图。STM32 MCU的启动过程与分散加载分析_第4张图片
  4. stm32 通过以下方式配置Boot引脚,以此来选择启动的内存区域。
    STM32 MCU的启动过程与分散加载分析_第5张图片

STM32 启动代码

该部分的内容主要参考st官方提供的汇编代码, 主要做了以下工作。

  1. 先从可读写区域创建空间初始化堆栈和栈顶地址。
  2. 开辟只读数据段来存储栈顶地址和所有异常向量的起始地址,也就是创建向量表并存储于内存中。这部分数据会根据只读的配置和分散加载文件,存储到指定地址的flash当中。要注意: 向量表的第一个地址存储的值会被拿来用于后续初始化栈指针MSP, 向量表的第二个地址存储的值 会被拿来用于初始化PC寄存器的初始值。
  3. 程序复位时,会取到PC寄存器的值, 然后跳转到Reset_Handler向量,之后跳转到SystemInit配置函数,最后再跳转到C库中的__main函数,初始化堆栈相关的工作, 最终跳转到main函数执行主程序。
    如下所示。
# 栈和堆的空间会通过向量表的第一个字存储的栈地址开辟使用。
Stack_Size      EQU     0x00000400    #定义宏的值,也就是开辟空间的大小。

                AREA    STACK, NOINIT, READWRITE, ALIGN=3   #在SRAM中, 开辟一个空间标号为STACK,可读写,并且不进行初始化
Stack_Mem       SPACE   Stack_Size
__initial_sp   # 该标号表示开辟空间之后的内存位置, 也就是栈顶地址。

Heap_Size       EQU     0x00000200    

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3      #在SRAM中, 分配完栈之后, 再次开辟一个HEAP的空间
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit     # 该标号表示开辟空间之后的内存位置, 也就是栈顶地址。

                PRESERVE8
                THUMB


; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY  #开辟只读数据段来存储数据并命名为RESET, 该数据段将会被按照MDK中地址的配置写入到MCU中。
                EXPORT  __Vectors
                EXPORT  __Vectors_End      #将标号声明为全局属性,并且该标号可以被C语言所使用。
                EXPORT  __Vectors_Size

__Vectors       DCD     __initial_sp              ; Top of Stack    #分配一个字的内存来存储栈顶地址。。
                DCD     Reset_Handler             ; Reset Handler   #分配一个字的内存来存储Reset_Handler标号的地址。
                DCD     NMI_Handler               ; NMI Handler
                DCD     HardFault_Handler         ; Hard Fault Handler
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                DCD     SVC_Handler               ; SVCall Handler
                DCD     0                         ; Reserved
                DCD     0                         ; Reserved
                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     RTC_IRQHandler                 ; RTC through EXTI Line
                DCD     FLASH_IRQHandler               ; FLASH
                DCD     RCC_IRQHandler                 ; RCC
                DCD     EXTI0_1_IRQHandler             ; EXTI Line 0 and 1
                DCD     EXTI2_3_IRQHandler             ; EXTI Line 2 and 3
                DCD     EXTI4_15_IRQHandler            ; EXTI Line 4 to 15
                DCD     0                              ; Reserved
                DCD     DMA1_Channel1_IRQHandler       ; DMA1 Channel 1
                DCD     DMA1_Channel2_3_IRQHandler     ; DMA1 Channel 2 and Channel 3
                DCD     DMA1_Channel4_5_6_7_IRQHandler ; DMA1 Channel 4, Channel 5, Channel 6 and Channel 7
                DCD     ADC1_COMP_IRQHandler           ; ADC1, COMP1 and COMP2 
                DCD     LPTIM1_IRQHandler              ; LPTIM1
                DCD     USART4_5_IRQHandler            ; USART4 and USART5
                DCD     TIM2_IRQHandler                ; TIM2
                DCD     TIM3_IRQHandler                ; TIM3
                DCD     TIM6_IRQHandler                ; TIM6
                DCD     TIM7_IRQHandler                ; TIM7
                DCD     0                              ; Reserved
                DCD     TIM21_IRQHandler               ; TIM21
                DCD     I2C3_IRQHandler                ; I2C3
                DCD     TIM22_IRQHandler               ; TIM22
                DCD     I2C1_IRQHandler                ; I2C1
                DCD     I2C2_IRQHandler                ; I2C2
                DCD     SPI1_IRQHandler                ; SPI1
                DCD     SPI2_IRQHandler                ; SPI2
                DCD     USART1_IRQHandler              ; USART1
                DCD     USART2_IRQHandler              ; USART2
                DCD     LPUART1_IRQHandler             ; LPUART1
                DCD     0                              ; Reserved
                DCD     0                              ; Reserved
   
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

                AREA    |.text|, CODE, READONLY  #开辟只读的程序段

; Reset handler routine  
Reset_Handler    PROC    # 编写Reset_Handler函数
                 EXPORT  Reset_Handler                 [WEAK]   #声明为全局标号, 并且可以被C语言所使用, 弱定义表示该标号可以被外部文件所取代。
        IMPORT  __main		   #引入外部的标号, 类似C语言的Extern, 引入C语言中标号。
        IMPORT  SystemInit      # 引入外部的标号, 类似C语言的Extern, 引入C语言中标号。
                 LDR     R0, =SystemInit  #将函数的入口地址加载到R0寄存器
                 BLX     R0						#跳转到寄存器
                 LDR     R0, =__main     #跳转到_main函数中。
                 BX      R0
                 ENDP      #定义一个执行过程, 由于main函数一般会写一个循环, 则下面的不执行。

; Dummy Exception Handlers (infinite loops which can be modified)

NMI_Handler     PROC    #编写NMI_Handler函数
                EXPORT  NMI_Handler                    [WEAK]   #此处的弱定义,表示会被我们在C语言写的函数所取代, 但是, 如果我们把中断函数名字写错了, 就会默认跳转到这个地方, 一致执行此处的死循环。
                B       .      											#  执行死循环。
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_Handler              [WEAK]
                B       .
                ENDP
SVC_Handler     PROC
                EXPORT  SVC_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  RTC_IRQHandler                 [WEAK]
                EXPORT  FLASH_IRQHandler               [WEAK]
                EXPORT  RCC_IRQHandler                 [WEAK]
                EXPORT  EXTI0_1_IRQHandler             [WEAK]
                EXPORT  EXTI2_3_IRQHandler             [WEAK]
                EXPORT  EXTI4_15_IRQHandler            [WEAK]
                EXPORT  DMA1_Channel1_IRQHandler       [WEAK]
                EXPORT  DMA1_Channel2_3_IRQHandler     [WEAK]
                EXPORT  DMA1_Channel4_5_6_7_IRQHandler [WEAK]
                EXPORT  ADC1_COMP_IRQHandler           [WEAK]
                EXPORT  LPTIM1_IRQHandler              [WEAK]
                EXPORT  USART4_5_IRQHandler            [WEAK]
                EXPORT  TIM2_IRQHandler                [WEAK]
                EXPORT  TIM3_IRQHandler                [WEAK]
                EXPORT  TIM6_IRQHandler                [WEAK]
                EXPORT  TIM7_IRQHandler                [WEAK]
                EXPORT  TIM21_IRQHandler               [WEAK]
                EXPORT  TIM22_IRQHandler               [WEAK]
                EXPORT  I2C1_IRQHandler                [WEAK]
                EXPORT  I2C2_IRQHandler                [WEAK]
                EXPORT  I2C3_IRQHandler                [WEAK]
                EXPORT  SPI1_IRQHandler                [WEAK]
                EXPORT  SPI2_IRQHandler                [WEAK]
                EXPORT  USART1_IRQHandler              [WEAK]
                EXPORT  USART2_IRQHandler              [WEAK]
                EXPORT  LPUART1_IRQHandler             [WEAK]

WWDG_IRQHandler
PVD_IRQHandler
RTC_IRQHandler
FLASH_IRQHandler
RCC_IRQHandler
EXTI0_1_IRQHandler
EXTI2_3_IRQHandler
EXTI4_15_IRQHandler
DMA1_Channel1_IRQHandler
DMA1_Channel2_3_IRQHandler
DMA1_Channel4_5_6_7_IRQHandler
ADC1_COMP_IRQHandler 
LPTIM1_IRQHandler
USART4_5_IRQHandler
TIM2_IRQHandler
TIM3_IRQHandler
TIM6_IRQHandler
TIM7_IRQHandler
TIM21_IRQHandler
TIM22_IRQHandler
I2C1_IRQHandler
I2C2_IRQHandler
I2C3_IRQHandler
SPI1_IRQHandler
SPI2_IRQHandler
USART1_IRQHandler
USART2_IRQHandler
LPUART1_IRQHandler

                B       .

                ENDP

                ALIGN

;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
                 IF      :DEF:__MICROLIB
                
                 EXPORT  __initial_sp        #将初始化栈顶地址声明为全局标号, 这几个标号最终会在_main函数中被调用。 
                 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

;************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE*****

过程总结

  • 首先, 烧写程序的时候,如何确保向量表的位置就是存储在指定位置上? 在MDK中,会通过分散加载文件来确定RO类型的数据段和RW类型的数据段的起始地址以及空间大小,并且会确保向量表(启动代码中的RESET数据段)放置在RO数据段的开头中。 关于分散文件的最终加载情况, 可以参考代码工程编译出来的map文件
  • 其次,启动代码会先开辟一段RW类型的数据段,命名为STACK标号(根据分散加载文件,会在RAM中开辟一段空间, 这个空间的大小不能超过RAM的尺寸),并且,汇编程序会将开辟出来的栈空间的栈顶地址值传递到向量表的第一个字的地址上, 最后, 会再创建RO数据段来存储向量表的数据(根据分散加载文件,会在Flash内存中开辟一段空间存储)。
  • 最后, stm32芯片会在复位的时候,通过地址映射从0x00000000地址上获得向量表,再从向量表的第一个字的地址取得栈空间的地址,也就是MSP寄存器的值, 从第二个字取得复位向量函数的地址,作为PC寄存器的值, 然后,进入复位向量函数Reset_Handler,注意,弱定义的函数,用户可再次在C语言中修改, 然后, 执行system_init初始化函数, 然后进入__main()库函数中, 通过获得的栈堆的空间地址和空间大小初始化堆栈, 最终跳转到用户的main()函数中。

你可能感兴趣的:(嵌入式软件开发,stm32,mcu,单片机)