CM3/4启动文件分析

发布于:2025-02-11 ⋅ 阅读:(103) ⋅ 点赞:(0)

STM32的启动文件通常是一个是系统上电复位后第一个执行的汇编文件(如 startup_stm32fxxx.s),主要做了以下工作:

  1. 初始化堆栈指针 SP = _initial_sp。
  2. 初始化程序计数器指针 PC = Reset_Handler。
  3. 栈和堆的初始化:定义栈和堆的大小及起始地址。
  4. 初始化中断向量表:定义中断服务例程(ISR)的入口地址。
  5. 复位处理程序:在芯片复位后执行,调用 __main 函数、SystemInit 函数(可选)。
    5.1 在SystemInit 函数中配置外部 SRAM 作为数据存储器(可选)。
    5.2 在SystemInit 函数中配置重映射的中断向量表地址(可选)。
    5.3 在 _main 函数中初始化用户堆栈。
    5.4 在 _main 函数中初始化 .data 和 .bss 段(将初始化的全局变量从Flash复制到RAM,并清零未初始化的全局变量)。

1. 栈空间的开辟

在这里插入图片描述

  1. EQU 给数字常量0x00000400取一个符号名Stack_Size
  2. AREA 汇编一个新的代码段或者数据段,段名为STACK,不初始化[NOINIT],可读可写[READWRITE],8 字节对齐[ALIGN=3表示 2^3 对齐]
  3. SPACE分配内存指令,分配大小为 Stack_Size[1KB] 字节连续的存储单元给栈空间,并初始化为 0。
  4. __initial_sp 紧挨着SPACE放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。

  • 33~39行:在存储器映射区 Block1[片内SRAM区]开辟 1KB 字节的连续空间给栈空间[可读可写],并将符号名__initial_sp指向栈顶地址。


注意:
栈主要用于存放局部变量函数形参等,属于编译器自动分配和释放的内存,栈的大小不能超过内部 SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改Stack_Size的值。如果程序出现了莫名其妙的错误,并进入了 HardFault 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。

CM3/4一致,开辟空间为:存储器映射 Block1 的片内SRAM区。

2. 堆空间的开辟

在这里插入图片描述

  1. EQU 给数字常量0x00000200取一个符号名Heap_Size
  2. AREA 汇编一个新的代码段或者数据段,段名为HEAP,不初始化[NOINIT],可读可写[READWRITE],8 字节对齐[ALIGN=3表示 2^3 对齐]
  3. SPACE分配内存指令,分配大小为 Heap_Size[512字节] 字节连续的存储单元给堆空间,并初始化为 0。。
  4. __heap_base 表示堆的起始地址。
  5. __heap_limit 表示堆的结束地址。
  6. PRESERVE8 指示编译器按照 8 字节对齐。
  7. THUMB 指示编译器之后的指令为 THUMB 指令。


40~55行:在存储器映射区 Block1[片内SRAM区]开辟 512 字节的连续空间给堆空间[可读可写],并将符号名__heap_bas 指向堆的起始地址,符号名__heap_limi 指向堆的结束地址,最后指示编译器按照 8 字节对齐,编译器之后的指令为 THUMB 指令。


注意:
堆主要用于动态内存的分配,像 malloc()、calloc()和 realloc()等函数申请的内存就在堆上面。堆中的内存一般由程序员分配和释放,若程序员不释放,程序结束时可能由操作系统回收。
CM3/4一致,开辟空间为:存储器映射 Block1 的片内SRAM区。


由于一般都会使用独立的内存管理实现方式(mymalloc,myfree 等),并不需要使用 C 库的 malloc 和 free 等函数,也就用不到堆空间,因此我们可以设置 Heap_Size 的大小为 0,以节省内存空间。

