☰
STM32F4分散加载文件SCT详解:从默认配置到高级定制
2026/10/5 1:22:01 网站建设 项目流程

1. SCT文件在STM32F4工程里的角色

1.1 先把它当成一张城市用地规划图

做STM32F4项目,比如基于STM32F4的音频信号采集与实时FFT频谱分析系统、多路定时器输入捕获、DAC波形发生这一类工程时,如果你一直用Keil的默认配置,可能从来都没主动打开过那个后缀叫.sct的文件。大多数情况下这完全没问题,Keil会按照你Target页面里填的IROM地址和IRAM地址自动生成一份“够用”的SCT。可一旦你开始碰Bootloader、想把大数组放到外部SDRAM、想把某些代码放到RAM里跑,SCT文件就是你绕不开的那道坎。

把SCT文件理解为一张城市用地规划图最贴切。芯片里的Flash和RAM就是这座城市的地皮,代码段、数据段、堆栈、堆分别是要落地的建筑。规划图规定了哪块地建什么、哪些建筑必须挨着大马路(特定地址)、哪些区域之间必须留出消防通道(对齐、预留空间)。没有这张图,编译器虽然也能把程序构建出来,但建筑落在哪里就完全随机了,出了问题你连排查方向都没有。

1.2 加载地址与执行地址,差在哪

理解SCT之前必须先分清两个概念:加载地址(Load Address)和执行地址(Execution Address)。

以STM32F407为例,程序烧录后躺在0x08000000开始的Flash里,CPU运行时通常也直接从Flash取指令,这种情况下加载地址等于执行地址。但在某些场景下,这两者必须分开。比如我把一段Flash擦写函数放在RAM里执行,那么它的机器码仍然存储在Flash(加载地址在Flash),程序启动后由启动代码把它拷贝到RAM(执行地址在RAM),CPU再从RAM去取指令。这种“Flash睡大觉,RAM里干活”的模式,就是分散加载的核心思路。

SCT文件所做的,就是把每个段(Section)在加载视图和执行视图中的位置都明确写清楚,同时告诉链接器什么时候该拷贝、哪些段要零初始化、哪些段只想保留地址不希望启动代码碰它。把它学透之后,你再也不是只会在Target选项卡里改个起始地址的新手。

2. 手把手读懂Keil自动生成的默认SCT

2.1 拿到一份真实的F407 SCT文件

在Keil MDK工程里打开Options for Target -> Linker,可以看到链接器用到的SCT文件路径。默认情况下,取消勾选“Use Memory Layout from Target Dialog”之后,这个文件才可以被手动编辑。STM32F407工程里,Keil生成的最典型SCT长这样:

; *************************************************************** ; *** Scatter-Loading Description File generated by uVision *** ; *************************************************************** LR_IROM1 0x08000000 0x00100000 { ; load region size_1MB ER_IROM1 0x08000000 0x00100000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00020000 { ; RW data .ANY (+RW +ZI) } }

这段文本看着不长,信息量却很大。我们可以把它拆成两大块:加载区(Load Region)和执行区(Execution Region)。加载区用LR_开头命名,执行区用ER_或自定义名字命名。STM32F407的Flash容量是1MB,从0x08000000开始,所以加载区的大小限制写的是0x00100000。RAM从0x20000000开始共128KB,所以RW_IRAM1的大小是0x00020000。

2.2 加载区里的“两层结构”

SCT文件使用了花括号嵌套,外层花括号定义的是加载区,内层花括号定义的是该加载区下的执行区。在我的理解里,加载区回答的问题是“这些数据物理上存在哪里”,执行区回答“运行时这些东西在哪里生效”。

LR_IROM1 0x08000000 0x00100000这一行,声明了一个名为LR_IROM1的加载区,起始地址是0x08000000,最大容量为0x00100000。编译出的所有RO(只读)内容、RW初始值都会被分配到这个范围内。内层ER_IROM1是一个执行区,它的地址与加载地址相同,所以这一块代码在Flash里直接运行,不需要启动拷贝。

如果你看到某个执行区地址和加载区地址不同,比如ER_RAMCODE 0x20002000出现在LR_IROM1内部,意味着这块内容“加载在Flash,执行在RAM”,启动代码要做一次搬运。这一点在后面实战案例里会展开。

