打印
[APM32F4]

APM32F411 启动文件学习分享

[复制链接]
556|3
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
1455555|  楼主 | 2023-11-19 17:44 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 1455555 于 2023-11-19 17:44 编辑

目录
1、 相关基础概念
2、 启动模式
3、 启动文件
A、启动文件的作用
B、启动文件中的汇编指令
C、启动文件代码
       a  开辟栈空间
       b  开辟堆空间
       c、中断向量表定义
       d、复位程序代码
       d-1 __main
             d-1-1 __scatterload
      d-1-2 __scatterload_null
      d-1-3 _scatterload_copy
      d-1-4 __scatterload_zeroinit
              d-2 __rt_entry
                    d-2-1 __user_setup_stackheap
                           d-2-1-1 __user_inital_stackheap
                   d-2-2 __rt _entry_main
             e、中断服务函数
             f、用户堆栈初始化
   D、启动文件内容示意图


1、相关基础概念
在学习启动文件之前,要了解一些有关的概念。
存储芯片根据断电后是否保留存储的信息可分为易失性存储芯片(RAM)和非易失性存储芯片(ROM)。非易失性存储器芯片在断电后亦能持续保存代码及数据,分为闪型存储器(Flash Memory)与只读存储器(Read-OnlyMemory),其中闪型存储器是主流,而闪型存储器又主要是NAND Flash和NOR Flash。APM32使用的为NOR FLASH。
如图1所示,内存可分为6 个储存数据段和3种存储属性区,下面是它们的简要介绍。
ZI:Zeroinitialized 的缩写,包含初始化为 0 的数据(ZIdata)。
Bss: Block Started by Symbol。储存未初始化的,或初始化为0的全局变量和静态变量。
Heap:由程序员分配。
Stack:系统自动分配,是用户存放程序临时创建的局部变量,由系统自动分配和释放。
RW:已经初始化的全局变量和静态变量
Data:数据段,储存已初始化且不为0的全局变量和静态变量。static声明的变量放在data段。
RO只读
Text:代码段,储存程序代码。
Constdata:储存只读变量。const修饰的只读变量,C语言中,const修饰的局部变量存放在栈上,全局变量放在constdata段。


但需要注意的是,在计算FLASH的使用大小时,要将RW字段算入FLASH中。因为编译器为了完成所有 RW 段数据赋值,其先将 RW 段的所有初值,先保存到 Flash 中,程序执行时,再 Flash 中的数据搬运到 RAM 中,所以 RW 段既占用 Flash又占用 RAM,且占用的空间大小是相等的。
为什么RW段会放到两个区呢?当你断电后,RAM里所有的数据都会丢失,那我们已经初始化不为零的全局变量该怎么办?这时候,就是将存在FLASH里的RW字段复制到RAM里面,栈也就在这个地方被引入。
举个例子,运行APM32F411SDK中的LED_TOGGLE例程,得到如图2结果:

RAM Size = RW-Data+ZI-Data = 1464+516+72=2052B
Flash Size = Code+RO-Data+RW-Data = 72+1632=1704B。
2、启动模式
如图3所示,APM32一共有三种不同的启动模式。
Flash memory启动方式
当boot0 = 0,boot1=x时,启动地址:0x08000000 是内置的Flash,一般我们使用JTAG或者SWD模式下载程序时,就是下载到这个里面,重启后也直接从这启动程序。基本上都是采用这种模式。
有部分MCU可以从外部的FLASH启动,比如STM32H750
System memory启动方式
当boot0 = 1;boot1 = 0时,启动地址:0x1FFF0000从系统存储器启动,这种模式启动的程序功能是由厂家设置的。系统存储器是芯片内部一块特定的区域,开发板在出厂时,在这个区域内部预置了一段BootLoader,也就是我们常说的ISP程序,这是一块ROM,出厂后无法修改。一般来说,我们选用这种启动模式时,是为了从串口下载程序,因为在厂家提供的BootLoader 中,提供了串口下载程序的固件,可以通过这个BootLoader将程序下载到系统的Flash中。
下载步骤
1、将BOOT0设置为1,BOOT1设置为0,然后按下复位键,这样才能从系统存储器启动BootLoader
2、最后在BootLoader的帮助下,通过串口下载程序到Flash中
3、程序下载完成后,需要将BOOT0设置为GND,手动复位,从Flash中启动。
SRAM启动方式
当boot0 = 1;boot1 = 1时,启动地址:0x20000000 内置SRAM,一般用于程序调试。

