【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 里 "划一块空地,然后扫干净(清零)" 的结果。

相关推荐
国科安芯15 小时前
微小卫星红外相机双MCU冗余架构的抗辐照可靠性评估
人工智能·单片机·嵌入式硬件·数码相机·架构·自动化·安全性测试
电子阿板15 小时前
STM32G0B1 NRST复位和其它IO复用了,如何设置成专用复位引脚,
stm32·单片机·嵌入式硬件
兆龙电子单片机设计15 小时前
【STM32项目开源】STM32单片机智慧农业大棚控制系统
stm32·单片机·物联网·开源·毕业设计
不脱发的程序猿15 小时前
使用Python高效对比多个相似的CAN DBC数据
python·单片机·嵌入式硬件·嵌入式
bai54593615 小时前
STM32 CubeIDE 串口通信
stm32·单片机·嵌入式硬件
国科安芯15 小时前
强辐射环境无人机视频系统MCU可靠性分析
人工智能·单片机·嵌入式硬件·音视频·无人机·边缘计算·安全性测试
代码游侠16 小时前
应用——基于 51 单片机的多功能嵌入式系统
笔记·单片机·嵌入式硬件·学习·51单片机
广药门徒16 小时前
为什么访问一地址存16bits的存储芯片需要字节对齐?为什么访问外部Flash需要字节对齐?——深入理解STM32 FMC的地址映射机制
stm32·单片机·嵌入式硬件
jh10_16 小时前
嵌入式硬件DAY5(ARM汇编)
汇编·arm开发·嵌入式硬件
国科安芯16 小时前
尺寸约束下商业卫星编码器系统的抗辐照MCU性能边界研究
运维·单片机·嵌入式硬件·安全·安全威胁分析