嵌入式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-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 思路):

  1. Build 后打开 .map :在 Global Symbols / Execution Region 相关段落里找 g_ro / g_rw / g_zi 的 Load / 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。

具体符号名(如 __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++ 静态初始化

  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 文本 为准。

相关推荐
额额额对了8 分钟前
嵌入式 Linux 启动程序文件
开发语言·嵌入式硬件·php
甜甜的大香瓜34 分钟前
【MT32F006】MT32F006之PWM控制背光灯(白光)
单片机
李日华大战鸡红35 分钟前
FOC SVPWM过调制(学习记录)
stm32·单片机·学习
独孤九剑打醒他42 分钟前
【原创开源·修订版】源-栅-漏-栅-源横向双栅MOS:从“被误解的短路”到“电流路径多值逻辑与顶层供电架构”
前端·嵌入式硬件·架构·开源·硬件工程
千里马021 小时前
STM32读取AD7606-4(HAL库)
stm32·单片机·嵌入式硬件
weixin_464078071 小时前
电路中补偿器的作用
嵌入式硬件
不会代码的小猴3 小时前
中断(NVIC)
单片机·嵌入式硬件
糖糖单片机设计3 小时前
基于STM32的空气污染检测与报警系统设计与实现
stm32·单片机·嵌入式硬件
盛世宏博北京3 小时前
工业环境稳定采集:配电室 IP 温湿度变送器 TCP/UDP 通信部署指南
单片机·tcp/ip·udp