STM32 芯片启动过程

目录

  • 一、前言
  • [二、STM32 的启动模式](#二、STM32 的启动模式)
  • [三、STM32 启动文件分析](#三、STM32 启动文件分析)
    • [1、栈 Stack](#1、栈 Stack)
    • [2、堆 Heap](#2、堆 Heap)
    • [3、中断向量表 Vectors](#3、中断向量表 Vectors)
      • [3.1 中断响应流程](#3.1 中断响应流程)
    • [4、复位程序 Reset_Handler](#4、复位程序 Reset_Handler)
    • 5、中断服务函数
    • 6、用户堆栈初始化
  • [四、STM32 启动流程分析](#四、STM32 启动流程分析)
    • [1、初始化 SP、PC 及中断向量表](#1、初始化 SP、PC 及中断向量表)
    • 2、设置系统时钟
    • [3、初始化堆栈并进入 main](#3、初始化堆栈并进入 main)
  • 五、总结

一、前言

下面主要讲解从上电复位到 main 函数的过程。主要有以下步骤:

  1. 初始化堆栈指针:SP = __initial_spPC = Reset_Handler
  2. 初始化中断向量表
  3. 配置系统时钟
  4. 调用 C 库函数 _main 初始化用户堆栈,然后进入 main 函数

二、STM32 的启动模式

启动模式决定了中断向量表的位置,STM32 有三种启动模式:

  1. 主闪存存储器启动 :从 STM32 内置的 Flash 启动( 0 x 08000000 − 0 x 0807 F F F F 0x0800 0000-0x0807 FFFF 0x08000000−0x0807FFFF),一般我们使用 JTAG 或者 SWD 模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。以 0x08000000 对应的内存为例,则该块内存既可以通过 0x00000000 操作也可以通过 0x08000000 操作,且都是操作的同一块内存。
  2. 系统存储器启动 :从系统存储器启动( 0 x 1 F F F F 000 − 0 x 1 F F F F 7 F F 0x1FFFF000-0x1FFF F7FF 0x1FFFF000−0x1FFFF7FF),这种模式启动的程序功能是由厂家设置的。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的 ISP 程序中,提供了串口下载程序的固件,可以通过这个 ISP 程序将用户程序下载到系统的 Flash 中。以 0x1FFFFFF0 对应的内存为例,则该块内存既可以通过 0x00000000 操作也可以通过 0x1FFFFFF0 操作,且都是操作的同一块内存。
  3. 片上 SRAM 启动 :从内置 SRAM 启动( 0 x 20000000 − 0 x 3 F F F F F F F 0x2000 0000-0x3FFFFFFF 0x20000000−0x3FFFFFFF),既然是 SRAM,自然也就没有程序存储的能力了,这个模式一般用于程序调试。SRAM 只能通过 0x20000000 进行操作,与上述两者不同。从 SRAM 启动时,需要在应用程序初始化代码中重新设置向量表的位置。

我们可以选择设置 BOOT0 和 BOOT1 的引脚电平状态,来选择复位后的启动模式,如下表:

BOOT1 BOOT0 自举模式 自举空间
x 0 主 Flash 选择主 Flash 作为自举空间
0 1 系统存储器 选择系统存储器作为自举空间
1 1 嵌入式 SRAM 选择嵌入式 SRAM 作为自举空间

启动模式只决定程序烧录的位置,加载完程序之后会有一个重映射(映射到 0x00000000 地址位置);真正产生复位信号的时候,CPU 还是从开始位置执行

值得注意的是 STM32 上电复位以后,代码区都是从 0x00000000 开始的,三种启动模式只是将各自存储空间的地址映射到 0x00000000 中。

三、STM32 启动文件分析

下面针对 startup_stm32f40_41xxx.s 文件进行简单的分析。

1、栈 Stack

c 复制代码
Stack_Size      EQU     0x00000400

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

这段代码的含义如下:开辟栈的大小为 0X00000400(1KB),名字为 STACK, NOINIT 即不初始化,可读可写, ALIGN=3 表示 8(2^3)字节对齐。

栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部 SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。

  • EQU:宏定义的伪指令,相当于等于,类似于 C 中的 define。
  • AREA:告诉汇编器汇编一个新的代码段或者数据段。STACK 表示段名,这个可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写,ALIGN=3,表示按照 2 3 2^3 23 对齐,即 8 字节对齐。
  • SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于 Stack_Size。
  • 标号 __initial_sp 紧挨着 SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的。

2、堆 Heap

c 复制代码
Heap_Size       EQU     0x00000200

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

				PRESERVE8
                THUMB

解释如下:开辟堆的大小为 0X00000200(512 字节),名字为 HEAP, NOINIT 即不初始化,可读可写,ALIGN=3 同理表示 8(2^3)字节对齐。__heap_base 表示堆的起始地址,__heap_limit 表示堆的结束地址。堆是由低向高生长的,跟栈的生长方向相反。

  • PRESERVE8:指定当前文件的堆栈按照 8 字节对齐。
  • THUMB:表示后面指令兼容 THUMB 指令。THUBMARM 指令集的一个子集重新编码二形成的一个指令集,其指令长度为 16bit,现在 Cortex-M 系列的都使用 THUMB-2 指令集,THUMB-2 是 32 位的,兼容 16 位和 32 位的指令,是 THUMB 的超集。

堆主要用来动态内存的分配,像 malloc() 函数申请的内存就在堆上面。不过这个在 STM32 里面用的比较少。

3、中断向量表 Vectors

c 复制代码
; Vector Table Mapped to Address 0 at Reset
                AREA    RESET, DATA, READONLY
                EXPORT  __Vectors
                EXPORT  __Vectors_End
                EXPORT  __Vectors_Size

这里定义一个数据段,名字为 RESET,可读。并声明 __Vectors__Vectors_End__Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。

  • EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是 IAR 编译器,则使用的是 GLOBAL 这个指令。

当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR 的入口地址, 内核使用了向量表查表机制。这里使用一张向量表。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。要注意的是这里有个另类:0 号类型并不是什么入口地址,而是给出了复位后 MSP 的初值。下图是 F407 的向量表的一部分,具体参阅想要使用的芯片对应的手册即可。

可以看到和代码中的定义是一一对应的:

c 复制代码
__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
				......
	
__Vectors_End

__Vectors_Size  EQU  __Vectors_End - __Vectors

向量表从 FLASH 的 0 地址开始放置,以 4 个字节为一个单位,地址 0 存放的是栈顶地址, 0X04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道 C 语言中的函数名就是一个地址。

  • DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中,DCD 分配了一堆内存,并且以 ESR 的入口地址初始化它们。

3.1 中断响应流程

这里既然提到了中断向量表,就额外补充以下中断响应流程:

  1. 中断信号发送到 NVIC
  2. NVIC 通知 CPU
  3. CPU 根据中断号得到中断服务程序地址(基地址 + 中断编号 * 4B)
  4. 保存现场
  5. 执行中断服务程序
  6. 恢复现场
  7. 继续执行程序

下面以 PendSV_Handler 为例,说明一下:

可以看到 PendSV_Handler 的中断号是 14,也就是 14 ∗ 4 = ( 56 ) 10 = ( 38 ) 2 14*4=(56)_{10}=(38)_2 14∗4=(56)10=(38)2。

而中断向量表的基地址为 0x08000000,故其服务函数的地址在 0x08000038,见下 bin 文件(下面会提到):

解析出来是:0x080002D7,而 map 文件中却是:0x080002D6

原因如下:ARM 指令集有 ARM 指令集和 Thumb 指令集。ARM 指令集位数长;而 Thumb 指令集位数短,故而占用内存比较小,所以编译器大部分时间采用 Thumb 指令集。

  • Thumb 指令集末尾是奇数位 1,像这里的 0x080002D7
  • ARM 指令集末尾是偶数位 0,也就是这里的 0x080002D6

而我们访问 0x080002D7 这个地址的时候,它实际上会跳转到 0x080002D6 这个地址。

4、复位程序 Reset_Handler

c 复制代码
				AREA    |.text|, CODE, READONLY

; Reset handler
Reset_Handler    PROC
                 EXPORT  Reset_Handler             [WEAK]
        IMPORT  SystemInit
        IMPORT  __main

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

首先定义一个名称为 .text 的代码段,仅可读。

复位子程序是系统上电后第一个执行的程序,调用 SystemInit 函数初始化系统时钟,然后调用 C 库函数 __main,最终调用 main 函数去到 C 程序中。

  • WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
  • IMPORT:表示该标号来自外部文件,跟 C 语言中的 EXTERN 关键字类似。这里表示 SystemInit__main 这两个函数均来自外部的文件。
  • LDR:从存储器中加载字到一个寄存器中
  • BL:跳转到由寄存器/标号给出的地址,并把跳转前的下一条指针地址保存到 LR
  • BLX:跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR
  • BX:跳转到由寄存器/标号给出的地址,不用返回

SystemInit() 是一个标准的库函数,在 system_stm32f4xx.c 这个库文件中定义。主要作用是配置系统时钟。__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈,并在函数的最后调用 main 函数去到 C 程序中。这就是为什么我们写的程序都有一个 main 函数的原因。

5、中断服务函数

在启动文件里面已经帮我们写好所有中断的中断服务函数,跟我们平时写的中断服务函数不一样的就是这些函数都是空的,真正的中断服务程序需要我们在外部的 C 文件里面重新实现,这里只是提前占了一个位置而已。

如果我们在使用某个外设的时候,开启了某个中断,但是又忘记编写配套的中断服务程序或者函数名写错,那当中断来临的时,程序就会跳转到启动文件预先写好的空的中断服务程序中,并且在这个空函数中无线循环,即程序就死在这里。

c 复制代码
NMI_Handler     PROC
                EXPORT  NMI_Handler                [WEAK]
                B       .
                ENDP
				...
SysTick_Handler PROC
                EXPORT  SysTick_Handler            [WEAK]
                B       .
                ENDP

Default_Handler PROC

                EXPORT  WWDG_IRQHandler                   [WEAK]    
				...
CRYP_IRQHandler                                                    
HASH_RNG_IRQHandler
FPU_IRQHandler
   
                B       .

                ENDP
  • B:跳转到一个标号。这里跳转到一个'.',即表示无线循环

6、用户堆栈初始化

c 复制代码
ALIGN

;*******************************************************************************
; User Stack and Heap initialization, 由 C 库函数 __main 来完成
;*******************************************************************************
                 IF      :DEF:__MICROLIB  ; 这个宏在 KEIL 里面开启
                
                 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
  • ALIGN:对指令或者数据存放的地址进行对齐,后面会跟一个立即数。缺省表示 4 字节对齐。
  • IF,ELSE,ENDIF:汇编的条件分支语句,跟 C 语言的 if ,else 类似
  • END:文件结束

首先判断是否定义了 __MICROLIB

  • 如果定义了这个宏则赋予标号 __initial_sp(栈顶地址)、__heap_base(堆起始地址)、__heap_limit(堆结束地址)全局属性,可供外部文件调用。有关这个宏我们在 KEIL 里面配置,具体见下图。然后堆栈的初始化就由 C 库函数 __main 来完成。
  • 如果没有定义 __MICROLIB,则才用双段存储器模式,且声明标号 __user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。

四、STM32 启动流程分析

经过刚才对启动文件的分析,下面对 STM32 启动流程的分析就要清晰不少了。

1、初始化 SP、PC 及中断向量表

当系统复位后,处理器首先读取向量表中的前两个字(8 个字节),第一个字存入 SP,第二个字存入 PC,也就是

程序执行的起始地址。

下面打开经过编译生成的 bin 文件,看到前 8 个字节的内容如下(小端模式):

Keil 默认生成 hex 文件,要想生成 bin 文件要自己添加命令,设置如下:

D:\Keil_v5\ARM\ARMCC\bin\fromelf.exe --bin --output=Objects/stm32f407.bin Objects/stm32f407.axf

0x200006600x0800020D,下面在 map 文件(存放链接地址)中查找这两个地址存放的是什么数据:


这正是中断向量表的前两项内容,这也印证了前面所说的内容:

2、设置系统时钟

接下来执行 PC 指向的 Reset_Handler,并调用 SystemInit 初始化系统时钟。

c 复制代码
void SystemInit(void)
{
  /* FPU settings ------------------------------------------------------------*/
  #if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
    SCB->CPACR |= ((3UL << 10*2)|(3UL << 11*2));  /* set CP10 and CP11 Full Access */
  #endif
  /* Reset the RCC clock configuration to the default reset state ------------*/
  /* Set HSION bit */
  RCC->CR |= (uint32_t)0x00000001;

  /* Reset CFGR register */
  RCC->CFGR = 0x00000000;

  /* Reset HSEON, CSSON and PLLON bits */
  RCC->CR &= (uint32_t)0xFEF6FFFF;

  /* Reset PLLCFGR register */
  RCC->PLLCFGR = 0x24003010;

  /* Reset HSEBYP bit */
  RCC->CR &= (uint32_t)0xFFFBFFFF;

  /* Disable all interrupts */
  RCC->CIR = 0x00000000;

#if defined(DATA_IN_ExtSRAM) || defined(DATA_IN_ExtSDRAM)
  SystemInit_ExtMemCtl(); 
#endif /* DATA_IN_ExtSRAM || DATA_IN_ExtSDRAM */
         
  /* Configure the System clock source, PLL Multiplier and Divider factors, 
     AHB/APBx prescalers and Flash settings ----------------------------------*/
  SetSysClock();

  /* Configure the Vector Table location add offset address ------------------*/
#ifdef VECT_TAB_SRAM
  SCB->VTOR = SRAM_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM */
#else
  SCB->VTOR = FLASH_BASE | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal FLASH */
#endif
}

前面的部分是配置时钟的,具体参考手册即可。需要注意的是最后一段代码:

默认是没有开启 VECT_TAB_SRAM,则从 FLASH 中启动,VTOR 寄存器存放的是中断向量表的起始地址,在 IAP 升级会修改这里的偏移量。

3、初始化堆栈并进入 main

执行完 SystemInit 后又调用了标准库中的 __main 函数:

在这里会初始化堆、栈、RO、RW、ZI 段。最后就进入到 C 文件中的 main 函数中。

有关内存分段的内存可以参考:单片机内存区域划分

五、总结

至此,启动过程圆满结束!

相关推荐
森焱森4 小时前
水下航行器外形分类详解
c语言·单片机·算法·架构·无人机
小殷学长6 小时前
【单片机毕业设计17-基于stm32c8t6的智能倒车监测系统】
stm32·单片机·课程设计
TESmart碲视8 小时前
HKS201-M24 大师版 8K60Hz USB 3.0 适用于 2 台 PC 1台显示器 无缝切换 KVM 切换器
单片机·嵌入式硬件·物联网·游戏·计算机外设·电脑·智能硬件
花落已飘9 小时前
STM32中实现shell控制台(shell窗口输入实现)
stm32·单片机·嵌入式硬件
花落已飘9 小时前
STM32中实现shell控制台(命令解析实现)
stm32·shell
没有钱的钱仔9 小时前
STM32低功耗模式全面指南
css·stm32·css3
牵牛老人11 小时前
Qt处理USB摄像头开发说明与QtMultimedia与V4L2融合应用
stm32·单片机·qt
宇钶宇夕12 小时前
针对工业触摸屏维修的系统指南和资源获取途径
单片机·嵌入式硬件·自动化
和风化雨13 小时前
stm32的三种开发方式
stm32·单片机·嵌入式硬件
kanhao10013 小时前
三态逻辑详解:单片机GPIO、计算机总线系统举例
单片机·嵌入式硬件