Keil ARMCC 链接文件(.sct)深度解析

📖 介绍

链接文件是嵌入式开发中最容易被忽视但极其重要的配置文件。很多开发者遇到链接错误就束手无策,或者在做 Bootloader、OTA 升级、CCMRAM 分配时不知道如何配置内存布局,本文帮你彻底搞懂 .sct 文件。

在 ARMCC(ARM Compiler v5)工具链中,链接器使用**分散加载文件(Scatter Loading File)**来描述程序的内存布局。这个文件的后缀通常为 .sct,它是 Keil 工程中控制"代码和数据放在哪里"的核心配置。理解了 .sct,你就掌握了嵌入式链接的命脉。

你将学到:

  • 分散加载文件的核心概念与语法规则
  • 加载域(LR)与执行域(ER)的本质区别
  • +RO / +RW / +ZI 三种段类型的含义
  • 链接器自动生成的代码(__scatterload__main
  • Keil 自动链接符号(Image$$)的使用方法
  • Bootloader + App 双固件架构的 .sct 配置
  • CCMRAM 分配、RAM 执行函数等实战场景
  • 常见链接错误的排查与解决

📌 系列文章导航:

  • 📘 上一篇:STM32F407 启动文件深度解析
  • 📗 本文:Keil ARMCC 链接文件(.sct)深度解析

📑 目录


一、链接文件概述

1.1 什么是链接文件?

链接文件是描述程序如何在物理内存中布局的配置文件。在 Keil MDK + ARM Compiler v5(ARMCC)工具链中,这个文件被称为分散加载描述文件(Scatter File),通常以 .sct 为后缀。

它告诉链接器两件事:

  1. 代码和数据的加载地址(烧录时放在哪里)
  2. 代码和数据的执行地址(运行时放在哪里)

一个典型的嵌入式程序包含多种类型的数据:中断向量表、用户代码、只读常量、全局变量、堆栈等。链接文件决定了这些内容在 Flash 和 RAM 中的精确位置。

💡 工程师经验: 很多人把链接文件当成"黑盒",完全依赖 Keil 的自动生成。这在简单项目中没问题,但一旦涉及多固件分区、特殊内存区域分配,就必须手动编写 .sct 文件。理解它是从"会用 Keil"到"精通 Keil"的关键一步。

1.2 链接文件在编译流程中的位置

.sct 文件在编译流程中处于链接阶段,是链接器(armlink)的核心输入之一。完整的编译流程如下:

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│  源文件   │───▶│  编译器   │───▶│  目标文件  │───▶│  链接器   │───▶│  .axf    │
│  .c .s   │    │  armcc   │    │   .o     │    │  armlink │    │  可执行   │
│  .cpp    │    │  armasm  │    │          │    │          │    │  文件     │
└──────────┘    └──────────┘    └──────────┘    └────┬─────┘    └────┬─────┘
                                                      │               │
                                              ┌───────┘               │
                                              ▼                       ▼
                                         ┌────────┐            ┌──────────┐
                                         │ .sct   │            │ .bin/.hex│
                                         │ 链接文件│            │ 烧录文件  │
                                         └────────┘            └──────────┘

各阶段说明:

阶段 工具 输入 输出 说明
编译 armcc / armasm .c / .s .o 词法分析、语法分析、生成目标文件
汇编 armasm .s .o 汇编指令翻译为机器码
链接 armlink .o + .sct .axf 符号解析、地址分配、段合并
格式转换 fromelf .axf .bin / .hex 生成烧录文件

💡 工程师经验: 链接阶段是最容易出问题的环节。编译能通过不代表链接能通过,常见的 L6236EL6218E 等错误都发生在链接阶段。理解 .sct 文件是排查这些错误的基础。

1.3 为什么需要手动编写 .sct?

对于简单的单程序工程,Keil 可以通过 Options for Target → Target 选项卡自动生成 .sct 文件。但在以下场景中,必须手动编写或修改 .sct 文件

场景 说明
Bootloader + App 需要精确控制 App 的起始地址,避免覆盖 Bootloader
OTA 升级 需要划分多个 Flash 分区(Boot、App1、App2、参数区)
CCMRAM 使用 STM32F4 系列的 CCMRAM 需要单独的执行域
RAM 执行函数 将关键函数放到 RAM 中执行以提升性能或支持 Flash 擦写时运行
外部 RAM 扩展 使用 FSMC/FMC 连接外部 SRAM/SDRAM
XIP(片外执行) 代码在外部 Flash 中执行
多核共享内存 双核 MCU 中定义共享内存区域

⚠️ 注意事项: 手动编写 .sct 文件后,需要在 Keil 的 Options for Target → Linker 选项卡中取消勾选 “Use Memory Layout from Target Dialog”,并指定你的 .sct 文件路径。否则 Keil 会用自动生成的布局覆盖你的配置。


二、分散加载文件核心概念

2.1 加载域(Load Region)

加载域(Load Region)是分散加载文件的最外层结构,定义了程序烧录到 Flash 时的布局。所有加载域以 LR_ 作为前缀。

语法格式:

LR_名称 起始地址 [最大大小] [属性]
{
    ...执行域...
}

参数说明:

参数 说明 示例
名称 加载域标识符,自定义命名 LR_IROM1
起始地址 加载域在 Flash 中的起始物理地址 0x08000000
最大大小 该加载域允许的最大字节数(可选) 0x00080000
属性 加载域属性(可选) NOCOMPRESS

示例:

; 定义一个从 0x08000000 开始、最大 512KB 的加载域
LR_IROM1 0x08000000 0x00080000
{
    ...
}

💡 工程师经验: 一个 .sct 文件可以包含多个加载域,这在多 App 分区方案中非常有用。每个加载域对应 Flash 上的一个独立区域,链接器会为每个加载域生成独立的加载描述信息。

2.2 执行域(Execution Region)

执行域(Execution Region)嵌套在加载域内部,定义了程序运行时各段在内存中的位置。所有执行域以 ER_ 作为前缀。

语法格式:

ER_名称 起始地址 [最大大小] [属性]
{
    ...模块选择规则...
}

参数说明:

参数 说明 示例
名称 执行域标识符,自定义命名 ER_IROM1
起始地址 执行域的起始地址(可以是 Flash 或 RAM 地址) 0x08000000
最大大小 该执行域允许的最大字节数(可选) 0x00080000
属性 段类型属性,如 +RO+RW+ZI +RO

示例:

; Flash 中的代码执行域
ER_IROM1 0x08000000 0x00080000  {
    *.o (RESET, +First)      ; 中断向量表放在最前面
    *(InRoot$$Sections)       ; C 库初始化代码
    .ANY (+RO)                ; 所有其他代码和只读数据
}

; RAM 中的数据执行域
ER_IRAM1 0x20000000 0x00020000  {
    .ANY (+RW +ZI)            ; 所有读写数据和零初始化数据
}

💡 工程师经验: 执行域的起始地址不一定要和加载域相同。当执行域的地址是 RAM 地址时,链接器会自动生成搬运代码,将数据从 Flash(加载地址)搬运到 RAM(执行地址)。这就是 .data 段初始化的原理。

2.3 加载域 vs 执行域的本质区别

这是理解分散加载文件最核心的概念:

┌─────────────────────────────────────────────────────────────────┐
│                        加载域 (Load Region)                      │
│                    = 烧录时 Flash 中的布局                        │
│                                                                  │
│  ┌──────────────────┐  ┌──────────────────┐  ┌───────────────┐  │
│  │   RESET 向量表    │  │   代码 +RO       │  │  .data 初始值  │  │
│  │   0x08000000     │  │   0x080000C0     │  │  0x08010000   │  │
│  └──────────────────┘  └──────────────────┘  └───────┬───────┘  │
│                                                        │          │
│                   运行时搬运(__scatterload)             │          │
│                                                        ▼          │
│                                              ┌──────────────────┐ │
│                                              │  .data (RAM)     │ │
│                                              │  0x20000000      │ │
│                                              └──────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                       执行域 (Execution Region)                   │
│                    = 运行时内存中的布局                            │
│                                                                  │
│  Flash 端:                                                       │
│  ┌──────────────────┐  ┌──────────────────┐                      │
│  │   RESET 向量表    │  │   代码 +RO       │                      │
│  │   0x08000000     │  │   0x080000C0     │                      │
│  └──────────────────┘  └──────────────────┘                      │
│                                                                  │
│  RAM 端:                                                         │
│  ┌──────────────────┐  ┌──────────────────┐  ┌───────────────┐  │
│  │   .data +RW      │  │   .bss +ZI       │  │  Heap / Stack │  │
│  │   0x20000000     │  │   0x20000100     │  │  0x2000XXXX   │  │
│  └──────────────────┘  └──────────────────┘  └───────────────┘  │
└─────────────────────────────────────────────────────────────────┘

核心区别:

概念 加载域 (LR) 执行域 (ER)
含义 烧录时在 Flash 中的物理布局 运行时代码/数据在内存中的布局
对应术语 LMA(Load Memory Address) VMA(Virtual Memory Address)
存储介质 通常是 Flash 可以是 Flash 或 RAM
数量关系 一个 .sct 可有多个 LR 每个 LR 内可有多个 ER
地址关系 LR 地址 = ER 地址(代码段) LR 地址 ≠ ER 地址(数据段)

💡 工程师经验: 当执行域的起始地址与加载域的起始地址相同时(如代码段),代码直接在 Flash 中原地执行(XIP)。当执行域的起始地址是 RAM 地址时,链接器会在 .sct 中记录加载地址和执行地址的对应关系,运行时由 __scatterload 自动完成搬运。这就是为什么 .data 段在 Flash 中有一份初始值副本,在 RAM 中又有一份运行时副本。

2.4 段类型:+RO / +RW / +ZI

ARMCC 链接器将程序内容分为三种基本段类型:

段类型 全称 含义 对应 C 语言概念 存储位置
+RO Read Only 只读段 const 变量、字符串常量、函数代码 Flash
+RW Read Write 可读写段 已初始化的全局/静态变量(.data Flash(加载时)→ RAM(运行时)
+ZI Zero Init 零初始化段 未初始化的全局/静态变量(.bss RAM(运行时清零)

详细说明:

C 语言代码                          段类型        加载域(Flash)     执行域(RAM)
─────────                          ──────        ─────────────     ────────────
const int g_val = 100;             +RO           ✅ 存放            ✅ 运行时读取
int g_init = 42;                   +RW           ✅ 存放初始值       ✅ 运行时读写
int g_uninit;                      +ZI           ❌ 不占空间         ✅ 运行时清零
void func(void) { ... }            +RO           ✅ 存放            ✅ 运行时执行
static int s_var = 1;              +RW           ✅ 存放初始值       ✅ 运行时读写

💡 工程师经验: +ZI 段在 Flash 中不占用任何空间,因为它在运行时直接在 RAM 中清零即可。这也是为什么 .bss 段不计入 .bin 文件大小的原因。但是,+ZI 段在 .map 文件中会显示其大小,因为它确实消耗了 RAM 空间。在计算 Flash 占用时,只需要关注 +RO+RW 的大小。

⚠️ 注意事项: +RW 段比较特殊:它在加载域中占用 Flash 空间(存储初始值),在执行域中占用 RAM 空间(运行时使用)。所以一个已初始化的全局变量会同时消耗 Flash 和 RAM。如果你发现 RAM 不够用,可以检查是否有大量已初始化的全局变量可以改为 const(移到 +RO)。


三、.sct 文件完整语法详解

3.1 基本语法结构

分散加载文件采用嵌套结构,从外到内依次是:加载域 → 执行域 → 模块选择规则。

; ═══════════════════════════════════════════════════════════
; 加载域(Load Region):定义烧录布局
; ═══════════════════════════════════════════════════════════
LR_名称 起始地址 [大小] [属性]
{
    ; ───────────────────────────────────────────────────────
    ; 执行域(Execution Region):定义运行时布局
    ; ───────────────────────────────────────────────────────
    ER_名称 起始地址 [大小] [属性]
    {
        ; 模块选择规则:决定哪些目标文件的哪些段放入此执行域
        *.o (RESET, +First)        ; 特定模块的特定段
        *(InRoot$$Sections)        ; C 库根段
        .ANY (+RO)                 ; 任意模块的只读段
        .ANY (+RW +ZI)             ; 任意模块的读写段
    }
}

💡 工程师经验: .sct 文件中的注释使用分号 ; 开头,不是 C 语言的 //。这是一个容易忽略的细节。

3.2 模块选择规则

模块选择规则(Module Selector)决定了哪些目标文件的哪些段被放入当前执行域。

语法格式:

模块名 (段名, 属性)

常用规则详解:

规则 含义 说明
*.o (RESET, +First) 所有目标文件中的 RESET 段,放在最前面 中断向量表必须放在 Flash 起始地址
*(InRoot$$Sections) C 库内部初始化段 包含 __scatterload 等关键代码
.ANY (+RO) 任意模块的只读段 匹配所有未被前面规则匹配的 +RO 段
.ANY (+RW +ZI) 任意模块的读写段和零初始化段 匹配所有未被前面规则匹配的 +RW 和 +ZI 段
main.o (+RO) main.o 的只读段 精确控制特定模块的放置

匹配优先级:

模块选择规则的匹配顺序非常重要,从上到下依次匹配,先匹配先生效

ER_IROM1 0x08000000 0x00080000
{
    ; 1. 最高优先级:中断向量表,必须放在 0x08000000
    *.o (RESET, +First)

    ; 2. 次高优先级:C 库初始化代码
    *(InRoot$$Sections)

    ; 3. 通配:所有剩余的只读段(代码 + 常量)
    .ANY (+RO)
}

💡 工程师经验: +First 属性确保 RESET 段(中断向量表)被放在执行域的最起始位置。这对于 Cortex-M 系列处理器至关重要,因为内核复位后会从 0x00000000(或 Flash 映射地址 0x08000000)读取初始栈指针和复位向量。如果向量表不在正确位置,程序将无法启动。

3.3 执行域属性

执行域可以携带多种属性,控制段的放置行为:

属性 含义 使用场景
+RO 只读段 代码和常量
+RW 可读写段 已初始化全局变量
+ZI 零初始化段 未初始化全局变量
+FIRST 放在域的最前面 中断向量表
+LAST 放在域的最后面 校验和、版本信息
NOCOMPRESS 不压缩 RW 数据 需要精确控制数据布局时
PADVALUE 填充值 对齐填充
OVERLAY 覆盖执行域 多个执行域共享同一块 RAM
FIXED 固定地址 执行域地址不可被链接器调整

属性组合示例:

; 不压缩 RW 数据,使用 0xFF 填充
ER_IRAM1 0x20000000 0x00020000 NOCOMPRESS PADVALUE 0xFF
{
    .ANY (+RW +ZI)
}

⚠️ 注意事项: 默认情况下,ARMCC 链接器会压缩 RW 数据的初始值以节省 Flash 空间。如果使用调试器直接查看 Flash 中的 RW 初始值,可能会看到压缩后的数据而非原始值。如果需要看到原始值,可以在执行域上添加 NOCOMPRESS 属性。

3.4 STM32F407 标准示例

以下是一个完整的 STM32F407VG(1MB Flash, 192KB RAM + 64KB CCMRAM)的标准 .sct 文件:

; *************************************************************
; *** Keil ARMCC Scatter-Loading Description File            ***
; *** Target: STM32F407VG                                   ***
; *** Flash: 1MB (0x08000000 - 0x080FFFFF)                  ***
; *** SRAM:  128KB (0x20000000 - 0x2001FFFF)               ***
; *** CCM:   64KB (0x10000000 - 0x1000FFFF)                ***
; *************************************************************

; ========================================
; 加载域:从 0x08000000 开始,最大 1MB
; ========================================
LR_IROM1 0x08000000 0x00100000
{
    ; ====================================
    ; 执行域 1:Flash 代码区
    ; 地址空间:0x08000000 - 0x080FFFFF
    ; ====================================
    ER_IROM1 0x08000000 0x00100000
    {
        ; 中断向量表必须放在最前面
        *.o (RESET, +First)

        ; C 库初始化代码(__scatterload 等)
        *(InRoot$$Sections)

        ; 所有代码和只读数据
        .ANY (+RO)
    }

    ; ====================================
    ; 执行域 2:SRAM 数据区
    ; 地址空间:0x20000000 - 0x2001FFFF
    ; ====================================
    ER_IRAM1 0x20000000 0x00020000
    {
        ; 所有已初始化和未初始化的全局变量
        .ANY (+RW +ZI)
    }
}

内存布局图:

Flash (0x08000000, 1MB)                    SRAM (0x20000000, 128KB)
┌────────────────────────┐                 ┌────────────────────────┐
│ 0x08000000             │                 │ 0x20000000             │
│ ┌────────────────────┐ │                 │ ┌────────────────────┐ │
│ │ RESET (向量表)      │ │                 │ │ .data (已初始化变量)│ │
│ │ +First             │ │   __scatterload │ │ 从 Flash 搬运过来   │ │
│ ├────────────────────┤ │ ──────────────▶ │ ├────────────────────┤ │
│ │ InRoot$$Sections   │ │    运行时搬运    │ │ .bss (未初始化变量) │ │
│ │ (__scatterload等)   │ │                 │ │ 运行时清零          │ │
│ ├────────────────────┤ │                 │ ├────────────────────┤ │
│ │ .text (代码段)      │ │                 │ │ Heap (堆)           │ │
│ │ .rodata (只读数据)  │ │                 │ │ 向高地址增长        │ │
│ │ +RO                │ │                 │ ├────────────────────┤ │
│ │                    │ │                 │ │ Stack (栈)          │ │
│ ├────────────────────┤ │                 │ │ 向低地址增长        │ │
│ │ .data 初始值副本    │ │                 │ │                    │ │
│ │ (用于搬运到 RAM)    │ │                 │ │                    │ │
│ └────────────────────┘ │                 │ └────────────────────┘ │
│ 0x080FFFFF             │                 │ 0x2001FFFF             │
└────────────────────────┘                 └────────────────────────┘

💡 工程师经验: 上述 .sct 文件中,ER_IROM1 的起始地址 0x08000000LR_IROM1 相同,说明代码直接在 Flash 中执行(XIP)。而 ER_IRAM1 的起始地址 0x20000000LR_IROM10x08000000 不同,说明 .data 段需要从 Flash 搬运到 RAM。

3.5 带多个执行域的示例(含 CCMRAM)

当需要使用 STM32F4 的 CCMRAM(Core Coupled Memory RAM)时,需要添加额外的执行域:

; *************************************************************
; *** STM32F407VG with CCMRAM                               ***
; *************************************************************

LR_IROM1 0x08000000 0x00100000
{
    ; Flash 代码区
    ER_IROM1 0x08000000 0x00100000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ; 主 SRAM 数据区 (128KB)
    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }

    ; CCMRAM 数据区 (64KB)
    ; 地址: 0x10000000 - 0x1000FFFF
    ER_IRAM2 0x10000000 0x00010000
    {
        ; 只有使用 __attribute__((section("CCMRAM_SECTION")))
        ; 标记的变量才会被放到这里
        *.o (CCMRAM_SECTION)
    }
}

对应的 C 代码使用方法:

/* 将变量放到 CCMRAM 中 */
__attribute__((section("CCMRAM_SECTION"))) uint32_t fast_buffer[1024];

/* 将数组放到 CCMRAM 中 */
__attribute__((section("CCMRAM_SECTION")))
uint8_t dma_rx_buffer[256];

内存布局图:

Flash (0x08000000)     SRAM (0x20000000, 128KB)    CCMRAM (0x10000000, 64KB)
┌──────────────┐       ┌──────────────────┐        ┌──────────────────┐
│ RESET 向量表  │       │ .data / .bss     │        │ CCMRAM_SECTION   │
│ 代码 +RO     │       │ Heap / Stack     │        │ (手动指定的变量)   │
│ .data 初始值  │       │                  │        │                  │
└──────────────┘       └──────────────────┘        └──────────────────┘

⚠️ 注意事项: CCMRAM 连接在 D-Bus 上,DMA 无法访问!如果你将 DMA 缓冲区放到 CCMRAM 中,DMA 传输将失败或读取到错误数据。这是一个非常常见的坑。CCMRAM 的优势是 CPU 访问零等待,适合存放高频访问的变量(如查找表、中间计算结果等)。

💡 工程师经验: 在 Keil 中配置 CCMRAM 时,除了修改 .sct 文件外,还需要确保启动代码中正确初始化了 CCMRAM 所在的地址范围。对于 STM32F4,CCMRAM 默认是可用的,但建议在系统初始化时显式使能。


四、链接器自动生成的代码

4.1 __main 函数

很多初学者以为程序从 main() 开始执行,但实际上,在 main() 之前,链接器已经做了大量工作。ARMCC 链接器会自动生成一个名为 __main 的函数作为程序的真正入口

⚠️ 注意事项: __main(双下划线)是链接器生成的函数,与用户编写的 main()(单下划线)完全不同!不要混淆。

__main 的内部执行流程:

复位向量 (RESET)
    │
    ▼
SystemInit()          ← 启动文件 startup_stm32f407xx.s 中的 Reset_Handler 调用
    │
    ▼
__main                ← 链接器自动生成的入口函数
    │
    ├──▶ __scatterload()    ← 第一步:分散加载(搬运 .data,清零 .bss)
    │
    └──▶ __rt_entry()       ← 第二步:C 运行时初始化
              │
              ├──▶ __rt_stackheap_init()    ← 初始化堆栈
              ├──▶ __rt_lib_init()          ← C 库初始化
              │     ├──▶ __fp_init()        ← 浮点运算初始化
              │     └──▶ 构造函数调用        ← C++ 全局对象构造
              │
              └──▶ main()                   ← 最终调用用户的 main()
                        │
                        ▼
                    用户程序

💡 工程师经验: 你可以在调试时查看反汇编窗口,确认程序确实经过了 __main__scatterload__rt_entrymain() 的完整流程。如果程序在 main() 之前就崩溃了,问题很可能出在 __scatterload 阶段(例如 .sct 配置错误导致搬运地址越界)。

4.2 __scatterload

__scatterload 是 ARMCC 链接器自动生成的核心函数,负责完成分散加载任务。它的主要工作包括:

  1. 读取分散加载描述表:链接器在编译时根据 .sct 文件生成一张描述表,记录了每个需要搬运的段的加载地址和执行地址。
  2. 搬运 .data:将已初始化全局变量从 Flash 搬运到 RAM。
  3. 清零 .bss:将未初始化全局变量所在的 RAM 区域清零。

工作原理图示:

编译时(链接器):
┌─────────────────────┐
│   .sct 文件          │
│   (分散加载描述)      │
└─────────┬───────────┘
          │ armlink 读取
          ▼
┌─────────────────────────────────────────────────────┐
│            分散加载描述表(Region Table)              │
│                                                     │
│  ┌──────────┬──────────────┬──────────────┬────────┐│
│  │ 段名称    │ 加载地址(LMA) │ 执行地址(VMA) │ 大小   ││
│  ├──────────┼──────────────┼──────────────┼────────┤│
│  │ .data    │ 0x08010000   │ 0x20000000   │ 0x100  ││
│  │ .data2   │ 0x08010100   │ 0x20000100   │ 0x200  ││
│  │ .bss     │ N/A          │ 0x20000300   │ 0x300  ││
│  └──────────┴──────────────┴──────────────┴────────┘│
└─────────────────────────────────────────────────────┘
          │ 编译到 .axf 中
          ▼

运行时(__scatterload):
┌─────────────────────────────────────────────────────┐
│  for each region in Region Table:                   │
│      if (region.type == RW):                        │
│          memcpy(region.VMA, region.LMA, region.size) │
│      if (region.type == ZI):                        │
│          memset(region.VMA, 0, region.size)         │
└─────────────────────────────────────────────────────┘

实际搬运过程:

搬运前:
  Flash:  [0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ...]  ← .data 初始值
  RAM:    [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...]  ← 未初始化

搬运后:
  Flash:  [0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ...]  ← .data 初始值(保留)
  RAM:    [0x42, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, ...]  ← .data 运行时值
          [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ...]  ← .bss 已清零

💡 工程师经验: __scatterload 的执行时间取决于需要搬运的数据量。如果一个项目有大量的已初始化全局变量(比如大的查找表),启动时间会明显变长。优化方法:将大的只读查找表声明为 const(放入 +RO 段,不需要搬运),或者使用 __attribute__((section("AT_FLASH"))) 配合手动初始化。

4.3 __rt_entry

__rt_entry 是 C 运行时库的入口函数,在 __scatterload 完成后调用。它负责:

  1. 堆栈初始化:根据 .sct 中的堆栈配置设置 Heap 和 Stack
  2. C 库初始化:初始化 stdiomalloc 等标准库功能
  3. 浮点初始化:配置浮点运算单元(如果使用 FPU)
  4. C++ 构造函数:调用全局 C++ 对象的构造函数(C++ 项目)
  5. 调用 main():最终跳转到用户编写的 main() 函数
__rt_entry 执行流程:
    │
    ├── __rt_stackheap_init()
    │     设置 Heap Base, Heap Limit, Stack Base, Stack Limit
    │
    ├── __rt_lib_init()
    │     ├── __fp_init()           ← FPU 初始化
    │     ├── __init_alloc()        ← 内存分配器初始化
    │     ├── __init_stdio()        ← 标准I/O初始化
    │     └── __cpp_initialize__()  ← C++ 构造函数
    │
    └── main()                      ← 跳转到用户 main()

💡 工程师经验: 在 Keil 的启动文件(如 startup_stm32f407xx.s)中,Stack_SizeHeap_Size 是通过汇编定义的。链接器会根据这些值自动配置 __rt_entry 中的堆栈参数。如果你需要精确控制堆栈大小,可以直接在 .sct 文件中定义,或者在启动文件中修改 Stack_SizeHeap_Size 的值。

4.4 Keil 的启动流程总结

将上述内容串联起来,完整的启动流程如下:

上电复位
    │
    ▼
CPU 从 0x00000000 读取初始 SP(MSP)
CPU 从 0x00000004 读取 Reset_Handler 地址
    │
    ▼
Reset_Handler (startup_stm32f407xx.s)
    │
    ├── 配置系统时钟 (调用 SystemInit)
    ├── 拷贝 .data 段(如果启动文件中有手动拷贝代码)
    ├── 清零 .bss 段(如果启动文件中有手动清零代码)
    │
    ▼
调用 __main(链接器生成)
    │
    ├── __scatterload
    │     ├── 根据 .sct 描述表搬运 .data
    │     └── 根据 .sct 描述表清零 .bss
    │
    ├── __rt_entry
    │     ├── 堆栈初始化
    │     ├── C 库初始化
    │     └── C++ 构造函数
    │
    └── main()
          │
          ▼
        用户程序开始运行

💡 工程师经验: 注意上面的流程中,ARMCC 的启动文件(startup_stm32f407xx.s)中通常也包含 .data 拷贝和 .bss 清零的代码。但实际上,当使用分散加载文件时,__scatterload 会完成这些工作。这意味着 .data 拷贝和 .bss 清零实际上会被执行两次。不过不用担心,重复执行不会造成问题(已初始化的值被覆盖为相同的值,已清零的区域再次清零)。Keil 官方模板中的启动文件保留了这些代码是为了兼容不使用 .sct 文件的情况。


五、Keil 自动链接符号

5.1 Image$$ 符号体系

ARMCC 链接器会为每个执行域自动生成一组符号,这些符号以 Image$$ 为前缀,可以在 C 代码中直接引用。这些符号对于获取段的地址和大小非常有用。

符号命名规则:

Image$$执行域名称$$属性

每个执行域自动生成以下 6 个符号:

符号 含义 说明
Image$$ER_IROM1$$Base 执行域的起始地址 对应 +RO 段的起始地址
Image$$ER_IROM1$$Length 执行域的总长度(字节) 包含 +RO + RW + ZI
Image$$ER_IROM1$$Limit 执行域的结束地址 Base + Length
Image$$ER_IROM1$$ZI$$Base ZI 段的起始地址 .bss 段的起始地址
Image$$ER_IROM1$$ZI$$Length ZI 段的长度(字节) .bss 段的大小
Image$$ER_IROM1$$ZI$$Limit ZI 段的结束地址 ZI B a s e + Z I Base + ZI Base+ZILength

对于数据执行域(如 ER_IRAM1),还有额外的符号:

符号 含义
Image$$ER_IRAM1$$RW$$Base RW 段(.data)的起始地址
Image$$ER_IRAM1$$RW$$Length RW 段(.data)的长度
Image$$ER_IRAM1$$RW$$Limit RW 段(.data)的结束地址

💡 工程师经验: 这些符号在 .map 文件的 “Image Symbol Table” 部分可以查看到完整的列表。如果你不确定某个符号是否存在,先查看 .map 文件。

5.2 在 C 代码中使用自动符号

在 C 代码中,需要通过 extern 声明来引用这些符号:

#include <stdint.h>

/* 声明链接器自动生成的符号 */
extern uint32_t Image$$ER_IROM1$$Base;
extern uint32_t Image$$ER_IROM1$$Length;
extern uint32_t Image$$ER_IROM1$$Limit;

extern uint32_t Image$$ER_IRAM1$$RW$$Base;
extern uint32_t Image$$ER_IRAM1$$RW$$Length;
extern uint32_t Image$$ER_IRAM1$$RW$$Limit;
extern uint32_t Image$$ER_IRAM1$$ZI$$Base;
extern uint32_t Image$$ER_IRAM1$$ZI$$Length;
extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;

/* 使用示例:打印内存布局信息 */
void print_memory_layout(void)
{
    uint32_t code_base    = (uint32_t)&Image$$ER_IROM1$$Base;
    uint32_t code_size    = (uint32_t)&Image$$ER_IROM1$$Length;
    uint32_t rw_base      = (uint32_t)&Image$$ER_IRAM1$$RW$$Base;
    uint32_t rw_size      = (uint32_t)&Image$$ER_IRAM1$$RW$$Length;
    uint32_t zi_base      = (uint32_t)&Image$$ER_IRAM1$$ZI$$Base;
    uint32_t zi_size      = (uint32_t)&Image$$ER_IRAM1$$ZI$$Length;

    printf("Code:  0x%08X - Size: %u bytes\r\n", code_base, code_size);
    printf("RW:    0x%08X - Size: %u bytes\r\n", rw_base, rw_size);
    printf("ZI:    0x%08X - Size: %u bytes\r\n", zi_base, zi_size);
}

⚠️ 注意事项: 声明 Image$$ 符号时,类型使用 uint32_t(或 unsigned int),但取地址时使用 & 运算符。这是因为链接器生成的符号是地址,在 C 中需要取其地址来获取实际值。这是 ARMCC 的特殊用法,不要尝试直接使用符号名而不加 &

5.3 实用技巧:计算可用堆空间

利用 Image$$ 符号,可以精确计算系统剩余的可用 RAM 空间:

#include <stdint.h>
#include <stdio.h>

/* 链接器符号声明 */
extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;  /* ZI 段结束地址 */
extern uint32_t Image$$RW_IRAM1$$ZI$$Limit;  /* 另一种命名格式 */

/*
 * 获取可用堆空间大小
 * 计算方法:RAM 结束地址 - ZI 段结束地址 - 栈大小
 */
uint32_t get_free_heap_size(void)
{
    extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;

    /* RAM 区域结束地址 */
    const uint32_t RAM_END = 0x20020000;  /* 0x20000000 + 128KB */

    /* 栈大小(与启动文件中的 Stack_Size 一致) */
    extern uint32_t Image$$ER_IRAM1$$ZI$$Limit;

    /* 栈顶地址(RAM 的最高地址) */
    uint32_t stack_top = RAM_END;

    /* ZI 段结束地址(栈底地址) */
    uint32_t zi_end = (uint32_t)&Image$$ER_IRAM1$$ZI$$Limit;

    /* 可用堆空间 = 栈底 - ZI 段结束地址 */
    uint32_t free_heap = stack_top - zi_end;

    return free_heap;
}

/* 在 main() 中使用 */
int main(void)
{
    printf("Free heap: %u bytes (%u KB)\r\n",
           get_free_heap_size(),
           get_free_heap_size() / 1024);

    while (1) {
    }
}

内存布局示意:

RAM (0x20000000 - 0x2001FFFF)
┌──────────────────────────────────┐ 0x20000000
│ .data (RW 段)                    │
├──────────────────────────────────┤
│ .bss  (ZI 段)                    │
├──────────────────────────────────┤ ← Image$$ER_IRAM1$$ZI$$Limit
│                                  │
│         可用 Heap 空间            │ ← get_free_heap_size() 计算的区域
│                                  │
├──────────────────────────────────┤
│         Stack (栈)               │ ← 向低地址增长
└──────────────────────────────────┘ 0x2001FFFF (RAM_END)

💡 工程师经验: 在使用 RTOS(如 FreeRTOS、RT-Thread)时,堆空间的管理由 RTOS 接管。此时 Image$$ 符号可以帮助你确定可以给 RTOS 分配多少堆内存。建议在系统初始化时打印一次内存布局信息,方便后续排查内存不足的问题。


六、实战场景

6.1 Bootloader + App 双固件架构

在 OTA 升级场景中,通常需要将 Flash 划分为 Bootloader 和 App 两个区域。

Flash 分区规划(STM32F407VG, 1MB Flash):

Flash (0x08000000 - 0x080FFFFF)
┌──────────────────────────────┐ 0x08000000
│                              │
│       Bootloader (64KB)       │
│       0x08000000-0x0800FFFF  │
│                              │
├──────────────────────────────┤ 0x08010000
│                              │
│       App (896KB)             │
│       0x08010000-0x080EFFFF  │
│                              │
├──────────────────────────────┤ 0x080F0000
│                              │
│       参数区 (64KB)           │
│       0x080F0000-0x080FFFFF  │
│                              │
└──────────────────────────────┘ 0x08100000

Bootloader 的 .sct 文件:

; *************************************************************
; *** Bootloader Scatter File                               ***
; *** Flash: 0x08000000 - 0x0800FFFF (64KB)                ***
; *** SRAM:  0x20000000 - 0x2001FFFF (128KB)               ***
; *************************************************************

LR_IROM1 0x08000000 0x00010000
{
    ER_IROM1 0x08000000 0x00010000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }
}

App 的 .sct 文件:

; *************************************************************
; *** App Scatter File                                      ***
; *** Flash: 0x08010000 - 0x080EFFFF (896KB)               ***
; *** SRAM:  0x20000000 - 0x2001FFFF (128KB)               ***
; *************************************************************

LR_IROM1 0x08010000 0x000E0000
{
    ER_IROM1 0x08010000 0x000E0000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }
}

