STM32 学习记录:内存分布、Bootloader 与 Keil 地址配置

发布时间:2026/10/26 18:16:38
STM32 学习记录:内存分布、Bootloader 与 Keil 地址配置

整理 MCU 内存这部分内容时,我比较容易混淆几个概念:Flash 地址和 RAM 地址有什么区别?Bootloader 是不是另一套程序?向量表放在 Flash 里,为什么第一项又是 RAM 地址?Keil 中不同位置的 Start、Size,是不是都在设置下载地址?

我的理解是,可以沿着“程序保存在 Flash → 复位时读取向量表 → 执行启动代码 → 进入 main → 使用 RAM 中的变量和栈”这条顺序看。下面以 STM32F103C8 为例,把地址配置、启动过程和编译后的内存统计放在一起,记录目前理解到的内容。

1. 先把地址、Flash 和 RAM 分清

MCU 的地址可以理解成 CPU 访问存储器或外设时使用的位置编号。0x表示十六进制,地址每增加 1,代表向后移动一个字节;地址范围里的空档也不一定有实际存储器。

STM32F103C8 的用户 Flash 为 64KB,SRAM 为 20KB。常见的几个区域如下,地址范围均包含两端。STM32F103C8 数据手册、ST Flash 编程手册 PM0075

区域地址范围容量主要用途
用户 Flash0x08000000~0x0800FFFF64KB保存自己烧录的程序、常量、需要持久化的数据,断电保留
SRAM0x20000000~0x20004FFF20KB保存运行时变量、缓冲区、堆和栈,断电后不能依赖其内容
系统存储器0x1FFFF000~0x1FFFF7FF2KB存放 ST 出厂预置的 Bootloader

地址范围与容量的关系是:容量 = 结束地址 - 起始地址 + 1。例如用户 Flash 的长度是0x10000字节,即 65536 字节;结束地址0x0800FFFF本身不是容量。

这里还容易混淆一个地址:0x00000000。Cortex-M3 复位时从这里读取启动向量;在 F103 选择用户 Flash 启动时,硬件会把 Flash 开头映射到这个地址。因此它可以理解成同一片内容的另一个访问入口,不是又增加了一块 Flash。

另一个问题是:静态局部变量退出函数后保存在哪里?答案仍然是 RAM。static改变了变量的存储期,让它在整个程序运行期间持续存在;并没有把它自动写入 Flash。

变量写法通常存放位置再次进入函数时断电或正常重新启动后
函数内int count = 0;栈或寄存器再次执行初始化不保留上次结果
函数内static int count = 0;RAM 的静态存储区域保留上次调用留下的值重新初始化为 0
显式写入 Flash 的计数或配置用户 Flash 的指定区域由程序读取使用可以保留,但需处理擦写和数据有效性

所以“函数退出后还在”和“断电后还在”是两件事:前者可以靠静态存储期,后者需要非易失存储。

2. Bootloader、APP 和向量表怎样连起来

两套工程,对应两份固件

Bootloader 是一套独立的启动程序,APP 是实现业务的应用程序。这里的 APP 指 MCU 应用,不是手机软件。两个工程通常分别编译,各有自己的启动代码和main(),最终烧到同一颗 MCU 的不同用户 Flash 区域。

程序主要职责存放位置
自定义 Bootloader检查镜像、选择启动版本,按方案安装或恢复固件用户 Flash 中自行预留的启动区
APP实现采集、通信、数据处理等业务用户 Flash 中自行预留的应用区
ST 出厂 Bootloader提供芯片预置的下载入口系统存储器

以“8KB Bootloader+56KB APP”为例,用户 Flash 可以这样划分。这里是单 APP 的布局示意,没有包含 A/B 双镜像:

用户 Flash 分区起始地址结束地址最大长度
Bootloader0x080000000x08001FFF0x2000,8KB
APP0x080020000x0800FFFF0xE000,56KB

