嵌入式MCU内存布局详解 Flash SRAM Keil MAP与启动分散加载实践

嵌入式MCU内存布局详解_Flash_SRAM_Keil_MAP与启动分散加载实践

本文面向 ARM Cortex-M 系 MCU 上、用 Keil MDK(µVision)+ ARM Compiler 一类工具链做 裸机或轻量 RTOS 的开发者:讲清 片内 Flash 与 SRAM 如何分工.text / .rodata / .data / .bss 在映像与运行期的位置上电启动如何把 RW 数据「搬进 RAM」MAP 里 Code / RO / RW / ZI 各栏在说什么 ,以及 分散加载(scatter)栈堆与 DMA 缓冲区 的常见工程注意点。

边界 :本文讨论 MCU 物理地址空间链接脚本/启动代码 。若你熟悉的是 Linux 用户态 ELF 下用 readelf/nm 对照 动态加载器与页式虚拟内存 的排障方式,请注意二者虽常有 同名段宿主与证据工具不同,请勿照搬步骤。

阅读提示 :正文含 Mermaid;静态站需开启 Mermaid 渲染。


目录

  • [0. 知识地图](#0. 知识地图)
  • [1. 片上存储:Flash 与 SRAM 各干什么](#1. 片上存储:Flash 与 SRAM 各干什么)
  • [2. 链接器视角:.text.rodata.data.bss](#2. 链接器视角:.text、.rodata、.data、.bss)
  • [3. Keil MAP:Code、RO、RW、ZI 与段的对应](#3. Keil MAP:Code、RO、RW、ZI 与段的对应)
  • [4. 最小可验证示例:data、bss 与只读常量](#4. 最小可验证示例:data、bss 与只读常量)
  • [5. 上电到 main:启动代码在做什么](#5. 上电到 main:启动代码在做什么)
  • [6. 加载域与执行域(分散加载在解决什么问题)](#6. 加载域与执行域(分散加载在解决什么问题))
    • [6.1 极简 scatter 文件模板(示意)](#6.1 极简 scatter 文件模板(示意))
  • [7. 栈、堆与「相向增长」风险](#7. 栈、堆与「相向增长」风险)
  • [8. 省 SRAM 的编码习惯](#8. 省 SRAM 的编码习惯)
  • [9. 把变量钉到指定 SRAM:__attribute__((section(...)))](#9. 把变量钉到指定 SRAM:attribute((section(...))))
  • [10. MAP 与启动参数的排障思路](#10. MAP 与启动参数的排障思路)
    • [10.1 HardFault 排错决策树](#10.1 HardFault 排错决策树)
  • [11. 常见坑:ISR 大栈、C++ 静态初始化](#11. 常见坑:ISR 大栈、C++ 静态初始化)
  • [12. 延伸阅读与免责声明](#12. 延伸阅读与免责声明)

0. 知识地图

SRAM(易失)
启动拷贝
不单独占 Flash 初值区
Flash(非易失)
.text 指令
.rodata 只读常量
.data 初值镜像
.data 运行副本
.bss 清零区
Stack
Heap
Reset_Handler / __main
进入 main


1. 片上存储:Flash 与 SRAM 各干什么

存储器 典型特性 运行时角色
Flash 掉电保持、按块擦写、随机读适合取指与读常量 机器码只读常量 、以及 .data 的初始值镜像(加载域)
SRAM 掉电丢失、读写快、容量通常远小于 Flash .data 副本.bss 、以及你希望 CPU/DMA 高速访问 的缓冲区

许多 Cortex-M 采用 改进哈佛总线 思路:I-Code / D-CodeSystem 总线分工,使 从 Flash 取指访问 SRAM 数据 可并行规划;具体总线图以 芯片参考手册 为准。


2. 链接器视角:.text.rodata.data.bss

段(常用名) 放什么 Flash 侧 SRAM 侧
.text 指令、部分常量池(与编译器相关) 一般不占(XIP 从 Flash 执行时)
.rodata 字符串字面量、const 大数组等只读数据 可不镜像(直接只读映射到 Flash,取决于分散加载与编译器选项)
.data 已初始化全局/静态变量 初值 运行期可读写的副本
.bss 未初始化零初始化全局/静态 通常 不占「初值字节」 上电后 清零占位

直觉.data 的「初值」若也只在 SRAM 里生成,掉电会丢;因此常见做法是 Flash 里放一份镜像 ,启动时再 拷到 SRAM.bss 则只需在 SRAM 里 留出大小并清零,映像文件不必携带全零字节。


3. Keil MAP:Code、RO、RW、ZI 与段的对应

MAP 文件是 「谁占了多少 Flash / SRAM」 的账本;不同工程模板行项略有出入,下表为 教学用对应关系 (以 ARM Compiler 常见输出习惯为参考,以你工程 MAP 原文为准)。

MAP 汇总项(常见) 典型含义 与段的直觉关系 Flash SRAM
Code 指令体积 主要来自 .text 否(XIP)
RO Data 只读数据 .rodata 通常否
RW Data 已初始化可写数据 .data(初值在 Flash,运行副本在 RAM) 初值镜像 RAM 副本
ZI Data Zero Init .bss 等零初始化区

注意ZI 在 MAP 里常主要指 .bss栈、堆 往往由 分散加载启动文件里的 Stack_Size / Heap_Size 单独划出 RAM 区间 ,有时显示为独立行或合并到 RAM 总用量 ------以 MAP Total RW + ZIscatter 为准,不要硬套「ZI 一定等于 bss+栈+堆」的单一公式。

粗算口诀(规划用)

  • Flash 压力Code + RO + RW 初值(与 MAP 各栏对齐看)。
  • SRAM 压力RW 运行区 + ZI(bss)+ Stack + Heap + 你自定义的 RAM 段

4. 最小可验证示例:data、bss 与只读常量

下面是一段 刻意缩小 的 C 程序,用于在 MAP调试器 Memory 窗口 里「对号入座」:只读常量带初值可写全局零初始化全局

c 复制代码
/* 建议先用 -O0 编译,避免整段被优化掉,便于对照符号与地址 */
static const int g_ro = 10;   /* 典型:进 RO / .rodata,随 Flash */
       int       g_rw = 20;   /* Flash 存初值镜像,运行副本在 SRAM .data */
       int       g_zi;        /* 典型:.bss,启动清零 */

int main(void) {
    for (;;) {
        g_rw++;
        g_zi++;
        (void)g_ro;
    }
}

自证步骤(Keil 思路)

  1. Build 后打开 .map :在 Global Symbols / Execution Region 相关段落里找 g_ro / g_rw / g_ziLoad / Execution 地址与所在 Region
  2. Debug → 全速或断在 main :打开 Memory 窗口,分别输入 &g_ro&g_rw&g_zi ,对照是否在 Flash 映射区SRAM 映射区 (与芯片 Memory map 一致)。
  3. 单步观察 g_rw 自增后 SRAM 副本 变化;g_ro 若在 Flash 只读区,尝试写入应触发 HardFault(教学演示即可,勿在产品里故意写)。

说明const 是否完全不可写、是否与其他常量合并,随 编译器优化是否取址 略有差异;以本机 MAP 与反汇编为准


5. 上电到 main:启动代码在做什么

典型 Reset_Handler 之后,在进 main() 之前,C 运行环境需要就绪:
main() __copy/__zero 等 启动汇编/库 上电/复位 main() __copy/__zero 等 启动汇编/库 上电/复位 关中断/时钟/重定位向量(依芯片) 初始化时钟与存储控制器(依芯片) 拷贝 .data 映像 → SRAM 清零 .bss 调用 main

  1. 硬件最小初始化 (时钟、Flash 等待周期等)------芯片手册厂商 startup
  2. .data 从加载地址拷到运行地址(若加载域与执行域分离)。
  3. .bss 清零
  4. (若用库) 初始化 __libc_init_array 等,再进入 main

具体符号名(如 __mainImage$$RW$$Base 一类)随 ARM Compiler 版本分散加载命名 变化,以 工程 map/listing官方启动例程 为准。


6. 加载域与执行域(分散加载在解决什么问题)

加载域(Load Region) :镜像 烧进 Flash 时各段的 排布与总大小 (「占 ROM 多少」)。
执行域(Execution Region) :CPU 真正访问 时的 RAM/ROM 地址(「跑起来在哪」)。

.data 的运行地址必须在 SRAM 、而 初值又必须随镜像进 Flash 时,就需要 分散加载文件(scatter file) 告诉链接器:这一段在 Flash 里占位 ,但 链接地址在 SRAM。这也是嵌入式与「纯 RAM 里展开 ELF」类环境的典型差异点。

工程提示ARM Compiler 6Compiler 5 的 scatter 语法、库初始化细节不同;迁移工程时 对照厂商模板 比背语法更安全。

6.1 极简 scatter 文件模板(示意)

下面给出 ARM Compiler 5 时代常见写法最小骨架 (Flash 起始 0x08000000、RAM 起始 0x20000000 仅为 STM32 类地址习惯示例务必改成你芯片手册中的真实范围):

text 复制代码
; LR_* = Load Region, ER_* = Execution Region(示意命名)
LR_IROM1 0x08000000 0x00100000  {    ; Flash:1MB 示例
  ER_IROM1 0x08000000 0x00100000  {
    *.o (RESET, +First)
    *(InRoot$$Sections)
    .ANY (+RO)                    ; 代码 + 只读数据:落在 Flash
  }
  RW_IRAM1 0x20000000 0x00020000  { ; SRAM:128KB 示例
    .ANY (+RW +ZI)                ; 已初始化 RW、零初始化 ZI:落在 RAM
  }
}

记一句RO(含 Code)进 Flash 执行域RW + ZI 进 RAM 执行域 ------再在此基础上拆 多段 SRAMTCM外扩 SDRAM 等。ARM Compiler 6 的 scatter 关键字与段选择语法可能不同,请以当前工具链文档与芯片厂商 .sct 模板为准,勿跨版本硬抄。


7. 栈、堆与「相向增长」风险

区域 谁分配 典型增长方向(示意)
Stack 编译器为局部变量、调用链、中断保存等自动使用 常向 低地址 增长(依 ABI/芯片)
Heap malloc/new(若启用) 常向 高地址 增长

栈太大 + 堆太猛 + 大数组 ,会在 SRAM 中部「相遇」 导致静默踩内存,表现为 随机 HardFault 、变量被改。应在 MAP + scatter + Stack_Size/Heap_Size 上留余量,并对 最深调用链 + 最坏中断嵌套栈水位 估计。


8. 省 SRAM 的编码习惯

做法 目的
大常量加 const 促使其进 RO ,避免占 可写 RAM
少用大块全局缓冲 直接缩小 .bss/.data 对 SRAM 的压力
能栈则栈、能静态生命周期则明确 控制 生存期可见性,避免无意全局
static const static作用域const只读语义 ;组合常用于 文件内只读表

9. 把变量钉到指定 SRAM:__attribute__((section(...)))

需要 DMA 可见、对齐、不与其他缓冲混放 时,常把缓冲区放进 自定义执行域 (例如 RW_IRAM2),并在源码里:

c 复制代码
__attribute__((section(".dma_buf"), aligned(4)))
static uint8_t g_dma_buf[512];

要点section 名 必须与 scatter 中执行域/输入段 一致;对齐 需满足 DMA 与外设 要求;Cache 一致性 (若带 Cache 的 M 系列)另需 维护/禁用策略,超出本文篇幅。


10. MAP 与启动参数的排障思路

现象 先查
HardFault 、单步进 main 前即崩 启动栈是否足够时钟/总线初始化向量表偏移
main 后随机崩 栈溢出堆损坏中断里用栈过多
SRAM 爆了 MAP 的 RW/ZI 、scatter 的 RAM 区总大小Stack_Size/Heap_Size
Flash 爆了 Code/RO 、是否误把 大表 放进 RW调试符号级别

栈水位(魔数填充) :上电时把栈区用固定图案填满,运行一段时间后检查 未被改写的最低位置 ,估算剩余栈(粗估,且需关优化/注意中断同时用栈)。

10.1 HardFault 排错决策树







发生 HardFault
fault 出现在进入 main 之前?
查启动栈大小 / Reset_Handler

时钟与 Flash 等待周期

VTOR 向量表偏移是否匹配 APP 起始地址
fault 仅在中断里或

与某 ISR 频率相关?
查 ISR 内大局部数组与调用深度

禁止 ISR 内 malloc/new

核对中断优先级与重入
fault 随机、难复现?
优先怀疑栈溢出与堆越界

魔数水位 / MAP 栈区余量

多线程与中断叠加的最坏栈
对齐访问、总线错误、MPU 配置

空指针与野指针等逻辑问题

结合 CFSR/BFAR 等寄存器分析

寄存器级 CFSR / HFSR / BFAR 解读依赖 ARMv7-M / v8-M 文档与芯片 Debug 章节,本文不展开位域表。


11. 常见坑:ISR 大栈、C++ 静态初始化

  1. 中断服务程序里定义大局部数组 :占用 当前栈 ,一次中断就可能 吃掉数 KB ,极易触发 栈溢出 。大缓冲应 静态化全局化 并明确 互斥/重入
  2. C++ 全局对象构造函数 :可能在 main 之前 执行;若依赖 尚未初始化的硬件假定 .bss 已清零的隐含顺序 ,会在 不同优化/链接顺序 下出现 难复现 问题。应在 厂商 C++ 启动说明ABI 文档 下核对 pre-main 初始化顺序
  3. malloc 在 ISR 中 :除 重入/锁 问题外,还会 动态拉升堆 ,与 实时性碎片 冲突------裸机项目通常 禁止限定在初始化阶段

12. 延伸阅读与免责声明

12.1 权威与厂商文档(技术依据)

  • ARM Compiler / scatter loading :以当前安装的 ARM Compiler 文档Keil 帮助 为准(版本差异大)。
  • 芯片 :以 具体 MCU 参考手册Memory Map、总线、Flash 控制器、DMA 章节为准。

12.2 免责声明

不同芯片的 SRAM 分区、是否 XIP、是否带 Cache、是否双区 Flash 都会影响段放置与启动流程。本文 不提供 针对某一型号寄存器级「照抄即跑」的脚本;所有 scatter 片段 须在 目标芯片模板 上验证。MAP 行项名称 随工具版本可能微调,排障以 实际 MAP 文本 为准。

相关推荐
qdprobot2 小时前
【无标题】
人工智能·单片机·嵌入式硬件·51单片机·硬件工程·iot·mixly
Hello:CodeWorld2 小时前
μC/OS vs FreeRTOS:嵌入式实时操作系统深度对比
c语言·开发语言·单片机
振南的单片机世界2 小时前
电机反电动势:断电瞬间的“高压反击”,续流二极管挡驾
单片机·嵌入式硬件
平凡灵感码头2 小时前
MCU 组成原理详解—— 从硬件框图透视微控制器的完整架构
单片机·嵌入式硬件·架构
山木嵌入式2 小时前
【STM32进阶】中断体系全解析:从核心原理到实战(含面试高频考点)
stm32·嵌入式硬件·面试·中断·nvic
puamac3 小时前
c#打开cmd然后输入claude
stm32·单片机·c#
搁浅小泽3 小时前
电子行业常用仪器设备介绍
嵌入式硬件·可靠性工程师
zd8451015003 小时前
[嘉立创EDA]导出BOM设置
嵌入式硬件