3、启动文件
以内部FLASH启动为例,学习启动文件相关知识。启动文件由汇编编写,是系统上电复位后第一个执行的程序。
A、启动文件的作用
启动文件主要做了以下工作:
1、初始化堆栈指针 SP= _initial_sp
2、初始化程序计数器指针 PC= Reset_Handler
3、设置堆和栈的大小
4、初始化中断向量表
5、配置外部 SRAM 作为数据存储器(可选)
6、配置系统时钟,通过调用 SystemInit 函数(可选)
7、调用 C 库中的_main 函数初始化用户堆栈,最终调用 main 函数。
B、启动文件中的汇编指令


图片4中提及的是比较常见的汇编指令,如果在阅读汇编代码时,发现不了解的汇编指令可以在KEIL编译器中搜索。具体步骤为MDK->Help->uVision Help。如图5,6所示。




C、启动文件代码
以APM32F411的启动代码为例,版本是:APM32F4xx_SDK_V1.4,启动文件名称是: startup_apm32f411.s。截图按照启动文件代码顺序。阅读者可以前往珠海极海半导体有限公司(geehy.com),极海半导体官网下载。
a  开辟栈空间


30 行 EQU:宏定义的伪指令,给数字常量取一个符号名,类似与C中的 define。定义栈大小为0x00000400字节,即 1024B(1KB),常量的符号是 Stack_Size。
31 行 AREA 汇编一个新的代码段或者数据段。段名为STACK,段名可以任意命名;NOINIT 表示不初始化; READWRITE 表示可读可写;ALIGN=3,表示按照2^3 对齐,即8 字节对齐。
33行 SPACE 分配内存指令,分配大小为Stack_Size 字节连续的存储单元给栈空间。
34 行__initial_sp 紧挨着 SPACE 放置,表示栈的结束地址,栈是从高往低生长,所以结束地址就是栈顶地址。 栈顶地址,可以通过.map 文件查看。


我们定义Stack_Size 的大小是 0x00000400,栈顶地址__initial_sp是0x200006a8,那栈底地址是0x200006a8 - Stack_Size0x400= 0x200002a8。栈是从高往低生长,所以每使用一个栈空间地址,栈顶地址__initial_sp 就减一。

b  开辟堆空间

开辟堆的大小为0x00000200(512 字节),段名为 HEAP,不初始化,可读可写,8 字节对齐。
__heap_base表示堆的起始地址,__heap_limit 表示堆的结束地址。
PRESERVE8:指示编译器按照 8 字节对齐。
THUMB:指示编译器之后的指令为 THUMB 指令

Thumb指令,Thumb代码使用的指令数要比ARM代码多约30%~40%,但最终生成的目标代码所需的存储空间约为ARM代码的60%~70%(因为每条指令所占空间是arm指令的一半)。在存储器是32位的情况下,ARM性能较好,因为同样的代码编译的结果Thumb指令将会比ARM多,Thumb指令仍旧花费指令周期来从32-bit块内存预取。在16-bit内存上,即使有比ARM多的代码,这时Thumb性能也较好,因为Thumb每一条指令预取需要一个周期而每条ARM指令需要两个周期。
ARM规定:PC 最低两位并不表示真实地址,最低位 LSB 用于表示是 ARM 指令(0)还是 Thumb 指令(1)。如图,在单步调试代码时,0x080001CA处,BX R3指令意为跳转至R3寄存器,此时R3寄存器里的值为0x080001D5,最后一位为奇数。单步执行,发现代码运行至0x080001D5处,原因为BIT0的1,并不表示实际地址,而是代表使用THUMB指令。




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

