摘要:在高性能嵌入式系统中,Flash 的读取速度往往是系统的最大瓶颈。为了追求极致的实时性,资深工程师会将关键 ISR 或算法函数通过 __attribute__((section)) 强制重定位到 ITCM (紧耦合指令内存) 或 SRAM 中运行。本文将解构链接脚本中的 >RAM AT>FLASH 语法,揭示 Load Address 与 Virtual Address 的本质区别,并演示如何在启动阶段完成这“惊险一跃”的数据搬运。


一、 速度的鸿沟:为什么 CPU 在空转?

想象一下,你的 STM32H7 核心频率是 480MHz(周期 2ns)。 但是你的外部 QSPI Flash 读取速度只有 100MHz,而且还要经过总线仲裁。 这就好比开着法拉利(CPU)在乡村土路(Flash)上跑。

为了不让 CPU 频繁停下来等指令(Stall),我们必须把最常用的那段路修成高速公路。 在 MCU 内部,有一块速度极快、和 CPU 同频的内存,通常叫 ITCM (Instruction Tightly Coupled Memory) 或 CCM RAM。 我们的目标:代码平时存在 Flash 里(掉电不丢),上电后搬到 ITCM 里(运行极快)。


二、 核心概念:LMA vs VMA

这是链接脚本最烧脑,也最迷人的地方。每一个符号(函数/变量)都有两个地址:

  1. LMA (Load Memory Address):“出生地”。

    • 程序烧录后,代码存储在 Flash 中的物理地址。

    • 掉电后它住在这里。

  2. VMA (Virtual Memory Address):“工作地”。

    • 程序运行时,CPU 去哪里找这个指令的地址。

    • 这是链接器在解析符号引用时使用的地址。

魔法在于: 对于普通代码,LMA == VMA(都在 Flash)。 对于 RAM 代码,LMA 在 Flash,VMA 在 RAM。


三、 实战:编写链接脚本 (.ld)

我们需要在 .ld 文件中定义一个特殊的 Section。 假设我们想把代码放到 RAM_ITCM 区域。

/* 1. 定义内存区域 */
MEMORY
{
  FLASH    (rx) : ORIGIN = 0x08000000, LENGTH = 1024K
  RAM_ITCM (rwx): ORIGIN = 0x00000000, LENGTH = 64K
  RAM_DTCM (rwx): ORIGIN = 0x20000000, LENGTH = 128K
}

/* 2. 定义段的映射 */
SECTIONS
{
  /* 普通代码放在 FLASH */
  .text :
  {
    *(.text*)
  } >FLASH

  /* 【关键】关键代码段 */
  /* >RAM_ITCM : 表示 VMA (运行地址) 在 RAM */
  /* AT> FLASH : 表示 LMA (存储地址) 在 FLASH */
  .fast_code :
  {
    . = ALIGN(4);
    _sfast_code = .;       /* 导出 VMA 起始符号 */
    *(.fast_run_func)      /* 收集所有标记为 .fast_run_func 的函数 */
    . = ALIGN(4);
    _efast_code = .;       /* 导出 VMA 结束符号 */
  } >RAM_ITCM AT>FLASH

  /* 获取该段在 Flash 中的起始地址 (LMA) */
  _sfast_code_load = LOADADDR(.fast_code);
}

这段脚本告诉链接器: “把所有标记为 .fast_run_func 的函数,逻辑地址算在 0x00000000 (RAM),但物理存储塞进 0x08xxxxx (Flash)。”


四、 C 代码的配合:指定 Section

在 C 代码中,我们需要用编译器属性将关键函数“打标签”。

// 定义一个宏,方便使用
#define RAM_FUNC __attribute__((section(".fast_run_func"))) __attribute__((noinline))

// 这是一个电机控制的关键计算函数
// 它将被链接到 RAM 地址空间
RAM_FUNC void Motor_FOC_Calc(float current) {
    // 这里的指令读取将是 0 等待周期的!
    // ... complex math ...
}

五、 最后的拼图:启动搬运 (Startup Copy)

链接器只是规划了地图,它不会自动帮你搬砖。 如果在上电瞬间直接调用 Motor_FOC_Calc,CPU 会跳到 RAM 地址去执行,但此时 RAM 里全是随机乱码(因为还没人把代码搬过去),导致 HardFault。

我们必须在 main() 之前,手动把代码从 Flash (LMA) 拷贝到 RAM (VMA)。这通常在 Reset_Handler 或 SystemInit 中完成。

// 引用链接脚本导出的符号
extern uint32_t _sfast_code;      // RAM 中的开始地址 (VMA)
extern uint32_t _efast_code;      // RAM 中的结束地址 (VMA)
extern uint32_t _sfast_code_load; // Flash 中的源地址 (LMA)

void Copy_FastCode_To_RAM(void) {
    uint32_t *pSrc = &_sfast_code_load;
    uint32_t *pDest = &_sfast_code;
    
    // 计算需要搬运的大小
    uint32_t size = (uint32_t)&_efast_code - (uint32_t)&_sfast_code;
    
    // 开始搬运
    for (uint32_t i = 0; i < size / 4; i++) {
        *pDest++ = *pSrc++;
    }
}

// 在 main 之前调用
void SystemInit(void) {
    // ... 时钟初始化 ...
    Copy_FastCode_To_RAM(); // 【重要】搬运代码
    // ...
}

六、 进阶陷阱:GOT 与 绝对地址

在 RAM 中运行代码有一个隐蔽的坑:中断向量表 和 绝对地址跳转。

  1. 中断向量表:如果你的 RAM 代码里有中断服务程序 (ISR),你必须重映射中断向量表(SCB->VTOR)到 RAM,否则 CPU 还是去 Flash 找中断入口。

  2. 长跳转:如果 RAM 代码调用了 Flash 里的函数(比如 printf),链接器会自动处理长跳转指令(Trampoline),不用担心。

  3. 调试困惑:在 Debugger 里,如果你在 Motor_FOC_Calc 打断点,你会发现地址是 0x0000xxxx。如果搬运没成功,断点会失效。


七、 总结:掌握时空的魔法

通过链接脚本的 LMA/VMA 分离技术,我们实际上是在操控时间(运行速度)与空间(存储位置)。

  1. CMake 负责产生指令。

  2. Linker Script 负责规划指令的时空坐标。

  3. Startup Code 负责在时间开始之前(main之前)构建好空间。

当你掌握了这套组合拳,你就不再受限于芯片的数据手册。Flash 慢?那只是一个存储介质。只要 RAM 够大,任何 MCU 都可以飞起来

更多推荐