STM32(Cortex-M)启动流程

启动模式

stm32有三种启动模式,由BOOT0和BOOT1引脚的电平决定,如下图所示:

image-20230412235306263

最常见的是第一种,从片上flash启动,也是芯片的正常运行模式。

第二种从system memory启动,仅适用于使用串口下载程序或者使用USB-DFU模式下载程序的情况,程序同样是下载到flash。

第三种从SRAM启动一般用于程序调试的情况,使用也较少。

启动文件

我们先来分析一下stm32的启动文件,即startup_stm32xxxxxx.s(只要是Cortex-M内核的芯片都会有这个启动文件,名称可能有所区别)。

Stack_Size		EQU     0x400

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

分配了一段大小为1KB的栈空间,段名STACK,可读写,ALIGN=3表示2^3=8字节对齐,__initial_sp紧挨着栈的结束地址,由于栈是从高往低生长,所以__initial_sp的位置就是栈顶。

Heap_Size      EQU     0x200

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

分配了一段大小为512字节的堆空间,段名HEAP,可读可写,8字节对齐,__heap_base和__heap_limit分别是堆的起始地址和结束地址。

                PRESERVE8
                THUMB

指定当前文件的堆栈按照8字节对齐,后面指令兼容16位的Thumb指令。Cortex-M内核实际使用的是Thumb-2指令集,将16位与32位指令混合使用。

                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

__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

                ; External Interrupts
                DCD     WWDG_IRQHandler                   ; Window WatchDog
                DCD     PVD_IRQHandler                    ; PVD through EXTI Line detection
                …………
				DCD     0                                 ; Reserved
				DCD     SPI4_IRQHandler                   ; SPI4

__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

定义了一个数据段,名为DATA,仅可读。

上文为堆栈分配的空间均位于SRAM中,不占用代码空间,从这个数据段开始才是stm32代码空间的起始位置,先定义并初始化了栈顶位置(__initial_sp)以及15个内核异常处理函数的入口地址,接下来是外部中断,最后用结束地址减去开始地址得到__Vectors_Size即本数据段的大小。

Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

reset_handler即复位程序的实际执行代码,上电或是复位都会先从这里开始执行然后进入main函数,具体的执行过程暂且按下不表,我们继续看启动文件的后续内容。

NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
HardFault_Handler\
                PROC
                EXPORT  HardFault_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  TAMP_STAMP_IRQHandler             [WEAK]  
                …………
				EXPORT  SPI4_IRQHandler                   [WEAK]
				
WWDG_IRQHandler                                                       
PVD_IRQHandler                                      
TAMP_STAMP_IRQHandler
…………
SPI4_IRQHandler
           
                B       .

                ENDP

                ALIGN

将除了reset_handler外的内核异常都分别写成无限循环(B .)的弱函数,外部中断也是如此,只是函数起始位置都是同一个地址。

                 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

如果使用了microlib则将栈顶地址和堆的起始、结束地址export出去,microlib会进行堆栈初始化的操作,若是没有使用,则堆栈初始化时会使用__user_initial_stackheap函数。

启动流程

stm32的代码是烧写到flash中的,通过查询手册可知,flash的起始地址是0x08000000:

image-20230413005748842

通过keil已配置好工程的flash download界面也可以查看烧写位置和大小。

image-20230413005521434

但是Cortex-M内核规定上电后必须从0x00000000的位置开始执行,这就需要一个地址映射的操作,不论stm32的启动模式是本文开头说的哪一种,都会将该启动区域的代码映射到0x00000000的位置,进入keil的调试模式打开memory窗口:

image-20230413010654085

可以看到,0x00000000开始的数据和flash起始位置0x08000000开始的数据是完全相同的。

分析启动代码时我们提到过,从DATA段开始才是代码段的起始位置,那么0x08000000作为起始地址的4字节空间存储的就是__initial_sp的地址,即栈顶地址,上电或复位后,硬件会自动将该地址赋给msp,即主栈指针,随后将0x08000004作为起始地址的4字节空间内容,也就是reset_handler函数入口地址赋给PC。

此时程序会立刻去执行reset_handler,让我们回过头来看看reset_handler中做了些什么:

Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

                 LDR     R0, =SystemInit
                 BLX     R0
                 LDR     R0, =__main
                 BX      R0
                 ENDP

可以看到,reset_handler中执行了两个程序,systeminit和__main。

其中不同型号芯片的默认systeminit函数有所区别,内容往往是初始化时钟、FPU等。

用户没有重写__main的情况下,该函数并不在库文件中,而是由armlink创建,如果想要在keil调试的时候查看__main的具体执行过程,需要在BX R0指令运行前点击汇编代码的内容框,再继续单步执行,则可以看到__main的汇编代码,否则会直接跳进main函数。

image-20230413013606826

通过查询官方文档可以得知,__main中调用了__scatterload和__rt_entry两个函数:

image-20230413013804469

补充一下官方文档的说明:应用代码和数据可能存于“根区域”或是“非根区域”,前者拥有相同的加载和执行地址,后者则不同。(这里结合文档,我的个人理解是:只读的代码和变量这类不需要移动的数据都属于“根区域”,即flash中,而可读可写的数据属于“非根区域”,因为要搬运到RAM中才能实现写操作,也不排除用户自行定义变量存储位置时,需要额外的搬运工作,这也属于“非根区域”)

默认的__scatterload函数做了两件事:

1.将ZI段数据全部初始化为0

2.将“非根区域”的数据从加载域复制到执行域(将可读可写的数据搬运到RAM)

在keil工程目录下的${工程名}.sct文件中可以查看加载域和执行域的地址以及数据存储位置:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x08000000 0x00080000  {    ; load region size_region
  ER_IROM1 0x08000000 0x00080000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x20000000 0x00018000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

可以看到,加载域的地址就是flash的地址,且只读的数据(RO、XO)放在flash(0x08000000–0x08080000)中,可读可写的数据(RW、ZI)放在RAM(0x20000000–0x20018000)中。

__scatterload函数执行完后接着调用__rt_entry,__rt_entry又调用了如下函数:

image-20230414005606288

其中__user_setup_stackheap就是启动文件末尾的函数,如果没有使用microlib的话就会调用它。

__rt_entry负责初始化堆栈以及C语言库子系统,即C语言代码运行所必需的环境,随后万事俱备,就可以跳入main函数执行C语言代码了。

当然,如果没有在main函数中写死循环的话,main函数执行完了后会到达exit退出程序。

总结

简单总结一下,芯片上电第一件事是把代码空间的第一行,即栈顶指针赋给msp,随后将第二行reset_handler的入口地址赋给pc,使程序立刻跳转去执行reset_handler,在reset_handler中先初始化时钟、FPU等硬件相关配置(具体内容根据芯片型号有所不同),再初始化ZI段,将代码和数据从加载域搬运到执行域,随后初始化堆栈和C语言库子系统,使得C语言代码能够正常运行,最后跳入main函数执行用户编写的C程序。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值