2.3 选择器行各自的含义

花括号内部每一行都是对输入段的筛选规则。*.o (RESET, +First)是优先级最高的一行,它把所有目标文件里的RESET段收集起来,放执行区的最前面。RESET段存放的是初始栈指针和中断向量表,必须放在Flash起始位置,CPU上电后第一件事就是从这里取栈指针和复位向量。

*(InRoot$$Sections)是很多新手看不懂的一行。这行是为了满足__main库函数的特殊要求。启动代码在调用main之前要执行一段运行时环境初始化,包括把RW数据从Flash拷贝到RAM、把ZI段清零、建立堆栈等。这些初始化库函数被放在名为InRoot$$Sections的段中,它们必须运行在加载地址等于执行地址的“根区域”里,否则CPU一开始连拷贝动作都没法执行,程序就起不来了。

.ANY (+RO)和.ANY (+XO)是兜底规则。.ANY和*.o不同的地方在于它的优先级更低,前面没有匹配上的RO段(只读代码和只读常量)、XO段(仅执行代码)最后由它收编。这也是为什么你随便加一个C文件,代码都能被分配进Flash,因为编译器不会让任何段落单。

2.4 RW_IRAM1里藏着启动拷贝的秘密

RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) }定义的是RAM区域。RW段是初始值非零的全局变量和静态变量,ZI段是初始值为零或未初始化的变量。编译后,RW段在Flash里存有一份初始值的映像,启动时__main会把这部分数据从Flash搬运到RAM的0x20000000处;ZI段则只占用RAM空间,启动时被清成全零。

这里要注意,链接器是把RW和ZI放在同一个执行区里连续排布的,然后启动代码根据链接器生成的符号知道从哪里拷贝、拷多长、清零多少。如果RAM区域规划出了问题,比如RW_IRAM1太小,链接时就会看到“Execution region RW_IRAM1 size overflowed”的报错,直接把工程Failed。很多时候我们点编译看到这个错,第一反应是怀疑代码太大,其实根因往往是SCT默认只给了RAM区128KB,而你塞进了一堆大缓冲。

3. 实战:按需求定制SCT文件

3.1 修改SCT的正确操作路径

在Keil中修改SCT文件有两大原则:第一,取消勾选“Use Memory Layout from Target Dialog”,否则你改了也不会生效,再次编译会被自动生成的SCT覆盖;第二,改完SCT后,Target选项卡里的IROM、IRAM设置只能当作参考,实际链接布局以SCT为准。

具体操作顺序是:

  1. 打开Options for Target -> Linker。
  2. 把“Use Memory Layout from Target Dialog”前面的勾去掉。
  3. 正常勾选“Scatter File”,在输入框里填写你要用的.sct文件路径,也可以点击旁边的“Edit”直接编辑当前生效的SCT。
  4. 改完保存,重新编译,建议先Clean Target再Build。

从这一刻起,Target页面里的地址配置就不再生效了。实际上很多人踩过这个坑:在Target页面把IROM1起始地址改成0x08008000,却发现编译出的工程还是从0x08000000开始输出,原因就是SCT文件仍然按旧的地址链接。你要么一直用自动生成模式,要么全手动SCT,不要混着来。

3.2 把Flash操作函数放到RAM里跑

先看一个我实际用过的案例:做IAP在线升级时,需要在应用里执行Flash擦写操作。如果擦写函数本身放在Flash里,擦除过程中CPU从Flash取指令可能会被卡住,因为Flash控制器正在执行擦除命令,同一时刻没法同时响应指令取指。稳妥做法是把擦写函数整体搬到RAM执行。

在代码里给函数指定段名,以AC6或GCC编译器为例:

#if defined(__ARMCC_VERSION) || defined(__CC_ARM) #define RAMFUNC __attribute__((section("RAM_CODE"), noinline)) #elif defined(__GNUC__) #define RAMFUNC __attribute__((section(".ramfunc"), noinline)) #endif RAMFUNC void flash_erase_sector(uint32_t sector) { // 这里放实际的擦除流程,比如操作FLASH_CR、FLASH_SR等 }

然后在SCT文件中单独划出一块RAM_CODE执行区:

