嵌入式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. 知识地图1. 片上存储Flash 与 SRAM 各干什么2. 链接器视角.text、.rodata、.data、.bss3. Keil MAPCode、RO、RW、ZI 与段的对应4. 最小可验证示例data、bss 与只读常量5. 上电到main启动代码在做什么6. 加载域与执行域分散加载在解决什么问题6.1 极简 scatter 文件模板示意7. 栈、堆与「相向增长」风险8. 省 SRAM 的编码习惯9. 把变量钉到指定 SRAM__attribute__((section(...)))10. MAP 与启动参数的排障思路10.1 HardFault 排错决策树11. 常见坑ISR 大栈、C 静态初始化12. 延伸阅读与免责声明0. 知识地图SRAM易失启动拷贝不单独占 Flash 初值区Flash非易失.text 指令.rodata 只读常量.data 初值镜像.data 运行副本.bss 清零区StackHeapReset_Handler / __main进入 main1. 片上存储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 MAPCode、RO、RW、ZI 与段的对应MAP 文件是「谁占了多少 Flash / SRAM」的账本不同工程模板行项略有出入下表为教学用对应关系以ARM Compiler常见输出习惯为参考以你工程 MAP 原文为准。MAP 汇总项常见典型含义与段的直觉关系FlashSRAMCode指令体积主要来自.text是否XIPRO Data只读数据.rodata等是通常否RW Data已初始化可写数据.data初值在 Flash运行副本在 RAM计初值镜像计RAM 副本ZI DataZero Init.bss等零初始化区否是注意ZI在 MAP 里常主要指.bss栈、堆往往由分散加载或启动文件里的Stack_Size/Heap_Size单独划出RAM 区间有时显示为独立行或合并到RAM 总用量——以 MAPTotal RW ZI与scatter为准不要硬套「ZI 一定等于 bss栈堆」的单一公式。粗算口诀规划用Flash 压力Code RO RW 初值与 MAP 各栏对齐看。SRAM 压力RW 运行区 ZIbss Stack Heap 你自定义的 RAM 段。4. 最小可验证示例data、bss 与只读常量下面是一段刻意缩小的 C 程序用于在MAP与调试器 Memory 窗口里「对号入座」只读常量、带初值可写全局、零初始化全局。/* 建议先用 -O0 编译避免整段被优化掉便于对照符号与地址 */staticconstintg_ro10;/* 典型进 RO / .rodata随 Flash */intg_rw20;/* Flash 存初值镜像运行副本在 SRAM .data */intg_zi;/* 典型.bss启动清零 */intmain(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 RegionCPU真正访问时的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 类地址习惯示例务必改成你芯片手册中的真实范围; LR_* Load Region, ER_* Execution Region示意命名 LR_IROM1 0x08000000 0x00100000 { ; Flash1MB 示例 ER_IROM1 0x08000000 0x00100000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) ; 代码 只读数据落在 Flash } RW_IRAM1 0x20000000 0x00020000 { ; SRAM128KB 示例 .ANY (RW ZI) ; 已初始化 RW、零初始化 ZI落在 RAM } }记一句RO含 Code进 Flash 执行域RW ZI 进 RAM 执行域——再在此基础上拆多段 SRAM、TCM、外扩 SDRAM等。ARM Compiler 6的 scatter 关键字与段选择语法可能不同请以当前工具链文档与芯片厂商.sct模板为准勿跨版本硬抄。7. 栈、堆与「相向增长」风险区域谁分配典型增长方向示意Stack编译器为局部变量、调用链、中断保存等自动使用常向低地址增长依 ABI/芯片Heapmalloc/new若启用常向高地址增长若栈太大 堆太猛 大数组会在SRAM 中部「相遇」导致静默踩内存表现为随机 HardFault、变量被改。应在MAP scatter Stack_Size/Heap_Size上留余量并对最深调用链 最坏中断嵌套做栈水位估计。8. 省 SRAM 的编码习惯做法目的大常量加const促使其进RO避免占可写 RAM少用大块全局缓冲直接缩小.bss/.data对 SRAM 的压力能栈则栈、能静态生命周期则明确控制生存期与可见性避免无意全局static conststatic管作用域const管只读语义组合常用于文件内只读表9. 把变量钉到指定 SRAM__attribute__((section(...)))需要DMA 可见、对齐、不与其他缓冲混放时常把缓冲区放进自定义执行域例如RW_IRAM2并在源码里__attribute__((section(.dma_buf),aligned(4)))staticuint8_tg_dma_buf[512];要点section 名必须与scatter 中执行域/输入段一致对齐需满足DMA 与外设要求Cache 一致性若带 Cache 的 M 系列另需维护/禁用策略超出本文篇幅。10. MAP 与启动参数的排障思路现象先查HardFault、单步进main前即崩启动栈是否足够、时钟/总线初始化、向量表偏移进main后随机崩栈溢出、堆损坏、中断里用栈过多SRAM 爆了MAP 的RW/ZI、scatter 的RAM 区总大小、Stack_Size/Heap_SizeFlash 爆了Code/RO、是否误把大表放进RW、调试符号级别栈水位魔数填充上电时把栈区用固定图案填满运行一段时间后检查未被改写的最低位置估算剩余栈粗估且需关优化/注意中断同时用栈。10.1 HardFault 排错决策树是否是否是否发生 HardFaultfault 出现在进入 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 文本为准。