概述
在复杂的嵌入式世界中,程序代码和数据往往分散存储在多种不同的内存芯片里(例如 \(\text{Flash}\)、\(\text{SRAM}\)、外部 \(\text{SDRAM}\) 等)。分散加载(Scatter-Loading)文件,就像一张精准的“内存地图”,告诉链接器(Linker):
- 程序存储在哪里(加载地址,LR): 你的代码、常量、变量的初始值应该被烧录到哪里(通常是 \(\text{Flash}\))。
- 程序在哪里运行(执行地址,ER): 哪些代码和数据将在 \(\text{CPU}\) 运行时被访问和使用(通常是 \(\text{RAM}\) 或支持 XIP 的 \(\text{Flash}\))。
理解这个文件,你就能完全掌握你的程序在芯片中是如何“安家落户”的。
分散加载文件
分散加载文件定义了三个核心层次,它们回答了三个问题:
- 加载区域 (Load Region, LR): 程序“存储”在哪里? — 对应非易失性存储器(\(\text{Flash}\)、\(\text{ROM}\))。
- 执行区域 (Execution Region, ER): 程序“运行”在哪里? — 对应 \(\text{CPU}\) 实际读写的内存位置(\(\text{RAM}\) 或支持 \(\text{XIP}\) 的 \(\text{Flash}\))。
- 输入段 (Input Section): 程序“由什么组成”? — 对应你的目标文件 (*.o) 中的代码段 (\(\text{RO}\))、数据段 (\(\text{RW}\))、未初始化段 (\(\text{ZI}\))。
分散加载文件的语法用 Backus-Naur Form(BNF)语言描述如下:- load_region_name start_address | "+"offset [attributes] [max_size]
- {
- execution_region_name start_address | "+"offset [attributes][max_size]
- {
- module_select_pattern ["("
- ("+" input_section_attr | input_section_pattern)
- ([","] "+" input_section_attr | "," input_section_pattern)) *
- ")"]
- }
- }
复制代码 将这些翻译成白话文就是:
- 定义一个加载区域: 必须指定一个名称和地址(绝对地址 \(\text{start\_address}\) 或者相对偏移 \(\text{"+"offset}\))。可以可选地指定 \(\text{[attributes]}\) 和 \(\text{[max\_size]}\)。
- 定义一个执行区域:必须指定一个名称和地址(绝对地址 \(\text{start\_address}\) 或者 相对偏移 \(\text{"+"offset}\))。可选地指定 \(\text{[attributes]}\) 和 \(\text{[max\_size]}\)。
- 选择输入段:必须指定一个模块选择模式(例如 \(\text{*.o}\))。可选地使用:第一组段(可以是按属性 \(\text{"+" input\_section\_attr}\) 或者 按名称 \(\text{input\_section\_pattern}\))。
- 然后,可以重复零次或多次 \(\text{(*)}\) 第二组段的选择(以可选的逗号 \(\text{[","]}\) 开头,然后选择属性 \(\text{"+" input\_section\_attr}\) 或者 名称 \(\text{input\_section\_pattern}\))。
下面是一个简单的分散加载文件的例子。- LR_ROM 0x08000000 0x00020000 {
- ER_ROM 0x08000000 0x00020000 {
- .ANY (+RO)
- }
- ER_RAM 0x20000000 0x00008000 {
- .ANY (+RW +ZI)
- }
- }
复制代码 在这个分散加载文件中,用户首先定义了一块加载区域,取名为 LR_ROM。程序映像文件烧录到这里,起始地址: 0x08000000, 最大容量: 0x20000 (128 KB)。
第二步,用户针对这一个程序映像定义了一块执行区域,取名为 ER_RAM。这块区域是执行区域,它起始地址和最大容量分别都为 0x08000000 和 0x20000 (128 KB)。只读代码和常量(. ANY (+RO))在这里存放或执行。
第三步,用户针对同一个程序映像还定义了一块执行区域,取名为 ER_RAM。读写变量 (复制) 和未初始化变量 (.ANY (+RW +ZI) 都在初始化的时候从加载区域被搬到了这块执行区域,具体的搬运过程不在本文的讨论范围之内。
存储器介质
现在你已经知道分散加载文件的基本语法了,但是你还需要了解嵌入式系统中不同存储介质的特性。一般来说你的程序代码和初始数据烧录在 Flash (ROM) 里,但你的程序在运行时,所有的变化数据(变量、堆栈)必须在RAM里进行读写。下表是嵌入式系统中常用介质的机制和加载/执行的机制。
方案 / 介质存储特性执行特性采用的加载/执行机制关键设计考量内置 Flash (MCU)非易失,容量有限速度较快,支持 \(\text{XIP}\)XIP (Execute-in-Place) + 分散加载 (\(\text{.data}\) 复制到 \(\text{SRAM}\))启动快,功耗低,但总容量受限。内置 SRAM (MCU/MPU)易失,容量有限速度极快分散加载目标 (用于 \(\text{.data}\), \(\text{.bss}\), \(\text{Stack}\), \(\text{Heap}\))存储关键高速数据和代码。外部 DRAM/SDRAM易失,容量大速度中等,可作为主内存复杂分散加载 或 OS 动态加载容量需求,需要外部总线和内存控制器,引入延迟。TCM易失,容量极小速度最快 (零等待)分散加载目标 (用于中断代码和关键算法)用于保障实时性(Real-Time)和性能关键任务。外置 QSPI/NOR Flash非易失,容量大有限 \(\text{XIP}\)XIP或 Bootloader 复制 (到 DRAM)性能依赖 \(\text{QSPI}\) 接口速度和内部缓存。代码覆盖 \(\text{Overlay}\)N/A (软件机制)N/A (软件机制)动态加载/覆盖克服 RAM 容量限制,但会牺牲性能。存储器介质上的映像加载
下面介绍一些不同的存储器介质上程序映像加载和执行的方案。
模式一:纯 RAM 加载模式
需求/问题:我正在开发一个桌面操作系统的引导程序(Bootloader),或者一个纯粹用于调试的全 \(\text{RAM}\) 运行环境。程序需要快速加载,并且不依赖任何 \(\text{Flash}\) 存储。我怎样才能让程序的所有部分(代码和数据)都在 \(\text{RAM}\) 中连续运行?
这种模式适用于将程序加载到 RAM(例如,操作系统引导加载程序或桌面系统)的系统。
下图由单个加载区域和三个执行区域组成,RO/RW/ZI 在 RAM 中连续存放,无需运行时复制。
模式二:单 ROM + 单 RAM 单应用程序
需求/问题:我使用的是典型的 Cortex-M 系列单片机,我如何实现这个最基本的嵌入式系统启动流程?
这是基于 \(\text{ROM}\) 的嵌入式系统中最常见的配置。 Cortex-M 系列芯片采用是内置 \(\text{Flash}\) (如 \(\text{128KB}\) 到 \(\text{2MB}\)) 和内置 \(\text{SRAM}\) (\(\text{32KB}\) 到 \(\text{512KB}\))。程序利用 \(\text{Flash}\) 的 XIP (Execute-in-Place) 特性,直接在 \(\text{Flash}\) 中运行代码(\(\text{RO}\) 部分)。读写数据(\(\text{RW}\)) 初始值存储在 \(\text{Flash}\) 中,程序启动时,\(\text{C}\) 运行时库 (__main) 会将这部分数据从 \(\text{Flash}\) 复制到 \(\text{RAM}\)。未初始化数据(\(\text{ZI}\)):在 \(\text{RAM}\) 中被清零或分配。
如下图:
- LR_IROM1 0x08000000 0x00010000 { ; 加载区域:64KB Flash (存储程序)
- ER_IROM1 0x08000000 0x00010000 { ; 执行区域:Flash (XIP运行)
- *.o (RESET, +First) ; 选中所有目标文件中的 `RESET` 段,并使用 `+First` 属性,强制它作为 ER 中的第一个段。
- *(InRoot$$Sections) ; 选中所有目标文件中的 C 运行时启动段。这一段必须在 Flash 中执行,负责 RW复制和 ZI 清零。
- .ANY (+RO) ; 选中其余所有的只读代码和常量,链接器会将其放在前面两个关键段之后。 **目的:** 确保 CPU 能够正确启动,并保证 C 环境正确初始化。
- .ANY (+XO) ; 所有仅执行代码
- }
- RW_IRAM1 0x20000000 0x00005000 { ; 执行区域:20KB RAM (数据读写)
- .ANY (+RW +ZI) ; 读写变量(复制) 和 未初始化变量(清零)
- }
- }
复制代码 注:
- RESET, +First:硬件复位后,CPU 会固定从 Flash 首地址读取栈指针 (MSP) 和复位入口 (Reset Handler)。+First 强制链接器把向量表放在这,防止被其他代码占位导致无法启动。
- InRoot$$Sections:这是 C 语言运行时的根代码(包括数据复制算法)。它必须放在根区域(Flash)中,否则 CPU 还没来得及把数据复制到 RAM,就尝试去 RAM 执行代码,会导致崩溃。
模式三:单 ROM + 单 RAM 多应用程序 (Bootloader + App)
需求/问题:我的产品需要支持 OTA(远程升级) 和多分区启动。我需要在 Flash 中划分出一个不可变的 Bootloader 区域,以及一个可擦除更新的 应用程序 (App) 区域。这两个程序如何安全地共享 RAM 并在不同 Flash 地址运行?
采用双映像策略。\(\text{Bootloader}\) 位于 \(\text{Flash}\) 起始地址 (0x08000000),\(\text{App}\) 位于 \(\text{Flash}\) 的偏移地址 (0x08010000)。由于它们分时运行,可以复用同一块 \(\text{RAM}\) 空间。App 启动时,必须修改向量表偏移寄存器 (VTOR),指向 App 在 Flash 中的新起始地址。
这里需要为 Bootloader 和 APP 编写两个独立的 .sct 文件。
1. Bootloader 的 SCT:- LR_BOOT 0x08000000 0x10000 { ; Bootloader 占用前 64KB Flash
- ER_BOOT 0x08000000 0x10000 {
- *.o (RESET, +First) ; 物理复位入口
- *(InRoot$$Sections)
- .ANY (+RO)
- }
- RW_BOOT 0x20000000 0x8000 { ; 使用 RAM
- .ANY (+RW +ZI)
- }
- }
复制代码 2. Application 的 SCT:- ; 注意:加载地址偏移到了 0x08010000
- LR_APP 0x08010000 0x40000 {
- ER_APP 0x08010000 0x40000 {
- *.o (RESET, +First) ; App 的向量表放在 App 区域的开头
- *(InRoot$$Sections)
- .ANY (+RO)
- }
- RW_APP 0x20000000 0x8000 { ; 复用相同的 RAM 地址 (因为 Boot 已停止运行)
- .ANY (+RW +ZI)
- }
- }
复制代码 模式四:多 Flash 区域配置:
需求/问题:内置 \(\text{Flash}\) 容量不足。我如何将体积庞大的只读资源(图片、字库)放在外部 \(\text{Flash}\) (\(\text{0x60000000}\)),同时核心代码仍在内置 \(\text{Flash}\) 运行?
采用混合模式,将核心代码、中断向量表和对实时性要求极高的算法放置在速度最快的 内置 Flash 中运行;而将体积庞大的图片、字库等资源放置在 外置 Flash 中。根据外置 Flash 的特性,这些资源可以通过 XIP 直接读取,或者在需要时加载到外部 SDRAM 中。- ; 主Flash区域 - 应用程序代码
- LR_MAIN_FLASH 0x08000000 0x00080000 {
- ER_VECTOR 0x08000000 0x400 {
- *.o (RESET, +First) ; 中断向量表
- }
-
- ER_CODE 0x08000400 (0x80000-0x400) {
- *(InRoot$$Sections)
- .ANY (+RO)
- .ANY (+XO)
- }
- }
- ; 外部Flash区域 - 资源数据
- LR_EXT_FLASH 0x60000000 0x00400000 {
- ER_RESOURCES 0x60000000 0x400000 {
- resources.o (+RO) ; 专门的资源文件
- fonts.o (+RO)
- images.o (+RO)
- }
- }
- ; 内部RAM
- LR_RAM 0x20000000 0x00020000 {
- RW_DATA 0x20000000 0x1F000 {
- .ANY (+RW +ZI)
- }
-
- ; 预留栈空间
- ARM_LIB_STACK 0x2001F000 EMPTY 0x1000 {
- }
- }
复制代码 模式五:多 RAM 区域配置
需求/问题:我需要利用 \(\text{MCU}\) 内建的多种 \(\text{RAM}\) 速度优势,将实时性要求高的 \(\text{DMA}\) 缓冲区放到最快的 \(\text{CCM}\) \(\text{RAM}\) 中,并将大型 \(\text{GUI}\) 缓冲区分配到外部 \(\text{SDRAM}\)。- LR_IROM1 0x08000000 0x00080000 {
- ER_IROM1 0x08000000 0x00080000 {
- *.o (RESET, +First)
- *(InRoot$$Sections)
- .ANY (+RO)
- .ANY (+XO)
- }
-
- ; 主RAM - 一般数据
- RW_IRAM1 0x20000000 0x00010000 {
- .ANY (+RW +ZI)
- }
-
- ; 快速RAM - 关键变量
- RW_FAST_RAM 0x10000000 0x00004000 {
- fast_data.o (+RW +ZI) ; 指定快速访问的数据
- dma_buffers.o (+RW +ZI) ; DMA缓冲区
- }
-
- ; 外部RAM - 大数据缓冲
- RW_EXT_RAM 0x60000000 0x00800000 {
- large_buffers.o (+RW +ZI)
- heap.o (+RW +ZI)
- }
- }
复制代码 模式六:代码覆盖 (Code Overlay)
需求/问题:我的 \(\text{RAM}\) 空间非常有限,但程序功能模块很多。我需要让几个不常用的模块 \(\text{A}\) 和 \(\text{B}\) 共享同一块 \(\text{RAM}\) 空间,在需要时才将它们加载进来。
当 RAM 资源有限时,可以使用代码覆盖技术。使用 \(\text{OVERLAY}\) 属性。模块 \(\text{A}\) 和 \(\text{B}\) 在 \(\text{Flash}\) 中拥有独立的加载位置,但它们被映射到 \(\text{RAM}\) 中的相同执行地址 (0x20008000)。运行时,程序需要手动将所需的模块从 \(\text{Flash}\) 复制到这个共享的 \(\text{RAM}\) 区域中运行。- LR_FLASH 0x08000000 0x00080000 {
- ER_CODE 0x08000000 0x00080000 {
- *.o (RESET, +First)
- *(InRoot$$Sections)
- main.o (+RO) ; 主程序始终在Flash中
- }
- }
- LR_RAM 0x20000000 0x00010000 {
- ; 覆盖区域1
- OVERLAY_1 0x20008000 OVERLAY 0x2000 {
- module_a.o (+RW +ZI +RO) ; 模块A的代码和数据
- }
-
- ; 覆盖区域2 - 与区域1共享相同地址空间
- OVERLAY_2 0x20008000 OVERLAY 0x2000 {
- module_b.o (+RW +ZI +RO) ; 模块B的代码和数据
- }
-
- ; 通用RAM区域
- RW_DATA 0x20000000 0x8000 {
- .ANY (+RW +ZI)
- }
- }
复制代码 来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |