嵌入式分散加载实战:STM32内存布局与链接脚本详解

发布时间:2026/9/10 3:31:19

嵌入式分散加载实战:STM32内存布局与链接脚本详解 做嵌入式这几年如果要我挑一个“学的时候觉得枯燥用起来真香出了问题掉头发”的知识点分散加载绝对排前三。很多朋友在STM32裸机或者RTOS项目里一直用默认的链接配置直到某天自绘PCB换了颗外部SDRAM、或者要给Bootloader划分APP区域、又或者想把一段高实时性的代码拷到内部SRAM里跑才发现自己对内存布局完全没有掌控力。这个主题讲清楚的就是这件事分散加载Scatter Loading——怎么在嵌入式系统里精确安排代码和数据的物理位置让每一块RAM和Flash都各司其职。如果你正被“为什么我的程序一上电就跑飞”“为什么在RAM里调试正常烧进Flash就崩”“怎么把一个函数固定到指定地址”这类问题折磨这篇文章就是给你写的。我会从分散加载的底层机制讲起再到实际工程里的编写方法、典型应用场景、以及我这些年踩过坑的调试经验尽量让你看完之后不仅知道怎么写还知道为什么这么写。1. 先从整体上理解分散加载1.1 加载域与执行域内存的两副面孔很多嵌入式初学者有个误区以为编译出来的hex文件里的内容就是程序运行时内存里的样子。实际上完全不是这么回事。ARM的分散加载机制把程序的内存状态分成了两个“视角”加载域Load Region和执行域Execution Region。加载域说的是程序烧录后停留在非易失存储器一般是Flash里的物理布局。执行域说的是程序启动后代码和数据在内存空间包括Flash和RAM中的实际运行布局。两者可以是同一个地址也可以完全不同。举一个最常见的例子一个STM32工程代码编译出来有200KB你的Flash有1MB内部SRAM只有128KB。这200KB代码几乎全部留在Flash里执行不占RAM这就是“加载域执行域”的场景。但如果你定义了一个很大的const数组又或者把某些频繁调用的函数放在RAM里跑情况就不一样了。用生活化的类比来解释就是加载域相当于你放在冰箱里的预制菜执行域是你在锅里炒好的成品菜。预制菜在哪放着、需要占多少冰箱格子是一回事成品菜摆在哪张桌子上、什么时候吃又是另一回事。分散加载文件.sct文件就是你的“分拣清单”告诉系统哪几袋菜要搬到灶台哪几袋菜就留在冰箱里。1.2 谁在搬代码__main 与 scatterload 的幕后工作那这个“搬运”动作是谁来完成的在ARMCC/Keil环境下答案是C库初始化代码__main。这里要特别提醒一下__main并不是我们写的那个main()函数。__main是C库提供的入口它负责三件事把加载域里需要搬到RAM的段逐个拷贝到执行域指定的地址把需要清零的ZI段零初始化段比如未初始化且默认值为0的全局变量对应的RAM区域清零调用__rt_entry完成C运行环境的初始化最后才跳转到我们写的main()。这个过程在ARM文档里叫scatterload也就是分散加载的运行时实现。编译器会根据分散加载文件里的描述自动生成一段搬运行代码。你看到的SystemInit、__main、__scatterload_copy这些符号在启动汇编代码里环环相扣。为什么理解这个机制很重要因为很多问题恰恰出在这个“搬运”环节。比如代码还没进main()之前外部SDRAM控制器尚未初始化而你的分散加载文件却把一个段的执行域定义在了SDRAM——这会导致搬运代码往一块未初始化的内存里写数据硬件上大概率直接HardFault。这种问题在调试器里看起来就像“明明代码没问题一上电就跑飞”实际上就是加载域和执行域规划不合理。2. 分散加载文件到底怎么写2.1 一个最小可用的分散加载文件Keil MDK环境下分散加载文件后缀是.sct。新建工程时编译器会自动生成一个默认版本。我们通常可以在魔法棒Options for Target的 Linker 选项卡里勾选Use Memory Layout from Target Dialog或者取消勾选后手动指定自己的.sct文件。一个典型的STM32F4工程默认分散加载文件长这样; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00100000 { ; 加载域从0x08000000开始最大1MBFlash ER_IROM1 0x08000000 0x00100000 { ; 执行域同样在0x08000000即Flash中执行 *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { ; 执行域从0x20000000开始128KB内部SRAM .ANY (RW ZI) } }这段文件看起来简单但它包含了分散加载的完整要素LR_IROM1是加载域的名字可以自己命名。后面的0x08000000是加载域在地址空间中的起始地址0x00100000是最大大小。花括号里包含一个或多个执行域。ER_IROM1就是“Flash中的代码执行域”RW_IRAM1是“RAM中的数据执行域”。选择器语法*(RESET, First)表示把RESET段放在最前面。.ANY (RW ZI)表示所有未特别指定的可读写段和零初始化段都放这里。这种默认配置对绝大多数单芯片裸机工程够用但一旦涉及自定义内存映射、Bootloader、或者外部存储器就得自己手写了。2.2 从ARMCC到GNU和链接脚本对话如果你用GCC工具链分散加载的对应物是链接脚本Linker Script.ld文件。两者思想完全一致只是语法不同。GNU链接脚本里用MEMORY命令描述物理内存用SECTIONS命令描述段的存放规则。一个等价的最小脚本大致是MEMORY { FLASH (rx) : ORIGIN 0x08000000, LENGTH 1M RAM (rwx) : ORIGIN 0x20000000, LENGTH 128K } SECTIONS { .isr_vector : { KEEP(*(.isr_vector)) } FLASH .text : { *(.text*) *(.rodata*) } FLASH .data : { _sdata .; *(.data*) _edata .; } RAM AT FLASH .bss : { _sbss .; *(.bss*) *(COMMON) _ebss .; } RAM }注意这里.data这一句 RAM AT FLASH意思是数据段的执行域在RAM但加载域在Flash。这句话就相当于ARM分散加载里“把Flash里的全局变量初始值搬到RAM”的声明。GCC环境里启动代码需要借助_sdata、_edata、_sbss、_ebss这几个符号处理搬运和清零这些符号恰恰是在链接脚本里导出的。很多从Keil转到GCC的朋友第一次看到_sdata这类符号会发懵。其实你在.ld文件里定义的这些点号赋值语句就是在给链接器一个“锚点”。启动汇编代码里引用这些锚点才能知道从哪里搬、搬多少、搬到哪。3. 实际项目里分散加载的典型应用场景3.1 Bootloader与APP的地址规划只要是带IAPIn-Application Programming功能的量产产品几乎都离不开自定义分散加载。最简单的BootloaderAPP架构Flash分区一般是这样的分区起始地址大小内容Bootloader0x0800000032KB引导、跳转、固件升级APP0x08008000480KB应用代码参数区0x080800008KB校准参数、配置字这种情况下APP工程的分散加载文件要改成LR_APP 0x08008000 0x00078000 { ER_APP 0x08008000 0x00078000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) .ANY (XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } }这里最关键的点是ER_APP的起始地址必须是0x08008000而不是0x08000000。同时APP里中断向量表的位置也必须跟着搬移。如果你的MCU是Cortex-M系列在APP的启动代码里还需要重新设置向量表偏移也就是SCB-VTOR 0x08008000这是M3/M4/M7的写法M0略有区别。实际写过Bootloader的朋友应该都知道这里有个非常容易踩的坑Bootloader跳转到APP之前必须关掉全局中断。为什么因为跳转的过程中如果栈指针MSP还在Bootloader的RAM区域内而APP代码重新初始化了RAM区域中断一来就会用已经改掉的栈直接跑飞。我见过好几个项目都是“Bootloader跳转后偶尔死机”查来查去最后定位到是没有关中断或者是跳转前没有正确设置主栈指针。3.2 大镜像在DDR未初始化前怎么跑如果你做过Cortex-A系列、或者是带外部SDRAM/DDR的高性能MCU这个问题几乎一定会遇到。硬件上电时DDR控制器还没有初始化但你的代码体积已经超过了内部SRAM的容量。那问题来了代码放哪典型方案是“两级启动”。第一级启动代码放在片内SRAM里运行因为片内SRAM上电即可用不需要配置DDR控制器。这段代码完成DDR初始化然后把第二级镜像从Flash拷贝到DDR里再跳转过去。分散加载文件在这个阶段需要分成两个独立的部分。拿一个简化模型举例; 第一阶段只在内部SRAM中运行 LR_SRAM 0x20000000 0x00080000 { ER_SRAM 0x20000000 0x00080000 { startup.o (RESET, First) ddr_init.o (RO) * (RW ZI) } } ; 第二阶段DDR初始化完成后主程序在DDR中运行 LR_DDR 0x80000000 0x01000000 { ER_DDR 0x80000000 0x01000000 { main_app.o (RO) .ANY (RO) .ANY (RW ZI) } }注意这里我把ddr_init.o单独挑出来放在SRAM执行域里就是为了确保这段代码不依赖DDR。而LR_SRAM和LR_DDR两个加载域都在同一片Flash里但它们的执行域不同。这种写法在工程里很实用但对启动流程的设计要求很高。你必须保证ddr_init.o里调用的每一个函数都在SRAM域内否则链接器可能会把某个依赖函数放到DDR域然后DDR还没初始化就执行了那行代码。要想排查这种依赖问题最好的工具就是编译生成的.map文件。我后文会专门讲怎么读map文件。3.3 函数级定位与内存映射的精细控制除了整体内存布局分散加载还能做到“单函数定点”。比如某段实时性要求很高的数据采集函数你希望它运行在零等待状态的紧耦合内存TCM里而普通代码放在Flash或者DDR里。这时候就需要把函数放进自定义段。在ARMCC/Keil里可以这样操作#if defined ( __CC_ARM ) __attribute__((section(FastCode))) void Fast_Task(void) { // 高实时性处理 } #endif然后在分散加载文件里单独声明这个段RW_IRAM1 0x20000000 0x00020000 { .ANY (RW ZI) } TCM_CODE 0x00000000 0x00008000 { * (FastCode) }GCC环境下对应语法是__attribute__((section(.fastcode))) void Fast_Task(void) { // 高实时性处理 }然后链接脚本里.fastcode : { *(.fastcode*) } ITCM AT FLASH这种做法的价值在于把关键函数从Flash搬到零等待RAM后执行时间能缩短数倍。尤其是那些被中断服务程序频繁调用的函数或者音频处理、电机FOC这种对时延敏感的计算效果立竿见影。不过要注意函数搬进RAM后全局变量的位置、栈的使用情况都可能变化调试时要用map文件反复确认内存没有溢出。4. 调试与排查分散加载踩坑指南4.1 常见的几类“启动即崩”现象我接触过的项目里分散加载导致的问题大概能归成四类。整理成一张表方便你对照排查现象大概率原因排查思路上电后直接HardFault复位无效执行域地址越界或者搬移代码访问了未初始化内存检查.map确认地址是否在有效范围内进了main之后全局变量值不对ZI清零或RW拷贝没有覆盖到对应段检查分散加载文件是否遗漏了相关段跳转APP后偶尔正常偶尔死机中断向量表偏移未设置或跳转时没关中断检查VTOR设置和跳转前的中断处理程序能跑但性能远低于预期代码执行域放在了慢速存储区检查是否把关键函数放在了DDR或外部Flash第一类问题最常见。有一次我在一块S32K1系列板子上把ZI段的执行域扩大到了不存在的地址范围编译器毫无提示因为外部RAM地址映射本身是合法的但实际没有接存储颗粒。程序一上电scatterload往那块不存在的RAM里执行清零操作总线直接报错。这种事靠看代码真的看不出来最快的定位方式就是打开map文件核对你每个执行域的起始地址和大小。4.2 如何高效读 .map 文件定位问题很多工程师平时不看map文件这其实是个巨大的损失。map文件是链接器给你的“内存账本”里面记录了每个段、每个符号最终落在哪个地址。当程序崩溃时先用调试器查看PC指针停在哪个地址然后去map文件里反查这个地址属于哪个执行域、哪个函数很快就能定位问题。Keil编译后map文件一般在Listings目录下GCC工具链可以加-Mapoutput.map选项生成。重点看这么几个部分Memory Map of the image每个执行域的起始地址、大小以及包含的每个输入段。这能帮你确认预期放RAM的段是不是最终放到了Flash或者预期在Flash执行的代码是不是被塞进了RAM。Image Symbol Table每个全局符号的地址。调试器显示PC指针在0x0800xxxx时在这里查一下就知道是哪个函数。Cross Reference有些工具链有函数的调用关系。排查“为什么这个函数被链接进来了”特别有用。比如你用GCC执行arm-none-eabi-nm -n output.elf arm-none-eabi-objdump -h output.elf arm-none-eabi-addr2line -e output.elf 0x08001234objdump -h看的是段表nm -n按地址列出所有符号addr2line能把地址翻译成具体的文件和行号。这三板斧下去绝大多数内存布局问题都能水落石出。4.3 几个提升效率的实用小技巧最后分享几个我自己项目里验证过的小技巧。第一个技巧让链接器生成内存使用报告。ARMCC环境下链接选项加--verbose能输出详细的scatterload过程GCC环境下看map文件里的Memory Configuration部分。这些信息能帮你快速判断“Flash是不是已经满了”“RAM剩余多少”。第二个技巧保留一份默认分散加载文件作为对照。修改前先用编译器自动生成一份默认配置每次改动都能对比“到底动了什么”。很多时候改着改着就忘了原始配置出问题后还不知道自己改了什么有对照就方便多了。第三个技巧不要在分散加载文件里用魔数地址。尽量在头文件里用宏定义统一管理地址比如#define APP_BASE_ADDR 0x08008000 #define APP_FLASH_SIZE 0x00078000 #define INTERNAL_RAM_ADDR 0x20000000 #define INTERNAL_RAM_SIZE 0x00020000分散加载文件虽然不直接包含C头文件但你可以用预处理方式Keil的--preinclude选项或者直接把.sct文件加入-D宏定义来传递这些值。这样改地址时只要改一个地方不用散落到处都改也能避免多处不一致的问题。第四个技巧写一个小的启动自检函数。在main()最开始检查关键内存区域比如向某块RAM写入特征值再读回或者CRC校验代码段。产品量产时尤其管用能在现场就快速判断是不是内存芯片虚焊、地址线错位这类硬件问题。5. 回到字面意义分散加载的“魔法”并没那么玄写了这么多其实我想说的是分散加载这个技术表面上看是“配置文件该怎么写”的问题本质上是对链接过程和启动过程的理解深度问题。你越清楚一条指令从Flash到CPU执行经历了什么、一个全局变量的初值从哪里来就越能在遇到问题时快速定位而不是反复改代码碰运气。我个人的建议是不要停留在Keil自动生成的默认配置里一定要在项目早期就主动尝试手写一版分散加载文件。哪怕只是简单地把某个段移到RAM、给自己的Bootloader划分地址空间或者把一段计算密集型的函数放到紧耦合内存里跑这种动手实践带来的体感比读十篇教程都管用。等你在实际项目里真的靠分散加载解决过一次棘手问题你会回来感谢ARM搞出这套机制的。
延伸阅读

