首页 / 资讯中心 / 文章详情

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

STM32 学习记录:内存分布、Bootloader 与 Keil 地址配置 ★ FEATURED ARTICLE
整理 MCU 内存这部分内容时我比较容易混淆几个概念Flash 地址和 RAM 地址有什么区别Bootloader 是不是另一套程序向量表放在 Flash 里为什么第一项又是 RAM 地址Keil 中不同位置的 Start、Size是不是都在设置下载地址我的理解是可以沿着“程序保存在 Flash → 复位时读取向量表 → 执行启动代码 → 进入 main → 使用 RAM 中的变量和栈”这条顺序看。下面以 STM32F103C8 为例把地址配置、启动过程和编译后的内存统计放在一起记录目前理解到的内容。1. 先把地址、Flash 和 RAM 分清MCU 的地址可以理解成 CPU 访问存储器或外设时使用的位置编号。0x表示十六进制地址每增加 1代表向后移动一个字节地址范围里的空档也不一定有实际存储器。STM32F103C8 的用户 Flash 为 64KBSRAM 为 20KB。常见的几个区域如下地址范围均包含两端。STM32F103C8 数据手册、ST Flash 编程手册 PM0075区域地址范围容量主要用途用户 Flash0x080000000x0800FFFF64KB保存自己烧录的程序、常量、需要持久化的数据断电保留SRAM0x200000000x20004FFF20KB保存运行时变量、缓冲区、堆和栈断电后不能依赖其内容系统存储器0x1FFFF0000x1FFFF7FF2KB存放 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 Bootloader56KB APP”为例用户 Flash 可以这样划分。这里是单 APP 的布局示意没有包含 A/B 双镜像用户 Flash 分区起始地址结束地址最大长度Bootloader0x080000000x08001FFF0x20008KBAPP0x080020000x0800FFFF0xE00056KB这只是对同一片 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 管的是不同阶段。位置或字段控制的事情使用时要注意TargetIROM1 Start / Size本工程默认代码和只读数据区域的起点、长度Size 是字节数不是结束地址TargetIRAM1 Start / Size本工程可用 RAM 的起点、长度它定义整片可用范围不等于初始栈顶Linker自动布局选项根据 Target 配置生成.sct布局当前工程使用这种方式LinkerR/O Base、R/W Base、Scatter File手工控制代码、变量和各段位置使用自定义布局时要核对实际链接结果Flash Download算法的 Flash 范围指定烧录算法处理的存储区域不能代替 APP 的链接地址配置Flash DownloadRAM 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 出厂系统 BootloaderErase Sectors擦除本次镜像涉及的 Flash 扇区两份程序共存时需要核对擦除范围下载地址还与文件格式有关Keil 的 AXF 和 HEX 带有地址信息裸 BIN 不带目标地址需要下载工具另行指定。无论哪一种都要与固件的链接布局匹配。把按0x08000000链接的普通 BIN 改烧到0x08002000不等于完成了程序重定位。J-Link 文件下载说明首次分别烧入 Bootloader 和 APP 时要避免第二次下载执行整片擦除。完成后应按复位键或重新上电观察完整启动路径不能只依赖调试器直接进入 APP 的现象判断。4. 从 Program Size 和 map 看程序实际放到了哪里学习时看到这样一组编译输出Program Size: Code12796 RO-data512 RW-data52 ZI-data1380这些数字单位都是字节。下面按常见的“代码在 Flash、变量运行在 RAM”布局解释这组数字用于演算不与工程目录里已有的旧.map强行对应。项目数值通常包含什么占用方式Code12796函数机器指令、启动代码、用到的库函数等通常在 FlashRO-data512常量表、字符串常量、向量表等只读数据通常在 FlashRW-data52需要保存初始内容的可修改全局或静态变量通常是非零初值初始内容在 Flash运行变量在 RAMZI-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.05KiBRAMRW-data ZI-data52 1380 1432字节约 1.40KiBRW 会同时占两边是因为设备重启后仍需恢复初值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第一项的值却指向 RAMRW 的初始内容在 Flash变量本身运行在 RAM。把“存储位置”和“里面保存的地址值”分开再看 Bootloader 跳转、Keil 配置和 map 报告关系就清楚了很多。
阅读完成 · 觉得有帮助?
咨询建站