概述
无论是Keil还是Gcc,对于应用了Cortex-M内核的 xx32 单片机,其从Flash启动流程都是从 0x8000000 取栈顶地址存至MSP,而后执行复位中断Reset_Handler()
,在复位中断内执行 系统时钟初始化、将可读可写数据从 Flash 搬运到 SRAM、清零 SRAM 的 bss 段数据等,最后跳转至main()
函数。
当然,芯片为何会从 0x8000000 开始执行,或者说芯片内部的出厂固化的ROM程序是如何执行的,此处暂不过多赘述,详见下文章节。
从源文件分析来看,Keil环境下的启动初始化源代码大多集成在库里面,用户不可见,因此本文主要从gcc环境下的源文件进行详细剖析启动流程。
另外,Keil下有所谓的$$Super$main
/$$Sub$main
,或者说gcc环境下.s源码最后是跳转至main
/entry
函数,此等都可由用户配置写就,暂不作讲解。
Keil环境下的启动文件分析
初始化栈顶指针
内核从0x0800 0000读取栈顶地址,并将该地址存入MSP中。
- 栈顶地址的值为0x2000 xxxx,工程所生成bin文件的前四个字节即为栈顶地址。(.s 启动文件中说明了程序的第一个字就是
__initial_sp
栈顶地址,第二个字是Reset_Handler
地址) - 从0x2000 0000到0x2000 xxxx即为程序所运行的范围,该段内存分布为:RW段、ZI段:其中RW段为可读写的非0数据段,ZI段包括了0数据段、堆区、栈区。
从0x08000004取出复位中断函数Reset_Handler
地址,装载至PC指针,跳转执行
复位中断
Keil环境代码如下:
- 先进行系统时钟初始化
SystemInit()函数定义在system_xxx.c中,主要为初始化系统时钟RCC、重定位中断向量表
- 跳转至_main()
1 | ; Reset handler |
_main()函数
__scatterload
- 将初始化的可读写数据段拷贝从Flash拷贝到SRAM
- 初始化清零未初始化数据段
__rt_entry
- 负责初始化堆栈,完成库函数的初始化,
- 最后跳转至main()函数
GCC编译环境下的启动文件分析
程序跳转至复位中断后,执行以下操作:
- 将初始化的可读写数据段拷贝从Flash拷贝到SRAM
- 初始化清零未初始化数据段
- 调用系统时钟初始化函数(SystemInit)
- 调用静态构造函数(__libc_init_array) ——参考自CHATGPT
- 调用所有静态构造函数->调用.preinit_array段中的所有函数指针->调用.init段中的_init函数->调用.init_array段中的所有函数指针->确保在程序执行主函数 main 之前,所有需要初始化的内容都已经完成。
- 当然,纯C语言部分不会实际调用到相关构造函数
- 其在链接脚本中有体现:
1
2
3
4
5
6
7
8
9
10
11
12
13.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array*))
PROVIDE_HIDDEN (__preinit_array_end = .);
} >FLASH
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH - 链接脚本中,可将用户函数指定链接到相应的init段,从而实现在main前的函数自动初始化?
- 进入用户函数(main)
复位中断代码如下所示,注意:其中所用变量_sidata、_edata等等须 搭配链接脚本内容 共同参阅
复位中断起始
1 | Reset_Handler: |
拷贝RW-Data
1 | // 实际的RW-Data拷贝工作 |
清零bss段
1 | // 清零未初始化数据段(bss segment) |
其它初始化及跳转至main
1 | /* 调用系统时钟初始化函数 */ |
GCC环境之复位中断源码总览
1 | Reset_Handler: |
常见疑惑解答
为什么芯片启动时会将栈顶地址存放到 MSP,然后跳转至复位中断执行?
.s 启动文件和链接脚本已经讲明了所生成的映像文件第一个字是栈顶地址,第二个字就是复位中断的地址。
有些人可能会疑惑,为什么系统启动会设置栈顶地址,并且会跳转到复位中断呢?(其实,这都是 Cortex-M 内核所实现的)
以下内容摘选自ARM Cortex-M3与Cortex-M4权威指南
的第4.8节:复位和复位流程
1 | 在复位后以及处理器开始执行程序前,Cortex-M处理器会从存储器中读出头两个字,如图4.30所示。 |
由此可见,所谓的启动文件和链接脚本 都是根据 内核特性和单片机厂商的设计特性 而制定的,前者是果,后者是因。
芯片启动时为何都是从 0x8000000 开始?
Cortex-M 内核复位都是固定从 0x00000000 开始,而在 STM32/xx32 中,程序通常存储在 0x8000000 地址并在此开始执行,这一点是由于芯片厂商的设计所决定的。
以下内容摘选自互联网:
在 STM32 等微控制器中,可以通过配置 BOOT 引脚或其他方式,将 Flash 的 0x08000000 地址重映射到 0x00000000。
这样,微控制器就可以在 0x00000000 地址处找到中断向量表,同时,程序代码仍然存储在 Flash 的0x08000000 地址处
这种设计使得微控制器能够在启动时正确地找到并执行存储在 Flash 中的程序,同时还能满足 ARM Cortex-M 系列微控制器的启动要求
通过 ISP 编程或者 SWD/JTAG 下载方式时,芯片是如何工作的?
在系统编程:毫无疑问,芯片内部是内置固化了一段出厂bootloader程序的,此程序用于实现串口升级,但是用户是不知道其源码实现的
在电路编程,使用SWD/JTAG接口