云计算百科
云计算领域专业知识百科平台

STM32_RAM_ROM_Flash

RAM、ROM、Flash 到底有什么区别?从 STM32 看懂代码、变量与函数存在哪里

写 STM32 程序时,我们常听到两句话:“程序放在 Flash,变量放在 RAM。”

这可以作为入门印象,却解释不了下面的问题:

  • Flash 可以反复烧录,为什么有人把它叫 ROM?
  • int count = 10; 的初值在哪里?修改后能不能掉电保存?
  • 全局变量、局部变量、static、const 分别占用什么空间?
  • 函数放在 Flash,为什么调用函数又会消耗 RAM?
  • 编译结果里的 Code、RO-data、RW-data、ZI-data 到底代表什么?

要回答这些问题,需要分清三个层次:硬件存储器、程序的组织方式,以及 C 语言对象的生命周期。

本文以 C 语言和常见 STM32 裸机工程为主,默认程序烧录到内部 Flash、运行数据使用 SRAM。文中的存储位置是常见工具链配置下的结果,并非 C 语言强制规定。开启优化或修改链接配置后,实际结果可能不同。

一、先分清 RAM、ROM、Flash:它们不是三个完全并列的概念

1. RAM:程序运行时的工作空间

RAM 全称是 Random Access Memory,随机存取存储器。

“随机存取”不是随机存数据,而是可以按地址直接访问目标位置,不必从头顺序查找。

在常见 MCU 中,RAM 主要用于存放运行过程中需要改变的数据,例如 ADC 采样数组、串口接收缓存、控制算法的中间结果,以及函数调用所需的数据。

常见的两类 RAM 是:

类型存储特点是否需要刷新常见用途
SRAM,静态 RAM 使用保持电路状态的存储单元 保持供电时不需要周期刷新 MCU 片内 RAM、缓存
DRAM,动态 RAM 依靠电荷保存数据 需要周期刷新 计算机内存、部分外接大容量内存

STM32F407 的片内运行内存主要是 SRAM。这里“静态”不表示掉电后还能保存;普通 SRAM 和 DRAM 都属于易失性存储器。

2. ROM:需要结合语境理解“只读”

ROM 全称是 Read-Only Memory,只读存储器。

严格来说,传统掩膜 ROM 的内容在制造时确定,用户不能像写 RAM 那样修改。后来出现了 PROM、EPROM、EEPROM 等可编程非易失性存储器,“ROM”也常被用作这个技术家族的宽泛称呼。

因此,资料中的“ROM”可能表示:

  • 严格意义上的只读存储器。
  • 广义的非易失性程序存储器。
  • 工具链中的只读存储区域,并不指定某一种物理器件。

看到 ROM 时,先判断它描述的是器件类型,还是工程里的存储用途。

3. Flash:可以电擦写的非易失性存储器

Flash 即闪存,属于非易失性存储器,通常放在电可擦写存储器家族中讨论。

它与常见 EEPROM 的一个重要区别,是 Flash 通常按页、块或扇区擦除,而不是像 RAM 一样随时按任意地址覆盖数据。具体编程单位和擦除单位由器件决定。

以 STM32 内部 Flash 的常见机制为例:擦除后数据位为 1,编程使指定数据位由 1 变为 0;要恢复为 1,通常需要擦除相应区域。实际操作还必须满足芯片对编程宽度、供电、电压和操作顺序的要求。

所以,Flash 可以保存程序,也可以保存校准参数、设备编号等数据,但有擦写寿命与操作耗时限制,不适合直接替代高频更新的 RAM 变量。

4. 放在同一张表里比较

项目SRAM传统掩膜 ROM常见 EEPROMFlash
掉电保持 否 是 是 是
用户修改 普通读写即可 通常不能 通过规定的写入流程 通过编程、擦除流程
更新粒度 通常支持字节、半字、字访问 不适用 常支持字节更新,也可能支持页写 编程粒度与擦除粒度通常不同
速度特点 适合频繁读写 适合读取固定内容 写入通常比读取慢 编程、擦除通常比读取慢
典型用途 变量、缓冲区、栈、堆 固定程序或数据 少量配置参数 固件、资源、持久化数据

