第三章 内存布局与分区策略

大家好,我是老张。做Bootloader这么多年,我最大的体会就是——内存布局搞不好,后面全是坑。今天咱们就聊聊Flash分区设计、RAM使用规划这些事儿。说白了,这就是给ECU画个「户型图」,哪里放代码,哪里存数据,都得提前想清楚。

3.1 Flash分区设计

Flash分区,我习惯把它分成三大块:Bootloader区、App区、参数存储区。你想想看,这三块功能完全不同,混在一起迟早出问题。

典型Flash分区结构(以1MB Flash为例):

地址范围              分区名称        大小
0x00000000-0x00003FFF  Bootloader     16KB
0x00004000-0x00007FFF  参数存储区     16KB  
0x00008000-0x000FFFFF  App区          992KB

我在项目中遇到过,有人把Bootloader和App的参数混放在一起。结果App升级时不小心擦除了Bootloader的配置参数,ECU直接变砖。嗯,这里要注意——Bootloader的参数区一定要独立,最好放在最前面。

3.1.1 Bootloader区设计要点

  • 大小要留余量:我建议至少预留16KB,有些复杂协议栈需要更多空间
  • 写保护:Bootloader区在运行时最好硬件写保护,防止意外篡改
  • 中断向量表:Bootloader有自己的中断向量表,要放在起始地址

3.1.2 App区设计要点

App区是重头戏。我个人习惯把App区再细分成两个Bank,用于A/B切换升级。说白了就是「双备份」——一个Bank跑着,另一个Bank等着被刷写。

小技巧:如果Flash空间紧张,可以只做单Bank升级。但代价是升级过程中ECU完全不可用,而且一旦断电就完蛋。我建议能用双Bank就用双Bank。

3.2 RAM使用规划

RAM规划比Flash更讲究。为什么?因为RAM是运行时用的,Bootloader和App共用同一片RAM,搞不好就冲突了。

RAM区域 用途 大小建议
全局变量区 Bootloader运行时变量 2-4KB
堆栈区 函数调用、中断嵌套 1-2KB
缓冲区 刷写数据临时存储 4-8KB
App保留区 跳转后留给App使用 剩余RAM

我曾经犯过一个错误——Bootloader用了大量全局变量,结果跳转到App时忘了清零,App启动时读到脏数据直接跑飞。后来我学乖了,Bootloader退出前把「自己的」RAM区域全部清零。

3.2.1 RAM分区策略

我建议把RAM分成两部分:

  1. Bootloader专用区:从RAM起始地址开始,大小固定。Bootloader运行期间随便用。
  2. App专用区:从某个地址开始到RAM结束。Bootloader跳转前必须释放这部分。

注意:Bootloader和App的堆栈指针要分开设置。跳转前一定要把SP切到App的堆栈区,否则中断一来就崩了。我见过有人忘了这步,查了三天才找到原因。

3.3 Sector/Page管理

Flash擦写是按Sector(扇区)来的,不是按字节。不同芯片的Sector大小不一样,有的4KB,有的16KB。你想想看,如果App代码刚好跨了两个Sector,升级时就得擦两个Sector,麻烦得很。

3.3.1 Sector对齐原则

  • Bootloader起始地址:必须对齐到Sector边界
  • App起始地址:同样要对齐到Sector边界
  • 参数存储区:最好单独占一个Sector,方便独立擦写

我记得有一次,客户给的芯片Sector大小是8KB,但Bootloader只用了6KB。剩下2KB空着也是空着,我就把参数存进去了。结果后来Bootloader升级,代码膨胀到7KB,参数区被覆盖了。嗯,这就是没留余量的教训。

3.3.2 Page管理

Page是比Sector更小的单位,通常用于编程(写入)。比如一个Sector是4KB,但Page可能是256字节。刷写数据时,按Page写入效率最高。

典型Page写入流程:

1. 接收一帧数据(通常128-256字节)
2. 写入Page缓冲区
3. 触发Page编程命令
4. 等待编程完成(轮询或中断)
5. 校验写入数据是否正确

3.4 Bootloader与App的地址映射

这是最容易出问题的地方。Bootloader和App各自有独立的地址空间,但中断向量表只有一个。怎么办?

3.4.1 中断向量表重映射

现代MCU都支持中断向量表偏移。说白了就是告诉CPU:「别去0x00000000找中断向量了,去0x00008000找吧。」

// 以STM32为例
// Bootloader中设置App的中断向量表偏移
SCB->VTOR = APP_BASE_ADDRESS;  // 0x00008000

// 跳转前一定要做这步
__set_MSP(*(uint32_t*)APP_BASE_ADDRESS);
// 然后跳转到App的复位向量
((void (*)(void))*(uint32_t*)(APP_BASE_ADDRESS + 4))();

避坑指南:我曾经在跳转前忘了关全局中断,结果App的中断向量表还没设好,一个定时器中断进来直接跑飞。正确的做法是:跳转前关中断 → 设置VTOR → 设置SP → 跳转 → App启动后再开中断。

3.4.2 链接脚本配置

地址映射最终要靠链接脚本(Linker Script)来实现。我习惯给Bootloader和App各写一份链接脚本:

/* Bootloader链接脚本片段 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x00000000, LENGTH = 16K
    RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 8K
}

/* App链接脚本片段 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x00008000, LENGTH = 992K
    RAM   (rwx) : ORIGIN = 0x20002000, LENGTH = 8K
}

你想想看,如果App的链接脚本里RAM起始地址写错了,跟Bootloader重叠了,那跳转后App一运行就把Bootloader的变量覆盖了。这种bug查起来特别痛苦。

3.5 实战经验总结

做了这么多年,我总结了几条铁律:

  1. 分区要留余量:Bootloader和App都留20%的余量,别卡着边界设计
  2. 参数区独立:不要跟代码混在一起,单独一个Sector
  3. RAM要隔离:Bootloader和App的RAM区域不要重叠
  4. 跳转前清理:关中断、设VTOR、设SP,一步都不能少
  5. 写保护要开:Bootloader区硬件写保护,防止意外擦除

最后提醒一句:内存布局不是一次定死的。产品迭代过程中,Bootloader可能会升级,App可能会变大。所以设计初期就要考虑扩展性。我见过太多项目因为内存布局不合理,后期改起来痛不欲生。

好了,这一章就聊到这儿。下一章咱们讲讲刷写协议和通信栈的实现,那才是真正考验功底的地方。