类别 内容
摘要 本文结合 SRAM 示例工程,说明如何在Cortex M( LCM32F067 )工程中使用 Keil 分散加载文件,将部分函数固定到指定 Flash 地址运行,并将部分函数搬运到指定 RAM 地址运行。
源代码路径 链接: https://pan.baidu.com/s/1De7GAP6OVmd0leHCKNNtag?pwd=desm 提取码: desm

目 录

  1. 适用范围
  2. 基础知识
  3. 分散加载文件概述
  4. 分散加载文件语法
  5. 分散加载应用实例
  6. 常见问题与验证方法

1. 适用范围

有时候用户希望在单片机工程中实现如下需求:

  • 某些函数固定在某个 Flash 地址,作为固定接口或服务入口
  • 某些函数在上电后搬运到 RAM 中执行,以提升速度或避免 Flash 操作时取指冲突
  • 普通代码和普通变量仍然按照默认方式分别放在 Flash 和 SRAM 中

在 Keil MDK 工程中,这类需求通常通过分散加载文件来实现。
本文结合 Project/Examples/SRAM 示例工程,重点讲解:

  • 如何通过 section("name") 给函数打标签
  • 如何在 .sct 文件中把这些 section 分配到不同地址
  • 如何理解加载域、执行域、FIXED__scatterload
  • 如何通过 map 验证最终结果

2. 基础知识

2.1 基本概念

在理解分散加载前,需要先清楚下面几类数据的含义:

  • Code 表示程序代码。
  • RO-Data 表示只读数据,例如 const 常量。
  • RW-Data 表示已初始化的全局变量或静态变量。
  • ZI-Data 表示未初始化的全局变量或静态变量。

在嵌入式工程中,通常有如下结论:

  • CodeRO-Data 一般放在 Flash 中
  • RW-Data 运行时在 RAM 中,但其初值镜像也要占用 Flash
  • ZI-Data 只占用 RAM,不占用 Flash 初值空间

因此,一个工程的空间占用通常可理解为:

  • Flash 占用 = Code + RO-Data + RW-Data
  • RAM 占用 = RW-Data + ZI-Data

2.2 启动阶段谁在搬运代码和数据

很多学习者第一次接触分散加载时,最容易产生的疑问是:

  • “既然某个函数要在 RAM 中运行,它是怎么从 Flash 到 RAM 的?”

答案在启动代码中。

在 Cortex-M + Keil 的典型启动流程里:

  1. Reset_Handler 进入运行库启动过程
  2. __main 调用 __scatterload
  3. __scatterload 根据 scatter 文件描述,把需要搬运的代码或数据从装载地址复制到执行地址
  4. RW 数据完成初始化,ZI 数据完成清零
  5. 最后才跳入 main()

因此,只要 scatter 文件描述正确,那么:

  • main() 运行之前
  • 所有要求“在 RAM 中执行”的函数
  • 就已经被搬运到目标 RAM 地址了

2.3 本工程中的 4 个目标函数

本工程中专门设计了 4 个函数用于演示:

  • RAM_FillPatternWindow()
  • RAM_MixAndAccumulate()
  • FLASH_FixedSignature()
  • FLASH_FixedParity()

它们分别用于演示:

  • 两个不同 RAM 地址运行
  • 两个不同 Flash 固定地址运行

最终链接结果为:

函数名 最终符号地址 说明
RAM_FillPatternWindow 0x20000001 在 SRAM 执行区 A 运行
RAM_MixAndAccumulate 0x20000101 在 SRAM 执行区 B 运行
FLASH_FixedSignature 0x08000801 固定在 Flash 窗口 A
FLASH_FixedParity 0x08000901 固定在 Flash 窗口 B

说明:

  • 地址最低位为 1 是 Thumb 状态位
  • 实际区域基地址仍然分别是 0x200000000x200001000x080008000x08000900

3. 分散加载文件概述

分散加载文件是一个文本文件,用于描述链接器如何组织最终映像,并决定:

  • 程序镜像装载到哪里
  • 代码和数据执行时位于哪里
  • 不同 section 应该进入哪个执行区

