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 的布局展示出来。你看一眼就明白了。

CMS32 Flash & RAM 内存布局全景图 Flash (128 KB) Bootloader (32 KB) 0x0000 0000 - 0x0000 7FFF App 向量表 (512 B) 0x0000 8000 App 代码区 (80 KB) 0x0000 8200 - 0x0001 81FF 参数存储区 (4 KB) 0x0001 8200 备份区 (剩余) 0x0001 9200+ RAM (16 KB) Bootloader 运行时 RAM 0x2000 0000 - 0x2000 1FFF App 运行时 RAM 0x2000 2000 - 0x2000 3FFF 关键说明: 1. Bootloader 和 App 在 Flash 中物理隔离,地址不重叠 2. RAM 可以分区使用,也可以完全交给 App(Bootloader 运行完后释放) 3. 参数存储区建议放在单独扇区,方便擦除和写入 4. 备份区用于 OTA 失败时的回滚,不是必须但强烈建议保留

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 如何验证分区是否正确?

配置写完了,怎么知道对不对?我一般用三个方法验证:

  1. 看 map 文件:编译完成后,打开 .map 文件,搜索 __region_ROM_start__ 和 __region_ROM_end__,确认地址和你预期的一致。
  2. 读实际烧录内容:用调试器读出 Flash 的前 32KB,看看是不是 Bootloader 的代码。再读出 0x8000 之后的内容,看看是不是 App 的向量表。
  3. 做跳转测试:在 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 就能和平共处。剩下的就是具体功能实现了。