Flash 还可分为 NOR Flash、NAND Flash。前者常用于代码存储和直接读取,后者常用于大容量存储。本文讨论的是 STM32 内部程序 Flash 的使用方式,不能把它直接等同于所有闪存设备。

在这里插入图片描述

二、以 STM32 为例:芯片内部有哪些存储区域?

1. 主 Flash:保存用户程序与初始内容

常见 STM32 工程中,主 Flash 通常保存:

  • 中断向量表与启动代码。
  • 函数编译后的机器指令。
  • 只读表格、字符串等数据。
  • 需要复制到 RAM 的变量初值。

CPU 通常直接从 Flash 取指执行,不需要先把整个程序复制到 RAM。

特定函数也可以通过链接配置安排到可执行 SRAM 中运行,但需要正确处理其加载、复制和执行地址。

2. SRAM:保存运行中的可变状态

SRAM 通常承载全局变量、静态变量、栈、堆和各种缓冲区。

需要注意:同一芯片里的不同 RAM 区域,访问能力未必相同。

以 STM32F407 为例,其 SRAM1、SRAM2 与 CCM 数据 RAM 不在同一地址区间。CCM 只供 CPU 通过数据通路访问,不能把需要 DMA 访问的缓冲区随意放进去,也不能把它当成普通可执行 SRAM 使用。[1]

这对 ADC + DMA 尤其重要:缓冲区“放在 RAM”还不够,还必须放在 DMA 能访问的 RAM 中。

3. 系统存储器:厂商预置 Bootloader

STM32 的系统存储器中通常预置 ST 的启动加载程序,用于芯片支持的系统下载方式。具体支持哪些接口,要查对应型号的启动模式资料。

它与自己编写、烧录到用户 Flash 中的 Bootloader 是两回事。

4. 外设寄存器:有地址,不代表属于 RAM

例如:

GPIOA->ODR = 0x0001;

这次写操作访问的是 GPIO 外设寄存器。地址译码电路把访问送到 GPIO 硬件,从而改变相关输出状态。

RAM、Flash、外设寄存器都可以出现在 CPU 的地址空间中,但地址背后连接的硬件不同。

概念回答的问题
地址空间 CPU 能使用哪些地址表示访问目标?
物理存储器 芯片实际实现了哪些存储硬件?
存储容量 某块存储器实际能保存多少数据?

32 位地址可以表示很大的地址范围,不意味着芯片内部真的装有同等容量的 RAM。

在这里插入图片描述

三、源码变成程序后,为什么会分成不同的“段”?

1. 编译器与链接器分别做什么?

源代码中的函数和变量,不是按照书写顺序直接塞进 Flash。

编译器将源码转换为目标代码,并把指令、只读数据、可写数据等分到不同节区;链接器再合并目标文件与库,解析函数和变量之间的引用,根据链接配置分配地址。

链接脚本或 Keil 的 scatter 文件,决定各类内容最终放到哪块存储区域。Keil 的 Target 设置也可以为链接器提供 ROM、RAM 区域配置。[2]

2. 常见区域分别存放什么?

名称常见内容典型运行位置是否需要保存初始内容到 Flash
.text 函数的机器指令 Flash 是
.rodata 只读对象、字符串字面量、常量表 Flash 是
.data 通常为具有非零初值的可写静态存储期对象 SRAM 是,保存初值
.bss 通常为零初始化的可写静态存储期对象 SRAM 不逐字节保存零值
栈 调用现场、部分参数和自动变量等 SRAM 通常不需要
堆 动态申请的内存 SRAM 通常不需要

这里沿用常见的“程序段”说法,更严格地说,.text 等通常是目标文件或可执行文件中的节(section)名称,不同工具链的命名、合并方式并不完全一致。