App 中设置中断向量表偏移(VTOR):

/* 在 App 的 SystemInit() 或 main() 开头设置 VTOR */
#define APP_FLASH_BASE  0x08010000U

void main(void)
{
    /* 设置向量表偏移到 App 起始地址 */
    SCB->VTOR = APP_FLASH_BASE;

    /* 后续初始化代码... */
    HAL_Init();
    SystemClock_Config();

    /* 用户应用逻辑 */
    while (1) {
    }
}

💡 工程师经验: VTOR(Vector Table Offset Register)必须在 App 初始化的最早期设置,最好在 SystemInit() 中就设置。如果在 VTOR 设置之前发生了中断,CPU 会跳转到 Bootloader 的中断处理函数,导致不可预期的行为。另外,App 的 .sctLR_IROM1 的起始地址必须与 VTOR 的值一致。

⚠️ 注意事项: App 编译时需要确保链接地址正确。在 Keil 的 Options for Target → C/C++ 选项卡中,如果定义了 VECT_TAB_OFFSET 宏,需要确保其值与 .sct 中的起始地址一致。例如,VECT_TAB_OFFSET=0x10000 对应 0x08010000

6.2 将函数放到 RAM 中执行

在某些场景下,需要将函数放到 RAM 中执行:

  • 提升性能:CCMRAM 和主 SRAM 的访问速度可能比 Flash 快
  • Flash 擦写时执行:在 OTA 升级过程中擦写 Flash 时,代码不能在 Flash 中执行
  • 降低功耗:某些低功耗场景下关闭 Flash 加速器

