3、内存布局与分区:Flash内存分区设计、Bootloader与应用程序的地址映射、向量表重定向

好,咱们今天聊点硬核的。内存布局,说白了就是给芯片的Flash空间「分房子」。你想想看,一个房子里住着Bootloader、应用程序,有时候还有参数存储区,大家得各住各的,不能串门,更不能打架。我刚开始做Bootloader那会儿,就吃过地址重叠的亏,程序跑着跑着就飞了,查了三天才发现是分区没划清楚。

3.1 Flash内存分区设计

Flash分区,核心就一句话:物理上连续,逻辑上隔离。每个分区要有明确的起始地址和大小,谁也别越界。

我一般把Flash分成这么几块:

  • Bootloader区:存放启动代码,通常64KB~128KB,看芯片。
  • 应用程序区:主程序,剩下的空间基本都给它。
  • 参数存储区:存放标定数据、配置字、故障码等。
  • 备份区(可选):做OTA时放新固件,防止刷死。

举个例子,假设芯片Flash总大小是1MB(0x100000),起始地址0x08000000。我习惯这么分:

分区名称 起始地址 大小 说明
Bootloader 0x08000000 64KB (0x10000) 启动代码,向量表
应用程序 0x08010000 896KB (0xE0000) 主程序
参数区 0x080F0000 64KB (0x10000) 标定数据,掉电保存
注意:分区大小必须是Flash擦除块(Sector/Page)的整数倍。我曾经图省事,随便写了个大小,结果擦除时把隔壁分区给抹了,那叫一个惨。

3.2 Bootloader与应用程序的地址映射

地址映射,说白了就是告诉链接器:Bootloader该放哪儿,应用程序该放哪儿。这得靠链接脚本(Linker Script)来搞定。

我拿STM32的GCC链接脚本举个例子。Bootloader的链接脚本里,Flash起始地址就是0x08000000:

/* Bootloader 链接脚本片段 */
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
  RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 20K
}

应用程序的链接脚本呢?起始地址就得往后挪,挪到0x08010000:

/* 应用程序链接脚本片段 */
MEMORY
{
  FLASH (rx) : ORIGIN = 0x08010000, LENGTH = 896K
  RAM (xrw)  : ORIGIN = 0x20000000, LENGTH = 20K
}

你看,两个程序各自认领自己的地盘。Bootloader编译出来,烧到0x08000000;应用程序编译出来,烧到0x08010000。谁也不碍谁的事。

小技巧:我习惯在应用程序的链接脚本里定义一个宏,比如APP_BASE=0x08010000,这样代码里可以直接引用,方便后期调整分区大小。

3.3 向量表重定向

嗯,这里要注意。芯片上电后,CPU默认从0x08000000取向量表。如果Bootloader跳转到应用程序,但向量表还在原地,那中断一来就乱套了。所以必须做向量表重定向。

说白了,就是告诉CPU:喂,应用程序的向量表在0x08010000,别找错了。

以ARM Cortex-M为例,有个寄存器叫SCB->VTOR(Vector Table Offset Register)。Bootloader跳转前,得把这个寄存器改成应用程序的起始地址:

/* 向量表重定向代码 */
#define APP_VECTOR_TABLE  ((uint32_t)0x08010000)

void JumpToApp(void)
{
    uint32_t appStack = *(volatile uint32_t*)APP_VECTOR_TABLE;
    uint32_t appReset  = *(volatile uint32_t*)(APP_VECTOR_TABLE + 4);

    /* 关闭全局中断,防止跳转过程中断捣乱 */
    __disable_irq();

    /* 设置主栈指针 */
    __set_MSP(appStack);

    /* 重定向向量表 */
    SCB->VTOR = APP_VECTOR_TABLE;

    /* 跳转到应用程序的复位向量 */
    void (*appEntry)(void) = (void (*)(void))appReset;
    appEntry();
}

这段代码我用了好多年,基本没出过问题。但有个坑:跳转前一定要关中断。我遇到过一回,跳转时一个定时器中断正好触发,结果向量表还没切过去,直接跑飞了。

核心要点:
  • 向量表重定向必须在跳转前完成。
  • 跳转前关闭所有外设中断,防止意外触发。
  • 应用程序自己的启动代码里,也要重新设置VTOR,防止二次跳转出错。

3.4 实战中的避坑指南

做内存分区,我踩过的坑不少,挑几个典型的说说:

  • 分区对齐问题:Flash擦除块大小不同,比如有些芯片前几个Sector是16KB,后面是64KB。分区边界必须对齐到Sector边界,否则擦除时会误伤邻居。
  • 参数区放最后:我习惯把参数区放在Flash末尾,这样调整应用程序大小时,参数区地址不用变,省心。
  • 备份区别省:做OTA升级时,没有备份区就等于裸奔。刷到一半断电,你就等着变砖吧。
警告:千万不要把Bootloader和应用程序的向量表放在同一个Sector里。否则擦除应用程序时,Bootloader的向量表也跟着没了,芯片直接变砖。

3.5 内存布局示意图

光说不够直观,我画张图给你看。这张图展示了典型的Flash分区和地址映射关系:

Flash内存布局与地址映射 Bootloader区 0x08000000 - 0x0800FFFF (64KB) 应用程序区 0x08010000 - 0x080EFFFF (896KB) 应用程序向量表 参数存储区 0x080F0000 - 0x080FFFFF (64KB) 0x08000000 0x08010000 0x080F0000 0x08100000

这张图里,Bootloader在最前面,应用程序紧随其后,参数区收尾。应用程序的向量表在0x08010000,跳转前必须把VTOR指向这里。

我的习惯:每次做新项目,我都会先画一张这样的内存布局图,贴在墙上。开发过程中随时对照,避免地址冲突。你也不妨试试。

好了,内存布局这块就聊到这儿。分区设计、地址映射、向量表重定向,这三板斧抡好了,Bootloader的底子就算打牢了。下次咱们聊聊刷写流程,那才是真正见真章的地方。

可可精品资料库