下面以ARM Cortex_M3裸核的启动代码为例,做一下简单的分析。首先,在启动文件中完成了三项工作:
1、 堆栈以及堆的初始化
2、 定位中断向量表
3、 调用Reset Handler。
在介绍之前,我们先了解一下ARM芯片启动文件中涉及到的一些汇编指令的用法。
补充一下,其中DCD相当于C语言当中的&,定义地址。
1、堆栈以及堆的初始化
1.1 堆栈的初始化
Startup_xxx.s中的堆栈初始化代码
Stack_Size EQU 0x00000400,这个语句相当于Stack_Size这个标号(标号:链接器的术语,下文中提到的所有“标号”,指的都是指的链接器中的标号)等于0x00000400相当于C语言中的#define Stack_Size 0x00000400 ,也就是说此语句只是一个声明,并未分配地址。
AREA STACK, NOINIT, READWRITE, ALIGN=3,此语句定义了一个叫STACK的代码段,并指明8字节对齐(ALIGN = 3)。其中NOINIT表示未初始化,READWRITE表示可读可写,ALIGN = 3,即表示2^3 = 8,八字节对齐。
Stack_Mem SPACE Stack_Size,为Stack_Mem分配Stack_Size大小的一块内存区域,注意这里分配的是RAM,即分配了大小为1KB的内存空间(0x00000400 = 1024)。
__initial_sp ,紧跟着栈分配内存后,所以其为栈顶(满递减栈)。此标号有一层隐含的意思就是在M3中堆栈是满递减堆栈,因为它指定了堆栈指针位于堆栈的高地址(在Stack_Mem之后),具体如下图所示。
堆栈指针sp位置
上图来自Cortex_M3的一个工程的xxx.map文件。可以看出栈的起始地址为0x20000c68,大小为1024字节(即0x00000400 = Stack_Size)。而堆栈指针的位置在0x20001068,其等于栈的起始地址0x2000c68+0x00000400,说明本系列的Cortex_M3微控制器的堆栈为满递减堆栈。
所以__initial_sp为1KB空间栈的栈顶,栈主要用于局部变量和形参的调用过程的临时存储,属于编译器自动分配和释放的内存,所以这里需要注意如果你的函数所占的内存过大,那么这个空间应调整其大小但一定要小于内部SRAM的大小。堆是程序员空间是程序员进行分配和释放的,如果程序中未释放最后由系统回收。
1.2 堆的初始化
Startup_xxx.s中的堆初始化代码
堆的初始化过程与堆栈的初始化相同。
2、中断向量表的初始化
中断向量表的初始化代码(部分)
PRESERVE8指定了以下的代码为8字节对齐,这是keil编译器的一个编程要求,对齐情况如下图所示:
xxx.list文件中的8字节对齐示意图
THUMB指定了接下来的代码为THUMB指令集。
AREA RESET, DATA, READONLY,此语句声明RESET数据段。
EXPORT __Vectors,导出向量表标号,EXPORT作用类似于C语言中的extern。之后的代码就是为向量表分配存储区域。中断向量表从FLASH的0x00000000地址开始放置,以4个字节为一个单位,地址0存放的是栈顶指针(sp)的地址,0x00000004存放的是复位程序的地址,往后以此类推,这里我们只设置了一个Reset_Handler向量。从代码上看,向量表中存放的都是中断服务函数的函数名,可我们知道C语言中的函数名就是一个地址。(由此我们知道,中断函数的函数名都已经知道了,我们在写对应的中断服务程序时,从对应的地址取服务例程的入口地址并跳入执行)。但是此处有一个要注意的,就是0号地址不是什么入口地址,而是给出的复位后的MSP的初值。
3、调用Reset Handler
调用Reset Handler的代码
此段代码只完成了一个功能,引导程序进入__main。__main的具体行为在后面做具体描述。
PROC与ENDP组合在汇编中定义了一段子函数。
用户堆栈的初始化
具体的堆栈以及堆的初始化行为
这一部分也就是把初始化的堆栈地址赋值给单片机的对应寄存器以方便C程序进行分配释放使用。
4、其他代码
有一些芯片厂商对芯片的加密的加密级别的代码也会放在这里,芯片上电后会自动读取这一地址的值以确定芯片的加密方式。
5、ARM芯片的启动过程详解
接下来介绍__main函数的具体实现过程。
首先在介绍__main函数之前,我们先了解一些关于ARM芯片在启动过程中的基本知识。
“ARM程序”是指在ARM系统中正在执行的程序,而非保存在ROM中的.bin(.axf,.hex)映像(image)文件。
一个ARM程序包含3部分:RO,RW和ZI
RO 就是只读数据,是程序中指令和常量;
RW是可读写的数据,程序中已初始化变量;
ZI 是程序中未初始化的变量和初始化为0的变量。
简单理解就是:
RO就是readonly,RW就是read/write,ZI就是zero initial。
ARM芯片的启动过程详解
注意,以上的过程并非绝对的,不同的ARM架构或者是不同的代码以上的执行过程是不同的。
复位处理程序是在汇编器中编写的短模块,系统一启动就立即执行。复位处理程序最少要为应用程序的运行模式初始化堆栈指针。对于具有本地内存系统(如缓存、TCM、MMU和MPU)的处理器,某些配置必须在初始化过程的这一阶段完成。复位处理程序在执行之后,通常跳到__main以开始C库初始化序列。
__main中的__scatterload负责设置内存,而__rt_entry负责设置运行时的环境。__scatterload中负责把RO/RW(非零)输出段从装载域地址复制到运行域地址(执行代码和数据复制、解压缩),并完成ZI段运行域数据的0初始化工作。然后跳到__rt_entry设置堆栈和堆、初始化库函数和静态数据。然后,__rt_entry跳转到应用程序的入口main()。主应用程序结束执行后,__rt_entry将库关闭,然后把控制权交换给调试器。函数标签main()具有特殊含义。Main()函数的存在强制链接器链接到__main和__rt_entry中的代码。如果没有标记为main()的函数,则没有链接到初始化序列,因而部分标准C库功能得不到支持。
6、结合代码来看芯片启动过程
上电后硬件设置sp、pc,刚上电复位后,硬件会自动根据向量表地址找到向量表。
在离开复位状态后, CM3 做的第一件事就是读取下列两个 32 位整数的值:
1、从地址 0x0000 0000 处取出 MSP 的初始值。
2、从地址 0x0000 0004 处取出 PC 的初始值,这个值是复位向量, LSB 必须是 1。 然后从这个值所对应的地址处取指。
硬件自动从0x0000 0000位置处读取数据赋给栈指针sp,然后从0x0000 0004位置处读取数据赋给pc指针,完成复位,结果为:
SP = 0x2000 1068
PC = 0x0000 011D
这与传统的 ARM 架构不同——其实也和绝大多数的其它单片机不同。传统的 ARM 架构总是从 0 地址开始执行第一条指令。它们的 0 地址处总是一条跳转指令。在 CM3 中,在 0 地址处提供 MSP 的初始值,然后紧跟着就是向量表。向量表中的数值是 32 位的地址,而不是跳转指令。向量表的第一个条目指向复位后应执行的第一条指令,就是我们上面分析的Reset_Handler这个函数。
进入__main
LDR R0, =__main
BX R0
执行上两条指令,跳转到__main程序段运行,__main的地址是0x0000 0080,上一步指令pc = 0x0000 011D的地址没有对齐,硬件自动对齐到0x0000 011C,执行__main。
pc指针通过立即数寻址,跳转到0x0000 0081处执行,同上这里也会自动对齐到0x0000 0080处。
在__scatterload函数中又会进入__scatterload_copy,在__scatterload_copy中进行代码搬运,主要是加载已经初始化的数据段和未初始化的数据段,同时还会初始化栈空间,即ZI段清零(其中搬运次数由代码中声明的变量类型和变量多少来决定)。
然后会跳转到__rt_entry函数执行,__rt_entry是使用ARM C库的程序的起点。将所有分散加载区重新定位到其执行地址后,会将控制权传递给__rt_entry。如下图,在__rt_entry中主要实现如下几个功能:
1、 设置用户的堆和堆栈
2、 调用__rt_lib_init以初始化C库
3、 调用main()
4、 调用__rt_lib_shutdown以关闭C库
5、 退出
__rt_lib_init函数是库函数初始化函数,它与__rt_lib_shutdown配合使用。并且这个函数紧靠__rt_stackheap_init()后面调用,即紧跟堆和堆栈初始化后面调用,并且传递一个要用作堆的初始内存块。此函数是标准ARM库初始化函数,不能重新实现此函数。
注意:最后两步是在程序退出main()函数的时候才会执行,而我们嵌入式程序一般都是死循环,所以基本上不会执行这两个过程。还有以上过程是针对使用标准C Library而言的,不包括使用MDK提供的microlib库的情况。
在__rt_entry_main中,用户程序就开始正式执行了(进入C的世界)。在此之前初始化 MSP 是必需的,因为可能第 1 条指令还没来得及执行,就发生了 NMI 或是其它 fault。 MSP 初始化好后就已经为它们的服务例程准备好了堆栈。这也就是__main中做的事情。
7、最后关于microlib库
Microlib 是缺省C库的备选库。它旨在与需要装入到极少量内存中的深层嵌入式应用程序配合使用。这些应用程序不在操作系统中运行,因此microlib 进行了高度优化以使代码变得很小,当然它的功能相比缺省C库少,并且根本不具备某些ISO C特性。某些库函数的运行速度也比较慢,比如memcpy()。
Microlib与缺省C库之间的主要差异是:
Microlib不符合ISO C 库标准。不支持,某些ISO特性,并且其他特性具有的功能也比较少;
Microlib不符合IEEE754 二进制浮点算法标准;
Microlib进行了高度优化以使代码变得很小;
无法对区域设置进行配置。缺省C区域设置是唯一可用的区域设置;
不能将main()声明为使用参数,并且不能返回内容;
不支持stdio,但未缓冲的stdin、stdout和stderr除外;
Microlib对C99函数提供有限的支持;
Microlib不支持操作系统函数;
Microlib不支持与位置无关的代码;
Microlib不提供互斥锁来防止非线程安全的代码;
Microlib不支持宽字符或多字节字符串;
与stdlib不同,microlib不支持可选的单或双区内存模型。Microlib只提供双区内存模型,即单独的堆栈和堆区。
8、关于生成的xxx.map文件
想要更好的了解启动代码的运行机制,我们就有必要了解一下由Keil的链接器“armlink”生成的描述文件,即xxx.map文件。
目标文件的组成
上图即是armlink的链接器为测试代码生成的xxx.map文件中的一部分,其描述了镜像文件的组成信息,其中可以明显看到其由两部分构成:
User Code生成的目标文件
C Library生成的目标文件
可见我们在上文中所描述的启动过程中看到的__main、__rt_entry、__scartterload以及__rt_lib_init等,就是C library中的代码。
所以,我们每次烧录的可执行的ARM的bin文件中不仅有开发者编写的代码,还有C Library的代码。
上图为存放在RAM中的RW段。
以上就是CM3芯片的基本启动过程,有高手发现有低级错误,还望指正,谢谢!
个人博客更新地址:
https://blog.strongwong.top/posts/%E5%85%B3%E4%BA%8E-ARM-Cortex-M3-%E7%9A%84%E5%90%AF%E5%8A%A8%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E5%8F%8A%E5%88%86%E6%95%A3%E5%8A%A0%E8%BD%BD.html