STM32代码执行位置性能对比:RAM与Flash的优化选择
2026/6/6 18:53:15 网站建设 项目流程

1. 项目概述:一个困扰嵌入式工程师的经典问题

在嵌入式开发,尤其是基于Cortex-M内核的MCU(如STM32)开发中,一个老生常谈但又时常让人困惑的问题是:代码究竟应该放在哪里执行,才能获得最佳的性能?是放在访问速度“理论上”更快的RAM里,还是放在非易失性的Flash里?这个问题看似简单,答案却并非一成不变。很多工程师凭直觉认为,RAM的访问速度远超Flash,代码在RAM中执行必然更快。但实际情况往往比直觉复杂得多,它涉及到CPU内核、总线架构、缓存机制、编译器优化以及代码本身的特性等多个层面的交互。

我自己在项目优化过程中也多次遇到这个抉择。比如,在为一个对实时性要求极高的电机控制算法寻找性能瓶颈时,我就曾尝试将关键循环体搬到RAM中执行,结果有时性能提升显著,有时却收效甚微,甚至偶尔还会变慢。这促使我深入去探究背后的原理,而不是停留在“RAM更快”的简单认知上。今天,我就结合一个具体的测试案例和STM32的架构,来彻底拆解这个问题,让你不仅知道“是什么”,更明白“为什么”,以及在实际项目中“怎么做”。

2. 测试环境与方法论:如何科学地比较执行速度

要回答“哪里更快”,空谈架构没有意义,我们必须设计一个可量化、可复现的测试。下面这个测试方法虽然简单,但抓住了问题的核心,并且避免了常见的测量陷阱。

2.1 测试核心思想与实现

测试的核心思想是测量一段固定代码在单位时间内的执行次数。次数越多,说明执行速度越快。

测试代码骨架如下:

volatile uint32_t sum1 = 0; // 使用volatile防止编译器过度优化 int main(void) { // 系统时钟、Systick等初始化 SysTick_Config(SystemCoreClock / 1000); // 配置Systick为1ms中断一次 // 其他外设初始化... while(1) { sum1++; // 核心测试语句 // 为了测试纯粹,循环体内尽量只有这一条有效语句 } } // Systick中断服务函数 void SysTick_Handler(void) { static uint8_t tick_count = 0; static uint32_t last_sum_value = 0; tick_count++; if (tick_count == 1000) { // 每隔1000ms(1秒)采样一次 uint32_t current_sum = sum1; uint32_t ops_per_second = current_sum - last_sum_value; // 计算一秒内的操作次数 last_sum_value = current_sum; tick_count = 0; // 可以通过串口打印 ops_per_second,或者用调试器观察此变量 } }

为什么这样设计?

  1. 操作简单sum1++操作在汇编层面对应几条清晰的指令(加载、递增、存储),易于分析。
  2. 避免溢出:使用足够大的uint32_t类型,在一秒内几乎不可能溢出。
  3. 时间测量:利用Cortex-M内核自带的Systick定时器,其精度与系统时钟同步,是测量短时间间隔的可靠工具。

2.2 关键测试技巧与避坑指南

直接测量从程序启动到第一秒的计数值是不准确的,因为从使能Systick到核心while(1)循环稳定执行,中间可能存在初始化代码、中断延迟等不可控因素。因此,科学的做法是测量稳定运行后,两个相邻1秒时间点之间的计数值差。正如测试方法中强调的:“观察第一秒到第二秒之间的计数效果”。这确保了测量的是纯粹的循环体执行时间,排除了启动阶段的干扰。

如何将代码定位到RAM或Flash执行?这是测试的前提。通常有两种方法:

  1. 链接脚本(Linker Script)修改:这是最常用和彻底的方法。例如,在GCC的链接脚本(.ld文件)中,你可以定义一个特殊的内存区域(如.ram_code),并将其VMA(虚拟内存地址)和LMA(加载内存地址)都设置为RAM的地址。然后在代码中,通过__attribute__((section(“.ram_code”)))将特定函数放入这个区域。这样,该函数在启动时会被加载到RAM(从Flash拷贝过来),并在RAM中执行。
  2. IDE配置:在一些集成开发环境(如Keil MDK、IAR EWARM)中,可以通过工程选项或分散加载文件(Scatter File)直观地指定某些源文件或函数在RAM中执行。

注意:将代码放到RAM中执行,意味着这部分代码在启动阶段需要从Flash拷贝到RAM,这会增加启动时间并占用宝贵的RAM空间。务必权衡利弊,通常只对最核心的性能瓶颈代码这样做。

