第3章:STM32内存映射与分区设计——Flash与SRAM地址空间、Bootloader与App分区规划、向量表重定向(VTOR)机制

做嵌入式开发,说白了就是在跟内存打交道。你写的每一行代码,最终都要落到某个地址上。我见过不少工程师,代码写得挺溜,一问内存映射就懵了。结果呢?Bootloader跳转失败、中断不响应、程序跑飞……这些坑,我早年都踩过。

这一章,咱们就把STM32的内存布局彻底讲透。尤其是分区设计和向量表重定向,这是Bootloader的核心基础。

3.1 STM32的Flash与SRAM地址空间

先看一张总图,把STM32的“地盘”搞清楚。

STM32F4系列典型内存映射(以F407为例)

  • Flash起始地址:0x0800 0000,大小通常为512KB~1MB
  • SRAM起始地址:0x2000 0000,大小通常为128KB~192KB
  • 系统存储器:0x1FFF 0000,内置Bootloader所在区域
  • 外设寄存器:0x4000 0000开始,GPIO、USART、TIM等都在这里

为什么Flash从0x0800 0000开始?这是STM32的硬件设计决定的。芯片上电后,内核从0x0000 0000取向量表,但实际物理Flash被映射到了这个地址。嗯,这里有个别名映射的机制,后面会细说。

我个人习惯,在工程里把内存分区画成一张表,贴在墙上。你想想看,每次写链接脚本都要查手册,多麻烦。

区域 起始地址 大小 用途
Flash(主存储区) 0x0800 0000 512KB~2MB 代码、常量、初始数据
系统存储器 0x1FFF 0000 30KB 出厂Bootloader
SRAM1 0x2000 0000 112KB 堆、栈、全局变量
SRAM2 0x2001 C000 16KB DMA、USB缓冲等
CCM SRAM 0x1000 0000 64KB CPU直接访问,高速数据

注意:CCM SRAM(0x1000 0000)虽然快,但DMA无法访问它。我在一个音频项目里吃过这个亏——把音频缓冲放在CCM,结果DMA读不到数据,折腾了两天才发现。

3.2 Bootloader与App分区规划

有了内存地图,接下来就是怎么切蛋糕了。Bootloader和App要共存,Flash必须分区。

我常用的分区方案是这样的:

/* Flash分区示例(512KB Flash) */
/* Bootloader区域:0x0800 0000 ~ 0x0800 FFFF (64KB) */
/* App区域:      0x0801 0000 ~ 0x0807 FFFF (448KB) */
/* 参数存储区:    0x0808 0000 ~ 0x0808 3FFF (16KB) */

为什么Bootloader只分64KB?因为Bootloader的功能很纯粹:检查升级标志、擦写Flash、跳转。代码量一般不超过32KB。留点余量就够了,把更多空间留给App。

分区时,有几个原则我建议你记住:

  • Bootloader放在最低地址:因为芯片上电默认从0x0800 0000执行
  • App紧跟Bootloader之后:地址连续,方便跳转
  • 参数区放在最后:用最后一个扇区,擦写次数多也不影响代码
  • 每个分区对齐到扇区边界:STM32的Flash擦除是按扇区来的,不对齐会擦掉别人的代码

实战技巧:我习惯在链接脚本里用符号定义分区边界,比如:

/* STM32F407VG链接脚本片段 */
MEMORY
{
    FLASH_BL (rx)  : ORIGIN = 0x08000000, LENGTH = 64K
    FLASH_APP (rx) : ORIGIN = 0x08010000, LENGTH = 448K
    FLASH_PARAM (r): ORIGIN = 0x08080000, LENGTH = 16K
    RAM (xrw)      : ORIGIN = 0x20000000, LENGTH = 128K
}

这样改分区大小只需要改LENGTH,不用到处翻代码。

3.3 向量表重定向(VTOR)机制

这是Bootloader跳转App时最容易出问题的地方。我曾经在客户现场调试,App就是跑不起来,中断一触发就死机。最后发现是向量表没重定向。

什么是向量表?就是中断服务函数的入口地址表。芯片复位后,从0x0000 0000取栈顶指针,从0x0000 0004取复位向量。但STM32内部做了别名映射,实际读的是0x0800 0004。

