常见MCU上电初始化逻辑 以STM32F1 CortexM3 为例

前言

每一款芯片是如何启动的都值得去研究,只有明白了它是怎么样启动的,你才能知道为什么你的程序可以运行?程序是从哪里运行来的?运行你写的函数之前执行了哪些操作?

也只有这样你才有对全局的掌控,才能对代码了然于心,提高你解决复杂问题的能力。

有一次踩坑,某MCU的BSS段未在main运行之前初始化导致程序运行异常,也是跟这个启动流程有关。

通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。

进入正题

STM32 上电后做了什么

图解启动流程

STM32启动流程

215103893_3_20210208091156505

文字详细解读启动流程

启动过程概览

1、初始化堆栈指针 SP=_initial_sp

2、初始化PC指针=Reset_Handler

3、初始化中断向量表

4、配置系统时钟

5、调用C 库函数_main初始化用户堆栈、.bss段、.data段,从而最终调用main 函数去到C 的世界

而Cortex-M3内核规定,起始地址必须存放堆顶指针,而第二个地址则必须存放复位中断入口向量地址,这样在Cortex-M3内核复位后,会自动从起始地址的下一个32位空间取出复位中断入口向量,跳转执行复位中断服务程序。

启动过程涉及的代码

启动过程涉及的文件不仅包含startup_stm32f10x_hd.s,还涉及到了MDK自带的连接库文件entry.o、entry2.o、entry5.o、entry7.o等(从生成的map文件可以看出来)

1
2
3
4
5
Stack_Size      EQU     0x00000400

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

开辟栈的大小为0X00000400(1KB),名字为STACK,NOINIT 即不初始化,可读可写,8(2^3)字节对齐。

栈的作用是用于局部变量,函数调用,函数形参等的开销,栈的大小不能超过内部SRAM 的大小。如果编写的程序比较大,定义的局部变量很多,那么就需要修改栈的大小。如果某一天,你写的程序出现了莫名奇怪的错误,并进入了 hardfault 的时候,这时你就要考虑下是不是栈不够大,溢出了。

EQU:宏定义的伪指令,相当于等于,类似与C 中的define。

AREA:告诉汇编器汇编一个新的代码段或者数据段。STACK 表示段名,这个可以任意命名;NOINIT 表示不初始化;READWRITE 表示可读可写,ALIGN=3,表示按照2^3对齐,即8 字节对齐。

SPACE:用于分配一定大小的内存空间,单位为字节。这里指定大小等于Stack_Size。标号__initial_sp 紧挨着SPACE 语句放置,表示栈的结束地址,即栈顶地址,栈是由高向低生长的。

1
2
3
4
5
6
7
8
9
10
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size EQU 0x00000200

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY ; 定义一个数据段,名字为RESET, 可读。并声明 __Vectors 、__Vectors_End 和__Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size

__Vectors DCD __initial_sp ; Top of Stack 栈顶位置,第一个表项是栈顶地址;该处物理地址值即为 __Vetors 标号所表示的值;该地址中存储__initial_sp所表示的地址值,
DCD Reset_Handler ; Reset Handler 复位程序地址
DCD NMI_Handler ; NMI Handler
...
__Vectors_End

__Vectors_Size EQU __Vectors_End - __Vectors

AREA |.text|, CODE, READONLY

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

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

__Vectors 为向量表起始地址,__Vectors_End 为向量表结束地址,两个相减即可算出向量表大小,也就是__Vectors_Size

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

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

1
2
3
4
5
6
7
8
9
10
11
; 这里调用 SystemInit 初始化时钟 时钟初始化完成后跳转到main函数
; Reset handler
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =SystemInit
BLX R0
LDR R0, =__main
BX R0
ENDP

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

WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。

IMPORT:表示该标号来自外部文件,跟C 语言中的EXTERN 关键字类似。这里表示SystemInit__main 这两个函数均来自外部的文件。

SystemInit()是一个标准的库函数,在system_stm32f10x.c这个库文件定义。主要作用是配置系统时钟,这里调用这个函数之后,单片机的系统时钟配被配置为72M。