2.3 测试平台与参数设定

本次参考的测试基于STM32(Cortex-M3内核),并设定了明确且合理的环境:

  • CPU频率:48MHz。这是一个典型的运行频率。
  • Flash加速配置预取缓冲区启用(Prefetch Buffer Enable)Flash延迟设置为2(Flash Latency=2)。这两项配置至关重要!在STM32中,当系统时钟超过一定值(例如24MHz)时,必须正确设置Flash等待周期(Latency),并启用预取指,否则CPU访问Flash会插入大量等待状态,性能急剧下降。这个配置是保证Flash性能的基础。

3. 测试结果呈现:一个反直觉的发现

在相同的硬件和基础配置下,我们得到了两组对比鲜明的数据。为了清晰,我将它们整理成下表:

测试场景代码优化等级执行位置每秒sum1++操作次数相对快慢
场景一无优化(-O0)RAM69,467
场景二无优化(-O0)Flash43,274
场景三速度优化(-O2/-O3)RAM98,993
场景四速度优化(-O2/-O3)Flash115,334

这个结果非常有意思,它直接挑战了“RAM无条件更快”的固有观念:

  1. 在无优化编译时,RAM中执行速度约为Flash的1.6倍,符合一般直觉。
  2. 在开启速度优化编译后,情况发生了逆转!Flash中执行速度反而比RAM中快了约16%。

为什么会有这样截然不同的结果?问题的关键就在于编译器优化改变了代码形态,进而影响了CPU访问指令和数据的方式,而RAM和Flash在系统总线上的位置不同,对这种访问模式的变化异常敏感。接下来,我们就深入到汇编和总线架构层面去揭秘。

4. 深度原理剖析:总线架构、优化与访问模式

要理解上述现象,我们必须先了解Cortex-M3(以及类似架构)的内存系统,特别是哈佛总线架构Flash加速机制

4.1 STM32的总线架构简析

以STM32F1系列(Cortex-M3)为例,其内部总线结构简化如下:

  • I-Code总线:专门用于从Flash存储器取指。这是一条高性能总线。
  • D-Code总线:专门用于从Flash存储器取数据(如常量、字面量)。这也是一条高性能总线。
  • 系统总线:用于访问内存(SRAM)和外设。注意,从RAM中取指令也需要通过这条总线

这意味着什么?当代码在Flash中执行时,CPU可以通过独立的I-Code和D-Code总线并行地取指和取数据。而当代码在RAM中执行时,无论取指还是取数据,都需要竞争同一条系统总线。这是Flash在执行代码时的一个潜在架构优势。

4.2 无优化代码(-O0)为何在Flash中慢?

让我们看看无优化时,sum1++可能对应的汇编代码序列(概念性示意):

