【STM32-启动文件 startup_stm32f103xe.s】

STM32-启动文件 startup_stm32f103xe.s

  • [■ STM32-启动文件](#■ STM32-启动文件)
  • [■ STM32-启动文件主要做了以下工作:](#■ STM32-启动文件主要做了以下工作:)
  • [■ STM32-启动文件指令](#■ STM32-启动文件指令)
  • [■ STM32-启动文件代码详解](#■ STM32-启动文件代码详解)
    • [■ 栈空间的开辟](#■ 栈空间的开辟)
    • [■ 栈空间大小 Stack_Size](#■ 栈空间大小 Stack_Size)
    • [■ .map 文件的详细介绍](#■ .map 文件的详细介绍)
      • [■ 打开map文件](#■ 打开map文件)
    • [■ 堆空间](#■ 堆空间)
    • [■ PRESERVE8 和 THUMB 指令](#■ PRESERVE8 和 THUMB 指令)
  • [■ 中断向量表定义(简称:向量表)???????](#■ 中断向量表定义(简称:向量表)???????)
  • [■ 复位程序](#■ 复位程序)
  • [■ weak](#■ weak)
  • [■ _main 函数的分析](#■ _main 函数的分析)
    • [■ __scatterload()函数](#■ __scatterload()函数)
    • [■ __rt_entry()函数](#■ __rt_entry()函数)
  • [■ 中断服务程序](#■ 中断服务程序)
  • [■ ALIGN指令](#■ ALIGN指令)
  • [■ 用户堆栈初始化](#■ 用户堆栈初始化)
  • [■ Use MicroLIB](#■ Use MicroLIB)
  • [■ 系统启动流程](#■ 系统启动流程)
    • [■ 代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行分析](#■ 代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行分析)
      • [■ 1.CM3 内核做的第一件事就是读取下列两个 32 位整数的值:](#■ 1.CM3 内核做的第一件事就是读取下列两个 32 位整数的值:)
      • [■ 2.MSP 和 PC](#■ 2.MSP 和 PC)
      • [■ 3.代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、 MSP 和 PC 后的内存情况。](#■ 3.代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、 MSP 和 PC 后的内存情况。)

■ STM32-启动文件

STM32 启动文件由 ST 官方提供

启动文件由汇编编写,是系统上电复位后第一个执行的程序。

■ STM32-启动文件主要做了以下工作:

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

2、初始化程序计数器指针 PC = Reset_Handler

3、设置堆和栈的大小

4、初始化中断向量表

5、配置外部 SRAM 作为数据存储器(可选)

6、配置系统时钟,通过调用 SystemInit 函数(可选)

7、调用 C 库中的 _main 函数初始化用户堆栈,最终调用 main 函数

■ STM32-启动文件指令

关于其他更多的 ARM 汇编,我们可以通过 MDK 的索引搜索工具中搜索找到。打开索引搜索工具的方法:

MDK->Help->uVision Help,

■ STM32-启动文件代码详解

例如:startup_stm32f103xe.s 把启动代码分成几个功能段进行详细的讲解

■ 栈空间的开辟

栈是从高往低生长,所以每使用一个栈空间地址,栈顶地址__initial_sp 就减一。

33 行 EQU:宏定义的伪指令, 给数字常量取一个符号名, 类似与 C 中的 define。

定义栈大小为 0x00000400 字节,即 1024B(1KB),常量的符号是 Stack_Size。

35 行 AREA 汇编一个新的代码段或者数据段。

段名为 STACK, 段名可以任意命名;NOINIT 表示不初始化; READWRITE 表示可读可写; ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。

36 行 SPACE 分配内存指令, 分配大小为 Stack_Size 字节连续的存储单元给栈空间。

37 行__initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址

栈主要用于存放局部变量,函数形参等, 属于编译器自动分配和释放的内存, 栈的大小不能超过内部 SRAM 的大小。如果工程的程序量比较大,定义的局部变量比较多,那么就需要在启动代码中修改栈的大小,即修改 Stack_Size 的值。

如果程序出现了莫名其妙的错误,并进入了 HardFault 的时候,你就要考虑下是不是栈空间不够大,溢出了的问题。

■ 栈空间大小 Stack_Size

  1. 打开.map文件 搜索__initial_sp

    栈顶地址 __initial_sp 的地址是 0x20000538
    栈低地址 STACK 0x20000138
    Stack_Size = 栈顶地址-栈低地址 Stack_Size 的大小是 0x00000400

■ .map 文件的详细介绍

■ 打开map文件

■ 堆空间

这部分代码的意思就是:开辟堆的大小为 0x00000200(512 字节),

段名为 HEAP ,NOINIT不初始化,READWRITE可读可写, ALIGN=3,表示按照 2^3 对齐,即 8 字节对齐。

__heap_base表示堆的起始地址,

__heap_limit 表示堆的结束地址。

堆和栈的生长方向相反的,堆是由低向高生长,而栈是从高往低生长。

注意点:

由于不需要使用 C 库的 malloc 和 free 等函数,也就用不到堆空间,因此我们可以设置 Heap_Size 的大小为 0,以节省内存空间

■ PRESERVE8 和 THUMB 指令

PRESERVE8: 指示编译器按照 8 字节对齐。

THUMB: 指示编译器之后的指令为 THUMB 指令。

■ 中断向量表定义(简称:向量表)???

定义一个数据段,

RESET, READONLY 表示只读。

EXPORT 表示声明一个标号具有全局属性,可被外部的文件使用。

这里是声明了__Vectors、 __Vectors_End 和 __Vectors_Size 三个标号具有全局性,可被外部的文件使用。

__Vectors 为向量表起始地址,

__Vectors_End 为向量表结束地址,

__Vectors_Size 为向量表大小, __Vectors_Size = __Vectors_End - __Vectors。

向量表其实是一个 WORD(32 位整数)数组,每个下标对应一种异常,该下标元素的值则是该 ESR 的入口地址。
向量表在地址空间中的位置是可以设置的,通过 NVIC 中的一个重定位寄存器来指出向量表的地址。

在复位后,该寄存器的值为 0。

因此,在地址 0 (即 FLASH 地址 0) 处必须包含一张向量表,用于初始时的异常分配。如下:

要注意的是这里有个另类: 地址 0x0000 0000 并不是什么入口地址,而是给出了复位后 MSP 的初值。

向量表格中灰色部分是系统内核异常

表格中位置 0 到 59 是外部中断

DCD: 分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。

???????????

■ 复位程序

定义一个段命为.text, 只读的代码段, 在 CODE 区。

利用 PROC、 ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。

关键词 描述
145 行 子程序开始
146 行 声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务。 WEAK:表示弱定义,如果外部文件优先定义了该标号则首先引用外部定义的标号,如果外部文件没有声明也不会出错。 这里表示复位子程序可以由用户在其他文件重新实现,这里并不是唯一的。
147 行和 148 行 IMPORT 表示该标号来自外部文件。 这里表示 SystemInit 和__main 这两个函数均来自外部的文件。
149 行 LDR 表示从存储器中加载字到一个存储器中。 SystemInit 是一个标准的库函数,在 system_stm32f1xx.c 文件中定义,主要作用是配置系统时钟、还有就是初始化 FSMC/FMC总线上外挂的 SRAM(可选),前面说配置外部 SRAM 作为数据存储器(可选)就是这个。
150 行 BLX 表示跳转到由寄存器给出的地址,并根据寄存器的 LSE 确定处理器的状态,还要把跳转前的下条指令地址保存到 LR。
151 行 把__main 的地址给 R0。 __main 是一个标准的 C 库函数,主要作用是初始化用户堆栈和变量等,最终调用 main 函数去到 C 的世界。 这就是为什么我们写的程序都有一个 main 函数的原因,如果不调用__main,那么程序最终就不会调用我们 C 文件里面的main,也就无法正常运行。
152 行 BX 表示跳转到由寄存器/标号给出的地址,不用返回。 这里表示切换到__main地址,最终调用 main 函数,不返回,进入 C 的世界。
153 行 ENDP 表示子程序结束。

LDR、 BLX、 BX 是内核的指令,可在《CM3 权威指南 CnR2》第四章-指令集里面查询到。

■ weak

weak 顾名思义是"弱"的意思,

在汇编中, 在函数名称后面加WEAK来表示

在 C语言中,在函数名称前面加上__weak 修饰符来表示, 这样的函数我们称为"弱函数"。

■ _main 函数的分析

_main 和 main 是两个完全不同的函数。

_main 代码是编译器自动创建的,因此无法找到_main 代码。

当编译器发现定义了 main 函数,那么就会自动创建_main。

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

__scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了 ZI运行域的初始化工作。

__rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数。

■ __scatterload()函数

■ __rt_entry()函数

■ 中断服务程序

这些中断服务函数都被WEAK声明为弱定义函数

中断函数分为系统异常中断和外部中断,外部中断根据不同芯片有所变化。

B 指令是跳转到一个标号,这里跳转到一个'.',表示无限循环。

中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,并且在 B 指令作用下跳转到一个'.'中,无限循环。

■ ALIGN指令

ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4 字节对齐。

要注意的是,这个不是 ARM 的指令,是编译器的。

■ 用户堆栈初始化

== IF, ELSE, ENDIF 是汇编的条件分支语句。==

关键词 描述
331 行 判断是否定义了__MICROLIB。 关于__MICROLIB 这个宏定义,我们是在 KEIL 里面配置。 勾选了 Use MicroLIB 就代表定义了__MICROLIB 这个宏。
333 行到 335 行 如果定义__MICROLIB, 声明__initial_sp、 __heap_base 和__heap_limit 这三个标号具有全局属性,可被外部的文件使用。 __initial_sp 表示栈顶地址, __heap_base 表示堆起始地址, __heap_limit 表示堆结束地址。
337 行 没有定义__MICROLIB,实际的情况就是我们没有定义__MICROLIB,所以使用默认的 C 库运行。 堆栈的初始化由 C 库函数__main 来完成。
339 行 IMPORT 声明__use_two_region_memory 标号来自外部文件。
340 行 EXPORT 声明__user_initial_stackheap 具有全局属性,可被外部的文件使用。
342 行 标号__user_initial_stackheap,表示用户堆栈初始化程序入口。 接下来进行堆栈空间初始化,堆是从低到高生长,栈是从高到低生长,是两个互相独立的数据段,并且不能交叉使用。
344 行 保存堆起始地址。
345 行 保存栈大小。
346 行 保存堆大小。
347 行 保存栈顶指针。
348 行 跳转到 LR 标号给出的地址,不用返回。
354 行 END 表示到达文件的末尾,文件结束。

■ Use MicroLIB

MicroLIB 是 MDK 自带的微库,是缺省 C 库的备选库, MicroLIB 进行了高度优化使得

其代码变得很小,功能比缺省 C 库少。

MicroLIB 是没有源码的,只有库。

关于 MicroLIB 更多知识可以看官方介绍 http://www.keil.com/arm/microlib.asp

■ 系统启动流程

Cortex-M3 内核复位后的起始地址和中断向量表的位置可以被重映射。充映射的方法是通过启动模式的

选择, 有以下 3 种情况:

1、 通过 boot 引脚设置可以将中断向量表定位于 SRAM 区,即起始地址为 0x2000000,同时复位后 PC 指针位于 0x2000000 处;

2、 通过 boot 引脚设置可以将中断向量表定位于 FLASH 区,即起始地址为 0x8000000,同时复位后 PC 指针位于 0x8000000 处;

3、 通过 boot 引脚设置可以将中断向量表定位于内置 Bootloader 区,本文不对这种情况做论述。

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

Cortex-M3 权威指南(中文)

■ 代码下载到内部 FLASH 的情况举例,即代码从地址 0x0800 0000 开始被执行分析

复位方式有三种:上电复位,硬件复位和软件复位当产生复位,并且离开 复位状态后,

■ 1.CM3 内核做的第一件事就是读取下列两个 32 位整数的值:

(1)从地址 0x0800 0000 处取出堆栈指针 MSP 的初始值,该值就是栈顶地址。

(2)从地址 0x0800 0004 处取出程序计数器指针 PC 的初始值,该值指向复位后执行的第一条指令。 下面用示意图表示

■ 2.MSP 和 PC

例如:

CM3内核是小端模式,所以倒着读。

0x08000000 的值是 0x20000788 堆栈指针 SP = 0x20000788

0x08000004 的值是 0x080001CD 程序计数器指针 PC = 0x080001CD

■ 3.代码从地址 0x0800 0000 开始被执行,讲解一下系统启动,初始化堆栈、 MSP 和 PC 后的内存情况。

ARM 规定: PC最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令( 0)还是 Thumb 指令( 1)

相关推荐
清风66666610 小时前
基于单片机与DAC0832的双路波形信号发生系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
azwsm11 小时前
电路元器件和GPIO控制器
单片机·嵌入式硬件
kebidaixu14 小时前
FreeRTOS 移植到 STM32F407VETX 记录(一)
stm32·单片机·嵌入式硬件
CSDN官方博客14 小时前
「谁说嵌入式只是调包和焊板子?」—— 2026嵌入式全栈技术征锋令
嵌入式硬件·物联网·embedding
半条-咸鱼14 小时前
【INACCESSIBLE_BOOT_DEVICE】安装 Config Tool 后 Windows 蓝屏,最终通过 VMware 虚拟机解决
windows·stm32·vmware·芯片
点灯小铭15 小时前
基于单片机的数码管定时插座设计与定时开关功能实现
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
云栖梦泽15 小时前
玩转RK3506SDK
linux·嵌入式硬件
数智工坊17 小时前
机器人四大主控板系统分层选型指南:树莓派、ESP32、STM32与Arduino的能力边界与实战定位
stm32·嵌入式硬件·机器人
某林21217 小时前
跨越底层与AI的鸿沟:ROS2+多模态大模型(Qwen-VL)机器人全链路排障实录
人工智能·stm32·机器人·人机交互·ros2·技术复盘
进击的小头18 小时前
第8篇:IGBT 从零到精通:核心原理、关键参数、选型指南与工业级应用要点
经验分享·嵌入式硬件·学习