__main 是一个标准的C 库函数,主要作用是初始化用户堆栈,并在函数的最后调用main 函数去到C 的世界。这就是为什么我们写的程序都有一个main 函数的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
;*******************************************************************************
; User Stack and Heap initialization
;*******************************************************************************
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,这样外部程序也可调用此标号,进行堆栈和堆的赋值,在__main函数执行过程中调用

__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,如果定义了这个宏则赋予标号__initial_sp(栈顶地址)、__heap_base(堆起始地址)、__heap_limit(堆结束地址)全局属性,可供外部文件调用。有关这个宏我们在KEIL 里面配置,具体见下图 。然后堆栈的初始化就由C 库函数_main 来完成。

img

如果没有定义__MICROLIB , 则采用双段存储器模式, 且声明标号__user_initial_stackheap 具有全局属性,让用户自己来初始化堆栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 初始化嵌入式闪存接口、PLL并更新系统核心时钟变量。
/**
* @brief Setup the microcontroller system
* Initialize the Embedded Flash Interface, the PLL and update the
* SystemCoreClock variable.
* @note This function should be used only after reset.
* @param None
* @retval None
*/
void SystemInit (void)
{
#if defined(STM32F100xE) || defined(STM32F101xE) || defined(STM32F101xG) || defined(STM32F103xE) || defined(STM32F103xG)
#ifdef DATA_IN_ExtSRAM
SystemInit_ExtMemCtl();
#endif /* DATA_IN_ExtSRAM */
#endif

/* Configure the Vector Table location -------------------------------------*/
#if defined(USER_VECT_TAB_ADDRESS)
SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation in Internal SRAM. */
#endif /* USER_VECT_TAB_ADDRESS */
}

__main() 做了什么

参考ARM文档

The entry point of a program is at __main in the C library where library code:

  1. Copies non-root (RO and RW) execution regions from their load addresses to their execution addresses. Also, if any data sections are compressed, they are decompressed from the load address to the execution address.
  2. Zeroes ZI regions.
  3. Branches to __rt_entry.

程序的入口点位于 C 库中的 __main,其中库代码:

  1. 将 non-root(RO 和 RW)从其加载地址复制到其执行地址。此外,如果任何数据部分被压缩,它们将从加载地址解压缩到执行地址。
  2. 将 ZI 区域归零。
  3. 跳转到 __rt_entry

The library function __rt_entry() runs the program as follows:

  1. Sets up the stack and the heap by one of a number of means that include calling __user_setup_stackheap(), calling __rt_stackheap_init(), or loading the absolute addresses of scatter-loaded regions.

  2. Calls __rt_lib_init() to initialize referenced library functions, initialize the locale and, if necessary, set up argc and argv for main().

    For C++, calls the constructors for any top-level objects by way of __cpp_initialize__aeabi_.

  3. Calls main(), the user-level root of the application.

    From main(), your program might call, among other things, library functions.

  4. Calls exit() with the value returned by main().

库函数__rt_entry()运行程序如下:

  1. 通过多种方式中的一种设置堆栈和堆,包括调用__user_setup_stackheap()、调用__rt_stackheap_init()或加载scatter-loaded 区域的绝对地址。

  2. 调用__rt_lib_init()初始化引用的库函数、初始化区域设置,并在必要时为main()设置argvargc参数。

    对于 C++,通过 __cpp_initialize__aeabi_ 调用任何top-level对象的构造函数。

  3. 调用main(),应用程序的 user-level 级别的起始。

    main()中,您的程序可能会调用库函数等。

  4. 根据main()的返回值调用exit()

一些参考

STM32启动过程–启动文件–分析:https://www.cnblogs.com/dreamboy2000/p/15806645.html

ARM编译器指南 执行环境的初始化和应用程序的执行:https://developer.arm.com/documentation/dui0808/c/chr1358938922456


常见MCU上电初始化逻辑 以STM32F1 CortexM3 为例
https://www.oikiou.top/2023/880e7a30/
作者
Oikiou
发布于
2023年6月20日
许可协议