常见MCU上电初始化逻辑 以STM32F1 CortexM3 为例
前言
每一款芯片是如何启动的都值得去研究,只有明白了它是怎么样启动的,你才能知道为什么你的程序可以运行?程序是从哪里运行来的?运行你写的函数之前执行了哪些操作?
也只有这样你才有对全局的掌控,才能对代码了然于心,提高你解决复杂问题的能力。
有一次踩坑,某MCU的BSS段未在
main
运行之前初始化导致程序运行异常,也是跟这个启动流程有关。
通过了解启动文件,我们可以体会到处理器的架构、指令集、中断向量安排等内容,是非常值得玩味的。
进入正题
STM32 上电后做了什么
图解启动流程
文字详细解读启动流程
启动过程概览
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 |
|
开辟栈的大小为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 |
|
开辟堆的大小为0X00000200(512 字节),名字为HEAP,NOINIT 即不初始化,可读可写,8(2^3)字节对齐。
__heap_base
表示对的起始地址,__heap_limit
表示堆的结束地址。堆是由低向高生长的,跟栈的生长方向相反。堆主要用来动态内存的分配,像malloc()函数申请的内存就在堆上面。这个在STM32里面用的比较少。
1 |
|
定义一个数据段,名字为RESET, 可读。并声明 __Vectors 、__Vectors_End 和__Vectors_Size 这三个标号具有全局属性,可供外部的文件调用。
EXPORT:声明一个标号可被外部的文件使用,使标号具有全局属性。如果是IAR 编译器,则使用的是GLOBAL 这个指令。
__Vectors
为向量表起始地址,__Vectors_End
为向量表结束地址,两个相减即可算出向量表大小,也就是__Vectors_Size
。向量表从FLASH 的0 地址开始放置,以4 个字节为一个单位,地址0 存放的是栈顶地址,0X04 存放的是复位程序的地址,以此类推。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道C 语言中的函数名就是一个地址。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。在向量表中,DCD 分配了一堆内存,并且以ESR 的入口地址初始化它们。
1 |
|
复位子程序是系统上电后第一个执行的程序,调用SystemInit 函数初始化系统时钟,然后调用C 库函数
_mian
,最终调用main 函数去到C 的世界。WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用该标号,如果外部文件没有声明也不会出错。这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
IMPORT:表示该标号来自外部文件,跟C 语言中的EXTERN 关键字类似。这里表示
SystemInit
和__main
这两个函数均来自外部的文件。
SystemInit()
是一个标准的库函数,在system_stm32f10x.c
这个库文件定义。主要作用是配置系统时钟,这里调用这个函数之后,单片机的系统时钟配被配置为72M。
__main
是一个标准的C 库函数,主要作用是初始化用户堆栈,并在函数的最后调用main 函数去到C 的世界。这就是为什么我们写的程序都有一个main 函数的原因。
1 |
|
首先判断是否定义了
__MICROLIB
,如果定义了这个宏则赋予标号__initial_sp
(栈顶地址)、__heap_base
(堆起始地址)、__heap_limit
(堆结束地址)全局属性,可供外部文件调用。有关这个宏我们在KEIL 里面配置,具体见下图 。然后堆栈的初始化就由C 库函数_main
来完成。如果没有定义
__MICROLIB
, 则采用双段存储器模式, 且声明标号__user_initial_stackheap
具有全局属性,让用户自己来初始化堆栈。
1 |
|
__main()
做了什么
The entry point of a program is at __main in the C library where library code:
- 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.
- Zeroes ZI regions.
- Branches to
__rt_entry
.
程序的入口点位于 C 库中的 __main,其中库代码:
- 将 non-root(RO 和 RW)从其加载地址复制到其执行地址。此外,如果任何数据部分被压缩,它们将从加载地址解压缩到执行地址。
- 将 ZI 区域归零。
- 跳转到
__rt_entry
。
The library function
__rt_entry()
runs the program as follows:
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.Calls
__rt_lib_init()
to initialize referenced library functions, initialize the locale and, if necessary, set up argc and argv formain()
.For C++, calls the constructors for any top-level objects by way of
__cpp_initialize__aeabi_
.Calls
main()
, the user-level root of the application.From
main()
, your program might call, among other things, library functions.Calls
exit()
with the value returned bymain()
.
库函数__rt_entry()
运行程序如下:
通过多种方式中的一种设置堆栈和堆,包括调用
__user_setup_stackheap()
、调用__rt_stackheap_init()
或加载scatter-loaded 区域的绝对地址。调用
__rt_lib_init()
初始化引用的库函数、初始化区域设置,并在必要时为main()
设置argv
和argc
参数。对于 C++,通过
__cpp_initialize__aeabi_
调用任何top-level对象的构造函数。调用
main()
,应用程序的 user-level 级别的起始。在
main()
中,您的程序可能会调用库函数等。根据
main()
的返回值调用exit()
。
一些参考
STM32启动过程–启动文件–分析:https://www.cnblogs.com/dreamboy2000/p/15806645.html
ARM编译器指南 执行环境的初始化和应用程序的执行:https://developer.arm.com/documentation/dui0808/c/chr1358938922456