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) { // 每隔1000ms1秒采样一次 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或者用调试器观察此变量 } }为什么这样设计操作简单sum1操作在汇编层面对应几条清晰的指令加载、递增、存储易于分析。避免溢出使用足够大的uint32_t类型在一秒内几乎不可能溢出。时间测量利用Cortex-M内核自带的Systick定时器其精度与系统时钟同步是测量短时间间隔的可靠工具。2.2 关键测试技巧与避坑指南直接测量从程序启动到第一秒的计数值是不准确的因为从使能Systick到核心while(1)循环稳定执行中间可能存在初始化代码、中断延迟等不可控因素。因此科学的做法是测量稳定运行后两个相邻1秒时间点之间的计数值差。正如测试方法中强调的“观察第一秒到第二秒之间的计数效果”。这确保了测量的是纯粹的循环体执行时间排除了启动阶段的干扰。如何将代码定位到RAM或Flash执行这是测试的前提。通常有两种方法链接脚本Linker Script修改这是最常用和彻底的方法。例如在GCC的链接脚本.ld文件中你可以定义一个特殊的内存区域如.ram_code并将其VMA虚拟内存地址和LMA加载内存地址都设置为RAM的地址。然后在代码中通过__attribute__((section(“.ram_code”)))将特定函数放入这个区域。这样该函数在启动时会被加载到RAM从Flash拷贝过来并在RAM中执行。IDE配置在一些集成开发环境如Keil MDK、IAR EWARM中可以通过工程选项或分散加载文件Scatter File直观地指定某些源文件或函数在RAM中执行。注意将代码放到RAM中执行意味着这部分代码在启动阶段需要从Flash拷贝到RAM这会增加启动时间并占用宝贵的RAM空间。务必权衡利弊通常只对最核心的性能瓶颈代码这样做。2.3 测试平台与参数设定本次参考的测试基于STM32Cortex-M3内核并设定了明确且合理的环境CPU频率48MHz。这是一个典型的运行频率。Flash加速配置预取缓冲区启用Prefetch Buffer Enable和Flash延迟设置为2Flash Latency2。这两项配置至关重要在STM32中当系统时钟超过一定值例如24MHz时必须正确设置Flash等待周期Latency并启用预取指否则CPU访问Flash会插入大量等待状态性能急剧下降。这个配置是保证Flash性能的基础。3. 测试结果呈现一个反直觉的发现在相同的硬件和基础配置下我们得到了两组对比鲜明的数据。为了清晰我将它们整理成下表测试场景代码优化等级执行位置每秒sum1操作次数相对快慢场景一无优化-O0RAM69,467快场景二无优化-O0Flash43,274慢场景三速度优化-O2/-O3RAM98,993慢场景四速度优化-O2/-O3Flash115,334快这个结果非常有意思它直接挑战了“RAM无条件更快”的固有观念在无优化编译时RAM中执行速度约为Flash的1.6倍符合一般直觉。在开启速度优化编译后情况发生了逆转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中执行的这段优化后代码取指通过I-Code总线从Flash顺序获取指令。由于指令流是顺序且紧凑的Flash的预取缓冲区Prefetch Buffer可以大显神威。它可以提前读取下几条指令当CPU需要时直接提供几乎消除了取指的等待时间。取/存数据通过系统总线访问RAM中的sum1变量。这与代码位置无关。优势结合I-Code总线专用于Flash取指且预取缓冲高效工作D-Code/系统总线用于数据访问。两者可以高效并行。对于在RAM中执行的这段优化后代码取指需要通过系统总线从RAM中取指令。取/存数据同样需要通过系统总线访问RAM中的sum1变量。总线竞争取指和数据访问不得不共享同一条系统总线。即使有总线矩阵的仲裁这种竞争也会带来延迟无法实现真正的并行。于是在优化后的场景下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.13564点代码和数据都在SRAM343235.75064点代码和数据都在Flash370538.594这个数据清晰地表明对于FFT这种计算密集、指令流规整的算法将代码放在Flash、数据放在SRAM是最快的配置甚至比全部放在SRAM中还要快约21%。这正是利用了Flash独立指令总线的优势避免了代码和数据在系统总线上的竞争。全部在Flash中反而最慢是因为数据访问也需要走较慢的Flash。这强有力地印证了我们的分析性能取决于代码的访问模式与存储架构的匹配程度。6. 实战指南如何为你的项目做出正确选择理解了原理我们就能在项目中做出明智的决策而不是盲目猜测。以下是我的实战经验总结6.1 决策流程图与核心考量面对“代码放哪”的问题你可以遵循以下思路评估性能需求这部分代码是否是系统的性能瓶颈是否对实时性有极致要求如果不是优先考虑简化开发默认Flash节省RAM。分析代码特性是否包含大量常量、跳转表、字符串字面量如果是无优化时在Flash中执行可能因非连续访问而慢。考虑优化等级或将其放入RAM。是否是紧凑的循环计算如DSP算法、电机控制PWM计算优化后这类代码在Flash中执行往往更有优势。中断服务程序ISR对延迟敏感。通常值得尝试放入RAM因为ISR通常短小且从RAM启动执行更可预测不受Flash预取状态影响。检查编译优化你项目采用的优化等级是什么高优化等级-O2, -O3, -Os会极大改善代码的访问模式增加Flash执行的竞争力。确认硬件配置务必确保Flash的等待周期Latency和预取缓冲区Prefetch已根据系统时钟正确配置这是Flash性能的基石。配置错误会导致性能灾难。实际测量在接近真实场景的环境下进行类似本文的基准测试。数据胜于一切猜测。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中。性能优化是一门实证科学测量和数据分析永远比凭感觉更可靠。