第1章 启动流程分析
嵌入式程序上电后的经典启动流程:上电 --> 执行初始化指令 --> 调用 SystemInit 配置时钟 --> 调用 __main 初始化 C 语言环境 --> 最终进入你写的 main() 函数开始执行主程序。
__main 函数是在keil软件中的一个库文件中。在该函数中才是调用我们自己写的main() 函数。

1.1 keil软件编译生成的axf文件分析
AXF 的全称是 ARM Executable File (ARM 可执行文件)。当你在 Keil 等 IDE 中写完 C 语言代码,点击"编译链接(Build)"后,编译器会生成这个 .axf 文件。

从上图可以看出整个汇编文件的启动流程。
1.2 单片机的硬件架构

1.3 存储器映射
CPU是通过地址来访问寄存器、RAM、FLASH、定时器、串口等片上外设。ARM寻址范围4GB,分为多个块:

1.3.1 代码区(CODE区)
ARM公司将0x0000 0000 ~ 0x2000 0000 设置为代码区。
- 0x0000 0000 - 0x002F FFFF:实现存储空间的"重映射",完成程序的跳转。
- 这个区域的作用是实现存储空间的"重映射"。它可以将Main Flash 或厂商预置的 Bootloader 映射到
0x00000000这个起始地址,具体映射哪一个,取决于 BOOT 引脚的配置。 - 单片机上电后,CPU 固定从
0x00000000取指执行;当内存控制器识别到该地址为映射窗口时,会自动将访问请求重定向至对应的物理存储区,从而引导程序执行。
- 这个区域的作用是实现存储空间的"重映射"。它可以将Main Flash 或厂商预置的 Bootloader 映射到
- 0x0800 0000 - 0x082F FFFF:Main Flash区域。该区域是用户烧写程序的地址区域。
- 0x1FFF B000 - 0x1FFF F7FF:厂商BootLoader区域。单片机厂商出厂时预制进去的一段Bootloader程序(出厂时固化)。该程序是完成ISP机制功能。
- ISP机制:借助下载器(如 J-Link 或 STLink),通过芯片的 SWD 引脚,把你编译好的bin文件里的机器码,直接写进 GD32 内部的 Flash 存储器里的。总之,ISP机制就完成两个功能,一是接受二进制镜像文件,二是将二进制镜像烧录到Main Flash中。

1.3.2 SRAM区(内存区)
ARM公司将0x2000 0000 ~ 0x4000 0000 设置为SRAM区。

1.3.2.1 栈空间
函数的代码存放在CODE区中的Main Flash【0x0800 0000~0x082F FFFF】中(下图的左侧),而函数运行时产生的数据(栈帧)存放在 RAM 的栈空间中(下图的右侧)。

(1)栈空间的大小
对于ARM 32位单片机,栈空间最大值是在startupARMCM4.s文件里定义的,0x00000400对应十进制是1024,代表栈空间最大值是1024个字节。

栈的大小可以根据工程师需求自己人为修改。工程的复杂度,以及工程师的代码水平都可能影响栈空间的大小。
1.3.2.2 栈空间的失败案例
cpp
#include <stdio.h>
#include <string.h>
// 1. 定义包含数组的结构体
typedef struct {
char name[50]; // 字符数组
int scores[512]; // 整数数组
int count; // 成绩数量
} Student;
// 2. 值传递(会拷贝整个结构体)
void printByValue(Student s) {
printf("【值传递】姓名: %s, 首个成绩: %d\n", s.name, s.scores[0]);
}
// 3. 指针传递(推荐,只读操作)
void printByPtr(const Student *s) {
printf("【指针传递】姓名: %s, 首个成绩: %d\n", s->name, s->scores[0]);
}
// 4. 指针传递(用于修改数据)
void updateScore(Student *s, int index, int newScore) {
s->scores[index] = newScore; // 直接修改原结构体中的数组元素
}
int main() {
Student stu;
strcpy(stu.name, "Alice");
stu.scores[0] = 85;
stu.count = 1;
printByValue(stu); // 传入结构体副本
printByPtr(&stu); // 传入结构体地址
updateScore(&stu, 0, 100); // 通过指针修改原数据
printf("修改后的首个成绩: %d\n", stu.scores[0]);
return 0;
}
结构体的内存占用说明。
char name[50]= 50 字节int scores[512]= 512 × 4 = 2048 字节int count= 4 字节- 加上内存对齐,总大小大约在 2100 字节左右。
- 进入
main函数(栈空间消耗 ~2100 字节)
当程序进入 main 函数时,编译器会在栈上为局部变量 stu 分配内存。因为结构体里有 512 个 int,所以 stu 会占用约 2100 字节的栈空间。
此时,栈指针(SP)向下移动了 2100 字节。
- 调用
printByValue(stu)(栈空间再次消耗 ~2100 字节)
因为 printByValue 的参数是按值传递 (没有带 * 号),C 语言的规则是:把实参的值原封不动地复制一份给形参 。
在单片机底层,这通常是通过一段汇编指令(比如 LDM/STM 或 memcpy)把 main 栈里的这 2100 字节,硬生生地再拷贝一份 ,压入到 printByValue 函数自己的栈帧中。
此时,栈指针(SP)会继续向下移动 2100 字节。
工程中的栈总共只有 1024 字节,而仅仅传一次参数就需要 2100 字节,这远远超出了栈的极限,必然引发 Stack Overflow(栈溢出)。
1.3.2 片上外设区
第2章 ARM单片机的启动模式
通过对单片机的引脚进行配置,单片机的启动方式大致分为三种:
(1)从0x0800 0000 - 0x082F FFFF:Main Flash方式启动。
(2)从0x1FFF B000 - 0x1FFF F7FF:Bootloader方式启动。
(3)从片上SRAM启动。
(1)和(2)这两种方式我们经常用。