如果不使用 scatter 文件,链接器会按默认规则放置代码和数据。
而在下面这些场景中,通常就需要显式编写 scatter 文件:

  • 代码必须放在多个不同内存区域
  • 部分函数必须在 RAM 中执行
  • 部分函数必须固定在某个 Flash 地址
  • 多块 RAM 或多块 Flash 需要分别利用
  • 需要精确控制向量表、启动区、应用区、固定接口区的地址布局

对本工程而言,使用分散加载的直接目的就是:

  • 固定两段 Flash 代码区
  • 划出两段 RAM 执行区
  • 普通代码和数据继续走默认的应用区与数据区

4. 分散加载文件语法

4.1 加载时域的描述

一个典型的加载域写法如下:

LR_IROM1 0x08000000 0x00010000 {
    ...
}

这里:

  • LR_IROM1 是加载域名称
  • 0x08000000 是加载域起始地址
  • 0x00010000 是该加载域大小

在本工程中,这表示:

  • 整个映像的主装载空间仍然是片上 Flash
  • 其组织起点是 0x08000000

4.2 运行时域的描述

一个典型的执行域写法如下:

ER_FLASH_FIXED_A 0x08000800 FIXED 0x00000100 {
    *.o (FLASH_FIXED_A)
}

这里:

  • ER_FLASH_FIXED_A 是执行域名称
  • 0x08000800 是执行地址
  • FIXED 表示装载地址与执行地址相同
  • 0x00000100 表示执行域大小

如果某个执行域不在 Flash,而是在 SRAM 中,例如:

ER_RAM_EXEC_A 0x20000000 0x00000100 {
    *.o (RAM_EXEC_A)
}

那么它的含义是:

  • 这段代码最终在 0x20000000 执行
  • 但初始镜像仍由主加载域统一组织在 Flash 中
  • 上电后由 __scatterload 拷贝到该 SRAM 地址

4.3 输入段描述

scatter 文件中的输入段匹配规则,用来决定“哪些内容放进这个执行域”。

例如:

*.o (RAM_EXEC_A)

它表示:

  • 匹配所有目标文件中的 RAM_EXEC_A 输入段
  • 并把这些输入段放到当前执行域中

本工程中在 C 代码里写了:

__attribute__((section("RAM_EXEC_A")))

这就会让该函数进入名为 RAM_EXEC_A 的输入段。
scatter 文件再用 *.o (RAM_EXEC_A) 将其准确分配到 ER_RAM_EXEC_A

类似地:

  • FLASH_FIXED_A 对应固定 Flash 区 A
  • FLASH_FIXED_B 对应固定 Flash 区 B
  • RAM_EXEC_B 对应第二个 RAM 执行区

4.4 .ANY 的意义

.ANY 可以理解成“兜底规则”。

例如:

.ANY (+RO)

表示:

  • 所有还没有被前面规则收走的只读代码、只读数据
  • 都放到当前执行域

因此在写 scatter 文件时,通常建议顺序为:

  1. 先写专用 section 的匹配规则
  2. 最后再写 .ANY

否则,很容易出现:

  • 你想单独分配的函数
  • 先被默认 .ANY 区域收走
  • 后面的专用区域反而匹配不到目标

5. 分散加载应用实例

5.1 本工程的地址规划

本工程使用的关键地址如下:

区域 起始地址 用途
ER_ROOT 0x08000000 向量表、启动代码、运行库根区
ER_FLASH_FIXED_A 0x08000800 固定 Flash 函数 A
ER_FLASH_FIXED_B 0x08000900 固定 Flash 函数 B
ER_IROM1 0x08000A00 普通应用代码区
ER_RAM_EXEC_A 0x20000000 RAM 执行函数 A
ER_RAM_EXEC_B 0x20000100 RAM 执行函数 B
RW_IRAM1 0x20000200 普通数据区

用图示表示如下:

flowchart TB
    subgraph FLASH["Flash 地址空间"]
        F0["0x08000000\nER_ROOT\n向量表 / 启动代码 / __main / __scatterload"]
        F1["0x08000800\nER_FLASH_FIXED_A\nFLASH_FixedSignature()"]
        F2["0x08000900\nER_FLASH_FIXED_B\nFLASH_FixedParity()"]
        F3["0x08000A00\nER_IROM1\n普通应用代码区"]
    end

    subgraph SRAM["SRAM 地址空间"]
        R0["0x20000000\nER_RAM_EXEC_A\nRAM_FillPatternWindow()"]
        R1["0x20000100\nER_RAM_EXEC_B\nRAM_MixAndAccumulate()"]
        R2["0x20000200\nRW_IRAM1\nRW / ZI 数据"]
    end

5.2 本工程的 scatter 文件配置

本工程实际采用的 scatter 文件结构如下:

LR_IROM1 0x08000000 0x00010000  {
  ER_ROOT 0x08000000 0x00000800  {
    *.o (RESET, +First)
    *(InRoot$$Sections)
  }

  ER_FLASH_FIXED_A 0x08000800 FIXED 0x00000100  {
    *.o (FLASH_FIXED_A)
  }

  ER_FLASH_FIXED_B 0x08000900 FIXED 0x00000100  {
    *.o (FLASH_FIXED_B)
  }

  ER_IROM1 0x08000A00 FIXED 0x0000F600  {
    .ANY (+RO)
    .ANY (+XO)
  }

  ER_RAM_EXEC_A 0x20000000 0x00000100  {
    *.o (RAM_EXEC_A)
  }

  ER_RAM_EXEC_B 0x20000100 0x00000100  {
    *.o (RAM_EXEC_B)
  }

  RW_IRAM1 0x20000200 0x00002600  {
    .ANY (+RW +ZI)
  }
}

其含义可概括为:

  • ER_ROOT 放根区内容
  • ER_FLASH_FIXED_AER_FLASH_FIXED_B 放两个固定地址函数
  • ER_IROM1 放其余普通应用代码
  • ER_RAM_EXEC_AER_RAM_EXEC_B 放两段不同地址的 RAM 执行代码
  • RW_IRAM1 放普通全局变量和静态变量

5.3 在 C 文件中定义自定义 section

本工程在 main.c 中使用了下面的写法:

#define RAM_EXEC_A_ATTR     __attribute__((noinline, section("RAM_EXEC_A")))
#define RAM_EXEC_B_ATTR     __attribute__((noinline, section("RAM_EXEC_B")))
#define FLASH_FIXED_A_ATTR  __attribute__((noinline, section("FLASH_FIXED_A")))
#define FLASH_FIXED_B_ATTR  __attribute__((noinline, section("FLASH_FIXED_B")))

然后分别将 4 个函数放入这些 section:

RAM_EXEC_A_ATTR uint32_t RAM_FillPatternWindow(uint32_t seed);
RAM_EXEC_B_ATTR uint32_t RAM_MixAndAccumulate(uint32_t value);
FLASH_FIXED_A_ATTR uint32_t FLASH_FixedSignature(void);
FLASH_FIXED_B_ATTR uint32_t FLASH_FixedParity(uint32_t value);

其中 noinline 的作用是:

  • 防止编译器把函数直接内联到调用者中
  • 便于在链接报告中清楚看到每个函数的独立落点

5.4 双 RAM 运行函数配置实例

本工程中两个运行在 RAM 的函数分别是:

  • RAM_FillPatternWindow()
  • RAM_MixAndAccumulate()

它们的功能并不复杂,目的是让学习者把注意力集中在“地址分配”上,而不是业务逻辑本身。

对应关系如下:

函数 section 执行区 执行地址
RAM_FillPatternWindow() RAM_EXEC_A ER_RAM_EXEC_A 0x20000000
RAM_MixAndAccumulate() RAM_EXEC_B ER_RAM_EXEC_B 0x20000100

程序启动时:

  1. 运行库进入 __scatterload
  2. RAM_EXEC_A 对应的镜像从 Flash 拷贝到 0x20000000
  3. RAM_EXEC_B 对应的镜像从 Flash 拷贝到 0x20000100
  4. main() 才开始执行

因此在 main() 中调用这两个函数时,它们已经在 RAM 中。