步骤一:修改 .sct 文件,添加 RAM 执行域

LR_IROM1 0x08000000 0x00100000
{
    ER_IROM1 0x08000000 0x00100000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }

    ; RAM 执行域:用于存放需要在 RAM 中执行的函数
    ; 注意:这个域的属性是 +RO(代码),但地址是 RAM
    ER_RAM_CODE 0x20000000 0x00001000
    {
        *.o (RAM_CODE_SECTION)
    }
}

步骤二:在 C 代码中使用 __attribute__((section))

/* 将函数放到 RAM 中执行 */
__attribute__((section("RAM_CODE_SECTION")))
void flash_erase_page(uint32_t page_addr)
{
    /* 解锁 Flash */
    HAL_FLASH_Unlock();

    /* 清除错误标志 */
    __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR |
                           FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR |
                           FLASH_FLAG_PGPERR | FLASH_FLAG_PGSERR);

    /* 擦除页 */
    FLASH_EraseInitTypeDef erase_init;
    uint32_t page_error = 0;

    erase_init.TypeErase    = FLASH_TYPEERASE_SECTORS;
    erase_init.Sector       = page_addr;
    erase_init.NbSectors    = 1;
    erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;

    HAL_FLASHEx_Erase(&erase_init, &page_error);

    /* 锁定 Flash */
    HAL_FLASH_Lock();
}

