嵌入式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-Code 与 System 总线分工,使 从 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 + ZI 与 scatter 为准,不要硬套「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 思路):
- Build 后打开
.map:在 Global Symbols / Execution Region 相关段落里找g_ro/g_rw/g_zi的 Load / Execution 地址与所在 Region。 - Debug → 全速或断在
main:打开 Memory 窗口,分别输入&g_ro、&g_rw、&g_zi,对照是否在 Flash 映射区 与 SRAM 映射区 (与芯片 Memory map 一致)。 - 单步观察
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
- 硬件最小初始化 (时钟、Flash 等待周期等)------芯片手册 与 厂商 startup。
- 把
.data从加载地址拷到运行地址(若加载域与执行域分离)。 .bss清零。- (若用库) 初始化
__libc_init_array等,再进入main。
具体符号名(如 __main、Image$$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 6 与 Compiler 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 执行域 ------再在此基础上拆 多段 SRAM 、TCM 、外扩 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++ 静态初始化
- 中断服务程序里定义大局部数组 :占用 当前栈 ,一次中断就可能 吃掉数 KB ,极易触发 栈溢出 。大缓冲应 静态化 或 全局化 并明确 互斥/重入。
- C++ 全局对象构造函数 :可能在
main之前 执行;若依赖 尚未初始化的硬件 或 假定.bss已清零的隐含顺序 ,会在 不同优化/链接顺序 下出现 难复现 问题。应在 厂商 C++ 启动说明 与 ABI 文档 下核对 pre-main 初始化顺序。 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 文本 为准。