LR_IROM1 0x08000000 0x00100000 { ER_IROM1 0x08000000 0x00100000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x0001F000 { ; 主RAM区域,留出4KB给RAM_CODE .ANY (+RW +ZI) } RW_RAMCODE 0x2001F000 UNINIT ALIGN 2 0x1000 { *.o (RAM_CODE) } }

这里有几个细节值得说。RW_RAMCODE起始地址我设在0x2001F000,这样既不让RAM_CODE和普通数据混杂,预留的大小也足够放几个擦写函数。UNINIT的意思是告诉链接器这块区域不要做零初始化。由于函数代码不是普通变量,启动时我们需要由代码自己把函数机器码拷贝到RAM,或者更常见的是直接把这块区域交给__main的RW拷贝机制。

如果你想让Keil启动代码自动完成拷贝,就不要加UNINIT,链接器会在Flash里为这个执行区单独分配一块加载空间,上电后由__scatterload把机器码从Flash拷到RAM。我习惯上不用UNINIT,让它走标准RW拷贝流程,这样最省心。

还有一个容易忽略的点:放到RAM里的函数如果调用了别的函数,被调的也要确保在RAM区或在Flash中可执行。如果被调函数在Flash里,擦写期间去调它一样会有风险。所以放到RAM里的代码段最好是自包含的,或者你把它依赖的工具函数一并放进去,不要只搬了一层壳。

3.3 把大数组放到外部SDRAM

以STM32F429这类带FMC外部存储控制器的芯片为例。做音频采集和FFT频谱分析时,需要连续存放4096点甚至8192点的float数据,一个float占4字节,4096点就是16KB,再加上双缓冲、窗函数表,内部128KB或192KB的RAM很容易被吃穿。此时最自然的想法是把大数组放到外部SDRAM。

SCT文件增加一个外部SDRAM执行区:

RW_IRAM1 0x20000000 0x00030000 { .ANY (+RW +ZI) } RW_SDRAM 0xD0000000 0x00800000 UNINIT { *.o (SDRAM_DMA_BUF) }

代码里这样指定段:

__attribute__((section("SDRAM_DMA_BUF"), aligned(32))) float fft_input[4096];

这里有两个关键词必须理解清楚。一个是UNINIT:SDRAM上电后内容不确定,而启动代码在RAM初始化阶段如果去清零这块区域,一旦FMC还没初始化,CPU访问SDRAM地址会直接HardFault。标注了UNINIT后,启动代码不会碰这块地址,等到main里把SDRAM控制器初始化好,再由应用代码自己填数据。

另一个是aligned(32)。FFT运算、DMA传输都对内存对齐有要求,Cortex-M4的DMA外设要求缓冲区地址按数据宽度对齐,FFT库更是要求缓冲区严格对齐到4字节以上。在SDRAM区里定义数组时显式加对齐属性,可以省掉一堆运行期对齐操作的麻烦。

这种用法在做实时频谱分析项目时非常实用。双缓冲采集中,一个缓冲区在DMA搬运,另一个在CPU做FFT,两个大块头都扔到SDRAM,内部RAM留给堆栈和实时性要求高的变量。我在F429上实测,把4096点FFT输入输出都放到SDRAM后,系统整体跑得很稳,CPU访问SDRAM虽然比内部SRAM慢一些,但对这种批次型的信号处理场景影响不大。

3.4 Bootloader与App的地址偏移

另一个高频需求是Bootloader加App结构。假设Bootloader占据Flash开头32KB,App从0x08008000开始。在App工程的SCT里这样改:

LR_IROM1 0x08008000 0x000F8000 { ER_IROM1 0x08008000 0x000F8000 { *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) .ANY (+XO) } RW_IRAM1 0x20000000 0x00020000 { .ANY (+RW +ZI) } }

同时在main函数的前几行设置中断向量表偏移:

SCB->VTOR = 0x08008000;

这行的意义是告诉CPU,中断向量表已经从Flash起始地址挪到了0x08008000处。STM32F4的向量表偏移量有硬件对齐要求,具体看参考手册,通常要求按中断向量个数向上取整对齐,比如0x400的倍数。需要强调的是,VTOR设置必须在任何依赖中断的功能开启之前完成,包括SysTick、串口中断等,否则中断一进来就找错向量表,程序直接飞掉。