第3章 单片机的复位
在嵌入式单片机(比如你之前看的 GD32、STM32 等 ARM Cortex-M 内核)中,程序的最开始确实是一张"中断向量表"(Interrupt Vector Table) 。这张表本质上是一个**"指针数组"**(里面存放的全是内存地址)。
- 第 0 个位置(地址 0x00000000) :存放的是初始主栈指针(MSP)的值。CPU 上电的第一件事,就是把这个值加载到 SP 寄存器里,从而开辟好内存堆栈空间。
- 第 1 个位置(地址 0x00000004) :存放的是复位中断服务函数(Reset_Handler)的地址 。CPU 初始化好栈之后,就会跳转到这里,开始执行我们之前分析过的启动汇编代码(
SystemInit->__main->main)。 - 后续的位置:依次存放着 NMI、硬件错误(HardFault)、定时器中断、串口中断等各种异常和中断的入口地址。
3.1 中断向量表

3.2 上电复位或系统复位
CPU 上电复位时,它的硬件逻辑被设计为固定去 0x0000 0000 这个地址取数据(读取 MSP 和 PC)。CPU 根本不关心这个地址背后到底是物理内存的哪一块,它只管这个绝对地址。
0x0000 0000 - 0x002F FFFF : Aliased to Main Flash or Boot loader (别名映射)
这就是单片机的"地址映射(Mapping)"机制。
- 物理上:你的中断向量表确实存放在 Main Flash 的最开头,即物理地址
0x0800 0000开始的地方。 - 逻辑上:单片机通过内部硬件电路(内存别名),把物理地址
0x0800 0000"投射"(镜像)到了0x0000 0000。也就是说放访问0x0000 0000地址时,其实是访问了0x08000 0000这个地址。
至此单片机上电复位或系统复位首先需要做的两件事情是:
(1)从地址0x0000 0000处取出MSP的初始值(栈顶地址),用于函数分配内存栈空间;
(2)从地址0x0000 0004处取出PC的初始值,这个值是Reset Handler复位函数的地址,然后从这个地址开始执行程序。

观察并分析上图我们发现:
-
第1个红框,也就是说Main Flash的第1个地址存放的是栈顶地址。栈顶地址为0x20000428。
-
第2个红框,也就是说Main Flash的第2个地址存放的是Reset Handler复位函数的地址 。Reset Handler函数的地址为0x08000145。
-
也就是说Main Flash的第3个地址存放的是NMI_Handler函数的地址 。NMI_Handler函数的地址为0x0800014D。
-
也就是说Main Flash的第4个地址存放的是HardFault_Handler函数的地址 。HardFault_Handler函数的地址为0x0800014F。
-
也就是说Main Flash的第5个地址存放的是MemManage_Handler函数的地址 。MemManage_Handler函数的地址为0x08000151。
-
也就是说Main Flash的第6个地址存放的是BusFault_Handler函数的地址 。BusFault_Handler函数的地址为0x08000153。
依此类推。
3.3 main函数的反汇编
- 0x0800044a 地址里存放是一条"跳转指令(命令),跳转指令要跳转到0x0800018C去执行。
0x0800044a:这是当前main函数所在的地址。0x800018c:这才是真正的延时函数初始化代码(DelayInit)所在的物理地址。- BL指令是跳转去执行完毕之后,要返回来的。
-
0x0800044e 地址里存放是一条"跳转指令(命令),跳转指令要跳转到0x0800020C去执行。
-
0x08000452地址里存放是一条"跳转指令(命令),跳转到0x08000498(即main函数起始地址偏移 78 个字节的位置)。
- 这里使用的是
B指令(普通跳转),意味着它只去不回 。CPU 跳到0x08000498后,就不会再回到0x08000452了,直接进入了主循环。

3.3 存储器映射
再次重复单片机的"地址映射(Mapping)"机制。
- 物理上:你的中断向量表确实存放在 Main Flash 的最开头,即物理地址
0x0800 0000开始的地方。 - 逻辑上:单片机通过内部硬件电路(内存别名),把物理地址
0x0800 0000"投射"(镜像)到了0x0000 0000。也就是说放访问0x0000 0000地址时,其实是访问了0x08000 0000这个地址。