3. 中断向量表定义

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. AREA 汇编一个新的数据段[DATA],段名为RESET,只读[READONLY]
  2. EXPORT 表示声明一个标号[__Vectors]具有全局属性,可被外部的文件使用。
  3. EXPORT 表示声明一个标号[__Vectors_End]具有全局属性,可被外部的文件使用。
  4. EXPORT 表示声明一个标号[__Vectors_Size]具有全局属性
  5. DCD 分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。
  6. __Vectors 为向量表起始地址。
  7. __Vectors_End 为向量表结束地址。
  8. EQU 给数字常量[__Vectors_End - __Vectors]取一个符号名 __Vectors_Size [向量表大小]

  • 56~59行:汇编一个段名为RESET的数据段[DATA],此数据段只读[READONLY]
  • 58~61行:声明标号[__Vectors、__Vectors_End、__Vectors_Size]具有全局属性,可被外部的文件使用。
  • 62~163行:使用 DCD 分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存[中断向量表]
  • 164~165行: 符号名__Vectors_End 指向向量表结束地址。
  • 166行:符号名__Vectors_Size 指向堆的大小【使用 EQU 计算出的向量表大小】


  当内核响应了一个发生的异常后,对应的异常服务例程(ESR)就会执行。为了决定 ESR的入口地址, 内核使用了向量表查表机制。向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。在复位后,该寄存器的值为 0。因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  举个例子,如果发生了异常 SVCall,则 NVIC 会计算出偏移移量是 11x4=0x2C,然后从那里取出服务例程的入口地址并跳入。要注意的是这里有个另类:地址 0x0000 0000 并不是什么入口地址,而是给出了复位后 MSP 的初值。

  中断向量表被放置在代码段的最前面。例如:当我们的程序在 FLASH 运行时,那么向量表的起始地址是:0x0800 0000。地址 0x0800 0000 存放的是栈顶地址。DCD:以四字节对齐分配内存,也就是下个地址是 0x0800 0004,存放的是 Reset_Handler 中断函数入口地址。
  从代码上看,向量表中存放的都是中断服务函数的函数名,所以 C 语言中的函数名对芯片来说实际上就是一个地址。


注意: 向量表格中灰色部分是系统内核异常,CM3和CM4系统内核异常是一样的。表格中位置 0 ~ 81 是外部中断,CM3/4内核的芯片最大支持 240 个外部中断,具体使用多少个由芯片厂家设计决定。如这个表格中的 CM4 只使用了81个,CM3 芯片只使用了 60 个。这里说的外部中断是相对内核而言。

4. 复位程序

在这里插入图片描述

  1. AREA 汇编一个新的代码段[CODE],段名为.text,只读[READONLY]
  2. PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
  3. EXPORT 声明复位中断向量Reset_Handler为全局属性,这样外部文件就可以调用此复位中断服务。
  4. WEAK 表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。
  5. IMPORT 表示该标号来自外部文件。
  6. LDR 是内核指令,表示用于从内存加载数据到寄存器。
  7. ORR 是内核指令,用于在两个操作数上进行逻辑或运算,并把结果放置到目的寄存器[R1]中。
  8. STR 是内核指令,用于将寄存器[R1]中的数据存储到内存中的指定地址[R0]
  9. BLX 是内核指令,用于跳转到指定的地址,并在跳转前将当前PC寄存器的值保存到链接寄存器LR(即R14)。
  10. BX 是内核指令,表示跳转到由寄存器/标号给出的地址,不用返回。

  • 167~169行:汇编一个段名为.text的代码段[CODE],此代码段只读[READONLY]
  • 170~171行: 复位子程序开始。
  • 172行: 表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
  • 173行: 表示 SystemInit 这个函数来自外部的文件。
  • 174行: 表示 __main 这个函数来自外部的文件。
  • 175~178行: 0xE000ED88 就是协处理器控制寄存器(CPACR)的地址,该寄存器的第 20~23 位用来控制是否支持浮点运算,这里我们全设置为 1[CP10/CP11完全访问协处理器(开启硬件FPU)],以支持硬件浮点运算。关于 CPACR 寄存器的详细描述,见《STM32F3 与 F4 内核编程手册.pdf》
  • 179~180行: 表示切换到SystemInit 地址,SystemInit 是一个标准的库函数,在 system_stm32f4xx.c 文件中定义,主要作用是配置系统时钟、还有就是初始化 FSMC/FMC总线上外挂的 SRAM(可选),配置外部 SRAM 作为数据存储器(可选)。
  • 181~182行: 表示切换到__main 地址,__main 是一个标准的 C 库函数,主要作用是初始化用户堆栈和变量等,最终调用 main 函数去到 C 的世界。这就是为什么我们写的程序都有一个 main 函数的原因,如果不调用__main,那么程序最终就不会调用我们 C 文件里面的main,也就无法正常运行。
  • 183行:表示子程序结束。

注意:CM3没有FPU所以175~178行屏蔽,寄存器版本中173行、179~180行屏蔽。
在寄存器版启动文件中屏蔽SystemInit函数的原因是为了避免不必要的初始化操作,从而简化启动过程和提高效率。
SystemInit函数是STM32官方库中的一个重要函数,它用于重置时钟控制寄存器(RCC)的配置到默认状态。这个函数通常在启动文件中被调用,以确保时钟系统处于一个已知的状态。然而,在寄存器版的开发中,开发者直接操作寄存器进行配置,因此不需要通过SystemInit函数来进行初始化。
具体来说,寄存器版开发直接通过编程来设置寄存器的值,这样可以更灵活地控制硬件的行为。如果使用SystemInit函数,它会重置所有的时钟配置,这可能会干扰开发者手动设置的寄存器值,导致不必要的麻烦和效率降低。因此,在寄存器版的启动文件中屏蔽SystemInit函数,可以避免这种不必要的初始化操作,使得启动过程更加简洁和高效‌。

