stm32启动文件详解
STM32的启动文件了解:
一、启动文件由汇编编写,是系统上电复位后第一个执行的程序。步骤分为以下五步:
1、初始化堆栈指针 SP = _initial_sp ;
2、初始化 PC指针 = Reset_Handler ;
3、初始化中断向量表 ;
4、配置系统时钟 ;
5、调用 C库函数_main 初始化用户堆栈,从而最终调用 main 函数去到 C的世界 ;
二、详细拆分如上五个步骤:
1、堆栈的基础概念
1)、在计算机系统中,堆栈是一种后进先出的数据结构(LIFO)的数据结构,用于存局部变量、函数调用时的返回地址、寄存器的值等。栈指针(SP)始终指向栈顶,在进行压栈(PUSH)和出栈(POP)操作时,SP的值会相应地改变。
2)、堆栈空间分配,在启动文件中,首先分配一定的内存空间如下:
; 定义栈的大小
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
说明:
Stack_Size:使用 EQU 伪指令定义了栈的大小,这里为 0x00000400 字节(即 1024 字节)。
AREA STACK, NOINIT, READWRITE, ALIGN=3:定义了一个名为 STACK 的存储区,NOINIT 表示该存储区不需要初始化,READWRITE 表示该存储区是可读写的,ALIGN=3 表示该存储区按 8 字节(2^3)对齐。
Stack_Mem SPACE Stack_Size:为栈分配 Stack_Size 字节的内存空间。
__initial_sp:标记栈顶地址,它位于栈空间的末尾
3)、向量表中栈顶地址的存储,在STM32的启动文件中,向量表的第一个条目就是栈顶地址,如下:
DCD是双字(32位)数据定义伪指令,DCD __initial_sp将栈顶地址存储在向量表的第一个位置。如此第二个位置为复位向量、第三个为NMI处理程序、第四个为硬件故障处理程序、、、
4)、初始化栈指针,在复位处理程序(Reset_Handler)中,会将栈指针(SP)初始化为向量表中存储的栈顶地址。
5)、初始化栈指针的意义,初始化栈指针的意义在于为后续的函数调用、局部变量的存储等操作提供一个正确的内存区域。在函数调用时,会将返回地址、寄存器的值等压入栈中,而这些操作都是基于栈指针进行的。如果栈指针没有正确初始化,那么在函数调用时就会出现栈溢出、数据混乱等问题,导致程序崩溃。
综上所述,在 STM32 启动文件中,通过为堆栈分配内存空间、将栈顶地址存储在向量表中,并在复位处理程序中加载栈顶地址到栈指针,完成了栈指针的初始化,确保了程序的正常运行
2、PC指针(程序计数器)是初始化的一个关键步骤,它决定了系统复位后从哪里开始执行代码。
1)、PC指针是一个特殊的寄存器,它存储着下一条要执行的指令地址。在程序运行过程中,CPU会根据PC指针的值从内存中取出指令并执行,然后自动更新PC指针指向下一条指令地址(记住,PC保存的永远是下一条要被执行的指令的地址,CPU从PC找到要执行指令的地址,去取读取指令)
2)、在向量表中确定初始PC值,在STM32的启动流程里,系统复位后会从向量表中获取初始的PC值。向量表是一段固定的地址的存储区域,它包含了一系列中断服务程序和异常处理程序的入口地址,其中第二个条目就是复位向量,也就是系统复位后PC指针要指向的初始地址。当系统复位后,硬件会自动将这个地址加载到PC指针中,从而让CPU从Reset_Handler这个位置开始执行代码。
3)、硬件自动加载PC值,当STM32芯片发生复位后(如上电复位、外部复位等)时,硬件会自动完成PC指针的初始化工作,具体流程如下;
a、系统复位信号触发后,CPU首先会从固定的地址(向量表的起始地址)读取栈顶地址,并i将其加载到栈指针(SP)中。
b、接着,CPU会从向量表的第二个 条目中读取复位向量(也就是PC值),并将其加载到PC指针中。
c、之后,CPU就会从PC指针所指向的地址(即Reset_Handler函数)开始执行代码。
4)、Reset_Handler函数,是系统复位后执行的第一段代码,它通常会完成一系列的初始化操作,如系统时钟初始化、栈指针设置等,然后跳转到用户的主程序入口。
3、中断向量表,中断向量表是存储中断服务程序(ISR)日后地址的表格,位于内存特定区域。当发生中断时,CPU通过该表快速定位到相应ISR执行。
1)、初始化步骤:
a、分配内存:在启动文件里为向量表分配一块只读存储区,使用汇编指令实现。
AREA RESET, DATA, READONLY
b、定位表项:按固定顺序依次定义各中断对应的入口地址。首个是栈顶地址,用于初始化栈指针;第二个是复位向量,指向复位后执行的代码起始处;后续为不同中断服务程序的入口。
__Vectors
DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位向量
DCD NMI_Handler ; 不可屏蔽中断处理程序入口
; 其他中断向量表项…
c、计算大小:明确向量表的结束位置,并计算其大小,方便后续使用
__Vectors_End
__Vectors_Size EQU __Vectors_End – __Vectors
硬件相应:系统复位时,硬件自动从向量表加载栈顶地址到栈指针(SP),加载复位向量到程序计数器(PC),让程序从指定的位置开始执行。发生中断时,硬件依据中断号从向量表找到对应ISR入口,跳转执行。
**4、STM32的系统时钟复杂且灵活,它为芯片的各个外设和内核提供不同频率的时钟信号。主要的时钟源有内部RC振荡器(HSI、LSI)和外部晶振(HES、LSE),可以通过PLL(锁相环)对时钟进行倍频等操作,以满足不同外设对时钟频率的需求。**
1)、首先是能所选时钟源,一般选择外部高速晶振(HSE)作为时钟源
// 使能HSE
RCC->CR |= RCC_CR_HSEON;
// 等待HSE就绪
while (!(RCC->CR & RCC_CR_HSERDY));
2)、配置PLL(可选),若需要更高的时钟频率,可以使用PLL对时钟源进行倍频。这涉及到设置PLL的输入时钟源、倍频系数等参数。
// 配置PLL,例如设置PLL源为HSE,倍频系数为9
RCC->CFGR |= RCC_CFGR_PLLSRC_HSE;
RCC->CFGR |= RCC_CFGR_PLLMULL9;
// 使能PLL
RCC->CR |= RCC_CR_PLLON;
// 等待PLL就绪
while (!(RCC->CR & RCC_CR_PLLRDY));
3)、配置AHB、APB总线分频系数,根据外设的需求,合理配置AHB(高级高性能总线)和APB(高级外设总线)的分频系数,以提供合适的时钟频率给外设。
// 设置AHB不分频
RCC->CFGR |= RCC_CFGR_HPRE_DIV1;
// 设置APB1二分频,APB2不分频
RCC->CFGR |= RCC_CFGR_PPRE1_DIV2;
RCC->CFGR |= RCC_CFGR_PPRE2_DIV1;
4)、选择系统时钟源,最后将配置好的时钟源(如PLL输出)选择为系统时钟
// 选择PLL作为系统时钟源
RCC->CFGR |= RCC_CFGR_SW_PLL;
// 等待系统时钟源切换完成
while ((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_PLL);
5、在进入 __main 之前,启动文件(通常是汇编代码)会完成一些基础工作:
1)、向量表初始化:定义并设置中断向量表,包含栈顶地址、复位向量等信息。系统复位后,硬件会从向量表中读取栈顶地址初始化栈指针(SP),读取复位向量设置程序计数器(PC),使程序跳转到复位处理函数执行。
2)、栈和堆空间分配:为栈和堆分配内存空间,栈用于函数调用、局部变量存储等,堆用于动态内存分配。
复位处理函数跳转至 __main
复位处理函数(如 Reset_Handler)是系统复位后执行的第一段代码,它会完成一些必要的初始化,然后调用 __main 函数,示例代码如下
Reset_Handler PROC
EXPORT Reset_Handler [WEAK]
IMPORT __main
IMPORT SystemInit
LDR R0, =__initial_sp ; 加载栈顶地址到 R0
MOV SP, R0 ; 设置栈指针
BL SystemInit ; 调用系统时钟初始化函数
BL __main
ENDP
先设置栈指针,确保后续操作有正确的栈空间。
调用 SystemInit 函数进行系统时钟等初始化。
通过 BL __main 指令跳转到 __main 函数执行。
__main 函数的作用
__main 函数一般由编译器提供,它主要完成以下关键任务:
3)、用户堆栈初始化:虽然启动文件中已初步设置栈指针,但 __main 可能会进行更细致的堆栈初始化工作,比如确保栈空间布局符合 C 语言运行时要求,为局部变量和函数调用做好准备。
4)、全局变量和静态变量初始化:将全局变量和静态变量初始化为指定的值。对于已经赋初值的全局和静态变量,从程序的只读数据段(RO-data)复制到对应的可读写数据段(RW-data);对于未赋初值的全局和静态变量,将其所在的 BSS 段清零。
5)、调用构造函数(针对 C++):如果使用 C++ 编程,__main 会调用全局对象和静态对象的构造函数,确保这些对象在 main 函数执行前正确初始化。
6)、调用 main 函数:完成上述初始化工作后,__main 会调用用户编写的 main 函数,使程序进入 C 语言代码的执行流程。
进入 main 函数
当 __main 完成所有初始化工作后,调用 main 函数,程序开始执行用户编写的 C 语言代码,示例如下:
int main(void)
{
// 用户代码逻辑
while (1)
{
// 主循环
}
}
综上所述,通过启动文件的前期准备、复位处理函数跳转、__main 函数的初始化工作,最终顺利调用 main 函数,使程序进入 C 语言的执行环境。
作者:@MengZhongHua