单片机内存分配基础:SCT文件的基本概念

1、链接脚本的概念

  链接脚本(Linker Script)和链接器(Linker)是嵌入式开发中非常重要的概念,尤其是在像 Keil 这样的集成开发环境(IDE)中,它们帮助决定程序在目标设备内存中的布局。

  在 Keil MDK(μVision)中,链接器脚本文件(分散加载文件,Scatter-Loading File)用于控制代码和数据在内存中的分布。它通常以 .sct 为扩展名,由 ARM 链接器 (armlink) 使用。

  C语言经典的“Hello World”小程序几乎是每个程序员闭着眼睛都能写出来的,基本成了入门教程和开发环境的默认标准,代码如下:

#include <stdio.h>
int main()
{
  printf("Hello World\n");
  return 0;
}

  如果在 Windows 下使用 Visual Studio 来编译,那么可以直接点击运行(Run)按钮或者构建(Build)按钮,在工程目录下就会看到生成的 .exe 程序。

  如果在 Linux 下使用 GCC 来编译,使用最简单的$gcc demo.c命令,就可以在当前目录下看到 a.out。

  事实上,从源代码生成可执行文件可以分为四个步骤,分别是预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。下图是 GCC 生成 a.out 的过程:

  编译流程:https://www.cnblogs.com/The-explosion/articles/13622629.html

  下面是对它们的详细介绍:
  (1)链接器(Linker)是什么?
  链接器(Linker)是将编译好的目标文件(object files)和库文件合并 在一起,生成最终可执行文件(如 .elf、.bin 或 .hex)的工具。它负责将程序中的不同部分(如代码、数据、堆栈、堆等)放置到目标设备的内存空间中,并处理以下工作:
    符号解析:链接器会解析并解决程序中的符号,如函数和变量的地址,确保所有调用的函数和引用的全局变量在程序中能找到其定义。
    地址分配:将程序的不同部分分配到目标设备的内存空间中,确保代码和数据都放在合适的地址。
    段的合并:将编译生成的多个目标文件中相同类型的段(如代码段 .text,数据段 .data,未初始化数据段 .bss)合并到一起,形成一个统一的可执行文件。
    链接器最终会生成一个可执行文件,这个文件可以烧录到嵌入式设备中运行。
  (2)链接脚本(Linker Script)是什么?
  链接脚本是链接器用来指导如何把目标文件、库文件中的代码和数据放置到内存的各个部分中的一份文本文件。对于 ARM Cortex-M 系列嵌入式开发而言,链接脚本通常会规定哪些代码和数据应该被放在闪存(Flash)中,哪些应该放在 SRAM(RAM)中,以及堆栈和堆的大小和位置。
  简单来说,链接脚本控制了程序在物理内存中的布局,是内存映射(Memory Mapping)的关键部分。
  链接脚本的作用:
    定义内存布局:指定闪存、RAM 等不同内存区域的起始地址和大小。
    指定段的放置:规定哪些部分(代码、全局数据、常量等)放在程序的哪些内存区域中。
    设置堆栈和堆的地址:为堆栈(stack)和堆(heap)指定内存地址和大小。
    控制初始化数据的放置:在设备启动时,决定如何初始化全局和静态变量。

2、KEIL中的链接脚本文件

  在 Keil MDK(μVision)中,链接器脚本文件(分散加载文件,Scatter-Loading File)用于控制代码和数据在内存中的分布。它通常以 .sct 为扩展名,由 ARM 链接器 (armlink) 使用。

image


  对于Keil,先是armcc编译器对每个单独的.c文件编译生成.o文件,然后使用armlink链接器组合所有的.o文件,即结合分散加载文件即SCT文件,为其各自分配存储地址。SCT文件用户可自己编辑,从而指定代码和数据的存储地址。

  2.1、找到链接器脚本文件
  方法 1:通过 Keil 工程选项查看
    打开 Keil 工程(.uvprojx 或 .uvmpw),点击菜单栏 Options for Target(或按 Alt+F7),切换到 Linker 选项卡,在 Use Memory Layout from Target Dialog 下方,可以看到:

image

  Scatter File 选项(默认可能未启用)。
  如果勾选了 Use Memory Layout from Target Dialog,则内存分布由 Target 选项卡中的设置决定。如果取消勾选,可以手动指定 .sct 文件路径。

  方法 2:在工程目录中查找
  默认情况下,Keil 会自动生成 .sct 文件,存放在 Objects 或 Listings 目录下,文件名通常为 工程名.sct。你也可以手动创建 .sct 文件并指定路径。

  2.2、修改内存分布
  方法 1:通过 Target 选项卡修改(简单方式)
  进入 Options for Target → Target 选项卡。在 Read/Write Memory Areas 和 Code Memory Areas 中,可以添加、修改 RAM 和 Flash 的地址范围,修改后,Keil 会自动更新 .sct 文件。

image

  方法 2:手动编辑 .sct 文件(高级方式)
  取消勾选 Use Memory Layout from Target Dialog,点击 Scatter File 旁边的 ... 按钮,选择或创建 .sct 文件,编辑 .sct 文件,调整内存布局。

  2.3、 链接器脚本(.sct 文件)基础解读
  基本结构:.sct 文件由 加载域(Load Region) 和 执行域(Execution Region) 组成:

    加载域:定义代码和数据的存储位置(如 Flash)。
    执行域:定义代码和数据在运行时的位置(如 RAM)。

image

    简单示例 .sct 文件:

LR_IROM1 0x08000000 0x00080000 
 {    ; 加载域:Flash(起始地址 0x08000000,大小 512KB)

  ER_IROM1 0x08000000 0x00080000  
  {   ; 执行域:Flash(代码运行位置)
  
           *.o (RESET, +First)  ; 首先存放中断向量表
         *(InRoot$$Sections)    ; 系统初始化代码
           .ANY (+RO)           ; 所有只读数据(代码、常量)
  }
  
  RW_IRAM1 0x20000000 0x00010000 
   {   ; 执行域:RAM(起始地址 0x20000000,大小 64KB)
         
         .ANY (+RW +ZI)     ; 可读写数据(全局变量、堆栈等)
  }
}

  关键语法解析

image

  例如:国名技术N32单片机需要使用官方例程中手动配置的 .sct 文件,使用 Target 选项卡配置的不知道为什么会使单片机运行速度很慢
  Target 选项卡配置的:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x15000000 0x001E0000  {    ; load region size_region
  ER_IROM1 0x15000000 0x001E0000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   .ANY (+RO)
   .ANY (+XO)
  }
  RW_IRAM1 0x24000000 0x00020000  {  ; RW data
   .ANY (+RW +ZI)
  }
}

  官方例程中手动配置的:

; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************

LR_IROM1 0x15000000 0x00100000  {    ; load region size_region
  ER_IROM1 0x15000000 0x00100000  {  ; load address = execution address
   *.o (RESET, +First)
   *(InRoot$$Sections)
   startup_n32h7*.o (+RO)
   system_n32h76x_78x.o (+RO)
  }
  
  ;ITCM 
  RW_ITCM 0x00000400 0x0003FC00  {  ;256K ITCM   offset 0x400 for VECTOR TABLE
   .ANY (+RO)
   .ANY (+XO)
  }
  
  ;AXI SRAM 
  RW_AXI_SRAM 0x24000000 0x00080000  {  ; 512K AXI-SRAM  stack
   *(STACK)
   .ANY (+RW +ZI) 
  }
  
;  DTCM 
;  RW_DTCM 0x20000000 0x00040000  {  ; 256K DTCM 
;  .ANY (+RW +ZI)
;  }

}

  究其原因:

  核心原因在于两种配置方案对TCM(紧耦合内存) 这个关键硬件资源的利用方式不同。
  N32H7系列是基于ARM Cortex-M7内核的高性能MCU。TCM是Cortex-M7内核实现高性能的关键特性之一。它通过专用的接口与CPU内核直接相连,访问速度极快且零等待,其性能远超挂载在系统总线(如AXI总线)上的普通SRAM。
  如果程序代码没有被正确地放入ITCM,而是运行在普通SRAM甚至内部Flash上,在高主频(如600MHz)下,CPU会因为等待从慢速存储器中取指令而频繁停顿,直观感受就是运行速度会慢好几倍。
  为什么两种配置方案速度差异巨大?
  (1)Target选项卡自动配置(速度慢的根源)
  这种默认配置简单地将所有代码(.ANY (+RO))放在地址 0x15000000 开始的Flash区域执行。由于代码运行在访问延迟较大的Flash上,完全没有发挥出TCM的性能优势,导致CPU性能严重受限,这就是你感觉速度慢的根源。
  (2)官方手动配置(性能优化的关键)
  官方配置则明确将绝大部分代码(.ANY (+RO) 和 .ANY (+XO))链接到了 ITCM 的地址空间:

