程序代码存储在FLASH中,但CPU执行代码时,指令和数据需要被读取到RAM中。然而,在某些嵌入式系统(尤其是资源受限的MCU)中,为了节省宝贵的RAM空间,开发者可以选择让代码直接在FLASH中执行(Execute In Place, XIP),也可以选择将代码或数据复制到RAM中执行。这两种方式在性能、功耗、可靠性和内存占用上存在显著差异。
下表从核心维度对比了程序运行在RAM与FLASH中的主要区别:
| 对比维度 | 程序在 FLASH 中执行 (XIP) | 程序在 RAM 中执行 |
|---|---|---|
| 执行速度 | 较慢。FLASH(尤其是NOR Flash)的读取速度远低于RAM。访问延迟高,通常需要插入等待状态,导致CPU流水线停顿。 | 极快。SRAM的访问速度与CPU主频匹配,可实现零等待访问,充分发挥CPU性能。 |
| 功耗 | 相对较低。仅在CPU取指时对FLASH进行读取操作,静态功耗小。 | 相对较高。RAM(特别是SRAM)为保持数据需要持续供电,其静态功耗和动态功耗通常高于FLASH。 |
| 启动时间 | 快。系统上电后,CPU可直接从FLASH固定地址(如0x08000000)取指执行,无需额外搬运。 | 慢(有搬运过程)。需要一段启动代码(Bootloader)将应用程序代码从FLASH复制到RAM,然后跳转到RAM执行,增加了启动延迟。 |
| 内存占用 | 不占用RAM空间。代码本身存储在FLASH,仅运行时栈、堆和全局变量占用RAM。 | 占用大量RAM空间。代码段(.text)和常量数据(.rodata)需要被完整复制到RAM,极大地挤占了本可用于数据和栈的RAM资源。 |
| 代码修改 | 困难/不可行。大多数用于存储代码的FLASH(NOR)不支持像RAM一样的字节级快速写入。若要更新代码,通常需要整扇区擦除再写入,过程复杂且耗时,无法作为动态修改代码的场所。 | 灵活。RAM支持字节级快速读写,可用于动态加载、解释执行(如字节码)或自我修改代码等高级场景。 |
| 可靠性 | 高。FLASH是非易失性存储器,断电后代码不丢失。代码存储区域通常受到写保护,不易被意外修改。 | 低。RAM是易失性存储器,断电后内容丢失。同时,RAM区域更容易受到程序跑飞或缓冲区溢出等错误的影响,导致代码被破坏。 |
| 典型应用场景 | 1.绝大多数MCU应用:资源有限,代码直接在FLASH中执行是标准模式。 2.启动代码(Bootloader):本身在FLASH中执行,负责初始化并加载主程序。 3.固件存储:存储不可变的核心程序。 | 1.性能关键代码:如中断服务程序(ISR)、数字信号处理(DSP)循环、高速通信协议处理函数。 2.需要动态加载的模块:如插件、脚本引擎。 3.从低功耗模式快速唤醒:将唤醒后要执行的代码放在RAM中,避免访问慢速FLASH。 |
深入分析:速度差异的原理与影响
速度差异是两者最核心的区别,其根本原因在于存储器技术本身。
FLASH(以NOR Flash为例)的读取操作涉及复杂的电压控制和解码过程,其随机读取延迟通常在几十到上百纳秒量级。而现代MCU的CPU周期可能只有几纳秒。因此,当CPU直接从FLASH取指时,常常需要插入等待周期(Wait States)。例如,STM32系列MCU的FLASH访问控制寄存器(FLASH_ACR)就需要根据CPU频率(HCLK)来配置正确的等待周期(LATENCY),否则CPU会读到错误的数据。
RAM(以SRAM为例)的访问则是通过静态触发器阵列实现,其访问时间与CPU时钟周期同量级,可以实现同步访问,无需等待状态。
这种速度差异对性能的影响是巨大的。以下面的一个简单延时循环为例,分析其在不同存储器中执行的时钟周期差异:
// 一个简单的空循环,常用于短延时 void delay_loop(uint32_t count) { while(count--) { __asm__ volatile ("nop"); // 执行一个空操作指令 } }假设CPU主频为72MHz,一个时钟周期约为13.9ns。
- 在FLASH中执行(假设需2个等待状态):每次从FLASH读取
nop指令都可能需要3个时钟周期(1个取指 + 2个等待)。那么执行一次nop循环迭代的实际耗时可能超过3个周期。 - 在RAM中执行(零等待):
nop指令可以每个时钟周期执行一次。
因此,将delay_loop这类被频繁调用的短函数或中断服务程序搬到RAM中执行,可以显著提升系统的实时响应能力和整体吞吐量。许多MCU的驱动库(如STM32 HAL)会提供编译指令或链接器修饰符,将特定的函数段(.ramfunc)分配到RAM中执行。
实践配置:如何将代码放入RAM执行
将代码放入RAM执行通常需要两个步骤:链接脚本配置和代码修饰。
1. 链接脚本(.ld文件)修改
需要明确定义一个RAM区域用于存放代码,并将特定的输入段映射到该区域。
/* 定义内存区域 */ MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 64K } /* 定义段 */ SECTIONS { /* .text 主代码段仍然放在FLASH */ .text : { *(.text) /* 所有 .text 输入段 */ *(.text*) /* 所有其他 .text 输入段 */ } >FLASH /* 新增一个名为 .ram_code 的段,将其放入RAM */ .ram_code : { . = ALIGN(4); _sram_code = .; /* 创建符号,记录段起始地址 */ *(.ram_code) /* 收集所有被标记为 .ram_code 的输入段 */ *(.ram_code*) . = ALIGN(4); _eram_code = .; /* 创建符号,记录段结束地址 */ } >RAM AT> FLASH /* VMA在RAM,但LMA(加载地址)在FLASH */ /* ... 其他段(.data, .bss等)... */ }关键点:>RAM AT> FLASH表示这个段的运行时地址(VMA)在RAM中,但它的加载地址(LMA)在FLASH中。这意味着代码被烧录在FLASH里,但上电后需要被复制到RAM的指定位置才能正确执行。
2. 代码修饰与启动搬运
在C代码中,使用特定属性将函数或变量分配到自定义的段中。
/* 使用GCC编译器属性,将函数 fast_isr 放到 .ram_code 段 */ void __attribute__((section(".ram_code"))) fast_isr(void) { // 高性能中断处理代码 // ... } /* 对于IAR编译器,通常使用 `#pragma location` */ // #pragma location = ".ram_code" // void fast_isr(void);然后,在系统的启动文件(如startup_*.s)或main()函数之前的初始化代码中,需要添加将.ram_code段从FLASH复制到RAM的代码。
/* 复制 .ram_code 段从FLASH到RAM */ extern uint32_t _sram_code, _eram_code, _sram_code_load; void copy_ram_code(void) { uint32_t *src = &_sram_code_load; // 在FLASH中的加载地址 uint32_t *dst = &_sram_code; // 在RAM中的运行地址 while (dst < &_eram_code) { *dst++ = *src++; } } // 在系统初始化时调用 copy_ram_code()总结与选型建议
选择在RAM还是FLASH中运行程序,是嵌入式系统设计中的一种重要权衡。
- 默认且最常用的模式是XIP(在FLASH中执行)。它节省RAM,启动快,可靠性高,足以满足大多数控制类、逻辑处理类应用的需求。
- 仅在必要时将关键代码搬运至RAM执行。这是一种优化手段,用于解决FLASH访问速度带来的性能瓶颈。其代价是牺牲了宝贵的RAM空间,并增加了启动复杂度和功耗。
决策流程建议:
- 优先XIP:所有代码默认在FLASH中运行。
- 性能剖析:使用性能分析工具或计时器,找出系统中的热点函数或最苛刻的中断服务程序。
- 局部优化:仅将这些经证实的性能瓶颈函数迁移到RAM中执行。
- 平衡验证:评估优化后,RAM的剩余容量是否仍能满足堆栈和全局数据的需求,确保系统稳定。
简而言之,FLASH是程序的“家”,用于可靠存储;RAM是程序的“工作间”,用于高速执行。合理规划两者的用途,是优化嵌入式系统性能与资源的关键。
参考来源
- 【物联网】ROM、RAM和FLASH的区别
- FLASH和EEPROM的区别
- ROM, FLASH和RAM的区别
- ROM、RAM、SRAM、DRAM、FLASH区别(转载+梳理)
- ROM 、RAM和FLASH 的区别
- ROM、RAM、DRAM、SRAM、SDRAM和FLASH的区别