第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的完整流程。我画了一张图,帮你理清思路。
跳转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,也能保证兼容性。