/* 将关键算法放到 RAM 中执行以提升性能 */
__attribute__((section("RAM_CODE_SECTION")))
void fast_fft_process(int16_t *input, int16_t *output, uint32_t len)
{
    /* FFT 算法实现... */
    /* 在 RAM 中执行可以避免 Flash 等待周期 */
}

RAM 执行函数的内存布局:

Flash (0x08000000)              RAM (0x20000000)
┌──────────────────┐            ┌──────────────────┐
│ RESET 向量表      │            │ RAM_CODE_SECTION │
│ 普通代码 +RO     │            │ (flash_erase_page│
│                  │  搬运      │  fast_fft_process│
│ RAM_CODE 函数    │ ─────────▶ │  等函数的副本)    │
│ 初始值副本       │ __scatter  │                  │
│ (加载时存储)     │   load     ├──────────────────┤
├──────────────────┤            │ .data / .bss     │
│ .data 初始值     │            │ Heap / Stack     │
└──────────────────┘            └──────────────────┘

💡 工程师经验: 将函数放到 RAM 中执行会同时消耗 Flash 和 RAM:Flash 中存储函数的初始值副本(加载域),RAM 中存储运行时代码(执行域)。因此需要权衡性能提升与内存开销。通常只将少量关键函数放到 RAM 中,而不是整个程序。