RW_ITCM 0x00000400 0x0003FC00 { ; ITCM 空间
.ANY (+RO) ; 所有只读代码段
.ANY (+XO) ; 所有只执行代码段
}

  但这只是分散加载文件的安排。要让代码真正在ITCM中高速运行,需要两个步骤的配合:
    链接地址:.sct 文件将代码的运行地址定为ITCM区域(0x00000000 区域)。
    代码拷贝:芯片上电后,启动文件(如 startup_n32h76x.s)和 SystemInit() 函数会将代码从Flash搬运到ITCM中,然后CPU才跳转到ITCM去执行。这正是官方配置能全速运行的原因。
  总结:如何正确配置以获得最佳性能?
  要让你的工程恢复高性能,可以参考官方的做法:
    手动修改散列文件(.sct):放弃自动配置,参照官方例程手动编写.sct文件。关键是把主要代码部分(+RO)的执行区放到ITCM地址(0x00000000),把数据部分(+RW/+ZI)放到DTCM或SRAM地址。
    确保搬运代码生效:检查工程中是否定义并正确调用了TCM配置和代码搬运的相关函数(通常在 system_n32h76x_78x.c 中,如 ConfigTcmSize())。这部分是确保代码从Flash被搬运到TCM的关键,.sct文件只是给链接器看的“地图”,实际的“搬家”动作需要软件来完成。
    验证链接结果:编译后,查看工程目录下的 .map 文件,确认 RESET 段、InRoot$$Sections 和你的应用代码函数是否都被分配到了 0x00000000 开头的ITCM地址空间。

  Target选项卡自动配置的话配不成N32官方配置的那样,只能自己手动配置,官方也是手动配置的,仅靠 Target 选项卡的自动配置,无法实现官方例程那种利用 TCM(紧耦合内存)的高性能配置,必须通过手动配置分散加载文件(.sct)来实现。

  Target 选项卡虽然能生成基本的 .sct 文件,但它有明确的局限性:
  (1)为什么 Target 选项卡自动配置不够用?

image

  (2)如何正确进行手动配置?
  操作路径很清晰:在工程选项(Options for Target)的 Linker 选项卡中,取消勾选 "Use Memory Layout from Target Dialog",然后点击 Edit 按钮,即可像编辑普通文本一样,手动编写或修改 .sct 文件 。
  手动配置是关键,但别忘了,要让代码真正在 ITCM 中运行,还需要启动文件(如 startup_n32h76x.s)中的代码将程序从 Flash 搬运 到 ITCM,并完成 TCM 的初始化配置。.sct 文件好比是 “设计蓝图” ,而启动代码的搬运和初始化过程,才是实际执行的 “施工队” 。

  2.4、常见修改场景
  (1)调整 Flash 和 RAM 大小

LR_IROM1 0x08000000 0x00100000  {    
; 修改 Flash 大小为 1MB
  ...
}

RW_IRAM1 0x20000000 0x00020000  {    
; 修改 RAM 大小为 128KB
  ...
}

  可以调整这个段的起始地址和空间大小,可以划分成多个区域段
  (2)将部分代码或数据放入指定 RAM

RW_IRAM2 0x20010000 0x00008000  {    ; 新增 RAM 区域
   my_library.o (+RW +ZI)            ; 指定某个库的变量放在这里
   *(my_library_section)             ; 划分出一个段
}

//变量user指定存放到my_library_section段
__attribute__((section("my_library_section"))) unsigned char user[10]; 

  先定义出一个区域段即RW_IRAM2 ,然后在此区域段里定义具体的存储段或者指定某些变量或代码放到这里。
  (3)使用外部 RAM(如 SDRAM)

RW_ESDRAM 0xC0000000 0x01000000  {   ; 外部 SDRAM
   .ANY (EXTERNAL_RAM)               ; 自定义段
}

  2.5、综合例子
  综合的使用例子

image

  2.6、 注意事项

  修改 .sct 后必须重新编译,否则不会生效。
  变量地址检查:可以使用 map 文件(工程名.map)查看变量和函数的实际地址。
  避免冲突:确保不同执行域不重叠。
  特殊段:
    RESET:存放中断向量表。
    InRoot$$Sections:存放 C 库初始化代码。
    Heap 和 Stack:可在 .sct 中指定大小。

 

posted @ 2026-03-27 17:25  孤情剑客  阅读(176)  评论(0)    收藏  举报