一、简介
对于代码运行除了存放代码的地方------Flash(闪存)非常重要,还有就是重要的内存RAM了,在这里对MCU一般讨论的是片内的SRAM(成本高,但是访问速度快,一般不会给太多),同时这里也就暂时讨论外扩的片外SRAM。
KEIL或者其他的芯片编译环境下编译完程序时,会标识出程序的分布情况,对于资源的使用情况必须合理安排,否则没有足够的SRAM支持程序正常运行。



注:SRAM与台式PC机的内存DDR(本质是DRAM)要区别开。
1.1 MCU RAM(SRAM)的核心物理属性
| 特性 | 详细说明 |
|---|---|
| 物理介质 | 纯 SRAM(静态随机存储器),无刷新电路,靠触发器存储数据,区别于外设寄存器 / Flash |
| 易失性 | 掉电后数据立即丢失;上电后需从 Flash 加载.data 段初始值,.bss 段自动清零 |
| 读写性能 | 速度与 CPU 主频完全匹配(无延迟),是 MCU 中读写最快的存储介质 |
| 容量限制 | 片内 SRAM 容量远小于 Flash(如 STM32F103C8T6:20KB SRAM vs 64KB Flash) |
| 地址特征 | 连续的线性地址空间,厂商预分配(如 Cortex-M 架构 RAM 起始地址多为 0x20000000) |
| 特殊分区 | 部分 MCU 划分 "备份 SRAM"(由备用电源供电,主电掉电仍保数据)、"DMA 专用 SRAM"(避免 CPU/DMA 冲突) |
1.2 MCU RAM 的核心逻辑分区(编译链接层面)
物理 SRAM 在编译阶段会被链接脚本(.ld/.sct)划分为多个逻辑段,所有段都落在厂商定义的 RAM 物理地址范围内,这是 RAM 使用的核心:
| 逻辑段 | 核心功能 | 关键特性 |
|---|---|---|
| .data | 存储初始化的全局 / 静态变量 | 1. 初始值存 Flash,上电后启动代码将值加载到 RAM;2. 运行时可读写;3. 占用 Flash(存初始值)+ RAM(存运行数据) |
| .bss | 存储未初始化 / 初始化为 0 的全局 / 静态变量 | 1. 无需在 Flash 存初始值(仅占 RAM 空间);2. 上电后启动代码自动清零;3. 最 "节省 Flash" 的全局变量存储方式 |
| .stack | 栈区:函数调用返回地址、局部变量、函数参数、中断现场保护 | 1. 编译器自动管理,增长方向:高地址→低地址;2. 大小在启动文件(.s)中配置(如 Stack_Size);3. 无硬件保护,溢出即崩溃(HardFault) |
| .heap | 堆区:动态内存分配(malloc/calloc/realloc/free) | 1. 手动管理,增长方向:低地址→高地址;2. 堆 / 栈之间为 "空闲 RAM",重叠则程序崩溃;3. 易产生内存碎片 / 泄漏 |
| 备份 SRAM | 特殊 RAM 分区(如 STM32),由 VDD 备份电源供电 | 主电源掉电后仍保数据,用于存储关键参数(如设备状态、RTC 数据);需手动配置电源和地址 |
很多开发者会混淆 "RAM" 和 "内存映射寄存器",需明确:
- RAM 是独立的地址段:如 Cortex-M 架构中,RAM 地址段(0x20000000~0x200FFFFF)与外设寄存器段(0x40000000~0x400FFFFF)、内核寄存器段(0xE0000000~0xE00FFFFF)完全分离,后者并非 RAM(本质是外设的控制 / 状态寄存器,物理介质不是 SRAM);
- 位带区是 RAM 的操作扩展:Cortex-M 的 RAM 位带区(0x22000000~0x23FFFFFF)并非独立 RAM,而是将普通 RAM 的每一位映射到新地址,实现 "按位操作"(如直接修改 RAM 某一位,无需掩码),本质是对 RAM 的访问方式扩展。
注意:一些地址是对其操作去控制寄存器的(如果有疑惑自行去了解地址总线和数据总线的相关概念)。
1.3 MCU RAM 开发的核心注意事项(纯 RAM 相关)
- 容量管控 :
- 编译后查看.map 文件,统计.data+.bss+.stack+.heap 的总大小,不得超过片内 SRAM 物理容量;
- 大数组(如 char buf [4096])避免定义为局部变量(占栈),改用 static(放入.data/.bss)或全局变量。
- 栈溢出防护 :
- 启动文件中配置合理的 Stack_Size(如 STM32F103 建议≥1KB,复杂程序≥2KB);
- 栈底设置 "哨兵值"(如
uint32_t stack_guard __attribute__((at(0x20004FFC))) = 0xDEADBEEF;),运行中检测值是否被覆盖,判断栈溢出; - 禁止无限制递归调用(直接耗尽栈)。
- 堆区优化 :
- MCU 中禁用裸 malloc/free(易产生碎片 / 泄漏),改用内存池(如 FreeRTOS 的 pvPortMalloc、自定义固定大小内存池);
- 限定 Heap_Size 大小,避免堆过度增长覆盖栈。
- 数据对齐 :
- ARM Cortex-M 要求 RAM 访问 4 字节对齐,非对齐访问(如强制操作
int* p = (int*)0x20000001;)会触发 HardFault; - 结构体用
__attribute__((packed))或#pragma pack时需注意对齐风险。
- ARM Cortex-M 要求 RAM 访问 4 字节对齐,非对齐访问(如强制操作
- 低功耗与 RAM :
- 进入 STOP/STANDBY 模式时,可关闭非必要 RAM 分区以节省功耗;
- 备份 SRAM 需配置 RTC 时钟和备用电源(VBAT),否则掉电仍丢失数据。
1.4 后续以STM32、ESP32或者W800等芯片为例参考内存分布
STM32 Memory 映射


启动方式(后续讲解boot loader):
| BOOT0 | BOOT1(仅部分型号) | 引导模式 | 核心用途 |
|---|---|---|---|
| 0 | X(无关) | 闪存(Flash)启动 | 正常运行(量产模式) |
| 1 | 0 | 系统存储器(System Memory) | ISP 下载(串口 / USB 烧录程序) |
| 1 | 1 | SRAM 启动 | 调试 / 临时程序运行 |

二、分布拆解
2.1 对于PC的常规内存分布

2.2 对于MCU的程序分布(Flash到SRAM)



2.3 使用了RTOS的线程任务的所在地
动态创建的任务 1,其任务堆栈既不在主栈(MSP)里,也不是 "FreeRTOS 内核管理的堆本身",而是从 FreeRTOS 管理的那块堆空间中分配出来的独立栈区域------ 得明确 "FreeRTOS 堆" 和 "任务堆栈" 的关系:
1. 先理清两个核心概念
-
FreeRTOS 管理的堆空间 :是一块由
configTOTAL_HEAP_SIZE定义的 SRAM 内存池(对应图里的ucHeap数组),它是 "内存分配的来源池",负责给两类对象分配内存:① 内核数据结构(任务控制块 TCB、队列控制块 QCB 等链表结构);② 动态创建的任务的任务堆栈。 -
任务堆栈:是每个任务独立的 "运行栈空间"(用于存放任务运行时的局部变量、函数调用栈帧、上下文切换数据),它是 "从堆里分配出来的一块内存区域",属于任务自身的资源,不是内核数据结构。
2. 动态创建任务 1 的堆栈流程
当你用xTaskCreate()(动态创建任务)时:
xTaskCreate(
Task1_Entry, // 任务函数
"Task1", // 任务名
128, // 任务堆栈大小(单位:字/字节,取决于端口)
NULL, // 任务参数
1, // 任务优先级
&xTask1Handle // 任务句柄
);
FreeRTOS 会做两件事:① 从ucHeap(FreeRTOS 堆)中分配一块内存,作为任务 1 的堆栈空间 ;② 同时从ucHeap中分配另一块内存,作为任务 1 的TCB(任务控制块,内核链表结构)。
3. 和主栈(MSP)的区别
主栈(MSP)是 MCU 启动时初始化的栈空间(通常对应裸机程序的栈),FreeRTOS 启动后,主栈一般仅用于:
- 中断服务程序(ISR)的运行栈;
- FreeRTOS 内核的启动代码、调度器初始化等早期流程。
任务 1 运行时,用的是自己从 FreeRTOS 堆里分配的任务堆栈,不会用到主栈。
4. 总结
┌─────────────────────────────────────────────────┐
│ FreeRTOS管理的堆空间(SRAM) │
│ (由configTOTAL_HEAP_SIZE定义 → static uint8_t ucHeap[]) │
└───────────┬─────────────────────────────────────┘
│
├──────────────────────────┐ ┌──────────────────────────┐
│ 步骤2:分配TCB内存 │ │ 步骤3:分配任务堆栈内存 │
│ (内核数据结构:任务控制块)│ │ (任务1的独立运行栈) │
↓ │ ↓ │
┌───────────────────────┐ │ ┌───────────────────────┐ │
│ Task1的TCB │←─────────────┘ │ Task1的任务堆栈 │←───┘
│ (存任务优先级、堆栈指针、任务名等)│ │ (存局部变量、函数栈帧等)│
└───────────┬───────────┘ └───────────┬───────────┘
│ │
┌───────────▼───────────┐ ┌───────────▼───────────┐
│ 步骤4:初始化TCB │ │ 步骤4:初始化任务堆栈 │
│ (关联任务堆栈、任务函数)│ │ (设置栈顶指针、上下文初始化)│
└───────────┬───────────┘ └───────────┬───────────┘
│ │
└──────────────────────┐ ┌─────────────┘
│ │
┌──────────────────────────────────▼────▼─────────────────────────┐
│ 步骤1:调用xTaskCreate(Task1_Entry, "Task1", ...) │
└──────────────────────────────────┬─────────────────────────────┘
│
┌──────────────────────────────────▼─────────────────────────────┐
│ 步骤5:将Task1的TCB加入FreeRTOS任务链表(内核管理) │
└──────────────────────────────────┬─────────────────────────────┘
│
┌──────────────────────────────────▼─────────────────────────────┐
│ 任务1运行时:使用"自己的任务堆栈"执行Task1_Entry函数 │
│ (与主栈MSP无关,主栈仅用于中断/内核启动流程) │
└─────────────────────────────────────────────────────────────────┘
动态创建的任务 1,其堆栈是从 FreeRTOS 管理的堆空间中分配的独立栈区域,既不是主栈,也不是 "内核堆本身"(内核堆是分配来源,任务堆栈是分配后的独立资源)。
三、部分详解
SRAM 是 MCU 运行时的读写内存(断电数据丢失),所有动态数据、运行时变量均存储于此。结合图中结构,其内部分为 4 个核心段,各段功能、存储内容如下:
3.1 .data 段(已初始化读写变量段)
-
存储内容:已初始化的可读写全局变量、可读写静态变量(包括文件级静态变量、函数内静态变量)。
-
核心特性 :
- 该段的初始值 并非存储在 SRAM 中,而是预先固化在 Flash 的
.rwdata段; - MCU 上电后,启动文件会自动将 Flash 中
.rwdata的初始值复制到 SRAM 的.data 段,程序运行时直接读写此段数据。
- 该段的初始值 并非存储在 SRAM 中,而是预先固化在 Flash 的
-
示例 :
int g_init_val = 100; // 全局已初始化变量 → .data段 static int s_init_val = 200; // 静态已初始化变量 → .data段 -
对应编译指标 :Keil 输出中的
RW-data,即.data 段的大小。
3.2 .bss 段(未初始化读写变量段)
-
存储内容:未初始化的可读写全局变量、未初始化的可读写静态变量。
-
核心特性 :
- 无需占用 Flash 空间(仅需记录段大小);
- MCU 上电后,启动文件会自动将此段全部清零,保证未初始化变量的初始值为 0。
-
示例 :
int g_uninit_val; // 全局未初始化变量 → .bss段 static int s_uninit_val; // 静态未初始化变量 → .bss段 -
对应编译指标 :Keil 输出中
ZI-data的一部分(另一部分为堆、栈)。
3.3 .heap 堆区
-
存储内容 :程序运行中动态分配的内存 (通过
malloc()、free()或 FreeRTOS 的pvPortMalloc()管理)。 -
核心特性 :
- 由用户程序手动申请 / 释放,内存 "向上生长"(从低地址向高地址分配);
- 堆的大小需在启动文件(如
startup_stm32xxxx.s)或 FreeRTOS 配置文件中手动定义(如configTOTAL_HEAP_SIZE)。
-
示例 :
int *p_buf = malloc(100); // 动态申请100字节 → .heap堆区 free(p_buf); // 释放堆内存 -
对应编译指标 :包含在 Keil 输出的
ZI-data中(属于未初始化全局内存池)。
3.4 .stack 栈区
-
存储内容 :函数调用的参数、返回地址 ,函数内的局部变量 ,以及中断上下文(寄存器数据)。
-
核心特性 :
- 由编译器 / MCU 硬件自动管理(函数调用时分配、函数返回时释放);
- 内存 "向下生长"(从高地址向低地址分配);
- 栈的大小需在启动文件中手动定义(如
Stack_Size),栈溢出会导致程序崩溃。
-
示例 :
void test_func(int param) { // param参数 → .stack栈区 int local_val = 50; // 局部变量 → .stack栈区 } -
对应编译指标 :包含在 Keil 输出的
ZI-data中(属于未初始化全局内存池)。
3.5 SRAM 各段的总占用逻辑
SRAM 的总占用空间 = .data段(RW-data) + .bss段 + .heap堆区 + .stack栈区,对应 Keil 编译输出中的 RW-data + ZI-data(ZI-data已包含.bss、堆、栈)。
四、实战分析
Flash(ROM)占用:需包含所有 "固化到 Flash 的内容"(代码、只读数据、已初始化变量的初始值),公式:
Flash占用 = Code + RO Data + RW Data
SRAM 占用:需包含所有 "运行时在 SRAM 的读写内容"(已初始化变量、未初始化变量、堆、栈),公式:
SRAM占用 = RW Data + ZI Data
4.1 STM32

- 计算逻辑:
Code(代码段) + RO Data(只读数据段) + RW Data(可读写数据初始值)
10852 + 496 + 152 = 11500(单位:字节),即 11.23KB
- 计算逻辑:
RW Data(可读写变量,运行时在SRAM) + ZI Data(未初始化变量,运行时在SRAM)
152 + 43392 = 43544(单位:字节)
4.2 W800

| 字段 | 数值 |
|---|---|
| Code(代码段) | 107748 |
| RO Data(只读数据段) | 102175 |
| RW Data(已初始化读写变量初始值) | 1486 |
| ZI Data(未初始化变量 + 堆 + 栈) | 12128 |
Flash占用 = 107748 + 102175 + 1486 = 211409 字节
SRAM占用 = 1486 + 12128 = 13614 字节
五、问题补充
5.1 .rwdata搬到SRAM会变成.data和.bss吗
这个说法不准确 ,.rwdata(Flash 中的段)搬到 SRAM 后仅对应.data 段,和.bss 段无关 ------ 两者的来源、功能完全不同,下面拆解清楚:
先明确三者的核心关系
- Flash 中的.rwdata 段 :仅存储已初始化可读写全局 / 静态变量的初始值 (比如
int g_val = 10;的 "10")。 - SRAM 中的.data 段 :是
.rwdata的 "运行时载体"------MCU 上电后,启动程序会把 Flash 中.rwdata 的初始值复制到 SRAM 的.data 段,程序运行时直接读写.data 段的变量。 - SRAM 中的.bss 段 :存储未初始化可读写全局 / 静态变量 (比如
int g_uninit_val;),这部分在 Flash 中没有对应的段(无需存储初始值),启动时仅需将.bss 段清零即可。
结论
.rwdata搬到 SRAM 后只对应.data 段,不会涉及.bss 段 ------ 因为.bss 的变量没有初始值,不需要从 Flash 的.rwdata 中复制数据。
5.2 .bss段
.bss 段里的 "数据"(本质是未初始化变量的内存空间)完全不从 Flash 加载任何内容 ,也没有任何来自 Flash 的数据源 ------ 它的 "产生" 是 MCU 上电后,在 SRAM 中划定空白区域并强制清零的纯内部操作,全程和 Flash 无数据交互。
下面用 "四阶段拆解" 讲清.bss 段的完整产生逻辑,彻底厘清 "无加载、仅生成" 的核心:
阶段 1:编译阶段 ------ 仅 "标记",不生成任何 Flash 数据
编译器扫描代码时,遇到未初始化的全局 / 静态变量(如int g_uninit; static int s_uninit;):
- 仅做 "类型标记":把这些变量归类到
.bss段,记录变量的大小、对齐要求(比如 int 占 4 字节,char 占 1 字节); - 不生成任何二进制数据:既不把变量写入 Flash 的二进制文件,也不存储任何初始值(因为变量无初始化);
- 对比:已初始化变量(
int g_init=10;)会被标记为.data,并把 "10" 这个初始值写入 Flash 的二进制文件(对应.rwdata 段),但.bss 变量无此步骤。
阶段 2:链接阶段 ------ 仅 "划定 SRAM 地址",仍和 Flash 无关
链接器根据链接脚本 (如STM32F103XE_FLASH.ld)的规则,为.bss 段分配 SRAM 地址:
- 先为
.data段分配 SRAM 地址(比如0x20000000~0x20000FFF),用于承接后续从 Flash 复制的初始值; - 紧接着在 SRAM 中划定
.bss段的连续空白地址范围 (比如0x20001000~0x20002000),并生成两个关键符号:__bss_start:.bss 段的起始地址(如0x20001000);__bss_end:.bss 段的结束地址(如0x20002000);
- 核心:此阶段仅 "确定地址范围",SRAM 中该区域的原始数据是随机的(上电后 SRAM 默认是乱码),且链接器不会向 Flash 写入任何和.bss 相关的数据。
阶段 3:烧录阶段 ------Flash 中无任何.bss 相关内容
烧录工具(ST-Link/JLink)把编译链接生成的.bin/.hex文件写入 Flash 时:
- 仅写入 Flash 需要的内容:.text(代码)、.rodata(只读数据)、.rwdata(已初始化变量的初始值);
- 完全忽略.bss 段:因为.bss 没有对应的二进制数据,烧录工具的 Flash 写入列表里,根本没有.bss 的相关地址和数据。
阶段 4:上电启动阶段 ------ 唯一 "生成数据(全 0)" 的步骤(纯 SRAM 操作)
MCU 上电复位后,启动文件(汇编代码)执行.bss 段的 "初始化"------ 这是.bss 段 "有内容(全 0)" 的唯一来源,全程无 Flash 参与:
- 读取链接器生成的
__bss_start和__bss_end,确定要清零的 SRAM 地址范围; - 用汇编指令循环把该范围的所有地址强制写入 0(覆盖 SRAM 原本的随机值);
- 完成后,未初始化变量(如
g_uninit)就对应到 SRAM 中被清零的地址,程序运行时可直接读写。
直观示例(以int g_uninit;为例)
| 阶段 | Flash 状态 | SRAM 状态 | .bss 段的变化 |
|---|---|---|---|
| 编译 | 无 g_uninit 相关数据 | 无(未上电) | 标记 g_uninit 为.bss,记录占 4 字节 |
| 链接 | 无 g_uninit 相关数据 | 划定 g_uninit 的地址:0x20001000 | 确定__bss_start=0x20001000 |
| 烧录 | 无 g_uninit 相关数据 | 无(未上电) | 无任何操作 |
| 上电启动 | 无交互 | 0x20001000 地址被写入 0 | g_uninit 的内存空间生成(值为 0) |
核心总结:.bss 段的 "产生" 和 Flash 无关,分两层
- 内存空间的产生:链接器在 SRAM 中划定的空白地址范围(不是从 Flash 加载,是提前规划的 SRAM 区域);
- 数据(全 0)的产生:启动文件对该地址范围强制写 0(不是从 Flash 复制,是 MCU 主动生成)。
简单说:.bss 段没有 "从 Flash 加载" 的过程,它的所有内容都是 MCU 上电后,在自己的 SRAM 里 "划一块空地,然后扫干净(清零)" 的结果。