栈和堆则是运行时使用内存的方式,可以由链接器预留,也可以由运行库或系统管理,不一定与上面四个节处于同一分类层级。

3. 为什么 .bss 能节省 Flash?

考虑一个全局数组:

unsigned char rx_buffer[1024];

它具有静态存储期,启动时应被初始化为零。工具链通常把它放入 .bss。

程序只需要记录这片区域的地址和长度,在启动时清零,无须在 Flash 中额外保存 1024 个零。因此它通常占用 1024 字节 RAM,却不需要同样大小的 Flash 初值副本。

这不意味着初始化“完全免费”:描述信息、清零代码,以及启动时执行清零的时间仍然存在。

四、重点:代码里的变量、函数,到底放在哪里?

1. 用一段代码建立对应关系

下面是用于分析的示例,不是地址分配的保证。未被使用的对象可能被优化或链接删除。

#include <stdlib.h>

#define BUFFER_SIZE 128

int g_count = 10;
int g_zero = 0;
int g_uninit;

static int s_mode = 2;
const int g_limit = 100;
char g_name[] = "STM32";

void demo(int input)
{
int local = input + 1;
int temp;
static int calls = 0;
const int limit = 20;

char text[] = "ABC";
const char *message = "Hello";
int *p = malloc(4 * sizeof *p);

calls++;

if (p != NULL)
{
p[0] = local + limit;
free(p);
}
}

对象常见位置解释
g_count .data,运行时位于 RAM 初值 10 通常保存在 Flash
g_zero .bss 显式初始化为零,也通常使用清零机制
g_uninit .bss 未显式初始化的全局变量会被零初始化
s_mode .data 文件作用域的 static 限制链接可见性,不意味着只读
g_limit 常见于 Flash 中的只读区域 也可能被常量折叠或移除
g_name .data 它是可修改数组,包含末尾的 \\0,共 6 字节
input 寄存器或栈 由调用约定、类型和优化结果决定
local、temp 寄存器或栈,也可能消失 普通自动变量,temp 未赋值前不能当有效数据使用
calls .bss 虽然写在函数内,生命周期覆盖整个程序运行期间
局部 limit 可能直接成为指令中的常量 const 不保证独立的 Flash 存储位置
text 通常是栈上的数组 数组内容可修改,初始化方式由编译器决定
message 寄存器或栈中的指针 指向通常位于只读区域的字符串字面量
p 寄存器或栈中的指针 指向动态分配区,并不意味着指针本身在堆里
malloc() 分配的空间 堆或运行库管理的动态内存区 需检查失败并按所有权规则释放
demo() 的机器指令 通常在 Flash 的代码区域 与它调用时产生的数据分开存放

在这里插入图片描述

2. 作用域、生命周期、存储位置要分开看

这三个问题分别是:

  • 作用域: 在源码的哪些地方能通过这个名字访问对象?
  • 生命周期: 对象从什么时候存在,到什么时候结束?
  • 存储位置: 最终使用哪块物理内存或哪个寄存器?

例如 calls:名字只能在函数内部使用,对象却不会在函数返回时消失,常见实现中保存在 RAM 的静态数据区域。

所以,“写在函数里”不能直接推导出“放在栈上”。

3. const 不等于“强制放进 Flash”

const 主要约束通过相应类型进行修改的行为,并不直接指定物理存储器。

全局只读数组通常很适合放在 Flash,例如:

const unsigned short sine_table[4] = {0, 1000, 2000, 1000};

但局部 const int limit = 20; 可能直接被编译器替换成常量 20,根本没有独立存储空间;需要实际存储时,也未必位于 Flash。

还要区分:

const char *p1; // 不能通过 p1 修改它所指向的字符
char * const p2 = buf; // p2 本身不能再改指向,buf 须已定义

第一种限制指向的数据,第二种限制指针对象。两者都不能单凭写法判断最终地址。