⚠️ 注意事项: 使用 RAM 执行函数时,函数内部调用的其他函数也需要在 RAM 中,否则调用链中只要有一环在 Flash 中,就无法在 Flash 擦写时安全执行。建议将 RAM 执行函数设计为自包含的(不调用外部函数),或者将整个调用链都放到 RAM 中。

6.3 使用 CCMRAM 存放高速数据

STM32F4 系列的 CCMRAM(Core Coupled Memory)是连接在 CPU D-Bus 上的 64KB RAM,CPU 访问零等待状态,但 DMA 不可访问

.sct 配置:

LR_IROM1 0x08000000 0x00100000
{
    ER_IROM1 0x08000000 0x00100000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }

    ; CCMRAM 执行域
    ER_IRAM2 0x10000000 0x00010000
    {
        *.o (CCMRAM_SECTION)
    }
}

C 代码使用示例:

/* CRC 查找表 - 放到 CCMRAM 加速计算 */
__attribute__((section("CCMRAM_SECTION")))
static const uint32_t crc32_table[256] = {
    0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
    /* ... 完整的 CRC32 查找表 ... */
};

/* 高频访问的缓冲区 */
__attribute__((section("CCMRAM_SECTION")))
static uint32_t adc_filter_buffer[1024];

/* 图像处理中间缓冲区 */
__attribute__((section("CCMRAM_SECTION")))
static uint8_t image_temp_buffer[4096];

