Keil5 MDK在Cortex M系列关于分散加载文件说明指导
本文介绍了在Keil MDK工程中使用分散加载文件(scatter)实现代码地址精确分配的方法。通过LCM32F067的SRAM示例工程,详细说明了如何将函数固定到指定Flash地址或搬运到指定RAM地址运行。内容包括:分散加载的基本概念、语法规则、实际应用实例和常见问题。重点讲解了如何通过section标签、执行域定义和FIXED属性,实现函数在0x08000800等Flash固定地址和0x20
| 类别 | 内容 |
|---|---|
| 摘要 | 本文结合 SRAM 示例工程,说明如何在Cortex M( LCM32F067 )工程中使用 Keil 分散加载文件,将部分函数固定到指定 Flash 地址运行,并将部分函数搬运到指定 RAM 地址运行。 |
| 源代码路径 | 链接: https://pan.baidu.com/s/1De7GAP6OVmd0leHCKNNtag?pwd=desm 提取码: desm |
目 录
- 适用范围
- 基础知识
- 分散加载文件概述
- 分散加载文件语法
- 分散加载应用实例
- 常见问题与验证方法
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表示未初始化的全局变量或静态变量。
在嵌入式工程中,通常有如下结论:
Code和RO-Data一般放在 Flash 中RW-Data运行时在 RAM 中,但其初值镜像也要占用 FlashZI-Data只占用 RAM,不占用 Flash 初值空间
因此,一个工程的空间占用通常可理解为:
- Flash 占用 =
Code + RO-Data + RW-Data - RAM 占用 =
RW-Data + ZI-Data
2.2 启动阶段谁在搬运代码和数据
很多学习者第一次接触分散加载时,最容易产生的疑问是:
- “既然某个函数要在 RAM 中运行,它是怎么从 Flash 到 RAM 的?”
答案在启动代码中。
在 Cortex-M + Keil 的典型启动流程里:
Reset_Handler进入运行库启动过程__main调用__scatterload__scatterload根据 scatter 文件描述,把需要搬运的代码或数据从装载地址复制到执行地址RW数据完成初始化,ZI数据完成清零- 最后才跳入
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 状态位 - 实际区域基地址仍然分别是
0x20000000、0x20000100、0x08000800、0x08000900
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 区 AFLASH_FIXED_B对应固定 Flash 区 BRAM_EXEC_B对应第二个 RAM 执行区
4.4 .ANY 的意义
.ANY 可以理解成“兜底规则”。
例如:
.ANY (+RO)
表示:
- 所有还没有被前面规则收走的只读代码、只读数据
- 都放到当前执行域
因此在写 scatter 文件时,通常建议顺序为:
- 先写专用 section 的匹配规则
- 最后再写
.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_A和ER_FLASH_FIXED_B放两个固定地址函数ER_IROM1放其余普通应用代码ER_RAM_EXEC_A和ER_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 |
程序启动时:
- 运行库进入
__scatterload - 将
RAM_EXEC_A对应的镜像从 Flash 拷贝到0x20000000 - 将
RAM_EXEC_B对应的镜像从 Flash 拷贝到0x20000100 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 文件更适合检查整体空间分布与是否重叠。
重点检查:
-
各执行区是否超出分配大小
例如ER_RAM_EXEC_A 0x00000100是否真的足够。 -
SRAM 代码区和普通数据区是否重叠
本工程中:ER_RAM_EXEC_A从0x20000000ER_RAM_EXEC_B从0x20000100RW_IRAM1从0x20000200
-
固定 Flash 区和普通应用区是否冲突
本工程中:0x080008000x080009000x08000A00这些边界都应保持清晰独立。
6.2 初学者常见错误
常见错误主要有以下几类:
- 只把顶层函数放到 RAM,却忽略其子函数仍在 Flash
- 把
.ANY (+RO)写在专用 section 规则前面 - 忘记给演示函数加
noinline - 没有给 RAM 代码区与普通数据区留出边界
- 在多块 Flash 切片时忘记使用
FIXED
结束语
对分散加载最实用的理解方式可以归纳为四句话:
- 先在 C 文件中用
section("name")给函数打标签 - 再在
.sct文件中把这个标签映射到指定执行域 - 如果执行地址在 RAM,启动时由
__scatterload搬运过去 - 如果执行地址在固定 Flash,使用
FIXED让其原地执行
掌握了这套方法后,就可以在单片机Cortex M系列工程中灵活实现:
- 固定地址函数接口
- RAM 中运行的关键函数
- 多执行区的代码布局设计
- 更复杂的 Boot / App 分区结构
更多推荐



所有评论(0)