这只是对同一片 Flash 的软件划分,没有物理隔板。程序越界写入、错误的下载地址或整片擦除,都可能破坏另一块区域;划分后还需要代码和工具配置共同遵守边界。

以前普通程序也有向量表

向量表是一张入口地址表,包含初始栈顶、复位入口,以及异常和外设中断处理入口。以前只有一个程序时,这张表同样存在,只是硬件和启动代码已经完成了前面的步骤。ARM 启动文件说明

现有 MDK 启动文件中,可以看到这样的定义:

__Vectors DCD __initial_sp ; 初始栈顶地址 DCD Reset_Handler ; 复位处理入口地址

我检查的中容量 F103 启动文件里,向量表总共有 59 项,每项 4 字节,总长 236 字节,也就是0xEC。这个大小取决于具体芯片和启动文件,并不是所有 MCU 都相同。

假设 APP 向量表放在0x08002000,前两项的含义如下:

表项所在位置表项保存的内容保存的值指向哪里
0x08002000开始的 4 字节APP 初始栈顶地址RAM 中的栈顶
0x08002004开始的 4 字节APP 的Reset_Handler入口地址APP 的启动代码
后续表项异常和中断入口地址各个处理函数

这里要区分“地址”和“该地址里存放的值”。0x08002000是向量表的位置;真正的 APP 启动入口,是从0x08002004读出的地址值。Bootloader 不是直接执行向量表中的数据,也不是直接跳进 APP 的main()。

启动场景谁完成最初的栈和入口加载后续过程
只有一个普通 APP复位硬件读取该程序的启动向量执行启动代码,初始化运行环境,再进入main()
Bootloader 加 APP复位硬件先读取 Bootloader 的启动向量Bootloader 检查 APP、完成栈和中断等运行状态的交接,再进入 APP 启动代码

以使用 ARMCC 5 和 ST 标准库启动模板的 STM32F103 工程为例,Reset_Handler先调用SystemInit(),再进入 C 运行库的__main,完成变量初始化等准备后调用用户编写的main()。因此,__main是运行库入口,main()才是应用入口;不同工具链的启动代码写法可能有所区别。

假设 Flash 起始地址为0x08000000,前 8KB 留给 Bootloader,那么 APP 的链接起始地址就是0x08002000。在本例的布局中,链接器将 APP 的向量表放在应用区开头,Bootloader 也从这个基址读取初始栈顶和复位入口。与此同时,还需要设置向量表寄存器VTOR,告诉 CPU 后续处理中断和异常时应查哪张表。

上述 ST 标准库模板在system_stm32f10x.c中提供了VECT_TAB_OFFSET,可以把偏移量设为:

#defineVECT_TAB_OFFSET0x2000/* 示例:APP向量表相对Flash起点偏移8KB */

SystemInit()会据此将 VTOR 设置为0x08002000。这里有两个不同的动作:链接配置决定向量表实际存放在哪里,VTOR 决定 CPU 去哪里查表。修改这个宏不会搬移固件,也不会改变链接结果;Bootloader 和 APP 的初始化代码需要保持一致,避免跳转后 VTOR 又被改回 Bootloader 的表。

烧录、复位和 A/B 是另外几件事

使用 J-Link 的 SWD 接口下载时,通常保持 BOOT0 为 0。进入 ST 出厂串口 Bootloader 时,需要按 F103 的启动配置切换 BOOT 引脚并复位;如果是从用户 Flash 启动自定义 Bootloader,则保持正常 Flash 启动配置。

两份正确链接的固件分别下载到各自区域后,芯片就保存了两套程序。复位时先进入自定义 Bootloader,再由它选择 APP;它们不是两项同时运行的后台任务。APP 调用NVIC_SystemReset()后,系统重新走启动流程,但升级标志必须先可靠写入,不能只放在普通全局变量里。

