分散加载(Scatter Loading)是一种由链接器(Linker)提供的、用于精确控制程序各个段(如代码、数据)在目标存储器(如 Flash、RAM)中加载地址和执行地址的机制。其核心在于将单一的、线性的加载视图(Load View)映射到可能不连续且具有不同特性的物理存储空间,形成执行视图(Execution View)。它通过一个称为分散加载描述文件(Scatter Loading Description File,通常为.sct文件)的文本脚本,指导链接器完成这一复杂的内存布局。
分散加载的核心原理:加载域与执行域
分散加载的运作基于两个核心概念:加载域(Load Region)和执行域(Execution Region)。
| 概念 | 描述 | 类比 |
|---|---|---|
| 加载域 | 程序映像(Image)最初被存储(烧录)的存储器区域。一个加载域对应一个物理存储块(如 Flash),具有起始地址和大小。 | 仓库的“货架”,货物(代码/数据)初始存放的位置。 |
| 执行域 | 程序在运行时,其代码或数据实际被访问(执行或读写)的存储器区域。一个加载域可以包含多个执行域,它们可以位于相同或不同的物理地址。 | 工厂的“生产线”或“工作台”,货物被搬移至此以供使用。 |
链接器的工作流程分为两步:
- 加载时:将编译器生成的各个目标文件(
.o)中的输入段(Input Section,如.text,.data)合并,并按照分散加载脚本的指示,放置到指定的加载域地址,生成最终的二进制映像文件(如.axf,.bin)。 - 运行时(启动时):在
main()函数执行前,由启动代码(Startup Code)中的__main函数(C库初始化例程)负责将需要移动的数据(如已初始化的全局变量.data段)从其加载地址(如 Flash)拷贝到其执行地址(如 RAM),并将未初始化的静态变量区域(.bss段)在 RAM 中清零。对于代码段(.text)和只读数据段(.rodata),如果其加载地址和执行地址相同(通常在 Flash 中原地执行,XIP),则无需移动。
分散加载描述文件(.sct)语法详解
以 ARM Compiler(Keil MDK)为例,一个.sct文件的基本结构如下:
LR_IROM1 0x08000000 0x00010000 { ; 定义一个加载域 (Load Region) ER_IROM1 0x08000000 0x00010000 { ; 定义一个执行域 (Execution Region) *.o (RESET, +First) ; 输入节描述,将RESET段放在最前 *(InRoot$$Sections) ; 特殊的库段,必须放在根域 .ANY (+RO) ; 所有RO(只读,代码和常量)内容 } RW_IRAM1 0x20000000 0x00005000 { ; 另一个执行域,位于RAM .ANY (+RW +ZI) ; 所有RW(已初始化变量)和ZI(零初始化变量)内容 } }关键组成部分解析:
加载域定义:
LR_IROM1 0x08000000 0x00010000LR_IROM1:加载域名,可自定义。0x08000000:加载域的起始地址(STM32 Flash 起始地址)。0x00010000:加载域的最大长度(64KB)。
执行域定义:
ER_IROM1 0x08000000 0x00010000- 嵌套在加载域内,定义了代码/数据在运行时的位置。
- 起始地址和长度属性与加载域可以不同。如果省略地址,链接器会自动安排在加载域之后;如果省略长度,则使用加载域的剩余空间。
输入节描述(模块选择模式):
*.o (RESET, +First):选择所有目标文件中的RESET段,并使用+First属性强制将其放置在该执行域的首位。这通常用于放置中断向量表。*(InRoot$$Sections):这是一个必须包含在根执行域(地址固定的执行域)的特殊符号集合,包含了 C 库初始化等关键代码。.ANY (+RO):选择所有未被前面模式匹配的模块中的只读(RO)段。+RO是属性选择器,代表.text(代码)和.rodata(只读数据)。+RW和+ZI:分别代表已初始化的读写数据段和零初始化数据段,它们通常需要被分配到 RAM 中的执行域。
分散加载的主要应用场景与用法示例
1. 配合 Bootloader 实现应用程序重定位
这是最经典的应用。Bootloader 固定在 Flash 起始地址,用户应用程序(APP)需要从后续地址开始存放。这需要修改 APP 工程的分散加载文件。
APP 的分散加载文件 (app.sct) 示例:
/* 假设 Bootloader 占用 0x08000000 - 0x08003FFF (16KB) */ LR_IROM1 0x08004000 0x0000C000 { ; APP加载域从0x08004000开始,长度48KB ER_IROM1 0x08004000 0x0000C000 { ; 执行域地址与加载域相同(XIP) *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RAM域不变 .ANY (+RW +ZI) } }同时,在 APP 的main()函数开始,需要重设中断向量表偏移寄存器(如 ARM Cortex-M 的SCB->VTOR)为0x08004000,确保中断能正确跳转到新的向量表。
2. 将关键代码或数据加载到高速内存(如 CCM RAM、TCM)执行
为了提升性能,可以将对实时性要求极高的函数(如中断服务程序、算法核心循环)放到零等待周期的 SRAM(如 STM32 的 CCM RAM)中执行。
实现步骤:
方法一:通过分散加载文件指定
LR_IROM1 0x08000000 0x00010000 { ER_IROM1 0x08000000 0x00010000 { .ANY (+RO) ; 大部分代码放在Flash } ER_CCM 0x10000000 0x00001000 { ; CCM RAM执行域,起始地址0x10000000 my_fast.o (+RO) ; 将my_fast.c文件中的所有代码段放到CCM *(.FastSection) ; 或者,将所有放在.FastSection段的内容放到CCM } RW_IRAM1 0x20000000 0x00005000 { .ANY (+RW +ZI) } }在 C 代码中,需要使用
__attribute__将特定函数或变量分配到自定义段。// 将函数放到名为 `.FastSection` 的段中 __attribute__((section(".FastSection"))) void critical_isr(void) { // 关键中断处理代码 }方法二:在代码中直接指定绝对地址(不推荐,灵活性差)
// 将变量定位到特定地址 uint32_t __attribute__((at(0x20001000))) high_speed_buffer[1024];
3. 实现非连续存储器的利用
当芯片具有多块不连续的物理内存时(如两块独立的 SRAM:SRAM1 和 SRAM2),分散加载可以高效地管理它们。
LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 0x00100000 { .ANY (+RO) } RW_IRAM1 0x20000000 0x00010000 { ; SRAM1 .ANY (+RW +ZI) ; 默认分配到这里 } RW_IRAM2 0x20010000 0x00008000 { ; SRAM2 large_buffer.o (+RW) ; 将某个模块的大缓冲区单独放到SRAM2 } }4. 将函数或变量固定在特定地址
例如,需要在两个独立的应用程序(如 Bootloader 和 APP)之间共享一段数据,或者为某个特定的函数提供固定的入口地址以供调用。
LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 { .ANY (+RO) } ER_SHARED 0x0800FC00 FIXED 0x400 { ; FIXED属性固定该执行域的地址和大小 shared_data.o (+RW) ; 共享数据模块 } RW_IRAM1 0x20000000 0x00010000 { .ANY (+RW +ZI) } }使用FIXED属性可以确保该执行域的地址不会被链接器优化或移动。在 C 代码中,对应的变量或函数通过自定义段名被引导至此。
在 Keil MDK 中的配置步骤
- 打开
Options for Target -> Linker选项卡。 - 取消勾选
Use Memory Layout from Target Dialog。 - 在
Scatter File输入框中,选择或输入自定义的.sct文件路径。 - 重新编译工程,链接器将依据此文件进行链接。
总结与注意事项
分散加载是进行复杂内存管理、性能优化和实现高级功能(如 Bootloader、双系统)的基石。其本质是通过脚本将程序的逻辑段与物理存储的地址和特性进行解耦和精确映射。在使用时,必须清晰理解加载域与执行域的区别,以及启动时代码和数据的搬移过程。对于简单的项目,IDE 默认的链接脚本通常足够;但当项目涉及多存储区域、性能关键代码、固件升级(IAP)或特定地址约束时,掌握并编写分散加载文件就成为嵌入式开发者的必备技能。调试时,可以通过生成的.map文件来验证各段的最终布局是否符合预期。
参考来源
- ARM Cortex-M底层技术(六)分散加载的简单介绍
- 分散加载的简单介绍
- STM32 分散加载
- ARM Cortex-M底层技术(十三)手把手教你写分散加载
- MDK 分散加载文件剖析(一)
- 分散加载的实现