14.1 ARM启动流程-复位函数
基于keil工程编译出来的AXF,单片机进入Reset_Handler()之后,会调用SystemInit初始化时钟,然后进入_main。

_main的具体实现在keil里面是闭源的,外网有大神通过反汇编破译了主要流程出来,_main并不等价于我们写的用户主函数,而是在流程的最后调用main。

CPU寻址图:

32位单片机的寻址区间都是0x0000 0000~0xFFFF FFFF,CODE区域存储BootRom(若有)、BootLoader以及User Application(我们自己烧录的);SRAM是片内的运存,我们在堆栈上开辟的存储空间最终都会映射到这个模块上;片上外设就是我们最熟悉的GPIO、ADC、Usart等等;EXMC外部存储器控制器,用来访问片外扩展的Nand Flash、SRAM或者LCD彩屏等;最后Cortex-Mx内核寄存器读写操作可以进行中断管理、休眠管理、debug、滴答计时、浮点数硬件运算等等。
下面先看code区,

最底下Aliased...叫做**"别名区"(Alias Region)或"启动空间"(Boot Space),** 这个区域是 Code(代码区)的起始段(0x0000 0000到0x002F FFFF)。
ARM Cortex-M 内核的设计规定:CPU 上电复位后,必须从0x0000 0000这个地址开始获取第一条指令(复位中断向量)和初始堆栈指针。但是,实际的代码物理地址并不一定在0x0000 0000例如 Main Flash 在0x0800 0000,系统存储区在0x1FFFF B000)。
为了解决这个问题,MCU 内部设计了一个硬件电路(地址映射器)。根据外部的 BOOT0和 BOOT1 引脚的电平状态,决定把哪一块物理存储器的内容"影射"(Alias)到 0x0000 0000 这个地址:

只要BOOT0=0,那么Main Flash(0x0800 0000 区域)就会映射到 0x0000 0000。此时 CPU 访问 0x0就是访问主 Flash,程序从用户代码启动。
如果 BOOT 1=0,BOOT0**=1** :把System Memory(即 Bootloader/BootROM**)**(0x1FFFF B000区域)映射到 0x0000 0000。此时 CPU 访问0x0就是访问系统存储区,程序进入厂家预置的 Bootloader 模式(STM32为首的厂商主流做法是使用串口0接受镜像文件,并且固化到Main Flash中)。
如果BOOT0=BOOT1,部分芯片还支持把 SRAM 映射到这里,从内存启动。
回到启动流程上面:

中断服务函数,中断向量表里面存储着中断服务函数的地址:

14.2 ARM启动流程-内存栈
ram区域的分布:

注:只有用const修饰的常量和初始化了的变量会被存储在Flash区域,并且后者在程序运行的时候搬移到bss段。

以最简单的点灯程序为例,绿色区域是栈,灰色区域是堆、.bss集合、.data段的集合。
栈指针(Stack Pointer, SP)是 CPU 内部的一个特殊寄存器。它的作用就像一个游标,始终指向当前栈的"栈顶"位置。
压栈(PSUH):当需要给函数分配空间时,SP 的数值会减小(向低地址移动),为函数预留出一块内存空间。
弹栈(POP):当函数执行完毕退出时,SP 的数值会增大(向高地址移动),这块预留的内存就被释放了。
- 程序刚启动时:进入
main()函数
-
动作 :程序启动,进入
main()函数。 -
SP 移动 :SP 从初始的栈底
0x20000428处向下移动(地址变小) ,留出main函数的栈帧空间。 -
当前状态 :
main函数栈帧建立完毕。
- 顺序执行初始化:
DelayInit()
-
动作 :执行到
DelayInit();。 -
SP 移动 :SP 继续向下移动 ,为
DelayInit函数建立栈帧。 -
当前状态 :栈中从底向上依次是
main帧 ->DelayInit帧。
- 顺序执行初始化:
LedDrvInit()
-
动作 :
DelayInit执行完毕,它的栈帧被销毁(SP 向上退回原位)。紧接着执行LedDrvInit();。 -
SP 移动 :SP 再次向下移动 ,建立
LedDrvInit的栈帧。 -
当前状态 :栈中从底向上依次是
main帧 ->LedDrvInit帧。
- 嵌套调用(关键点):
TurnOnLed(LED1)
-
动作 :
LedDrvInit执行完并销毁(SP退回)。接着进入while(1)循环,执行到TurnOnLed(LED1);。 -
SP 移动:
-
TurnOnLed被调用,SP 向下移动 ,建立TurnOnLed的栈帧。 -
假设
TurnOnLed内部调用了其他函数(如DelayNms),SP 会继续向下移动 ,在TurnOnLed的栈帧下方再建立新的栈帧。
-
-
当前状态 :此时栈中的深度达到最大,从底到顶依次是
main帧 ->TurnOnLed帧 -> 其他子函数帧。这对应了图中灰色区域最上方的一小块。
- 函数返回与空间复用
-
动作 :当
DelayNms(1000)执行完毕,程序返回到TurnOnLed。 -
SP 移动 :
DelayNms的栈帧销毁,SP 向上退回 。接着TurnOnLed执行完毕,它的栈帧也被销毁,SP 继续向上退回 ,恢复到执行TurnOnLed之前的位置。 -
当前状态 :
TurnOnLed和DelayNms的栈帧虽然被释放了,但它们占据的内存地址(即栈顶空间)变成了"可用状态"。
- 空间复用的发生
-
动作 :接着程序执行
DelayNms(1000);。 -
SP 移动 :SP 再次向下移动 ,为
DelayNms建立栈帧。 -
核心点 :此时
DelayNms所占据的栈空间,正是刚刚由TurnOnLed释放出来的空间。 -
这就实现了栈空间复用 :CPU 不需要给每个函数都准备一块永久独占的内存,而是所有被调用的函数共享同一块栈空间。谁当前正在运行,谁就"租用"栈顶的空间,运行完就"退租",下一个被调用的函数可以无缝衔接使用这块空间。
程序员划分堆栈时需要考虑的因素:
①时否需要在函数体内部开辟大数组,注释的写法就是开辟栈空间,不过后来我改成分配堆了。

②函数调用的层级嵌套关系。
Flash空间布局
