armlink 与分散加载文件
字数: 0 字 阅读时间: 0 分钟
Scatter-loading Features - ARM Compiler armlink User Guide Version 5.06
Scatter File Syntax - ARM Compiler armlink User Guide Version 5.06
Keil MDK(ARM 编译器)分散加载特性(下):使用分散加载文件(.sct)控制 – 哈冬猪的小站 – 个人学习记录
TIP
AC6 的 armlink 移植自 AC5,所以下面的内容 AC5/AC6 通用
简介
分散加载文件(scatter-loader file,文件后缀为 sct)是 ARM Compiler (AC5/AC6) 所需要的链接控制文件。简单来说,这个文件描述了 RAM/ROM 的地址与大小,变量、代码放哪里,怎么放,这些事是链接器来干的。
简单的内存布局不需要手动编写 sct 文件,使用 MDK, EIDE 自带的 RAM/ROM Layout 足矣。只需要设置起始地址与大小(偏移量)就可以了。

其实,在 MDK, EIDE 等 IDE 编译之前,会自动根据这个 GUI 里的设置生成一个 sct 文件供 armlink 使用,仔细观察编译目录就会发现 sct 文件。上面 STM32F103RC 的 RAM/ROM Layout 设置,EIDE 生成的 sct 文件如下:
; ******************************************************************
; *** Scatter-Loading Description File generated by Embedded IDE ***
; ******************************************************************
LR_IROM1 0x08000000 0x00040000 {
ER_IROM1 0x08000000 0x00040000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x0000C000 {
.ANY (+RW +ZI)
}
}
但是,如果遇到一些复杂内存布局的 MCU,或者需要用到外部 ROM/RAM 的程序,需要管理多块内存的时候,仅仅使用此 GUI 就显得捉襟见肘了。什么时候建议使用分散加载文件呢?下面是 ARM 官方的建议:
When to use scatter-loading - ARM Compiler armlink User Guide Version 5.06
- 复杂的存储布局:代码和数据必须明确指定存储区域
- 多种存储类型:例如一个系统包含多种存储区域,如内部 Flash、内部 SRAM、高速 SRAM、外部 SDRAM、外部 ROM 等。这时候就需要 sct 文件去描述这些存储区域
- 地址映射的外设:例如 FMC、QSPI 等外设。通常扩展的 SDRAM 需要用到 FMC,sct 文件可以描述这些存储区域,并且可以很方便地访问
- 固定常量内容位置:如果一些代码内容不会变化,且在一个固定的地址,不需要重新擦写(Flash 擦写也是有寿命的)即使周围的程序已经修改后重新编译,这部分固定位置的代码不会受影响。这对一些 GUI 程序非常有用,图片、字库这类资源通常非常大,但是又不会经常修改,每次改代码重新编译不仅下载费时间,芯片频繁擦写还会导致 Flash 寿命变短
- 指定堆栈位置:默认情况下,堆栈由链接器自动分配。可以手动指定堆栈的分配区域
以下应用场景我建议手动编写分散加载文件:
- 内存布局较为复杂,需要手动指定存储区域,例如 STM32H7
- 应用程序需要用到 Bootloader,一部分 ROM 存储 Bootloader 代码;另一部分 ROM 存储应用代码
- 需要用到外部 Flash 外扩 ROM 空间,例如使用 QSPI Flash 存储数据或者代码
加载域与运行域
WARNING
这里以及后面所说的变量都指的是静态变量,函数的局部变量、malloc 分配的动态内存都不属于此部分变量。
简单来说,一个程序要运行,需要 ROM 存储代码,运行时需要将数据从 ROM 加载到 RAM 来执行。那么,ROM 就可以理解为加载域,负责将数据和指令加载到 RAM 中执行;RAM 就可以理解为执行域,负责存储数据执行代码。下图是一个简单的内存布局图:
左侧是加载域视图;右侧是运行域视图。加载域中包含 RO Section 和 RW Section,存储在 ROM 中;在上电后,先将加载域 RW Section 的数据复制或者解压到运行域 SRAM 中的 RW Section 中,SRAM 中的 ZI Section 进行零填充,RO Section 仍然在 ROM 区域,等待 CPU 调用。一个加载域可以包含多个运行域。
在 C 程序中,程序段可以分为 .bbs
, .text
段等(内存布局),在 armlink 中定义的数据类型以及他们对应的程序段如下:
- XO (Execute Only): 只执行数据,就是 Code,对应
.text
,即代码段,通常存储在 ROM 中 - RO (Read Only) Data: 只读数据,对应
.rodata
段,通常存储在 ROM 中,const 修饰的静态变量 - RW (Read and Write) Data: 可读写数据,对应
.data
段,存储在 RAM 中,赋初始值的静态变量 - ZI (Zero Initialized) Data: 初始零数据,对应
.bss
段,存储在 RAM 中,静态变量区,与 RW 的区别在于 ZI 没有赋初始值,默认为 0
RW 的初始值需要存储在 ROM 中,上电需要加载到 RAM 中,给变量赋初值。所以镜像大小等于 Code + RO Data + RW Data。这就是 ARM Compiler 每次编译完成输出的信息:
# 编译工程 1 输出信息:
Program Size: Code=25340 RO-data=2272 RW-data=655520 ZI-data=33562800
Total RO Size (Code + RO Data) 27612 ( 26.96kB)
Total RW Size (RW Data + ZI Data) 34218320 (33416.33kB)
Total ROM Size (Code + RO Data + RW Data) 32804 ( 32.04kB)
# 编译工程 2 输出信息:
Program Size: Code=18516 RO-data=1056 RW-data=80 ZI-data=8472
Total RO Size (Code + RO Data) 19572 ( 19.11kB)
Total RW Size (RW Data + ZI Data) 8552 ( 8.35kB)
Total ROM Size (Code + RO Data + RW Data) 19652 ( 19.19kB)
armlink 对 RW data 的压缩
Optimization with RW data compression - ARM Compiler armlink User Guide Version 5.06
我们发现,上面工程 1 的 ROM Size != Code + RO Data + RW Data (32804 != 25340 + 2272 + 655520)
,这是怎么回事呢?原来是 armlink 会压缩 RW-data。
当 压缩后的RW-data + 解压缩程序大小 < 解压后的RW-data大小
时,armlink 会对 RW-data 压缩,在上电后解压到 RAM 中。所以实际镜像大小不完全等于 Code + RO + RW。
可以使用 --datacompressor off
选项关闭压缩,关闭后编译工程 1 的信息如下:
Program Size: Code=25276 RO-data=2272 RW-data=655520 ZI-data=33562800
Total RO Size (Code + RO Data) 27548 ( 26.90kB)
Total RW Size (RW Data + ZI Data) 34218320 (33416.33kB)
Total ROM Size (Code + RO Data + RW Data) 683068 ( 667.06kB)
可以发现 ROM 大小是对得上 Code + RO + RW 的,打开压缩可以减小镜像体积,上面打开压缩选项的情况下镜像大小仅为 32.04 kB,不压缩大小为 667.06 kB。这对于体积敏感的应用场景十分重要。
我们可以输入以下指令查看压缩器:
➜ ~ armlink --datacompressor list
Product: Keil MDK Community (non-commercial free of charge)
Component: Arm Compiler for Embedded 6.24
Tool: armlink [xxxxxx]
To use a specific compressor pass the number of the compressor.
Num Compression algorithm
=============================================================================
0 Run-length encoding
1 Run-length encoding, with LZ77 on small repeats
2 Complex LZ77 compression
这三种压缩器的特点如下:
- 压缩器 0 在有大量零字节数据,很少非零字节数据的情况下表现较好
- 压缩器 1 在有很多重复的非零数据的情况下表现较好
- 压缩器 2 在数据包含很多重复值的情况下表现较好
当数据中包含很多零字节数据(>75%)时,链接器倾向使用压缩器 0 和 1;当数据包含较少零字节数据(<10%)时,链接器会选择压缩器 2。如果镜像没有明显的特征,链接器会使用所有压缩器压缩一遍,选择压缩体积最小的那个。
在编译时可以用 --datacompressor x
可以指定上述三种压缩器, x
可以是 0-2
。目前不支持自己添加压缩器。
armlink 会根据镜像代码自动选择解压器的代码类型。如果镜像只包含 ARM code,解压器会使用 ARM code;如果镜像包含 Thumb code,解压器会使用 Thumb code。
分散加载文件语法
WARNING
以下代码仅在 Arm Compiler for Embedded 6.24 测试通过,测试平台为 macOS Sequoia 15.5 (Apple M4),未在其他版本的编译器、AC5 下测试,可能有不兼容的语法。
在搞清楚什么是加载域、运行域以及 RO、ZI、RW 这些名词的概念后,我们来看看分散加载文件的语法。以上面自动生成的 STM32F103RC 的分散加载文件为例,来分析一下分散加载文件的语法,以及如何编写分散加载文件。
LR_IROM1 0x08000000 0x00040000 { ; 加载域 LR_IROM1, 起始地址 0x08000000, 大小 0x0004000
ER_IROM1 0x08000000 0x00040000 { ; 运行域 ER_IROM1, 起始地址 0x08000000, 大小 0x0004000
*.o (RESET, +First) ; 所有的 .o 文件, RESET放在最前面`优先(+First)`放这里
*(InRoot$$Sections) ; 库文件也放这里
.ANY (+RO) ; 所有`未指定`地址的 RO 数据放这里
.ANY (+XO) ; 所有`未指定`地址的 XI(代码) 数据放这里
}
RW_IRAM1 0x20000000 0x0000C000 { ; 运行域 RW_IRAM1, 起始地址 0x20000000, 大小 0x0000C000
.ANY (+RW +ZI) ; 所有`未指定`位置的 RW, ZI 数据放这里
}
}
注释以分号开始。
定义加载域与运行域
一级大括号定义加载域,二级大括号定义运行域。加载域的大括号中只能写运行域定义,运行域的大括号中只能写输入节描述。不可以在加载域里写输入节描述,也不可以在运行域中定义运行域。
加载域 1 名称 起始地址 大小 属性 {
运行域 1 名称 起始地址 大小 属性 {
输入节描述
}
运行域 2 名称 起始地址 大小 属性 {
输入节描述
}
}
加载域 2 名称 起始地址 大小 属性 {
运行域 3 名称 起始地址 大小 属性 {
输入节描述
}
运行域 4 名称 起始地址 大小 属性 {
输入节描述
}
}
大小可以省略(省略后默认为 4 GB),属性也可以省略。名称不能重复,区域不能重叠,起始地址必须 8 字节对齐(也就是 8 的整数倍)。这里的大小单位均为字节。
加载域 (Load region)
Load region attributes - ARM Compiler armlink User Guide Version 5.06。
运行域 (Execution region)
Execution region attributes - ARM Compiler armlink User Guide Version 5.06。
ABSOLUTE
ALIGN x
,x 字节对齐UNINIT
:即不要初始化运行域。这个属性在外部 RAM 中很重要,一般情况下,想要访问外部 RAM 需要初始化控制器,例如 STM32 外扩 RAM 通过 FMC 总线实现,使用前需要初始化 FMC 控制器,否则进入HardFault
。默认情况下,定义的 RAM 块在进入main
函数前会 Zero fill,也就是清零。这时候外部 RAM 还没有初始化,访问这块内存会直接进入HardFault
输入节描述 (Input Section Description)
Section-related symbols - ARM Compiler armlink User Guide Version 5.06
Components of an input section description - ARM Compiler armlink User Guide Version 5.06
Placement of ARM C and C++ library code - ARM Compiler armlink User Guide Version 5.06
编译器编译完源代码生成的对象文件中包含代码、变量等各种不同类型的数据。这些都会标记为不同的属性,即 RO, RW, XO, ZI
,它们将会作为链接器的输入。我们需要知道编译器是如何描述它们的,才可以把数据和代码放在指定位置。
一个输入节描述构成如下:
输入节模块名可以是编译生成的对象文件 (.o),可以是库文件 (.lib)。也可以是文件的路径,可以用引号包裹。
可以使用通配符匹配文件,例如 *.o
,大小写不敏感。如果只写一个 *
,代表匹配所有对象文件和库。
WARNING
不可以在多个运行域中用 *
匹配,最好使用 .ANY
来代替。 *
的优先级比 .ANY
高。
TIP
通常情况下,编译的对象文件名和源文件名称是一样的。如果一个工程中有重名的源文件,有些 IDE(例如 Keil)可能会加数字后缀来区分,这一点是需要注意的。
输入节属性也就是我们说的不同类型的数据,大小写不敏感,属性前需要一个加号,例如 +RO
属性名称 | 别名 | 说明 |
---|---|---|
RO-CODE | CODE | RO 代码部分 |
RO-DATA | CONST | const 常量部分 |
RO | TEXT | 包括 RO-CODE 和 RO-DATA |
RW-DATA | 有初始值的静态变量 | |
RW-CODE | RW 代码部分 | |
RW | DATA | 包括 RW-CODE 和 RW-DATA |
XO | 只执行部分 | |
ZI | BSS | 无初始值的静态变量 |
ENTRY | 包括 ENTRY 入口点的 section |
还有两个伪属性 FIRST
和 LAST
。为什么叫伪属性呢?因为它不属于上述任何一种数据类型,它的作用是指定数据存放位置,不让链接器自动排序。 FIRST
是放在运行域的开头, LAST
是放在运行域的末尾。需要注意,运行域中的 FIRST
和 LAST
只能个出现一次。
下面是一个例子:
LR_IROM1 0x08000000 0x00040000 {
ER_IROM1 0x08000000 0x00040000 {
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
RW_IRAM1 0x20000000 0x0000C000 {
.ANY (+RW +ZI)
main.o (+RO)
}
RW_IRAM1 0x20001000 0x00001000 {
main.o (+ZI)
"/path/to/example/lib/example.lib" (+RW +ZI)
}
}
这里第三行我们给 RESET
加了 First
,也就是将 RESET
放在 ER_IROM1
的开头处。而 RESET
是在启动文件中定义的中断向量表:
; Vector Table Mapped to Address 0 at Reset
AREA RESET, DATA, READONLY
EXPORT __Vectors
EXPORT __Vectors_End
EXPORT __Vectors_Size
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
...
DCD SysTick_Handler ; SysTick Handler
; External Interrupts
DCD WWDG_IRQHandler ; Window WatchDog interrupt ( wwdg1_it)
...
DCD WAKEUP_PIN_IRQHandler ; Interrupt for all 6 wake-up pins
__Vectors_End
__Vectors_Size EQU __Vectors_End - __Vectors
AREA |.text|, CODE, READONLY
我们可以查看 map 文件来验证 RESET
是否在最开头:
Memory Map of the image
Image Entry point : 0x08000299
Load Region LR_IROM1 (Base: 0x08000000, Size: 0x000a76b0, Max: 0x80000000, ABSOLUTE, COMPRESSED[0x00009418])
Execution Region ER_IROM1 (Base: 0x08000000, Size: 0x00007604, Max: 0x00008000, ABSOLUTE)
Base Addr Size Type Attr Idx E Section Name Object
0x08000000 0x00000298 Data RO 93 RESET startup_stm32h743xx.o
...
除了我们编写的代码外,还有一些编译器内建组件,例如 C/C++ 库函数。可以使用 *armlib*
或 *cpplib*
来指定。
但是有些 C 库的 section 必须在根域,例如 __main.o
, __scatter*.o
, __dc*.o
, 和 *Region$$Table
。只需要用 InRoot$$Sections
,就可以可以让链接器自动指定存放位置。
示例
以 STM32H743II 为例,该芯片的 RAM 分为 TCM、AXI、SRAM1-4,这几块内存区域速度不同、访问权限不同,且地址不连续。例如,STM32H743 中仅 MDMA 能访问 TCM 内存,BDMA 仅能访问 SRAM4,还有 Cache, MPU 来掺合一脚,这时候就需要手动管理内存区域,设置不同 DMA 使用不同的内存区域。下表是 STM32H743II 的内存表以及 DMA 访问权限,同时我们添加一个外部 SRAM,我们以此为例来编写一个 sct 文件。
RAM 区域 | 起始地址 | 空间大小(偏移量) | MDMA | DMA | BDMA |
---|---|---|---|---|---|
DTCM | 0x20000000 | 128 KB (0x20000) | x | ||
AXI-SRAM | 0x24000000 | 512 KB (0x80000) | x | x | |
SRAM1 | 0x30000000 | 128 KB (0x20000) | x | x | |
SRAM2 | 0x30020000 | 128 KB (0x20000) | x | x | |
SRAM3 | 0x30040000 | 32 KB (0x8000) | x | x | |
SRAM4 | 0x38000000 | 64 KB (0x10000) | x | x | x |
SDRAM | 0xC0000000 | 32 MB (0x2000000) | x | x |