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 函数中。

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

五、总结

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

相关推荐
编码追梦人20 分钟前
如何实现单片机的安全启动和安全固件更新
单片机
电子工程师UP学堂24 分钟前
电子应用设计方案-16:智能闹钟系统方案设计
单片机·嵌入式硬件
blessing。。2 小时前
I2C学习
linux·单片机·嵌入式硬件·嵌入式
嵌新程3 小时前
day03(单片机高级)RTOS
stm32·单片机·嵌入式硬件·freertos·rtos·u575
Lin2012304 小时前
STM32 Keil5 attribute 关键字的用法
stm32·单片机·嵌入式硬件
电工小王(全国可飞)4 小时前
STM32 RAM在Memory Map中被分为3个区域
stm32·单片机·嵌入式硬件
maxiumII4 小时前
Diving into the STM32 HAL-----DAC笔记
笔记·stm32·嵌入式硬件
美式小田7 小时前
单片机学习笔记 9. 8×8LED点阵屏
笔记·单片机·嵌入式硬件·学习
兰_博7 小时前
51单片机-独立按键与数码管联动
单片机·嵌入式硬件·51单片机
时光の尘8 小时前
C语言菜鸟入门·关键字·float以及double的用法
运维·服务器·c语言·开发语言·stm32·单片机·c