4. 字符串字面量与字符数组不是同一个对象

char a[] = "ABC";
const char *b = "ABC";

若它们定义在函数内,a 是一个包含 4 个字符的可写自动数组,通常位于栈;b 是指针,指向具有静态存储期的字符串字面量。

可以执行 a[0] = 'X';,但不能修改字符串字面量。即使通过类型转换绕过类型检查,尝试修改字面量依然属于未定义行为。[3]

在这里插入图片描述

5. 三个常被误解的关键字

写法真正影响的事情不能据此断定的事情
#define N 100 预处理阶段的文本替换 不会因为定义了宏就生成一个名为 N 的变量
volatile int flag 对该对象访问的优化约束 不指定放在哪,不保证原子性或线程安全
register int x 传统 C 中向实现提供快速访问的建议,并限制取地址 不保证一定占用 CPU 寄存器

宏展开后的表达式、字符串或代码仍然可能占用空间。“宏本身不是变量”不等于“使用宏不会增加程序大小”。

五、为什么一个变量会同时涉及 Flash 和 RAM?

1. 从 int count = 10; 看两份不同职责的内容

假设这是一条全局定义:

int count = 10;

在常见工程中,Flash 保存它的初始内容,RAM 保存程序实际读写的对象。启动时,初始化代码把初值复制到变量的 RAM 地址。

执行:

count = 20;

修改的是 RAM 中的运行值,不会自动更新 Flash 中的初值。下次按正常启动流程重新初始化后,count 又会变成 10。

若想掉电保存 20,需要显式执行非易失性存储操作,并设计存储格式、擦写与恢复流程。

在这里插入图片描述

2. 加载地址与运行地址

地址概念对 .data 的典型含义
加载地址,LMA 固件中初始内容保存的位置,通常位于 Flash
运行地址,VMA 程序实际访问对象的位置,通常位于 RAM

对于直接在 Flash 执行的代码,加载地址和运行地址通常相同。对于先保存在 Flash、再复制到 SRAM 执行的函数,它们也可以不同。

因此,“一个对象属于哪个段”和“这个段加载、运行在哪里”需要结合起来理解。

3. 从复位到 main(),发生了什么?