更多相关文章

2026/9/10 3:31:19

STM32 PWM调光实战:从TIM3配置到LED呼吸灯与RGB混色

简介:这套压缩包以STC12C2052单片机为核心,完整演示了利用PWM脉宽调制技术调节LED灯亮度的过程,面向单片机初学者和电子爱好者,用于学习无级调光、占空比概念及PWM模块配置。包内共55个文件,以C源码、Keil工程&#xf…

2026/9/10 3:31:19

C++实现AVL树:旋转操作、插入删除与验证全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 3:26:18

深入GPU用户态驱动:命令提交、显存管理与同步机制实战解析

如果你正在读这篇,说明大概率已经看完了GPU UMD学习指南的stage1part1,或者至少已经知道用户态驱动这五个字大概指的是什么。Part1主要是建环境和建立整体观:驱动栈分几层、UMD和KMD各管什么、一套最基础的开发环境怎么搭。到了stage1part2&a…

2026/9/10 4:41:26

硬件应届生核心竞争力:工具实操、故障归因与成本意识

1. 招聘启事里没写的“真实需求清单”“硬件工程师(应届)”——这行字在招聘网站上出现的频率,可能比你每天喝的咖啡次数还高。但真正点开详情页,你会发现:JD(职位描述)写得像一份通用说明书&am…

2026/9/10 4:41:26

CANN/ge融合Pass捕获张量示例

Sample Usage Guide 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Tensor…

2026/9/10 4:41:26

AB编码器测速全解析:原理、定时器配置与工程实战

作为一个常年跟电机、小车、自动化设备打交道的嵌入式工程师,我对 AB 编码器测速这个需求再熟悉不过了。不管你是做平衡车、AGV、机械臂关节还是简单的循迹小车,只要涉及到闭环控制,速度反馈就绕不开编码器。而增量式 AB 编码器,基…

2026/9/10 4:36:26

如何在 ESP-IDF 中快速获取 WiFi TSF 时间戳:一份完整指南

如何在 ESP-IDF 中快速获取 WiFi TSF 时间戳:一份完整指南 【免费下载链接】esp-idf Espressif IoT Development Framework. Official development framework for Espressif SoCs. 项目地址: https://gitcode.com/GitHub_Trending/es/esp-idf 想在 ESP-IDF 项…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码