欢迎光临
我们一直在努力

Keil5 MDK在Cortex M系列关于分散加载文件说明指导

类别内容
摘要 本文结合 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 中,但其初值镜像也要占用 Flash
    • ZI-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 区 A
    • FLASH_FIXED_B 对应固定 Flash 区 B
    • RAM_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 从 0x20000000
    • ER_RAM_EXEC_B 从 0x20000100
    • RW_IRAM1 从 0x20000200
  • 固定 Flash 区和普通应用区是否冲突 本工程中:

    • 0x08000800
    • 0x08000900
    • 0x08000A00 这些边界都应保持清晰独立。
  • 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)
    未经允许不得转载:171主机测评 » Keil5 MDK在Cortex M系列关于分散加载文件说明指导
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址