如图所示,代码定义了一个数据段,名字为RESET, READONLY 表示只读。EXPORT 表示声明一个标号具有全局属性,可被外部的文件使用。__Vectors为向量表起始地址,__Vectors_End 为向量表结束地址,__Vectors_Size 为向量表大小,__Vectors_Size = __Vectors_End - __Vectors。
DCD:分配一个或者多个以字为单位的内存,以四字节对齐,并要求初始化这些内存。

内核使用了向量表查表机制。向量表是一个WORD(32 位整数)数组, 每个下标对应一种异常,该下标元素的值则是该异常服务函数的入口地址。举个例子,如果发生了异常SVCall,则 NVIC 会计算出偏移移量是 11x4=0x2C,取出服务例程的入口地址并跳入。
要注意的是:地址0x0000 0000 并不是入口地址,而是给出了复位后 MSP 的初值。F407 的向量表格中红色框住部分是系统内核异常。85 个可屏蔽中断通道。


d、复位程序代码
定义一个段命为.text,只读的代码段,在 CODE 区。 利用 PROC、ENDP 这一对伪指令把程序段分为若干个过程,使程序的结构加清晰。
167 行子程序开始
168 行声明复位中断向量 Reset_Handler 为全局属性,这样外部文件就可以调用此复位中断服务。169行和170 行 IMPORT 表示该标号来自外部文件。
171 行 LDR 表示从存储器中加载字到一个存储器中。SystemInit 是一个标准的库函数,在 system_stm32f4xx.c 文件中定义,主要作用是配置系统时钟。
172 行 BLX 表示跳转到由寄存器给出的地址,把跳转前的下条指令地址保存到LR。
173 行把__main 的地址给 R0。__main 是一个标准的C 库函数,最终调用main 函数去到 C 的世界。
174 行 BX 表示跳转到由寄存器/标号给出的地址,不用返回。这里表示切换到__main 地址,最终调用 main 函数,不返回,进入 C 的世界。
175 行 ENDP 表示子程序结束。




d-1 __main
当编译器发现定义了main 函数,那么就会自动创建_main。主要包含下面两个函数。
__scatterload():负责把 RW/RO 输出段从装载域地址复制到运行域地址,并完成了ZI 运行域的初始化工作。
__rt_entry():负责初始化堆栈,完成库函数的初始化,最后自动跳转向 main()函数
(1)段是__main 函数,
(2)段是__scatterload 函数,
(3)段是__scatterload_null 函数。当程序运行到__main 函数,先跳转到__scatterload 函数运行,执行__scatterload 函数。


后文的说明中有观察寄存器的值,为了查看寄存器的方便和文章的统一性,这里给出调试时的汇编窗口,但反汇编代码更清晰直观,所以截出反汇编文件中相同部分的图片。这两张图内容是一样的,选其一看即可。
*对应反汇编生成文件部分
d-1-1 __scatterload
当程序运行到__main 函数,先跳转到__scatterload 函数运行,执行完__scatterload 函数后,R10 和 R11 会被赋值。






d-1-2 __scatterload_null





第1、2 行比较 r10、r11 是否相等,如果不等则跳转到 0x080001B6。
第4行是把 0x080001AF 赋值给 lr,即是保存_scatterload_null 的入口地址;
第5 行是把 r10 对应地址存放的 4 个字复制到 r0-r3 中,执行后r0,r1,r2,r3,r10的值都得到了更改。
此时,
R0: 0x080007BC 表示的是加载域起始地址。
R1: 0x20000000 为运行域地址。
R2: 0x00000048 为要复制的 RWData 大小,也可以在 map 文件查找得知。
R3:0x080001D5 是_scatterload_copy 函数的起始地址。

d-1-3 _scatterload_copy

