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

启动文件干了四件大事:

  1. 分配栈和堆:在SRAM中划出空间,栈顶地址写入向量表第一项。
  2. 定义中断向量表:按固定顺序列出所有中断入口。
  3. 初始化数据段:把Flash里存的初始值复制到SRAM的.data区,把.bss区清零。
  4. 调用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到底干了啥

下面这张图把整个启动流程串起来了。你跟着箭头走一遍,就全明白了。

STM32从Flash启动完整流程 STM32从Flash启动完整流程 ① 上电/复位 ② 从0x08000000加载向量表 ③ SP = 向量表[0] ④ PC = Reset_Handler ⑤ 初始化.data/.bss/SystemInit ⑥ 跳转main() 关键地址 Flash起始: 0x08000000 向量表偏移: SCB->VTOR SRAM起始: 0x20000000 IAP特殊处理 Bootloader先跑 再跳转到App区 App需重设VTOR

整个流程用文字描述就是:

  1. 上电复位:CPU从0x08000000读取栈顶地址,存入SP。
  2. 加载PC:从0x08000004读取复位向量地址,存入PC。
  3. 执行Reset_Handler:启动文件中的汇编代码开始运行。
  4. 数据初始化:把Flash中的.data复制到SRAM,清零.bss。
  5. 调用SystemInit:配置系统时钟、Flash等待周期等。
  6. 跳转main:进入C语言世界,你的代码开始跑。

IAP视角:Bootloader的main()里做完升级判断后,需要手动执行一次类似的跳转——关中断、改VTOR、设置栈指针、跳转到App的Reset_Handler。这个我们会在后面章节详细讲。

2.6 本章小结

这一章我们把STM32的内存布局和启动流程彻底讲透了。你记住几个核心点:

  • Flash放代码,SRAM跑数据,两者地址空间不重叠。
  • 向量表是CPU的导航图,IAP时必须重映射。
  • 启动文件替我们干了脏活累活,但做Bootloader时你得自己控制跳转。
  • 栈大小别抠门,留够余量,否则升级过程中随时可能翻车。