4. 链接脚本与内存布局:分散加载文件配置与分区规划
说到 Bootloader,第一个绕不开的就是内存怎么划分。很多新手拿到 CMS32 芯片,上来就写代码,结果 Bootloader 和 App 互相踩踏,或者跳转失败。说白了,就是没把链接脚本吃透。
我个人习惯,做任何带 Bootloader 的项目,第一件事不是写代码,而是先把 .icf 或 .sct 文件搞定。这就像盖房子先画图纸,图纸错了,后面全是白费功夫。
4.1 分散加载文件是什么?
CMS32 系列用的 IAR 环境,链接脚本是 .icf 文件;如果你用 Keil,那就是 .sct 文件。名字不同,干的活一样——告诉编译器:你的代码放哪,数据放哪,堆栈放哪。
你想想看,MCU 的 Flash 和 RAM 就那么大一块。Bootloader 和 App 要共存,就得把这块地皮划清楚。谁占哪个区间,边界在哪,都得在链接脚本里写死。
核心原则:Bootloader 和 App 的地址空间必须完全隔离,不能有任何重叠。哪怕重叠一个字节,都可能造成灾难性后果。
4.2 Bootloader 与 App 分区规划
我在项目中遇到过好几次,客户把 Bootloader 分区设得太小,结果功能迭代几次就塞不下了。所以分区规划一定要留余量。
以 CMS32 常见的 128KB Flash 为例,我一般这样划分:
| 分区 | 起始地址 | 大小 | 内容 |
|---|---|---|---|
| Bootloader | 0x0000 0000 | 32 KB | 启动代码、升级协议、Flash 驱动 |
| App 向量表 | 0x0000 8000 | 512 B | App 的中断向量表(可重映射) |
| App 代码区 | 0x0000 8200 | 80 KB | 应用程序主体 |
| 参数存储区 | 0x0001 8200 | 4 KB | 升级标志、配置参数 |
| 备份区(可选) | 0x0001 9200 | 剩余空间 | App 备份,用于失败回滚 |
注意:CMS32 的中断向量表默认在 0x0000 0000。App 启动后必须通过 SCB->VTOR 寄存器重映射到 App 自己的向量表地址。这个操作必须在 App 的 main() 第一行完成,否则任何中断都会跑回 Bootloader。
4.3 IAR 下 .icf 文件配置实战
嗯,这里要注意。.icf 文件的语法看着简单,但坑不少。我直接给一个我常用的模板,你照着改就行。
// Bootloader 的 .icf 文件
define symbol __ICFEDIT_region_ROM_start__ = 0x00000000;
define symbol __ICFEDIT_region_ROM_end__ = 0x00007FFF; // 32KB
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x20003FFF; // 16KB
define region ROM_region = mem:[from __ICFEDIT_region_ROM_start__
to __ICFEDIT_region_ROM_end__];
define region RAM_region = mem:[from __ICFEDIT_region_RAM_start__
to __ICFEDIT_region_RAM_end__];
place in ROM_region { readonly section .text };
place in RAM_region { readwrite, block HEAP, block CSTACK };
App 的 .icf 就要把起始地址偏移到 0x8000:
// App 的 .icf 文件
define symbol __ICFEDIT_region_ROM_start__ = 0x00008000;
define symbol __ICFEDIT_region_ROM_end__ = 0x0001FFFF; // 96KB
define symbol __ICFEDIT_region_RAM_start__ = 0x20000000;
define symbol __ICFEDIT_region_RAM_end__ = 0x20003FFF;
// 注意:向量表要放在最前面
place at address mem:0x00008000 { readonly section .intvec };
我的经验:App 的 .icf 里一定要用 place at address 把 .intvec 段固定在分区起始地址。否则编译器可能把向量表放到别处,导致中断重映射失败。我曾经因为这个 bug 调了整整两天。
4.4 Keil 下 .sct 文件配置
如果你用 Keil,.sct 的写法稍微不同,但逻辑一样。Bootloader 的 .sct:
; Bootloader 分散加载文件
LR_IROM1 0x00000000 0x00008000 { ; 加载区,32KB
ER_IROM1 0x00000000 0x00008000 { ; 执行区
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00004000 { ; RAM区,16KB
.ANY (+RW +ZI)
}
}
App 的 .sct:
; App 分散加载文件
LR_IROM1 0x00008000 0x00018000 { ; 加载区,96KB
ER_IROM1 0x00008000 0x00018000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
}
RW_IRAM1 0x20000000 0x00004000 {
.ANY (+RW +ZI)
}
}
4.5 内存布局全景图
光说数字可能不够直观。我画了一张图,把整个 Flash 和 RAM 的布局展示出来。你看一眼就明白了。
4.6 避坑指南:我踩过的那些坑
做 Bootloader 分区,有几个坑我几乎每次培训都要讲一遍。因为我自己都栽过不止一次。
- 向量表对齐问题:CMS32 要求向量表必须按 512 字节对齐。如果你把 App 起始地址设在 0x8100,那完了,中断一个都进不去。我曾经犯过这个错,查了三天才发现是地址没对齐。
- RAM 重叠:Bootloader 和 App 共用 RAM 时,跳转前一定要把 Bootloader 用的全局变量清零,或者把栈指针重新初始化。否则 App 一启动,栈里还残留着 Bootloader 的数据,跑着跑着就飞了。
- Flash 扇区边界:CMS32 的 Flash 擦除是按扇区来的。分区时一定要让分区边界和扇区边界对齐。比如扇区大小是 4KB,那分区起始地址必须是 4KB 的整数倍。否则你擦除 App 区时,可能把 Bootloader 的尾巴也擦掉了。
- 链接脚本里忘了加 .intvec:这个我前面提过。App 的 .icf 或 .sct 里,必须显式把中断向量表固定在分区头部。编译器默认会把它放在最前面,但如果你用了复杂的 section 布局,它可能被优化掉。
血的教训:有一次我在一个量产项目上,因为 .icf 里少写了一行 place at address,导致 2000 台设备升级后全部变砖。后来不得不派人一台台拆机用烧录器恢复。从那以后,我每次改链接脚本都要做三件事:编译后看 map 文件确认地址、用 Hex 工具查看实际烧录内容、在开发板上做 100 次升级测试。
4.7 如何验证分区是否正确?
配置写完了,怎么知道对不对?我一般用三个方法验证:
- 看 map 文件:编译完成后,打开 .map 文件,搜索
__region_ROM_start__和__region_ROM_end__,确认地址和你预期的一致。 - 读实际烧录内容:用调试器读出 Flash 的前 32KB,看看是不是 Bootloader 的代码。再读出 0x8000 之后的内容,看看是不是 App 的向量表。
- 做跳转测试:在 Bootloader 里写一个函数,直接跳转到 App 的复位向量地址。如果跳转后 App 正常运行,说明分区没问题。
// 跳转测试代码示例
void jump_to_app(void)
{
uint32_t app_reset_addr;
// 关闭全局中断
__disable_irq();
// 从 App 向量表取出复位向量
app_reset_addr = *(volatile uint32_t*)(APP_START_ADDR + 4);
// 重映射中断向量表到 App
SCB->VTOR = APP_START_ADDR;
// 设置主栈指针
__set_MSP(*(volatile uint32_t*)APP_START_ADDR);
// 跳转
((void (*)(void))app_reset_addr)();
}
小技巧:跳转前记得把外设都复位一遍。尤其是定时器、UART、ADC 这些,如果不复位,App 初始化时可能读到奇怪的状态。我习惯在跳转前调用 HAL_RCC_DeInit() 或者直接操作寄存器把所有外设时钟关掉。
好了,链接脚本和内存布局这块,核心就是这些。你只要把分区规划好,.icf/.sct 配正确,向量表重映射做对,Bootloader 和 App 就能和平共处。剩下的就是具体功能实现了。