升级布局新版本怎样运行需要重点解决的问题
内部 A/B 直接启动Bootloader 选择 A 或 B 槽启动每个槽的镜像链接地址与向量表要匹配,不能默认同一个 BIN 随便换地址就能运行
固定 APP 区加暂存、备份区Bootloader 把新镜像安装到固定 APP 地址安装中掉电的恢复,以及旧镜像的保留

A/B 中的 A 不永远是旧版,B 也不永远是新版。新固件需要经过试运行确认后才成为稳定版本;如果卡死,要由看门狗等机制触发复位,再由 Bootloader 根据状态执行回退。

3. Keil 中几个地址到底控制什么

一开始看 Keil,容易把所有 Start 都当成下载地址。实际上,Target、Linker 和 Flash Download 管的是不同阶段。

位置或字段控制的事情使用时要注意
Target:IROM1 Start / Size本工程默认代码和只读数据区域的起点、长度Size 是字节数,不是结束地址
Target:IRAM1 Start / Size本工程可用 RAM 的起点、长度它定义整片可用范围,不等于初始栈顶
Linker:自动布局选项根据 Target 配置生成.sct布局当前工程使用这种方式
Linker:R/O Base、R/W Base、Scatter File手工控制代码、变量和各段位置使用自定义布局时,要核对实际链接结果
Flash Download:算法的 Flash 范围指定烧录算法处理的存储区域不能代替 APP 的链接地址配置
Flash Download:RAM for Algorithm下载期间供烧录算法执行和暂存数据的 MCU RAM不是 APP 的栈,也不是电脑内存

勾选Use Memory Layout from Target Dialog后,Keil 会根据 Target 页生成布局;关闭后可以使用手工.sct,把指定模块或数据段安排到不同区域。最终地址以实际参与链接的配置和.map报告为准。Keil 链接器说明

界面里的I表示 Internal,即片内。IROM1/IROM2和IRAM1/IRAM2是内部区域配置栏;没有 I 的ROM1/RAM1等在界面中归为片外区域。编号 2 不意味着芯片自动多出一片存储器,也不等于 A/B 升级功能。外部存储器还要具备实际硬件连接和适合的访问方式,普通 SPI Flash 不能只填地址就变成可直接取指的 ROM。

其他选项含义与启动或升级的关系
default让该区域参与默认分配多区域时决定默认内容可以放到哪里
Startup指定启动代码所在的只读区域不负责改变芯片 BOOT 引脚
NoInit对指定 RAM 区域不做常规启动初始化不等于断电保存,也不保证调试下载时内容不被覆盖
Erase Full Chip整片擦除用户 Flash会清除其中的自定义 Bootloader、APP 和持久化数据,不会擦掉 ST 出厂系统 Bootloader
Erase Sectors擦除本次镜像涉及的 Flash 扇区两份程序共存时,需要核对擦除范围

下载地址还与文件格式有关:Keil 的 AXF 和 HEX 带有地址信息;裸 BIN 不带目标地址,需要下载工具另行指定。无论哪一种,都要与固件的链接布局匹配。把按0x08000000链接的普通 BIN 改烧到0x08002000,不等于完成了程序重定位。J-Link 文件下载说明

首次分别烧入 Bootloader 和 APP 时,要避免第二次下载执行整片擦除。完成后应按复位键或重新上电观察完整启动路径,不能只依赖调试器直接进入 APP 的现象判断。

4. 从 Program Size 和 map 看程序实际放到了哪里

学习时看到这样一组编译输出:

Program Size: Code=12796 RO-data=512 RW-data=52 ZI-data=1380

这些数字单位都是字节。下面按常见的“代码在 Flash、变量运行在 RAM”布局解释;这组数字用于演算,不与工程目录里已有的旧.map强行对应。

项目数值通常包含什么占用方式
Code12796函数机器指令、启动代码、用到的库函数等通常在 Flash
RO-data512常量表、字符串常量、向量表等只读数据通常在 Flash
RW-data52需要保存初始内容的可修改全局或静态变量,通常是非零初值初始内容在 Flash,运行变量在 RAM
ZI-data1380零初始化全局或静态变量,以及可能计入其中的栈、堆预留空间通常在 RAM,不逐字节保存一份 Flash 零数据