可以按下面的概念流程理解:

  • Cortex-M 从启动映射对应的向量表取得初始主栈指针和复位入口。
  • 进入复位处理程序,执行必要的底层初始化。
  • 启动代码或 C 运行库把 .data 的初始内容复制到 RAM。
  • 将 .bss 对应的 RAM 区域清零,并完成其他运行环境初始化。
  • 进入 main()。
  • 具体工程中,系统时钟初始化、运行库初始化等调用顺序可能不同,不能只凭一个函数名推断全部流程。Keil 工程通常还需要继续查看复位处理程序调用的运行库入口,而不是只在启动汇编里找一段明显的复制循环。

    4. 为什么全局变量默认是零,普通局部变量却不是?

    int global_value;

    void test(void)
    {
    int local_value;
    }

    global_value 具有静态存储期,C 语言要求它得到默认初始化。对于这里的整数类型,结果是零;STM32 工程通常通过启动时清零 .bss 来实现。

    local_value 是未初始化的自动对象,不能把它当成已有有效数值来读取。把它解释为“随机数”也不严谨:涉及的是不确定值及相应的语言规则,某些读取会产生未定义行为。

    全局变量初始为零,是语言规则与启动初始化共同保证的结果,不是 RAM 上电后天然全零。

    另外,某些工程会显式设置 noinit 区域用于特定保留用途,这属于专门配置,需要另外建立有效性判断,不能套用普通 .bss 的结论。

    六、函数运行时,栈和堆到底发生了什么?

    1. 函数代码与调用现场分开存储

    int add(int a, int b)
    {
    int result = a + b;
    return result;
    }

    add() 编译后的加法、返回等机器指令通常位于 Flash。调用它时,参数、结果和必要的现场数据则可能位于 CPU 寄存器或 RAM 中的栈。

    函数的每次调用并不会把机器指令重新复制一遍。递归时通常是多份调用现场共用同一份函数代码。

    简单函数经过优化后,参数和结果可能全部通过寄存器传递,甚至被内联,未必建立独立栈帧。对 Cortex-M 来说,返回信息也可能先保存在链接寄存器中,仅在需要时入栈。

    在这里插入图片描述

    2. 为什么不能返回普通局部变量的地址?

    int *wrong(void)
    {
    int value = 123;
    return &value; // 错误示例
    }

    函数返回后,value 的生命周期结束。即使那片内存的字节暂时没有变化,也不能继续通过该地址访问已经结束生命周期的对象。

    常见改法是返回值本身,或让调用者提供缓冲区。改成 static 虽能延长生命周期,却也会让所有调用共享同一对象,需要考虑重入和并发访问问题。

    3. 大数组和递归为什么容易耗尽栈?

    void process(void)
    {
    unsigned char buffer[4096];
    // 假设后续实际使用该数组
    }

    如果该数组被实际保留在栈上,单它就需要约 4 KiB,再加上其他局部数据、保存的寄存器以及中断嵌套开销,可能超出栈空间。

    改成 static 通常能把它移到静态数据区,但它仍然占用 RAM,而且整段程序运行期间都要保留。它解决的是栈压力,并不会让总 RAM 开销消失。

    4. 堆:对象何时消失由分配与释放流程决定

    int *p = malloc(100 * sizeof *p);

    if (p != NULL)
    {
    p[0] = 123;
    free(p);
    p = NULL;
    }

    p 若是局部指针,其自身通常在寄存器或栈中;申请到的空间则由动态分配器管理。

    malloc() 不保证返回零初始化的内存,也可能分配失败。函数返回不会自动释放成功申请的那块空间。free() 后再访问该空间属于错误;把 p 设为 NULL 也不会自动修复其他指向同一块内存的指针。

    对于嵌入式控制程序,还应关注动态分配的时间波动、管理开销和碎片问题。固定大小缓冲区、内存池或启动时集中分配,往往更容易分析资源上限。

    5. 优化会改变“看起来应该存在”的变量

    int calculate(void)
    {
    int a = 10;
    int b = 20;
    return a + b;
    }

    编译器可能直接生成“返回 30”的代码。此时并不需要为 a 和 b 分配独立 RAM 空间。

    因此,调试器提示变量被优化掉,不一定是程序出错。存储位置必须以最终构建结果为准。

    七、如何验证,而不是只背结论?

    1. 先看 Keil 的大小统计

    例如:

    Program Size: Code=12000 RO-data=2000 RW-data=100 ZI-data=3000

    字段含义
    Code 代码统计类别,主要包含机器指令
    RO-data 只读数据
    RW-data 有初始内容的可写数据
    ZI-data 零初始化数据统计类别

    在普通、未压缩、代码与只读数据位于 Flash 的配置下,可以先作近似估算:[4]

    Flash 占用 ≈ Code + RO-data + RW-data
    RAM 中已统计的 RW/ZI 区域 ≈ RW-data + ZI-data

    这个例子得到 Flash 约 14100 字节,RW/ZI 区域约 3100 字节。

    但是,不能只凭第二个数就宣布“运行时 RAM 一定够用”。

    • 栈和堆是否已经作为预留区计入统计,要看工程的启动文件、链接配置与 map 文件。
    • 若已计入,不能再重复相加;若未计入,则需要单独预算。
    • 实际栈峰值与调用深度、中断嵌套有关,不能只看预留空间的大小。
    • 对齐、填充、数据压缩、自定义段及 RAM 执行代码可能使简单公式与最终装载范围不同。

    2. 查看 .map 文件

    生成链接 map 文件后,可以搜索变量名或函数名,重点看:

  • 符号是否真实存在,是否被删除或内联。
  • 符号所属节区及大小。
  • 执行区域的地址范围。
  • 对应加载区域在哪里。
  • 栈、堆、自定义区是否被预留和统计。
  • 对常见 STM32F407 工程来说,主 Flash 从 0x08000000 开始,普通 SRAM 区从 0x20000000 开始,CCM 数据 RAM 从 0x10000000 开始。实际有效范围与容量需要结合具体型号及手册判断,不能仅凭地址前缀确认所有地址都有效。[1]

    函数与变量的地址也不是由源码顺序固定决定的。增加一个库函数、调整优化等级或修改链接配置,都可能改变布局。

    3. 做一个小实验

    准备以下定义,并在 main() 中实际使用它们:

    int g_a = 123;
    int g_b;
    const int g_c = 456;

    void inspect(void)
    {
    int local = 789;
    static int persistent;
    const char *text = "STM32";

    // 在此设置断点,结合 Watch、Memory、反汇编观察
    g_b = local + g_c;
    persistent++;
    g_a += text[0] + persistent;
    }

    建议先使用便于调试的较低优化设置,再做以下观察:

    观察项目要验证的事情
    &g_a、&g_b 可写全局对象的运行地址通常在 RAM
    &g_c 被实际保留的只读对象是否位于 Flash
    &persistent 函数内 static 的地址通常属于静态数据区
    &local 自动变量被实际分配到内存后的地址
    text 与 &text 字符串地址与指针对象自身的地址不同
    反汇编窗口 哪些值通过寄存器传递,哪些对象被优化掉

    注意:在源码中增加取地址操作,尤其把地址传给其他函数时,可能迫使对象实际落到内存;调试器也不一定能为寄存器中的对象提供可取的内存地址。实验得到的是当前代码、当前配置下的结果。

    另外,普通“复位”不代表所有 SRAM 物理字节都会被硬件清零;调试器也可能参与初始化。判断变量恢复原因时,要区分断电、复位,以及启动代码重新执行这几件事。

    文末速查:C 语言对象通常存在哪里?

    下表沿用本文的常见 STM32 配置,掉电保持指实际运行值能否保留。

    内容常见存储区域运行值掉电保持初始化方式
    函数机器指令 Flash,代码区 是,指令内容保留 烧录
    全局只读常量、常量表 Flash,只读区 是,若对象被保留在此 烧录
    字符串字面量 通常在 Flash,只读区 通常是 烧录
    非零初始化的可写全局、静态变量 RAM,.data 否,Flash 仅保存初始内容 启动时复制初值
    零初始化或未显式初始化的可写全局、静态变量 RAM,.bss 否 启动时清零
    普通局部变量、参数 寄存器或栈,也可能被优化掉 否 按声明、赋值或调用过程建立
    局部字符数组 通常在栈,也可能被优化 否 进入相应执行路径时初始化
    动态分配对象 堆或其他动态内存区 否 malloc() 不负责清零
    外设寄存器 对应外设硬件 通常不能作为掉电保存手段 依复位规则及配置写入

    遇到一段 C 代码时,可以按这个顺序判断:先看对象的生命周期和可写性,再看编译后的节区,最后用链接配置和 map 文件确认物理位置。

    这比简单记忆“程序放 Flash,变量放 RAM”更可靠,也能直接帮助定位 RAM 不足、栈溢出、DMA 缓冲区不可访问,以及启动初始化异常等实际问题。

    参考资料

  • ST RM0090:STM32F4 参考手册,重点查看 Memory and bus architecture、Embedded Flash memory interface。
  • Keil µVision User’s Guide:Target(Arm Compiler),查看 ROM、RAM 区域配置。
  • C11 委员会草案 N1570,重点查看 6.2.4(存储期)、6.4.5(字符串字面量)、6.7.9(初始化)和 7.22.3(内存管理)。
  • Nuvoton FAQ:Keil 中的代码与 SRAM 占用统计。
  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » STM32_RAM_ROM_Flash
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!