/* ❌ 错误示范:DMA 缓冲区不能放到 CCMRAM!*/
/*
__attribute__((section("CCMRAM_SECTION")))
static uint8_t uart_dma_rx_buffer[256];  // DMA 无法访问!
*/

/* ✅ 正确做法:DMA 缓冲区放到主 SRAM */
static uint8_t uart_dma_rx_buffer[256];  /* 默认放到主 SRAM */

CCMRAM 使用注意事项清单:

✅ 适合放到 CCMRAM 的数据:
   - CRC/加密查找表
   - 高频访问的滤波器缓冲区
   - 图像/音频处理中间结果
   - 频繁读写的计算变量

❌ 不能放到 CCMRAM 的数据:
   - DMA 缓冲区(UART/SPI/I2C DMA)
   - ADC/DAC DMA 缓冲区
   - 以太网 DMA 缓冲区
   - 需要被其他总线 Master 访问的数据

⚠️ 注意事项: CCMRAM 在 STM32F4 中的地址是 0x10000000,它不在主 SRAM 的地址范围内。如果使用 MPU(Memory Protection Unit),需要为 CCMRAM 单独配置区域。此外,CCMRAM 在睡眠模式下可能会丢失数据(取决于具体型号),需要查阅数据手册确认。

6.4 自定义段放置

除了预定义的段类型,ARMCC 还支持通过 __attribute__((section)) 创建自定义段,并在 .sct 文件中精确控制其放置位置。

通用方法:

/* 定义自定义段变量 */
__attribute__((section("MY_CUSTOM_SECTION")))
uint32_t custom_var1 = 0x12345678;

__attribute__((section("MY_CUSTOM_SECTION")))
uint32_t custom_var2 = 0xABCDEF00;

__attribute__((section("MY_FAST_SECTION")))
uint32_t fast_var1;

对应的 .sct 配置:

LR_IROM1 0x08000000 0x00100000
{
    ER_IROM1 0x08000000 0x00100000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }

    ; 自定义段:精确控制变量位置
    ER_CUSTOM 0x20000000 0x00001000
    {
        *.o (MY_CUSTOM_SECTION)
    }
}

使用 Image$$ 符号访问自定义段:

extern uint32_t Image$$ER_CUSTOM$$Base;
extern uint32_t Image$$ER_CUSTOM$$Length;

void process_custom_section(void)
{
    uint32_t *base = (uint32_t *)&Image$$ER_CUSTOM$$Base;
    uint32_t len   = (uint32_t)&Image$$ER_CUSTOM$$Length;

    printf("Custom section at 0x%08X, size %u bytes\r\n",
           (uint32_t)base, len);

    /* 遍历自定义段中的所有变量 */
    for (uint32_t i = 0; i < len / 4; i++) {
        printf("  [%u] = 0x%08X\r\n", i, base[i]);
    }
}

💡 工程师经验: 自定义段在模块化设计中非常有用。例如,你可以定义一个 CONFIG_SECTION,将所有配置参数集中放在一个段中,然后通过 Image$$ 符号统一遍历和序列化,实现配置的保存和加载。

6.5 多 App 分区方案

在更复杂的 OTA 升级方案中,可能需要 App1、App2 两个分区实现 A/B 升级:

Flash 分区规划:

Flash (STM32F407VG, 1MB)
┌──────────────────────────────┐ 0x08000000
│       Bootloader (32KB)       │
│       0x08000000-0x08007FFF  │
├──────────────────────────────┤ 0x08008000
│                              │
│       App1 (464KB)            │
│       0x08008000-0x0807FFFF  │
│                              │
├──────────────────────────────┤ 0x08080000
│                              │
│       App2 (464KB)            │
│       0x08080000-0x080FFFFF  │
│                              │
├──────────────────────────────┤ 0x08100000
│                              │
│       参数/日志区 (64KB)       │
│       0x08100000-0x0810FFFF  │
│                              │
└──────────────────────────────┘

App1 的 .sct 文件:

; *************************************************************
; *** App1 Scatter File                                     ***
; *** Flash: 0x08008000 - 0x0807FFFF (464KB)               ***
; *** SRAM:  0x20000000 - 0x2001FFFF (128KB)               ***
; *************************************************************

LR_IROM1 0x08008000 0x00078000
{
    ER_IROM1 0x08008000 0x00078000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }
}

App2 的 .sct 文件:

; *************************************************************
; *** App2 Scatter File                                     ***
; *** Flash: 0x08080000 - 0x080FFFFF (464KB)               ***
; *** SRAM:  0x20000000 - 0x2001FFFF (128KB)               ***
; *************************************************************

LR_IROM1 0x08080000 0x00078000
{
    ER_IROM1 0x08080000 0x00078000
    {
        *.o (RESET, +First)
        *(InRoot$$Sections)
        .ANY (+RO)
    }

    ER_IRAM1 0x20000000 0x00020000
    {
        .ANY (+RW +ZI)
    }
}

💡 工程师经验: App1 和 App2 的 .sct 文件结构完全相同,只是 LR_IROM1ER_IROM1 的起始地址不同。在实际项目中,通常通过 Keil 的工程配置或构建脚本来切换不同的 .sct 文件。建议使用宏定义来管理不同的 App 版本,避免维护两份几乎相同的代码。


七、链接文件调试技巧

7.1 查看 Keil 生成的 .map 文件

.map 文件是链接器输出的最重要文件之一,包含了完整的内存布局信息。在 Keil 的 Options for Target → Listing 选项卡中勾选 “Linker Listing” 即可生成。

.map 文件关键部分解读:

===============================================================================
   Memory Map of the image

  Image Entry Point : 0x08000189

  Load Region LR_IROM1 (Base: 0x08000000, Size: 0x00001234, Max: 0x00100000, ABSOLUTE)

    Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00001000, Max: 0x00100000, ABSOLUTE)
    Base Addr    Size         Type   Attr      Idx    E Section Name        Object
    0x08000000   0x00000188   Data   RO          776    RESET               startup_stm32f407xx.o
    0x08000188   0x00000000   Zero   RO            0    * .ARM.Collect$$$$00000000  (from __scatter.o)
    ...
    0x080001C0   0x00000234   Code   RO         1234    i.main              main.o
    ...

    Execution Region ER_IRAM1 (Base: 0x20000000, Size: 0x00000800, Max: 0x00020000, ABSOLUTE)
    Base Addr    Size         Type   Attr      Idx    E Section Name        Object
    0x20000000   0x00000100   Data   RW          890    .data               main.o
    0x20000100   0x00000300   Zero   RW          891    .bss                main.o
    ...

===============================================================================
   Image Symbol Table

    Local Symbols
    Symbol Name                              Value     Ov Type        Size  Object(Section)
    ...
    Image$$ER_IROM1$$Base                   0x08000000   Number         0  anon$$obj.o(ER_IROM1)
    Image$$ER_IROM1$$Length                 0x00001000   Number         0  anon$$obj.o(ER_IROM1)
    Image$$ER_IROM1$$Limit                  0x08001000   Number         0  anon$$obj.o(ER_IROM1)
    Image$$ER_IROM1$$ZI$$Base               0x20000100   Number         0  anon$$obj.o(ER_IRAM1)
    Image$$ER_IROM1$$ZI$$Length             0x00000300   Number         0  anon$$obj.o(ER_IRAM1)
    Image$$ER_IROM1$$ZI$$Limit              0x20000400   Number         0  anon$$obj.o(ER_IRAM1)
    ...

===============================================================================
   Image component sizes

      Code (inc. data)   RO Data    RW Data    ZI Data      Debug
    125,432    12,345     3,456      1,024      8,192   234,567   Grand Totals
    ...
      Total RO  Size (Code + RO Data)           128,888 ( 125.88 kB)
      Total RW  Size (RW Data + ZI Data)         9,216 (   9.00 kB)
      Total ROM Size (Code + RO Data + RW Data) 129,912 ( 126.87 kB)
===============================================================================

关键信息说明:

区域 含义 关注点
Memory Map 完整的内存布局 每个段的确切地址和大小
Image Symbol Table 所有链接符号 Image$$ 符号的值
Image component sizes 各类数据大小汇总 Flash 和 RAM 的总消耗
Total RO Size Flash 中代码+只读数据大小 决定 .bin 文件大小
Total RW Size RAM 中读写数据大小 决定 RAM 消耗
Total ROM Size Flash 总占用 = RO + RW(RW 初始值也在 Flash 中)

💡 工程师经验: 每次编译后都应该快速浏览 .map 文件底部的 “Image component sizes” 部分,关注 Total ROM SizeTotal RW Size 的变化。如果发现 Flash 或 RAM 占用突然增大,可以在这里快速定位是哪个模块的增长导致的。

7.2 使用 fromelf 工具分析段布局

fromelf 是 ARMCC 工具链自带的命令行工具,可以用来分析 .axf 文件的段信息。

常用命令:

# 查看所有段的大小
fromelf --text --section_sizes project.axf

# 查看详细的段信息
fromelf --text -z project.axf

# 生成反汇编文件
fromelf --text -c -o project_disasm.txt project.axf

# 生成二进制文件
fromelf --bin -o project.bin project.axf

# 生成 Intel HEX 文件
fromelf --i32 -o project.hex project.axf

# 查看符号表
fromelf --text -s project.axf

# 查看指定段的详细信息
fromelf --text --section=ER_IROM1 project.axf

输出示例:

==============================================================================
** Section #1 'ER_IROM1' (SHT_PROGBITS) [SHF_ALLOC + SHF_EXECINSTR]
   Size   : 128888 bytes (126.88 kB)
   Max    : 1048576 bytes (1024.00 kB)
   Base   : 0x08000000
   Attrs  : Code, Read-Only, Execute
==============================================================================
** Section #2 'ER_IRAM1' (SHT_NOBITS) [SHF_ALLOC + SHF_WRITE]
   Size   : 9216 bytes (9.00 kB)
   Max    : 131072 bytes (128.00 kB)
   Base   : 0x20000000
   Attrs  : Read-Write, Zero-Init
==============================================================================

💡 工程师经验: 在 CI/CD 流水线中,可以使用 fromelf --text --section_sizes 自动提取段大小信息,并与预设的阈值进行比较。如果 Flash 或 RAM 占用超过阈值,自动触发编译告警或失败,防止固件超出硬件限制。

7.3 常见链接错误及解决方法

以下是 ARMCC 链接器最常见的错误及其解决方法:

错误代码 含义 常见原因 解决方法
L6236E No section matches selector .sct 中的模块选择规则没有匹配到任何段 检查 .sct 中的段名是否与代码中的 section 属性一致
L6218E Undefined symbol 引用了未定义的符号 检查是否缺少源文件或库文件;检查函数名拼写
L6406E No space in execution region 执行域空间不足 增大执行域大小或减少代码/数据量
L6220E Load region size exceeds limit 加载域超出最大大小 增大加载域的 Max 值或减少 Flash 占用
L6248E Cannot place region 无法放置执行域 检查执行域地址是否在有效范围内
L6306E Type mismatch 段类型不匹配 检查 .sct 中的属性是否与实际段类型一致
L6667E Invalid line in scatter file .sct 文件语法错误 检查语法格式,注意括号匹配和地址格式

典型错误分析:

错误示例 1:L6406E
┌─────────────────────────────────────────────────────────┐
│ .\Objects\project.axf: Error: L6406E: No space in      │
│ execution region ER_IROM1 with size 0x00010000;         │
│ region needs to be at least 0x00012000 bytes            │
└─────────────────────────────────────────────────────────┘

原因:代码大小(0x12000 = 72KB)超过了执行域分配的空间(0x10000 = 64KB)
解决:
  方法1:增大执行域大小
          ER_IROM1 0x08000000 0x00020000  ← 从 64KB 改为 128KB
  方法2:优化代码,减小体积
          - 开启编译优化 -O2
          - 移除未使用的函数(-ffunction-sections + .ANY)
          - 检查是否有大的查找表可以放到 Flash 外部
错误示例 2:L6236E
┌─────────────────────────────────────────────────────────┐
│ .\Objects\project.axf: Error: L6236E: No section       │
│ matches selector - no section to be FIRST/LAST          │
└─────────────────────────────────────────────────────────┘

原因:*.o (RESET, +First) 没有找到 RESET 段
解决:
  方法1:确保启动文件(startup_stm32f407xx.s)已添加到工程
  方法2:检查启动文件中是否正确定义了 RESET 段
  方法3:如果使用自定义向量表,确保段名与 .sct 中一致
错误示例 3:L6218E
┌─────────────────────────────────────────────────────────┐
│ .\Objects\project.axf: Error: L6218E: Undefined symbol  │
│ HAL_GPIO_WritePin (referred from main.o)                │
└─────────────────────────────────────────────────────────┘

原因:main.c 调用了 HAL_GPIO_WritePin 但没有链接 HAL 库
解决:
  方法1:将 stm32f4xx_hal_gpio.c 添加到工程
  方法2:检查是否在 stm32f4xx_hal_conf.h 中启用了 HAL_GPIO_MODULE_ENABLED
  方法3:检查文件是否被排除编译(灰色图标)

💡 工程师经验: 遇到链接错误时,首先阅读错误信息的完整描述,不要只看错误代码。ARMCC 的错误信息通常包含足够的上下文来定位问题。如果错误信息不够明确,可以查看 .map 文件中的内存布局,确认各段的大小和地址是否合理。

7.4 使用 __attribute__((section)) 的注意事项

__attribute__((section("name"))) 是将变量或函数放到指定段的标准方法,但使用时需要注意以下事项:

1. 命名规则

/* ✅ 正确:段名使用大写字母和下划线 */
__attribute__((section("CCMRAM_SECTION")))
__attribute__((section("RAM_CODE_SECTION")))
__attribute__((section("MY_CUSTOM_DATA")))

/* ❌ 错误:段名包含特殊字符 */
__attribute__((section("my-section")))    /* 连字符不合法 */
__attribute__((section("my section")))    /* 空格不合法 */

2. 对齐要求

/* 指定对齐方式 */
__attribute__((section("MY_SECTION"), aligned(4)))
uint32_t aligned_var;

__attribute__((section("MY_SECTION"), aligned(32)))
uint32_t cache_aligned_buffer[8];

/* DMA 缓冲区通常需要 4 字节对齐 */
__attribute__((section("DMA_BUFFER"), aligned(4)))
uint8_t uart_rx_buffer[256];

3. 与 .sct 的配合

/* C 代码中的段名必须与 .sct 中的段名完全一致(区分大小写) */

/* C 代码 */
__attribute__((section("MY_FAST_DATA")))
uint32_t fast_array[128];

/* .sct 文件 */
ER_FAST 0x20000000 0x00001000
{
    *.o (MY_FAST_DATA)    /* 必须与 C 代码中的段名一致 */
}

4. 初始化变量的特殊处理

/* 已初始化变量放到自定义段时,链接器会自动处理搬运 */
__attribute__((section("MY_INIT_DATA")))
uint32_t config_data[] = {0x01, 0x02, 0x03, 0x04};

/* 未初始化变量放到自定义段时,链接器会自动清零 */
__attribute__((section("MY_ZERO_DATA")))
uint32_t work_buffer[256];

⚠️ 注意事项: __attribute__((section)) 的作用域是符号级别的,不是文件级别的。每个需要放到自定义段的变量或函数都需要单独添加属性。如果忘记添加,变量会被放到默认的执行域中,而不是你期望的位置。建议在代码审查时特别关注这一点。

💡 工程师经验: 可以使用宏来简化段属性的书写,提高代码可读性:

/* 定义宏简化段属性 */
#define PLACE_IN_CCMRAM  __attribute__((section("CCMRAM_SECTION")))
#define PLACE_IN_RAMCODE __attribute__((section("RAM_CODE_SECTION")))
#define PLACE_IN_FAST    __attribute__((section("FAST_DATA_SECTION"), aligned(32)))

/* 使用宏 */
PLACE_IN_CCMRAM uint32_t fast_buffer[1024];
PLACE_IN_RAMCODE void critical_function(void) { /* ... */ }
PLACE_IN_FAST uint32_t cache_friendly_array[64];

总结

本文从分散加载文件的核心概念出发,深入剖析了 ARMCC 链接文件的语法规则、链接器自动生成的代码机制、Image$$ 符号体系,以及多个实战场景的配置方法。以下是核心要点回顾:

  1. 加载域(LR)= 烧录布局,执行域(ER)= 运行布局。当两者地址不同时,链接器自动生成搬运代码。

  2. +RO 是代码和常量,+RW 是已初始化变量,+ZI 是未初始化变量。+RW 同时消耗 Flash(存储初始值)和 RAM(运行时使用)。

  3. __main 不是用户的 main()。它是链接器生成的入口,负责调用 __scatterload(搬运数据)和 __rt_entry(C 运行时初始化)后,才跳转到用户的 main()

  4. Image$$ 符号是获取段信息的利器。可以用来计算可用堆空间、遍历自定义段、实现配置参数的序列化等。

  5. 手动编写 .sct 是 Bootloader、OTA、CCMRAM 等场景的必备技能。掌握模块选择规则和执行域属性的配置,可以精确控制程序的内存布局。

  6. 善用 .map 文件和 fromelf 工具。每次编译后检查段大小变化,是预防内存溢出的最佳实践。

参考资源:

  • ARM Compiler v5 armlink User Guide - Scatter File Syntax
  • ARM Compiler v5 armasm User Guide
  • STM32F407xx Reference Manual (RM0090)
  • Keil MDK Documentation - Linker Reference
  • ARM DUI 0377B - Using the Linker

如果这篇文章对你有帮助,欢迎点赞收藏关注!

📌 系列文章导航:

  • 📘 上一篇:STM32F407 启动文件深度解析
  • 📗 本文:Keil ARMCC 链接文件(.sct)深度解析
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