这里也不是“有初值就是 RW,没有初值就是 ZI”。全局或静态变量显式写= 0,通常仍然进入 ZI;普通自动局部变量则是另一种情况。

代码示例通常归类初始化和使用方式
全局int count;或int count = 0;ZI-data程序启动时清零
全局int gain = 3;RW-data启动时将初始内容从 Flash 复制到 RAM,之后可以修改
全局const int table[3] = {1, 2, 3};RO-data通常直接读取 Flash 中的常量
全局char msg[] = "OK";RW-data可修改数组,初始内容在 Flash,运行数组在 RAM
全局const char msg[] = "OK";RO-data通常是 Flash 中的只读数组
函数内static int times;ZI-data启动时为 0,函数返回后仍保留当前值
函数内非静态int temp;通常使用栈或寄存器没有默认的有效初值,赋值前不能直接读取

这些例子假设对象确实被保留,没有被编译器优化掉,也未指定特殊段。局部变量通常不会逐个作为 RW/ZI 的固定对象出现在报告中;它们使用运行时栈或寄存器,相关初始化指令属于 Code。

按常规统计,这组数字对应:

资源计算方式结果
FlashCode + RO-data + RW-data12796 + 512 + 52 = 13360字节,约 13.05KiB
RAMRW-data + ZI-data52 + 1380 = 1432字节,约 1.40KiB

RW 会同时占两边,是因为设备重启后仍需恢复初值;ZI 可以在启动时清零,所以不需要在固件里保存完整的零数组。这也是常见 Keil 镜像大小统计的理解方式。Keil MDK 官方入门示例

实际 BIN 长度还可能受对齐、地址空隙和初始化数据压缩影响。RAM 汇总也不能直接当作最坏运行用量:栈和堆的预留可能已计入 ZI,仍需检查运行期间是否超出预留,不能重复加一次,也不能因为芯片还有空闲 RAM 就认定没有溢出风险。

map 是链接后的分布报告

.map可以看作程序的内存分布清单,它不是待烧录固件,而是用来检查最终布局。生成和查阅时,要保证它与正在讨论的固件属于同一次构建。

想查的问题搜索项或区域能看到什么
程序从哪个地址加载和执行Load Region、Execution Region区域起点、大小、加载与执行位置
某个函数或变量在哪里对应符号名地址、大小、所属目标文件
哪个模块占空间最多Image component sizes每个.o的 Code、RO、RW、ZI 占用
向量表在哪、有多大__Vectors、__Vectors_Size向量表起点和长度
栈在哪、预留多少STACK、Stack_Mem、__initial_sp栈区边界与初始栈顶

Load Address是内容随镜像存放的位置,Execution Address是运行时使用的位置。代码直接从 Flash 执行时,两者通常相同;RW 的初始内容在 Flash,而运行中的变量在 RAM,因此两者通常不同。

栈大小只是预留,不是自动保护

当前启动文件中有Stack_Size EQU 0x00000400,表示预留 1024 字节栈。

栈通常向低地址增长。初始栈顶位于预留区的上边界之外,压栈后向下使用空间;超过低边界,就可能覆盖其他 RAM 数据。Stack_Size并不会自动给这片空间加一道运行时保护,后续仍需要结合调用深度、中断嵌套、局部数组以及栈水位测量判断是否够用。

我现在看地址时,会先区分它是“内容保存在这里”,还是“运行时使用这里”。例如向量表保存在 Flash,第一项的值却指向 RAM;RW 的初始内容在 Flash,变量本身运行在 RAM。把“存储位置”和“里面保存的地址值”分开,再看 Bootloader 跳转、Keil 配置和 map 报告,关系就清楚了很多。