【Embedded Development】对于MCU的片内内存里的分布区域结构详解

一、简介

对于代码运行除了存放代码的地方------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" 和 "内存映射寄存器",需明确:

  1. RAM 是独立的地址段:如 Cortex-M 架构中,RAM 地址段(0x20000000~0x200FFFFF)与外设寄存器段(0x40000000~0x400FFFFF)、内核寄存器段(0xE0000000~0xE00FFFFF)完全分离,后者并非 RAM(本质是外设的控制 / 状态寄存器,物理介质不是 SRAM);
  2. 位带区是 RAM 的操作扩展:Cortex-M 的 RAM 位带区(0x22000000~0x23FFFFFF)并非独立 RAM,而是将普通 RAM 的每一位映射到新地址,实现 "按位操作"(如直接修改 RAM 某一位,无需掩码),本质是对 RAM 的访问方式扩展。

注意:一些地址是对其操作去控制寄存器的(如果有疑惑自行去了解地址总线和数据总线的相关概念)。

1.3 MCU RAM 开发的核心注意事项(纯 RAM 相关)

  1. 容量管控
    • 编译后查看.map 文件,统计.data+.bss+.stack+.heap 的总大小,不得超过片内 SRAM 物理容量;
    • 大数组(如 char buf [4096])避免定义为局部变量(占栈),改用 static(放入.data/.bss)或全局变量。
  2. 栈溢出防护
    • 启动文件中配置合理的 Stack_Size(如 STM32F103 建议≥1KB,复杂程序≥2KB);
    • 栈底设置 "哨兵值"(如uint32_t stack_guard __attribute__((at(0x20004FFC))) = 0xDEADBEEF;),运行中检测值是否被覆盖,判断栈溢出;
    • 禁止无限制递归调用(直接耗尽栈)。
  3. 堆区优化
    • MCU 中禁用裸 malloc/free(易产生碎片 / 泄漏),改用内存池(如 FreeRTOS 的 pvPortMalloc、自定义固定大小内存池);
    • 限定 Heap_Size 大小,避免堆过度增长覆盖栈。
  4. 数据对齐
    • ARM Cortex-M 要求 RAM 访问 4 字节对齐,非对齐访问(如强制操作int* p = (int*)0x20000001;)会触发 HardFault;
    • 结构体用__attribute__((packed))#pragma pack时需注意对齐风险。
  5. 低功耗与 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 段,程序运行时直接读写此段数据。
  • 示例

    复制代码
    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-dataZI-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 地址:

  1. 先为.data段分配 SRAM 地址(比如0x20000000~0x20000FFF),用于承接后续从 Flash 复制的初始值;
  2. 紧接着在 SRAM 中划定.bss段的连续空白地址范围 (比如0x20001000~0x20002000),并生成两个关键符号:
    • __bss_start:.bss 段的起始地址(如0x20001000);
    • __bss_end:.bss 段的结束地址(如0x20002000);
  3. 核心:此阶段仅 "确定地址范围",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 参与:

  1. 读取链接器生成的__bss_start__bss_end,确定要清零的 SRAM 地址范围;
  2. 用汇编指令循环把该范围的所有地址强制写入 0(覆盖 SRAM 原本的随机值);
  3. 完成后,未初始化变量(如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 无关,分两层

  1. 内存空间的产生:链接器在 SRAM 中划定的空白地址范围(不是从 Flash 加载,是提前规划的 SRAM 区域);
  2. 数据(全 0)的产生:启动文件对该地址范围强制写 0(不是从 Flash 复制,是 MCU 主动生成)。

简单说:.bss 段没有 "从 Flash 加载" 的过程,它的所有内容都是 MCU 上电后,在自己的 SRAM 里 "划一块空地,然后扫干净(清零)" 的结果。

相关推荐
国科安芯11 小时前
AS32A601型MCU芯片flash模块的擦除和编程
java·linux·前端·单片机·嵌入式硬件·fpga开发·安全性测试
创界工坊工作室19 小时前
DPJ-120 基于STC89C52的多功能清扫消杀车控制系统设计(源代码+proteus仿真)
stm32·单片机·嵌入式硬件·51单片机·proteus
Darken0320 小时前
引脚重映射是什么意思?如何使用?
stm32·单片机·引脚重映射
知南x20 小时前
【正点原子STM32MP157 可信任固件TF-A学习篇】(2) STM32MP1 中的 TF-A
stm32·嵌入式硬件·学习·stm32mp157
逐步前行21 小时前
C51_OLED
单片机
欢鸽儿1 天前
Vitis】Linux 下彻底清除启动界面 Recent Workspaces 历史路径
linux·嵌入式硬件·fpga
Bona Sun1 天前
单片机手搓掌上游戏机(二十三)—esp32运行简单街机模拟器软硬件准备
c语言·c++·单片机
做一道光1 天前
电机控制——电流采样(三电阻)
单片机·嵌入式硬件·学习·电机控制
d111111111d1 天前
STM32外设学习-WDG看门狗-(学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习