还经常有人出现这种情况:App能跑起来,但一切换到某个中断就HardFault。排查了一圈最后发现Bootloader在跳转前把外设中断关了,而App没有重新初始化,表现倒是很像向量表问题,实际上两边中断向量表配置都要检查到位。SCT保证了地址布局正确,但外设状态切换属于运行期协作问题,光靠SCT解决不了。

3.5 用后备SRAM保存关键掉电数据

STM32F4系列还有一小块4KB的后备SRAM,地址在0x40024000,主电源断电后由VBAT引脚供电,里面的数据不会丢。很多设备用它保存校准参数、运行状态计数、非法掉电标志,比外部EEPROM速度更快,也不用担心擦写寿命问题。

在SCT里把它单独划出来:

RW_BKPSRAM 0x40024000 0x00001000 UNINIT { *.o (BKPSRAM) }

代码里这样定义变量:

__attribute__((section("BKPSRAM"))) uint32_t boot_count;

然后在使用前使能备份域访问权限:

__HAL_RCC_PWR_CLK_ENABLE(); HAL_PWR_EnableBkUpAccess();

使用UNINIT非常重要,因为后备SRAM启动时不应该被清零,否则掉电保存就失去意义了。即使标明UNINIT,编译时链接器还是会为这个段保留地址,只是不生成初始化动作,这样掉电重启后变量内容仍然存在。要注意的是,这个区域地址在0x4002_4000附近,属于外设地址空间,并不是常规RAM区间,如果在调试时访问它会被Debugger特殊处理,需要使能备份访问后才能正常读写,否则读出来全是0xFF。

4. SCT与启动代码、链接器符号的配合

4.1 __main和__scatterload到底做了什么

很多工程师虽然熟悉main函数,但很少去想main之前发生了什么。在Keil/ARMCC环境下,Reset_Handler在执行完SystemInit后跳转到的是__main,而不是直接跳进我们写的那个main。__main内部会调用__scatterload,这一步就是真正执行分散加载的地方。

链接器根据SCT文件内部每个执行区的描述,为每一个带加载地址和执行地址的区域生成拷贝任务。__scatterload把这些任务依次执行:把初始化数据从Flash拷到RAM、把ZI段清零、然后跳转到__rt_entry,完成堆栈和库函数初始化,最后才进入C语言的main。SCT里哪些区域需要拷贝、哪些不需要,就是这个阶段的执行依据。

如果你把SCT中某个执行区域配置得过于复杂,或者漏了某个区域,可能导致启动阶段就直接HardFault。比如上电时FMC外设还没初始化,而SCT里定义了从Flash拷贝数据到SDRAM执行区,那么__scatterload在拷贝时访问SDRAM地址就会触发总线错误。前面说的用UNINIT规避这个问题,本质上就是在告诉__scatterload:这块不要你管。

4.2 Image$$和Load$$符号怎么用

链接器在生成SCT布局时,会为每个执行区生成一些特殊符号,格式一般是Image$$区名$$Base、Image$$区名$$Length、Load$$区名$$Base。这些符号暴露给C代码后,就可以在运行期拿到区域地址。

比如我在代码里可以通过下面方式获取RW_IRAM1的起始地址和长度:

extern int Image$$RW_IRAM1$$Base; extern int Image$$RW_IRAM1$$Length; uint32_t ram_base = (uint32_t)&Image$$RW_IRAM1$$Base; uint32_t ram_size = (uint32_t)&Image$$RW_IRAM1$$Length;

这在调试和分析内存使用情况时非常有用。我曾经在排查堆栈溢出问题时,就是在main里把这个起始地址和长度打印出来,再结合栈指针SP的实际值,判断栈有没有越过RW段的边界。SCT和启动代码、链接器符号三者配合好了,你就等于是从链接层看到了整个内存地图,很多“神秘崩溃”都能从这个层面找到线索。

5. 高频踩坑与排查方法

5.1 编译报错对照速查表

实际调试中,SCT相关的编译错误最常出现下面几种:

报错信息常见原因解决思路
Region LR_IROM1 size overflowedFlash容量不够,或加载区地址/大小设置不匹配检查代码体积,或把部分只读数据挪到外部存储
Execution region RW_IRAM1 size overflowedRAM区域的RW+ZI数据超过设置空间减少全局缓冲,或把大数组放入SDRAM
Undefined symbol Image$$...$$Base使用的执行区名称与SCT中定义不一致核对符号名大小写及区名拼写
L6218E: Undefined symbol __mainInRoot$$Sections没有被正确包含检查是否使用了非标准启动文件
Error: L6406E: No space in execution regions输入段没有匹配到任何执行区域检查选择器写法,确认段名正确
Error: L6407E: Sections of aggregate size ... could not fit某个指定段放不进对应区域扩大对应区域,或把指定段移到别的执行区

看到这几类错误别慌,第一步先打开编译生成的.map文件,在里面搜索关键段名,比如RESET、RAM_CODE、SDRAM_DMA_BUF,看链接器到底把段放到了哪里、占用了多大空间。Map文件是排查SCT布局问题最直接的工具,比对着报错瞎猜效率高得多。

5.2 运行时HardFault的排查思路

SCT设置错误不总是出现在编译期,更多时候是编译通过,一运行就崩。我遇到过的典型场景是:把变量放到SDRAM之后,启动时忘记加UNINIT,结果__scatterload去访问还没初始化的SDRAM控制器,上电瞬间直接HardFault。这种问题光靠看代码很难发现,因为代码逻辑完全正确。

排查思路是按阶段定位:

  1. 在Reset_Handler入口打断点,全速跑到这个断点,确认上电复位是否正常。
  2. 单步跟踪SystemInit,看它是否正常完成时钟配置和外设初始化。
  3. 进入__main后继续单步,看是执行到哪一步崩的。如果是跳到某个外部SDRAM相关的拷贝动作时崩,基本可以确定是SDRAM未初始化或UNINIT缺失。
  4. 用Keil的寄存器窗口查看PC指针当时停在什么地址,如果是0xD0000000附近,就能确定是在访问SDRAM。

另外还有一类运行期问题:堆和栈和某个大数据区挤在一起。Cortex-M4的堆栈是由启动文件和库函数设置的,默认情况栈顶就是RAM的起始地址加RAM大小。如果你在SCT里把RAM区大小改小了,而启动文件里的堆栈尺寸没同步更新,栈就可能跑到SCT定义区域之外,被别的段覆盖。这种问题通常表现为程序时好时坏、调用层级一深就死机。排查方法有几种:在main里打印SP寄存器值和数组地址,对比栈底和SCT区域关系;或者直接把启动文件里Stack_Size调大;更彻底的是在SCT中单独划分一个STACK执行区并配合__user_initial_stackheap来实现,但一般中小型工程用不到那么复杂。

5.3 几个实用小习惯

经历过几次SCT文件相关的折腾后,我总结出几个能救命的小习惯。

第一个习惯是每次改SCT之前,先在Map文件里给当前工程留一份“基线地图”。编译后打开.map文件,记录当前各区域大小和关键符号地址。改完SCT再对比差异,如果出现莫名其妙的段迁移,看map马上就能定位。

第二个习惯是尽量少用绝对地址段,多用带名字的section配合SCT区域统一管理。直接写__attribute__((section(".ARM.__at_0x20001000")))虽然省事,但很容易在后续增删变量时发生地址冲突。用SCT配合段名,整个工程的内存分配一目了然,后续加功能也能有序扩展。

第三个习惯是始终把启动文件和SCT文件放在版本管理里。很多人工程目录里只有源码和工程文件,但SCT文件丢失后,Keil会自动按Target配置重新生成一份默认的,你的特殊布局规划就全没了。把这个文件纳入版本管理,换电脑、换工程时都不怕。

我自己在F407和F429上折腾SCT,其实最开始也是因为一次IAP升级,觉得Flash擦写老是不稳定,查来查去发现就是擦写函数在Flash里执行引发的。把函数弄到RAM后问题立刻消失,从那以后SCT就不再是一个“看了就困”的配置文件,而是一张能帮我在链接层面解决问题的最底层地图。后来做大数组和外部SDRAM,也是靠SCT把内存架构理清楚的。这个文件你越早吃透,后面碰到的很多疑难杂症就越少。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询