当Bootloader跳转到App后,App的向量表在0x0801 0000。如果不告诉CPU“向量表搬家了”,中断来了CPU还是去0x0800 0000找,找到的是Bootloader的向量表,程序就乱了。

解决办法就是VTOR寄存器。它是Cortex-M内核提供的,专门用来设置向量表偏移。

/* 在App启动代码中设置VTOR */
/* 假设App起始地址为0x0801 0000 */
#define APP_BASE_ADDRESS  0x08010000

void SystemInit(void)
{
    /* 设置向量表偏移 */
    SCB->VTOR = APP_BASE_ADDRESS;
    
    /* 其他初始化... */
}

注意:VTOR的值必须对齐到128字节(0x80)边界。因为向量表最多有240个中断,每个4字节,总共960字节,对齐到1KB更保险。我一般直接对齐到扇区大小。

VTOR设置要点:

  • 必须在App启动的最早期设置,通常在SystemInit()里
  • 设置前要确保App的向量表已经烧录到目标地址
  • 如果使用RTOS,还要注意中断向量表是否被RTOS接管
  • 调试时可以在VTOR寄存器里看到当前值,确认是否生效

3.4 完整的内存分区与跳转流程

把上面这些串起来,就是Bootloader跳转App的完整流程。我画了一张图,帮你理清思路。

STM32 Bootloader跳转App完整流程 上电复位 执行Bootloader代码 检查升级标志 有升级请求? (按键/串口命令) 是 执行固件升级 否 跳转到App ① 关闭全局中断 __disable_irq() ② 设置主栈指针 __set_MSP(*(uint32_t*)APP_ADDR) ③ 跳转到复位向量 ((void(*)())app_code)() 接收固件数据 (Ymodem/Xmodem/自定义) 擦写Flash扇区 HAL_FLASHEx_Erase() 校验CRC/校验和 验证通过后清除升级标志

跳转App的代码,我一般这样写:

/* Bootloader跳转函数 */
typedef void (*pFunction)(void);

void JumpToApp(uint32_t appAddress)
{
    uint32_t stackPtr;
    pFunction appEntry;
    
    /* 1. 关闭全局中断 */
    __disable_irq();
    
    /* 2. 关闭所有外设中断(可选,但推荐) */
    for(int i = 0; i < 8; i++) {
        NVIC->ICER[i] = 0xFFFFFFFF;
        NVIC->ICPR[i] = 0xFFFFFFFF;
    }
    
    /* 3. 从App向量表取栈顶指针和复位向量 */
    stackPtr = *(uint32_t*)appAddress;
    appEntry = (pFunction)(*(uint32_t*)(appAddress + 4));
    
    /* 4. 设置主栈指针 */
    __set_MSP(stackPtr);
    
    /* 5. 跳转 */
    appEntry();
    
    /* 不会回到这里 */
}

踩坑记录:我曾经在跳转前没有关闭SysTick中断,结果App启动时SysTick还在跑,中断向量表还没重定向,直接进了HardFault。所以跳转前一定要把所有中断都关了,包括SysTick、PendSV、SVC这些系统中断。

3.5 分区设计的避坑指南

做分区设计时,有几个坑我反复踩过,分享给你:

  • 扇区大小不一致:STM32不同系列、不同容量,扇区大小不一样。F1系列小容量是1KB/扇区,F4系列大容量是128KB/扇区。分区前一定要查手册。
  • Bootloader不能太大:我见过有人把Bootloader做到128KB,结果App只剩64KB。Bootloader只做升级和跳转,别塞太多功能。
  • 参数区要防写坏:Flash擦写次数有限(通常1万次),参数频繁更新时要用磨损均衡算法。我一般用两个备份区,轮流写。
  • App起始地址要固定:一旦产品发布,App的起始地址就不能改了。否则旧版Bootloader跳不到新版App。

我的习惯:在Bootloader里加一个版本号,放在固定地址(比如0x0800 0000 + 0x100)。App启动时读取这个版本号,判断Bootloader是否支持自己。这样即使以后升级Bootloader,也能保证兼容性。