4.1 SystemInit函数

在这里插入图片描述

在系统启动之后,程序会先执行 SystemInit 函数,进行系统一些初始化配置:

  1. 外部存储器配置(初始化 FSMC 总线上外挂的 SRAM)
  2. 中断向量表地址配置


注意:
DATA_IN_ExtSRAMUSER_VECT_TAB_ADDRESS 这两个宏实际并没有定义,实际上 SystemInit 并没有起作用。
保留这个接口,是为了避免去修改启动文件。另外,还可以把一些重要的初始化放到 SystemInit 这里,在 main 函数运行前就把重要的一些初始化配置好,这个一般用不到,而是直接在main函数中处理。

4.2 __main函数

_main 代码是编译器(如Keil、IAR、GCC)自动生成的,当编译器发现定义了 main 函数,就会自动创建_main,因此无法找到_main 代码。_main 主要负责初始化 C 运行时环境。


__main 函数的作用:

  • 初始化 .data 段:将Flash中的已初始化的全局变量复制到RAM中。
  • 清零 .bss 段:将未初始化的全局变量所在的内存区域清零。
  • 设置堆和栈:根据启动文件中的定义,初始化堆和栈的起始地址和大小。
  • 调用用户的 main 函数:在完成上述初始化工作后,跳转到用户的 main 函数。

在这里插入图片描述

在这里插入图片描述

程序经过汇编启动代码,执行到__main()后,可以看出有两个大的函数:

  1. __scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了 ZI 运行域的初始化工作。即完成 .data 段和 .bss 段的初始化。
  2. __rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数。


__main 函数的实现:

  1. 当程序运行到__main 函数,先跳转到__scatterload 函数运行,执行完__scatterload 函数后,R10 和 R11 会被赋值;
    在这里插入图片描述
    r10=0x08001630,r11=0x08001650

  2. 接着执行__scatterload_null 函数
    在这里插入图片描述
    2.1 第 1、2 行比较 r10、r11 是否相等,如果不等则跳转到 0x0800014E。明显两个值不等,所以程序跳转到 0x0800014E,
    2.2 第 4 行是把 0x08000147 赋值给 lr,即是保存_scatterload_null 的入口地址;
    2.3 第 5 行是把 r10 对应地址存放的 4 个字复制到 r0-r3 中,执行后得到:
      r0=0x08001650, 表示的是加载域起始地址。
      r1=0x20000000,表示运行域地址。
      r2=0x0000001C,表示要复制的 RW Data 大小,也可以在 map 文件查找得知。
      r3=0x0800016C,表示 _scatterload_copy 函数的起始地址。
      r10=0x08001640,表示运行域地址。

  3. 步骤2最后一行跳转到_scatterload_copy 函数
    在这里插入图片描述
    _scatterload_copy 复制好 RW Data 后,最后跳转回到__scatterload_null。回到__scatterload_null 函数后同样是先判断 r10 和 r11 是否相等,明显也是不等的,代码继续运行,最后跳转到 r3 寄存器存的地址。此时是循环回来再执行完__scatterload_null 函数后,即将进入了__scatterload_zeroinit 函数,先来看一下 r0 到 r3 的值变化。
      r0=0x0800166C, 表示的是加载域结束地址。
      r1=0x2000001C,表示ZI 段的起始地址。
      r2=0x0000076C,表示ZI 段大小,即 ZI Data 大小,也可以在 map 文件查找得知。
      r3=0x08000189,表示__scatterload_zeroinit 函数的起始地址

  4. 步骤3最后跳转到 __scatterload_zeroinit 函数
    在这里插入图片描述
    __scatterload_zeroinit 代码其实就是对 ZI 段清零的过程,从 ZI 段的起始地址 0x2000001C 开始,大小为 0x0000076C,进行清零操作。

  5. 步骤4执行完跳转回__scatterload 函数,再接着跳转到__rt_entry 函数
    在这里插入图片描述

  6. __rt_entry 函数先调用__user_setup_stackheap 函数来建立堆栈
    在这里插入图片描述
    第一条指令是保存函数的返回地址;
    第二条指令是跳转到__user_libspace 进行一些微库的初始化工作;
    后面的几条语句是建立一个临时栈;
    然后程序跳转到__user_inital_stackheap 进行用户栈的初始化,初始化完后 [ r0=0x20000188,r1=0x20000788,r2=0x20000388, r3=0x20000388 ]
    执行完__rt_entry 代码,用户栈顶地址为设置成 0x20000788,完成了堆栈的初始化。

  7. 最后运行到__rt _entry_main,去到 C 的世界
    在这里插入图片描述


