找回密码
 立即注册
首页 业界区 业界 嵌入式系统内存魔法之分散加载

嵌入式系统内存魔法之分散加载

梅克 3 小时前
1.png
概述

在复杂的嵌入式世界中,程序代码和数据往往分散存储在多种不同的内存芯片里(例如 \(\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)语言描述如下:
  1. load_region_name  start_address | "+"offset  [attributes] [max_size]
  2. {
  3.     execution_region_name  start_address | "+"offset  [attributes][max_size]
  4.     {
  5.         module_select_pattern  ["("
  6.                                     ("+" input_section_attr | input_section_pattern)
  7.                                     ([","] "+" input_section_attr | "," input_section_pattern)) *
  8.                                ")"]
  9.     }
  10. }
复制代码
将这些翻译成白话文就是:

  • 定义一个加载区域: 必须指定一个名称和地址(绝对地址 \(\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}\))。
下面是一个简单的分散加载文件的例子。
  1. LR_ROM 0x08000000 0x00020000  {
  2.     ER_ROM 0x08000000 0x00020000  {
  3.         .ANY (+RO)
  4.     }
  5.     ER_RAM 0x20000000 0x00008000  {
  6.         .ANY (+RW +ZI)
  7.     }
  8. }
复制代码
在这个分散加载文件中,用户首先定义了一块加载区域,取名为 LR_ROM。程序映像文件烧录到这里,起始地址: 0x08000000, 最大容量: 0x20000 (128 KB)。
第二步,用户针对这一个程序映像定义了一块执行区域,取名为 ER_RAM。这块区域是执行区域,它起始地址和最大容量分别都为 0x080000000x20000 (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 中连续存放,无需运行时复制。
2.png
模式二:单 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}\) 中被清零或分配。
如下图:
3.png
  1. LR_IROM1 0x08000000 0x00010000  {    ; 加载区域:64KB Flash (存储程序)
  2.   ER_IROM1 0x08000000 0x00010000  {  ; 执行区域:Flash (XIP运行)
  3.    *.o (RESET, +First)               ; 选中所有目标文件中的 `RESET` 段,并使用 `+First` 属性,强制它作为 ER 中的第一个段。
  4.    *(InRoot$$Sections)               ; 选中所有目标文件中的 C 运行时启动段。这一段必须在  Flash 中执行,负责 RW复制和 ZI 清零。
  5.    .ANY (+RO)                        ; 选中其余所有的只读代码和常量,链接器会将其放在前面两个关键段之后。 **目的:** 确保 CPU 能够正确启动,并保证 C 环境正确初始化。
  6.    .ANY (+XO)                        ; 所有仅执行代码
  7.   }
  8.   RW_IRAM1 0x20000000 0x00005000  {  ; 执行区域:20KB RAM (数据读写)
  9.    .ANY (+RW +ZI)                    ; 读写变量(复制) 和 未初始化变量(清零)
  10.   }
  11. }
复制代码
注:

  • 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:
  1. LR_BOOT 0x08000000 0x10000 {         ; Bootloader 占用前 64KB Flash
  2.     ER_BOOT 0x08000000 0x10000 {
  3.         *.o (RESET, +First)          ; 物理复位入口
  4.         *(InRoot$$Sections)
  5.         .ANY (+RO)
  6.     }
  7.     RW_BOOT 0x20000000 0x8000 {      ; 使用 RAM
  8.         .ANY (+RW +ZI)
  9.     }
  10. }
复制代码
2. Application 的 SCT:
  1. ; 注意:加载地址偏移到了 0x08010000
  2. LR_APP 0x08010000 0x40000 {         
  3.     ER_APP 0x08010000 0x40000 {
  4.         *.o (RESET, +First)          ; App 的向量表放在 App 区域的开头
  5.         *(InRoot$$Sections)
  6.         .ANY (+RO)
  7.     }
  8.     RW_APP 0x20000000 0x8000 {       ; 复用相同的 RAM 地址 (因为 Boot 已停止运行)
  9.         .ANY (+RW +ZI)
  10.     }
  11. }
复制代码
模式四:多 Flash 区域配置:

