第三章 内存布局与链接脚本:Flash与RAM分区规划

好,咱们进入第三章。这一章说实话,是Bootloader开发里最容易被忽视、但出事最要命的部分。我见过太多工程师,功能逻辑写得飞起,结果一链接就崩,或者下载后直接变砖。为什么?内存布局没搞对。

说白了,Bootloader和应用程序要共存于一颗芯片里。它们各自占哪块Flash,用哪段RAM,中断向量表怎么切——这些必须在链接脚本里定死。今天我就把这块掰开揉碎了讲清楚。

3.1 Flash分区规划:谁住哪里

先看Flash。一颗典型的MCU,比如Infineon TC3xx或者NXP S32K,Flash通常从0x00000000开始。Bootloader和APP怎么分?

我个人习惯这样分:

  • Bootloader区:放在最低地址,比如0x00000000 ~ 0x0001FFFF(128KB)。
  • 应用程序区:紧跟着Bootloader,比如0x00020000开始。
  • 参数存储区:单独划一块,存放校准数据、升级标志等。

你可能会问:为什么Bootloader一定要放最低地址?因为芯片复位后,PC指针默认指向复位向量,而复位向量通常就在Flash起始位置。这是硬件决定的,改不了。

关键点:Bootloader和APP的Flash分区必须连续,且不能重叠。中间留一点空隙?可以,但浪费空间。我建议精确计算。

我在项目中遇到过一个问题:某团队把Bootloader放在0x00000000,APP放在0x00010000,中间留了64KB。结果APP编译后体积刚好超过64KB,直接覆盖到Bootloader区。嗯,那次刷了十几台样机,全变砖。后来我强制在链接脚本里加了区域边界检查。

3.2 RAM分区规划:谁用哪块

RAM比Flash更敏感。为什么?因为Bootloader和APP都可能用到RAM,但启动时不能互相踩踏。

我一般这样规划:

  • Bootloader专用RAM:比如从0x1FFF0000开始,大小16KB。存放Bootloader的栈、全局变量。
  • APP专用RAM:从0x1FFF4000开始,大小48KB。APP自己用。
  • 共享RAM区:很小一块,比如0x1FFFE000开始的4KB。用于传递升级命令、状态标志。

你想想看,如果Bootloader和APP共用同一段RAM,APP跑起来后把Bootloader的变量覆盖了,下次跳回Bootloader时直接崩溃。这种事我见过不止一次。

注意:共享RAM区必须定义明确的数据结构,双方用同一个头文件。我曾经见过两边对同一个标志位的偏移理解不一致,结果升级指令传过去,APP以为是擦除命令,直接把自己擦了……

3.3 链接脚本编写要点

链接脚本,说白了就是告诉链接器:这段代码放哪,那段数据放哪。以GCC的.ld文件为例,我直接给个典型结构:

MEMORY
{
    FLASH_BL (rx)  : ORIGIN = 0x00000000, LENGTH = 128K
    FLASH_APP (rx) : ORIGIN = 0x00020000, LENGTH = 896K
    RAM_BL (rw)    : ORIGIN = 0x1FFF0000, LENGTH = 16K
    RAM_APP (rw)   : ORIGIN = 0x1FFF4000, LENGTH = 48K
    RAM_SHARED (rw): ORIGIN = 0x1FFFE000, LENGTH = 4K
}

SECTIONS
{
    .text :
    {
        *(.vectors)
        *(.text*)
    } > FLASH_BL

    .data :
    {
        *(.data*)
    } > RAM_BL AT > FLASH_BL

    .bss :
    {
        *(.bss*)
    } > RAM_BL
}

这里有几个要点:

  • 向量段(.vectors)必须放在Flash起始位置。这是硬性要求。
  • .data段要同时指定运行地址(VMA)和加载地址(LMA)。运行时在RAM,但初始值存在Flash里。
  • .bss段只指定运行地址,启动时清零。

我刚开始写链接脚本时,总搞不清VMA和LMA的区别。有一次把.data的加载地址写错了,结果变量初始值全是乱的,程序跑起来跟鬼畜一样。后来我养成了一个习惯:每次改链接脚本,先反汇编确认地址对不对。

小技巧:在链接脚本里加一个ASSERT语句,检查各区域是否重叠。比如:ASSERT( SIZEOF(.text) <= 128K, "Bootloader too big!" )

3.4 向量表重映射

这是Bootloader的核心机制之一。芯片复位后,默认从Flash起始地址取向量表。但APP不在起始地址啊,怎么办?

答案是:修改向量表偏移寄存器。以ARM Cortex-M为例,有个SCB->VTOR寄存器。Bootloader在跳转到APP之前,必须把这个寄存器改成APP的向量表基址。

代码示例:

/* 假设APP向量表在0x00020000 */
#define APP_VECTOR_TABLE 0x00020000

void jump_to_app(void)
{
    uint32_t app_sp;    /* 栈指针 */
    uint32_t app_pc;    /* 程序计数器 */

    /* 1. 从APP向量表取栈指针和复位向量 */
    app_sp = *(volatile uint32_t*)APP_VECTOR_TABLE;
    app_pc = *(volatile uint32_t*)(APP_VECTOR_TABLE + 4);

    /* 2. 修改向量表偏移 */
    SCB->VTOR = APP_VECTOR_TABLE;

    /* 3. 关闭全局中断 */
    __disable_irq();

    /* 4. 设置主栈指针 */
    __set_MSP(app_sp);

    /* 5. 跳转 */
    ((void (*)(void))app_pc)();
}

这里有个坑:修改VTOR之后,所有中断都会走新的向量表。如果Bootloader和APP用了不同的中断处理函数,跳转前必须确保所有外设中断已关闭。我曾经没关定时器中断,跳转后APP还没初始化,定时器先触发了,直接跑飞到异常处理里。

重要:跳转前还要清理外设状态。比如DMA正在传输、CAN正在收发,这些都要停下来。否则APP接管后,外设可能处于未知状态。

另外,有些MCU支持双向量表——就是Bootloader和APP各有一套向量表,通过硬件机制切换。但大多数芯片还是单向量表,需要软件重映射。我个人更倾向于软件方式,因为可控性更强。

3.5 避坑指南

最后,我总结几个实战中容易踩的坑:

  • 链接脚本里忘了放向量段:结果芯片复位后找不到复位向量,直接死机。我曾经帮同事排查过这个问题,查了两天才发现是.vectors段被优化掉了。
  • RAM地址写死:有些工程师在代码里硬编码RAM地址,换了芯片型号就崩。我建议用链接脚本导出的符号,比如&_sdata、&_ebss。
  • 跳转前没关中断:这个前面说了,后果很严重。我建议在跳转函数里加一个循环,确认所有中断标志都清了再跳。
  • 向量表对齐问题:有些MCU要求向量表必须按256字节或512字节对齐。VTOR寄存器有对齐掩码,没对齐的话,写进去的值会被截断。

嗯,这一章内容不少。内存布局和链接脚本,说白了就是给Bootloader和APP画好地盘。地界画清楚了,后面的事就顺了。下一章咱们讲升级流程设计,那才是真正动刀子的地方。