第三章 内存布局与分区策略
大家好,我是老张。做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分成两部分:
- Bootloader专用区:从RAM起始地址开始,大小固定。Bootloader运行期间随便用。
- 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 实战经验总结
做了这么多年,我总结了几条铁律:
- 分区要留余量:Bootloader和App都留20%的余量,别卡着边界设计
- 参数区独立:不要跟代码混在一起,单独一个Sector
- RAM要隔离:Bootloader和App的RAM区域不要重叠
- 跳转前清理:关中断、设VTOR、设SP,一步都不能少
- 写保护要开:Bootloader区硬件写保护,防止意外擦除
最后提醒一句:内存布局不是一次定死的。产品迭代过程中,Bootloader可能会升级,App可能会变大。所以设计初期就要考虑扩展性。我见过太多项目因为内存布局不合理,后期改起来痛不欲生。
好了,这一章就聊到这儿。下一章咱们讲讲刷写协议和通信栈的实现,那才是真正考验功底的地方。