5.5 固定 Flash 地址函数配置实例

本工程中两个固定在 Flash 中的函数分别是:

  • FLASH_FixedSignature()
  • FLASH_FixedParity()

对应关系如下:

函数 section 执行区 固定地址
FLASH_FixedSignature() FLASH_FIXED_A ER_FLASH_FIXED_A 0x08000800
FLASH_FixedParity() FLASH_FIXED_B ER_FLASH_FIXED_B 0x08000900

由于这两个执行区都带 FIXED,因此:

  • 装载地址 = 执行地址
  • 函数直接在 Flash 中原地执行
  • 不需要启动时拷贝

这类配置很适合实现:

  • 固定服务入口
  • 版本接口
  • 固件标识函数
  • 固定跳转入口

5.6 普通应用区为什么也要使用 FIXED

本工程里普通应用区写成了:

ER_IROM1 0x08000A00 FIXED 0x0000F600

这里的 FIXED 很重要。
原因是:

  • 前面已经人为切出了两个固定 Flash 小窗
  • 普通应用区从 0x08000A00 开始执行
  • 如果这里不加 FIXED,链接器可能会把装载镜像往前压缩
  • 造成 Flash 中装载布局和执行布局发生交叉覆盖风险

本工程第一次修改 scatter 文件时,就出现了这类错误。
最终修复办法就是:

  • 给普通应用区也加上 FIXED

这是一条很重要的实践经验:

  • 只要 Flash 代码区需要精确切片,而且这些区域本身就在 Flash 原地执行,就优先考虑 FIXED

5.7 本工程中的 section、执行域与函数对应关系总表

名称 类型 位置 作用
RAM_EXEC_A 自定义输入段 C 文件定义 作为 RAM 函数 A 的标签
RAM_EXEC_B 自定义输入段 C 文件定义 作为 RAM 函数 B 的标签
FLASH_FIXED_A 自定义输入段 C 文件定义 作为固定 Flash 函数 A 的标签
FLASH_FIXED_B 自定义输入段 C 文件定义 作为固定 Flash 函数 B 的标签
ER_RAM_EXEC_A 执行域 SRAM 接收 RAM_EXEC_A
ER_RAM_EXEC_B 执行域 SRAM 接收 RAM_EXEC_B
ER_FLASH_FIXED_A 执行域 Flash 接收 FLASH_FIXED_A
ER_FLASH_FIXED_B 执行域 Flash 接收 FLASH_FIXED_B

6. 常见问题与验证方法

6.1 如何看 map 文件

map 文件更适合检查整体空间分布与是否重叠。

重点检查:

  1. 各执行区是否超出分配大小
    例如 ER_RAM_EXEC_A 0x00000100 是否真的足够。

  2. SRAM 代码区和普通数据区是否重叠
    本工程中:

    • ER_RAM_EXEC_A0x20000000
    • ER_RAM_EXEC_B0x20000100
    • RW_IRAM10x20000200
  3. 固定 Flash 区和普通应用区是否冲突
    本工程中:

    • 0x08000800
    • 0x08000900
    • 0x08000A00 这些边界都应保持清晰独立。

6.2 初学者常见错误

常见错误主要有以下几类:

  • 只把顶层函数放到 RAM,却忽略其子函数仍在 Flash
  • .ANY (+RO) 写在专用 section 规则前面
  • 忘记给演示函数加 noinline
  • 没有给 RAM 代码区与普通数据区留出边界
  • 在多块 Flash 切片时忘记使用 FIXED

结束语

对分散加载最实用的理解方式可以归纳为四句话:

  1. 先在 C 文件中用 section("name") 给函数打标签
  2. 再在 .sct 文件中把这个标签映射到指定执行域
  3. 如果执行地址在 RAM,启动时由 __scatterload 搬运过去
  4. 如果执行地址在固定 Flash,使用 FIXED 让其原地执行

掌握了这套方法后,就可以在单片机Cortex M系列工程中灵活实现:

  • 固定地址函数接口
  • RAM 中运行的关键函数
  • 多执行区的代码布局设计
  • 更复杂的 Boot / App 分区结构
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