【Linker 硬核】突破 Flash 速度瓶颈:深度解析 LMA/VMA 与“代码驻 RAM”的链接脚本黑魔法
摘要:在高性能嵌入式系统中,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
这是链接脚本最烧脑,也最迷人的地方。每一个符号(函数/变量)都有两个地址:
-
LMA (Load Memory Address):“出生地”。
-
程序烧录后,代码存储在 Flash 中的物理地址。
-
掉电后它住在这里。
-
-
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 中运行代码有一个隐蔽的坑:中断向量表 和 绝对地址跳转。
-
中断向量表:如果你的 RAM 代码里有中断服务程序 (ISR),你必须重映射中断向量表(
SCB->VTOR)到 RAM,否则 CPU 还是去 Flash 找中断入口。 -
长跳转:如果 RAM 代码调用了 Flash 里的函数(比如
printf),链接器会自动处理长跳转指令(Trampoline),不用担心。 -
调试困惑:在 Debugger 里,如果你在
Motor_FOC_Calc打断点,你会发现地址是0x0000xxxx。如果搬运没成功,断点会失效。
七、 总结:掌握时空的魔法
通过链接脚本的 LMA/VMA 分离技术,我们实际上是在操控时间(运行速度)与空间(存储位置)。
-
CMake 负责产生指令。
-
Linker Script 负责规划指令的时空坐标。
-
Startup Code 负责在时间开始之前(main之前)构建好空间。
当你掌握了这套组合拳,你就不再受限于芯片的数据手册。Flash 慢?那只是一个存储介质。只要 RAM 够大,任何 MCU 都可以飞起来
更多推荐


所有评论(0)