Skip to content

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 足矣。只需要设置起始地址与大小(偏移量)就可以了。

STM32F103RC 的 RAM/ROM 布局

其实,在 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 就可以理解为执行域,负责存储数据执行代码。下图是一个简单的内存布局图:

STM32F103RC 内存布局

左侧是加载域视图;右侧是运行域视图。加载域中包含 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 每次编译完成输出的信息:

bash
# 编译工程 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 的信息如下:

bash
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。这对于体积敏感的应用场景十分重要。

我们可以输入以下指令查看压缩器:

bash
  ~ 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

Input sections, output sections, regions, and program segments - 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-CODECODERO 代码部分
RO-DATACONSTconst 常量部分
ROTEXT包括 RO-CODERO-DATA
RW-DATA有初始值的静态变量
RW-CODERW 代码部分
RWDATA包括 RW-CODERW-DATA
XO只执行部分
ZIBSS无初始值的静态变量
ENTRY包括 ENTRY 入口点的 section

还有两个伪属性 FIRSTLAST 。为什么叫伪属性呢?因为它不属于上述任何一种数据类型,它的作用是指定数据存放位置,不让链接器自动排序。 FIRST 是放在运行域的开头, LAST 是放在运行域的末尾。需要注意,运行域中的 FIRSTLAST 只能个出现一次。

下面是一个例子:

ts
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 是在启动文件中定义的中断向量表:

asm
; 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 是否在最开头:

txt
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 区域起始地址空间大小(偏移量)MDMADMABDMA
DTCM0x20000000128 KB (0x20000)x
AXI-SRAM0x24000000512 KB (0x80000)xx
SRAM10x30000000128 KB (0x20000)xx
SRAM20x30020000128 KB (0x20000)xx
SRAM30x3004000032 KB (0x8000)xx
SRAM40x3800000064 KB (0x10000)xxx
SDRAM0xC000000032 MB (0x2000000)xx

Powered by VitePress, deployed by Github & Vercel.