; 假设 sum1 的地址存储在 Flash 的某个常量池中 LDR R0, [PC, #0x154] ; (1) 从Flash(通过D-Code总线)加载sum1的地址到R0 LDR R1, [PC, #0x154] ; (2) 从Flash(通过D-Code总线)加载另一个值(可能是无关的)到R1?这里看起来像未优化的冗余代码 LDR R1, [R1, #0] ; (3) 从内存(地址在R1中)加载值到R1?此条指令意义不明,可能是示例中的笔误或特定上下文。 ; 更典型的无优化代码是:LDR R1, [R0] ; 从sum1地址加载当前值到R1 ADDS R1, R1, #0x1 ; (4) R1加1 STR R1, [R0, #0] ; (5) 将新值存回sum1地址

关键在于第(1)条指令LDR R0, [PC, #0x154]。这条指令的目的是从Flash中加载sum1变量的地址(因为sum1是全局变量,其地址在链接时确定,并作为常量存储在Flash的常量池中)。

问题来了:CPU执行第(1)条指令时,它需要从Flash中取数据(即sum1的地址)。这个访问对于Flash控制器来说,是一个非连续访问(Non-sequential Access)。因为当前通过I-Code总线正在取指(假设是顺序取指),突然D-Code总线要来访问一个不连续的地址取数据,这会打断Flash的流水线或预取缓冲区,导致额外的等待周期。

简单类比:Flash像一个图书馆,I-Code总线是正在按顺序取书的读者A。D-Code总线是另一个突然要借一本完全不相关书籍的读者B。图书管理员(Flash控制器)不得不停下服务读者A,去为读者B找书,然后再回来继续为读者A服务。这个过程产生了额外的“寻址”开销。

在无优化代码中,这种需要从Flash常量池加载地址或常量的操作很频繁,导致Flash的“非连续访问”惩罚不断发生,严重拖慢了执行速度。而在RAM中执行代码时,虽然取指要走较慢的系统总线,但取数据(变量地址和值)也在同一块RAM,访问模式相对简单,避免了Flash的非连续访问惩罚,因此整体更快。

4.3 优化后代码(-O2/-O3)为何在Flash中快?

开启速度优化(如-O2)后,编译器会施展浑身解数。对于我们的简单循环,优化可能包括:

  • 常量传播:将sum1的地址直接计算出来,而不是每次都从常量池加载。
  • 寄存器分配:将变量地址甚至变量值尽可能保留在寄存器中,减少内存访问。
  • 循环展开:虽然对这个简单循环可能不明显,但优化会消除冗余操作。

优化后的汇编代码可能简化为:

; 假设编译器将sum1的地址优化到了寄存器R0中,或者直接使用PC相对寻址 ; 并且可能将sum1的值也优化到寄存器R1中,在循环中只操作寄存器 LDR R1, [R0] ; (1) 从RAM加载sum1的值到R1 (如果值未保留在寄存器) ADDS R1, #1 ; (2) R1加1 STR R1, [R0] ; (3) 将新值存回RAM

或者,在极度优化下,编译器甚至可能识别出整个循环对 volatile 变量的写入,但为了满足 volatile 语义,它仍然会生成存储指令,但代码序列变得非常紧凑。

此时的关键变化:循环体内的指令序列变得简单、规整,并且不再需要从Flash的常量池中加载数据。所有需要的数据(变量地址、变量值)访问都只涉及RAM。

对于在Flash中执行的这段优化后代码:

  1. 取指:通过I-Code总线从Flash顺序获取指令。由于指令流是顺序且紧凑的,Flash的预取缓冲区(Prefetch Buffer)可以大显神威。它可以提前读取下几条指令,当CPU需要时直接提供,几乎消除了取指的等待时间。
  2. 取/存数据:通过系统总线访问RAM中的sum1变量。这与代码位置无关。
  3. 优势结合:I-Code总线专用于Flash取指,且预取缓冲高效工作;D-Code/系统总线用于数据访问。两者可以高效并行。

对于在RAM中执行的这段优化后代码:

  1. 取指:需要通过系统总线从RAM中取指令。
  2. 取/存数据:同样需要通过系统总线访问RAM中的sum1变量。
  3. 总线竞争取指和数据访问不得不共享同一条系统总线。即使有总线矩阵的仲裁,这种竞争也会带来延迟,无法实现真正的并行。

于是,在优化后的场景下,Flash执行的架构优势(独立的指令总线)就体现出来了,而RAM执行则受到了总线竞争的拖累,结果就是Flash反超。

4.4 预取缓冲区与指令队列的作用

Flash的预取缓冲区是提升性能的关键。当CPU顺序执行代码时,Flash控制器会提前读取后续指令放入这个缓冲区。CPU需要下一条指令时,可以直接从缓冲区快速获取,无需等待Flash的读周期。这有效隐藏了Flash的访问延迟。

在无优化代码中,由于频繁的非连续数据访问(加载常量),会清空或打断预取缓冲区的流水线,使其优势无法发挥。而在优化后的顺序代码中,预取缓冲区可以持续工作,将Flash的延迟影响降到最低。

指令预取队列(Instruction Prefetch Queue)是CPU内核侧的概念,它与Flash的预取缓冲区协同工作。非连续访问会打断这个队列的填充,导致CPU流水线出现“气泡”(等待指令),降低效率。

5. 更广泛的证据与案例参考

上述测试和原理并非特例。在ST官方提供的STR91x(同样基于ARM9xx内核,有类似总线架构)DSP库文档中,有一个关于FFT运算速度的实测数据,极具参考价值:

FFT运算模式周期数时间 (微秒 @ 96MHz)
64点,代码在Flash,数据在SRAM270128.135
64点,代码和数据都在SRAM343235.750
64点,代码和数据都在Flash370538.594

这个数据清晰地表明:对于FFT这种计算密集、指令流规整的算法,将代码放在Flash、数据放在SRAM是最快的配置,甚至比全部放在SRAM中还要快约21%。这正是利用了Flash独立指令总线的优势,避免了代码和数据在系统总线上的竞争。全部在Flash中反而最慢,是因为数据访问也需要走较慢的Flash。

这强有力地印证了我们的分析:性能取决于代码的访问模式与存储架构的匹配程度

6. 实战指南:如何为你的项目做出正确选择

理解了原理,我们就能在项目中做出明智的决策,而不是盲目猜测。以下是我的实战经验总结:

6.1 决策流程图与核心考量

面对“代码放哪”的问题,你可以遵循以下思路:

  1. 评估性能需求:这部分代码是否是系统的性能瓶颈?是否对实时性有极致要求?如果不是,优先考虑简化开发(默认Flash),节省RAM。
  2. 分析代码特性
    • 是否包含大量常量、跳转表、字符串字面量?如果是,无优化时在Flash中执行可能因非连续访问而慢。考虑优化等级或将其放入RAM。
    • 是否是紧凑的循环计算(如DSP算法、电机控制PWM计算)?优化后,这类代码在Flash中执行往往更有优势。
    • 中断服务程序(ISR)?对延迟敏感。通常值得尝试放入RAM,因为ISR通常短小,且从RAM启动执行更可预测(不受Flash预取状态影响)。
  3. 检查编译优化:你项目采用的优化等级是什么?高优化等级(-O2, -O3, -Os)会极大改善代码的访问模式,增加Flash执行的竞争力。
  4. 确认硬件配置务必确保Flash的等待周期(Latency)和预取缓冲区(Prefetch)已根据系统时钟正确配置!这是Flash性能的基石。配置错误会导致性能灾难。
  5. 实际测量:在接近真实场景的环境下,进行类似本文的基准测试。数据胜于一切猜测。

6.2 具体操作建议与避坑点

  • 默认策略:对于大多数应用,将全部代码放在Flash中执行,并开启合理的优化等级(如-Os兼顾尺寸和速度),是最简单、最可靠的方式。现代MCU的Flash加速技术已经做得很好。
  • 针对性优化:仅将经过性能分析工具(如Keil MDK的Performance Analyzer, Segger SystemView)定位到的、最热点的函数(通常是1%的代码消耗了50%的时间)移到RAM中。使用编译器的section属性或链接脚本实现。
  • RAM代码的初始化:别忘了,放在RAM中执行的代码,其二进制内容需要在上电初始化时从Flash拷贝到RAM。这通常由启动文件(startup_*.s)中的代码完成,或者需要你在main()之前自己实现拷贝。忘记拷贝会导致程序跑飞
  • 调试注意事项:代码在RAM中执行时,断点、单步等调试行为可能与在Flash中不同,因为RAM是可写的而Flash通常不是。有些调试器需要特殊设置。
  • 功耗考量:从Flash取指通常比从RAM取指功耗更高。在极端低功耗应用中,如果某段代码在深度睡眠后被频繁唤醒执行,将其放入RAM可能有助于降低整体功耗。

6.3 一个常见的误解澄清

“我把函数声明为inline内联,是不是就相当于在RAM里执行了?”不是的。inline是编译器的优化建议,它尝试将函数体直接插入调用处,消除函数调用的开销。但插入后的代码仍然存储在它原本该在的地方(Flash或RAM),并遵循相同的取指规则。它改变了代码布局,但没有改变代码的存储介质。一个内联函数如果其指令本身需要从Flash取指,依然会受到Flash访问延迟的影响。

7. 总结与个人体会

回到最初的问题:“STM32的代码,跑在RAM里快?还是跑在Flash里快?” 现在我们可以给出一个更准确的回答:这没有绝对的答案,它高度依赖于具体的代码模式、编译器优化等级以及芯片的总线架构。

  • 对于未优化、且频繁访问Flash中常量数据的代码,在RAM中执行可能更快,因为它避免了Flash的非连续访问惩罚。
  • 对于经过高度优化、指令流顺序且规整的代码,在Flash中执行往往更有优势,因为它能充分利用独立的指令总线和预取缓冲机制,避免与数据总线竞争。

从我个人的项目经验来看,在STM32这类现代Cortex-M MCU上,随着编译器优化技术的进步和Flash加速技术的成熟,绝大多数情况下,将代码放在Flash中并配合适当的优化,已经能够获得非常好的性能。盲目地将代码搬到RAM中,不仅增加了工程复杂度、占用了宝贵的内存,有时还会带来意想不到的性能下降。

因此,我的建议是:首先相信你的工具链(编译器优化),并正确配置硬件(Flash等待状态)。然后,使用性能分析工具找到真正的瓶颈。最后,如果确实需要,再针对性地、有测量依据地将关键代码段迁移到RAM中。性能优化是一门实证科学,测量和数据分析永远比凭感觉更可靠。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询