通过__scatterload_null 函数的最后一行跳转到_scatterload_copy函数。_scatterload_copy 复制好 RWData 后,最后跳转回到__scatterload_null。回到__scatterload _null 函数,判断 r10 和 r11 是否相等,不等,代码继续运行,最后跳转到r3 寄存器存的地址。
此时是循环回来再执行完__scatterload_null 函数后,即将进入__scatterload_zeroinit函数,先来看一下 r0 到 r3 的值变化。
R0: 0x08000804 表示的是加载域结束地址。
R1: 0x20000048 为 ZI 段的起始地址。
R2: 0x00000660 为 ZI 段大小,即ZI Data 大小,也可以在 map 文件查找得知。
R3: 0x080001F1 是__scatterload_zeroinit函数的起始地址。

d-1-4 __scatterload_zeroinit
__scatterload_zeroinit 代码对ZI 段清零的过程,从ZI 段的起始地址 0x20000048
开始,大小为0x00000660,进行清零操作。最后跳转回__scatterload 函数。




这一小部分的执行顺序可以用下图表示。
第一步:使用__scatterload和__scatterload_null进行数据处理,得到__scatterload_copy函数所需要的关键数据。
第二步:执行__scatterload_copy函数,完成负责把 RW 输出段从装载域地址复制到运行域地址。
第三步:执行__scatterload_null,继续进行数据处理,得到__scatterload_zeroinit函数所需关键数据。
第四步:执行__scatterload_zeroinit函数,完成了ZI 运行域的初始化工作。
第五步:跳回__scatterload函数,准备执行__rt_entry函数。

d-2 __rt_entry
d-2-1 __user_setup_stackheap
__rt_entry 函数开始就先调用__user_setup_stackheap函数来建立堆栈。


__user_setup_stackheap 函数的第一条指令是保存函数的返回地址。第二条指令是跳转到__user_libspace 进行一些微库的初始化工作,后面的几条语句是建立一个临时栈。
d-2-1-1
程序跳转到__user_inital_stackheap 进行用户栈的初始化。


下面为__user_inital_stackheap 代码,这段代码就是启动文件中初始化堆栈的代码。



d-2-2 __rt _entry_main
运行到__rt _entry_main,到main(),执行到我们自己写的程序。

e、中断服务函数
B 指令是跳转到一个标号,这里跳转到一个‘.’,表示无限循环。 开启了中断,但忘记写对应的中断服务程序函数又或者把中断服务函数名写错,那么中断发生时,程序就会跳转到启动文件预先写好的弱定义的中断服务程序中,B 指令作用下跳转到一个‘.’中,无限循环。


f、用户堆栈初始化
ALIGN 表示对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,空缺则表示4字节对齐。要注意的是,这个不是 ARM 的指令,是编译器的。
383 行判断是否定义了__MICROLIB。
385 行到 387 行如果定义__MICROLIB,声明__initial_sp、__heap_base 和__heap_limit
这三个标号具有全局属性,可被外部的文件使用。__initial_sp 表示栈顶地址,__heap_base
表示堆起始地址,__heap_limit 表示堆结束地址。
337 行没有定义__MICROLIB,使用默认的C 库运行。堆栈的初始化由 C 库函数__main 来完成。
接下来指令为:保存堆起始地址;保存栈大小;保存堆大小;保存栈顶指针;跳转到LR 标号给出的地址,不用返回。
406 行 END 表示到达文件的末尾,文件结束

D、启动文件内容示意图







_initial_sp.png (49.03 KB )

_initial_sp.png

使用特权

评论回复
沙发
daichaodai| | 2023-11-20 08:54 | 只看该作者
M内核的MCU启动过程基本一致

使用特权

评论回复
板凳
储小勇_526| | 2023-11-20 10:22 | 只看该作者
作为一个程序员真得要研究的这么细吗

使用特权

评论回复
地板
1455555|  楼主 | 2023-11-22 09:19 | 只看该作者
储小勇_526 发表于 2023-11-20 10:22
作为一个程序员真得要研究的这么细吗

多学无害嘛

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

8

主题

17

帖子

1

粉丝