总结:
1. 初始化 .data 段
 data 段存储已初始化的全局变量和静态变量。这些变量的初始值存储在Flash中,__main 函数会将这些值复制到RAM中。复制步骤如下:
 步骤1:获取 .data 段在Flash中的起始地址和大小。
 步骤2:将数据从Flash复制到RAM中的目标地址。

2. 初始化 .bss 段
 .bss 段存储未初始化的全局变量和静态变量。__main 函数会将 .bss 段对应的RAM区域清零。清零步骤如下:
 步骤1:获取 .bss 段的起始地址和大小。
 步骤2:将对应内存区域清零。

3. 设置堆和栈
 栈(Stack)用于存储局部变量和函数调用信息。栈的大小在启动文件中定义,通常位于RAM的顶部。
 堆(Heap)用于动态内存分配(如 malloc 和 free)。堆的大小也在启动文件中定义。
 __main 函数根据启动文件中的定义,初始化堆和栈的起始地址和大小。

4. 调用用户的 main 函数
 在完成上述初始化工作后,__main 函数会跳转到用户的 main 函数,开始执行用户代码。

5. 中断服务程序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. B 表示跳转到一个标号。
  2. PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
  3. EXPORT 声明符号名为全局属性,可被外部文件使用。
  4. WEAK 表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。

  • 185~228行:系统内核异常中断子程序,当中断到来时,如果外部有定义则调用外部定义的,否则调用此处[进行无限循环]
  • 229~397行:外部中断子程序,当中断到来时,如果外部有定义则调用外部定义的,否则调用此处[进行无限循环]


注意:
如果我们开启了某个中断,但是忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个‘.’中,无限循环。
这里的系统异常中断是内核的,外部中断是外设的。

6. 用户堆栈初始化

在这里插入图片描述

  1. ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示 4字节对齐。
  2. IF, ELSE, ENDIF 是汇编的条件分支语句。
  3. EXPORT 声明符号名为全局属性,可被外部文件使用。
  4. IMPORT 表示该标号来自外部文件。
  5. LR是内核指令, 用于保存函数调用的返回地址。
  6. BX 是内核指令,表示跳转到由寄存器/标号给出的地址,不用返回。
  7. LDR 是内核指令,表示用于从内存加载数据到寄存器。
  8. END 表示程序结束。


在这里插入图片描述
  404行判断是否定义了__MICROLIB[勾选了 Use MicroLIB 就代表定义了__MICROLIB 这个宏]
  如果定义__MICROLIB,声明__initial_sp [栈顶地址]__heap_base [堆起始地址]__heap_limit [堆结束地址] 这三个标号具有全局属性,可被外部的文件使用。
  如果没有定义__MICROLIB,使用默认的 C 库运行。那么堆栈的初始化由 C 库函数__main 来完成。


注意:
__user_initial_stackheap函数的主要作用是为编译器提供初始化C库函数所需的堆栈信息‌。这个标号用于设置用户程序的堆栈,包括堆和栈的初始化和配置‌。堆栈是程序运行时用于存储局部变量、函数调用参数、返回地址等信息的内存区域。具体来说,堆(Heap)用于动态内存分配,如通过malloc申请内存;栈(Stack)则用于存储函数的局部变量和返回地址等‌。


MicroLIB 是 MDK 自带的微库,是缺省 C 库的备选库,MicroLIB 进行了高度优化使得其代码变得很小,功能比缺省 C 库少。MicroLIB 是没有源码的,只有库。


__use_two_region_memory 函数是ARM Cortex-M系列处理器的一个编译器(如MDK)指令,用于指定处理器的内存管理单元(MMU)使用两个不同的地址空间,分别用于存储代码和数据。这种技术被称为“双区域内存”,它可以提高系统的可靠性和安全性。在使用双区域内存时,MMU将RAM分为两个部分:代码区和数据区。代码区用于存放代码和只读数据,数据区用于存放可读可写的数据。由于代码区是只读的,因此当程序出现错误时,它不会影响代码的运行。同时,数据区也可以设置为只读或只写区域,以保护程序的安全性。双区域内存还可以提高系统的性能。由于代码和数据被存储在不同的地址空间中,处理器可以同时从代码区和数据区读取数据,从而提高了内存访问的效率。


网站公告

今日签到

点亮在社区的每一天
去签到