需求/问题:内置 \(\text{Flash}\) 容量不足。我如何将体积庞大的只读资源(图片、字库)放在外部 \(\text{Flash}\) (\(\text{0x60000000}\)),同时核心代码仍在内置 \(\text{Flash}\) 运行?
采用混合模式,将核心代码、中断向量表和对实时性要求极高的算法放置在速度最快的 内置 Flash 中运行;而将体积庞大的图片、字库等资源放置在 外置 Flash 中。根据外置 Flash 的特性,这些资源可以通过 XIP 直接读取,或者在需要时加载到外部 SDRAM 中。
  1. ; 主Flash区域 - 应用程序代码
  2. LR_MAIN_FLASH 0x08000000 0x00080000 {
  3.     ER_VECTOR 0x08000000 0x400 {
  4.         *.o (RESET, +First)          ; 中断向量表
  5.     }
  6.    
  7.     ER_CODE 0x08000400 (0x80000-0x400) {
  8.         *(InRoot$$Sections)
  9.         .ANY (+RO)
  10.         .ANY (+XO)
  11.     }
  12. }
  13. ; 外部Flash区域 - 资源数据
  14. LR_EXT_FLASH 0x60000000 0x00400000 {
  15.     ER_RESOURCES 0x60000000 0x400000 {
  16.         resources.o (+RO)            ; 专门的资源文件
  17.         fonts.o (+RO)
  18.         images.o (+RO)
  19.     }
  20. }
  21. ; 内部RAM
  22. LR_RAM 0x20000000 0x00020000 {
  23.     RW_DATA 0x20000000 0x1F000 {
  24.         .ANY (+RW +ZI)
  25.     }
  26.    
  27.     ; 预留栈空间
  28.     ARM_LIB_STACK 0x2001F000 EMPTY 0x1000 {
  29.     }
  30. }
复制代码
模式五:多 RAM 区域配置

需求/问题:我需要利用 \(\text{MCU}\) 内建的多种 \(\text{RAM}\) 速度优势,将实时性要求高的 \(\text{DMA}\) 缓冲区放到最快的 \(\text{CCM}\) \(\text{RAM}\) 中,并将大型 \(\text{GUI}\) 缓冲区分配到外部 \(\text{SDRAM}\)。
  1. LR_IROM1 0x08000000 0x00080000 {
  2.     ER_IROM1 0x08000000 0x00080000 {
  3.         *.o (RESET, +First)
  4.         *(InRoot$$Sections)
  5.         .ANY (+RO)
  6.         .ANY (+XO)
  7.     }
  8.    
  9.     ; 主RAM - 一般数据
  10.     RW_IRAM1 0x20000000 0x00010000 {
  11.         .ANY (+RW +ZI)
  12.     }
  13.    
  14.     ; 快速RAM - 关键变量
  15.     RW_FAST_RAM 0x10000000 0x00004000 {
  16.         fast_data.o (+RW +ZI)        ; 指定快速访问的数据
  17.         dma_buffers.o (+RW +ZI)      ; DMA缓冲区
  18.     }
  19.    
  20.     ; 外部RAM - 大数据缓冲
  21.     RW_EXT_RAM 0x60000000 0x00800000 {
  22.         large_buffers.o (+RW +ZI)
  23.         heap.o (+RW +ZI)
  24.     }
  25. }
复制代码
模式六:代码覆盖 (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}\) 区域中运行。
  1. LR_FLASH 0x08000000 0x00080000 {
  2.     ER_CODE 0x08000000 0x00080000 {
  3.         *.o (RESET, +First)
  4.         *(InRoot$$Sections)
  5.         main.o (+RO)                 ; 主程序始终在Flash中
  6.     }
  7. }
  8. LR_RAM 0x20000000 0x00010000 {
  9.     ; 覆盖区域1
  10.     OVERLAY_1 0x20008000 OVERLAY 0x2000 {
  11.         module_a.o (+RW +ZI +RO)     ; 模块A的代码和数据
  12.     }
  13.    
  14.     ; 覆盖区域2 - 与区域1共享相同地址空间
  15.     OVERLAY_2 0x20008000 OVERLAY 0x2000 {
  16.         module_b.o (+RW +ZI +RO)     ; 模块B的代码和数据
  17.     }
  18.    
  19.     ; 通用RAM区域
  20.     RW_DATA 0x20000000 0x8000 {
  21.         .ANY (+RW +ZI)
  22.     }
  23. }
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册