2. STM32内存架构与启动流程:Flash与RAM分区、中断向量表、启动文件分析、堆栈设置、从Flash启动的完整流程
做IAP Bootloader,说白了就是跟内存打交道。你得清楚芯片上电后干了什么,代码放在哪,数据又往哪跑。这一章我们就把STM32的内存架构和启动流程彻底捋一遍。我当年刚接触IAP时,就是没搞懂向量表重映射,结果程序死活跳不过去——嗯,这种坑咱们今天一并填上。
2.1 Flash与RAM分区:你的代码和数据住哪儿
STM32的存储器映射是固定的。以F103为例,Flash从0x08000000开始,SRAM从0x20000000开始。你想想看,Bootloader和App都挤在Flash里,怎么分家?
| 区域 | 起始地址 | 大小 | 用途 |
|---|---|---|---|
| Flash(主存储区) | 0x08000000 | 芯片型号决定(如512KB) | 存放代码、常量、初始数据 |
| Flash(系统存储区) | 0x1FFFF000 | 约2KB | 出厂Bootloader(串口ISP) |
| SRAM | 0x20000000 | 芯片型号决定(如64KB) | 运行时数据、堆栈、全局变量 |
| 外设寄存器 | 0x40000000起 | — | GPIO、USART、TIM等 |
关键点:Flash是只读的(运行时),SRAM是读写的。IAP升级的本质就是把新固件写入Flash,然后跳过去执行。
我个人习惯把Flash分区规划成三块:
- Bootloader区:0x08000000 ~ 0x0800FFFF(64KB),存放IAP程序。
- App区:0x08010000 ~ 0x0807FFFF(448KB),存放用户固件。
- 参数区:Flash末尾留几个扇区,存升级标志、CRC校验值。
为什么Bootloader要放在最前面?因为芯片复位后永远从0x08000000取向量表。你没法改这个起点,只能让Bootloader先跑,再由它跳转到App。
2.2 中断向量表:CPU的紧急联络簿
中断向量表,说白了就是一张地址表。每个中断源对应一个入口地址,CPU响应中断时自动从这里加载PC指针。
这张表必须放在Flash的起始位置(0x08000000)。表的前四个字尤其重要:
| 偏移 | 内容 | 说明 |
|---|---|---|
| 0x0000 | 栈顶地址(MSP初始值) | 复位后SP自动加载此值 |
| 0x0004 | 复位向量(Reset_Handler) | 复位后PC跳转到这里 |
| 0x0008 | NMI中断入口 | 不可屏蔽中断 |
| 0x000C | HardFault入口 | 硬件错误处理 |
注意:做IAP时,App的向量表不能放在0x08000000了,因为那里被Bootloader占了。你必须把App的向量表搬到偏移地址,比如0x08010000,然后在App启动代码里设置SCB->VTOR寄存器。我曾经见过有人忘了这步,中断一触发就跑飞了——查了两天才找到原因。
2.3 启动文件分析:谁在main之前干活
启动文件(startup_stm32f10x_hd.s)是汇编写的,它负责在main()之前把CPU伺候好。我拆开给你看:
; 1. 定义栈大小
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; 2. 定义堆大小
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
__heap_limit
; 3. 中断向量表
AREA RESET, DATA, READONLY
DCD __initial_sp ; 栈顶地址
DCD Reset_Handler ; 复位向量
DCD NMI_Handler
DCD HardFault_Handler
; ... 其他中断向量 ...
; 4. 复位处理函数
Reset_Handler PROC
EXPORT Reset_Handler
; 初始化.data段(从Flash复制到SRAM)
; 清零.bss段
; 调用SystemInit()
; 跳转main()
ENDP
启动文件干了四件大事:
- 分配栈和堆:在SRAM中划出空间,栈顶地址写入向量表第一项。
- 定义中断向量表:按固定顺序列出所有中断入口。
- 初始化数据段:把Flash里存的初始值复制到SRAM的.data区,把.bss区清零。
- 调用SystemInit和main:配置时钟,然后进入C世界。
我的经验:做IAP时,App的启动文件需要改两个地方:一是向量表偏移(通过修改链接脚本实现),二是栈大小要留够。我一般给App的栈开到0x800(2KB),Bootloader的栈0x400就够了。
2.4 堆栈设置:别让局部变量撑破天
堆和栈都在SRAM里,但管理方式完全不同:
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配方式 | 自动(函数调用/返回) | 手动(malloc/free) |
| 生长方向 | 向下(高地址→低地址) | 向上(低地址→高地址) |
| 典型用途 | 局部变量、函数参数、返回地址 | 动态分配的大块数据 |
| 风险 | 栈溢出(静默崩溃) | 内存泄漏、碎片化 |
为什么会发生栈溢出?你想想看,函数嵌套太深,或者局部数组太大(比如char buf[1024]),栈指针一路往下跑,跑到堆的区域甚至越界到全局变量区——程序就乱套了。
我个人习惯在启动文件里把栈设大一点,然后在代码里加栈溢出检测:
// 在main()开头检查栈指针是否低于安全阈值
extern uint32_t __initial_sp;
void check_stack(void) {
uint32_t current_sp = __get_MSP();
if (current_sp < (uint32_t)&__initial_sp - 0x200) {
// 栈空间不足200字节,报警
Error_Handler();
}
}
避坑指南:我曾经在一个项目中把栈设得太小,结果Bootloader接收固件时用了大缓冲区,直接栈溢出覆盖了全局变量。升级到一半系统复位,变砖了。从那以后,我所有IAP工程的栈都至少留1KB余量。
2.5 从Flash启动的完整流程:上电后CPU到底干了啥
下面这张图把整个启动流程串起来了。你跟着箭头走一遍,就全明白了。
整个流程用文字描述就是:
- 上电复位:CPU从0x08000000读取栈顶地址,存入SP。
- 加载PC:从0x08000004读取复位向量地址,存入PC。
- 执行Reset_Handler:启动文件中的汇编代码开始运行。
- 数据初始化:把Flash中的.data复制到SRAM,清零.bss。
- 调用SystemInit:配置系统时钟、Flash等待周期等。
- 跳转main:进入C语言世界,你的代码开始跑。
IAP视角:Bootloader的main()里做完升级判断后,需要手动执行一次类似的跳转——关中断、改VTOR、设置栈指针、跳转到App的Reset_Handler。这个我们会在后面章节详细讲。
2.6 本章小结
这一章我们把STM32的内存布局和启动流程彻底讲透了。你记住几个核心点:
- Flash放代码,SRAM跑数据,两者地址空间不重叠。
- 向量表是CPU的导航图,IAP时必须重映射。
- 启动文件替我们干了脏活累活,但做Bootloader时你得自己控制跳转。
- 栈大小别抠门,留够余量,否则升级过程中随时可能翻车。