第三章 内存布局与链接脚本: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画好地盘。地界画清楚了,后面的事就顺了。下一章咱们讲升级流程设计,那才是真正动刀子的地方。