引子
在上篇文章中我们使用Keil MDK提供的GUI控制分散加载,实际上,工程会根据GUI中的这些设置生成一个分散加载文件(.sct),而这个文件才直接控制链接器分散加载,有时GUI提供的设置并不能满足我们的要求,就需要直接编写这个分散加载文件。
查看根据GUI设置生成的分散加载文件(.sct)
若使用GUI控制分散加载(Project – Options for Target ‘xxx’ > Linker 中已勾选Use Memory Layout From Target Dialog),会在Objects文件夹(默认情况,或在Project – Options for Target ‘xxx’ > Output中Select Folder for Objects中设置的其他文件夹)生成一个工程名.sct文件,这个文件就是根据GUI中的设置生成的,并真正用于控制链接器的分散加载。
同时,我们如果想使用分散加载文件实现某个GUI存在的功能,但是不清楚怎么写时,可以参考这个由Keil MDK生成的分散加载文件。
认识分散加载文件(.sct)
这里以Keil MDK生成的默认分散加载文件为例,解读其意义。
; *************************************************************
; *** Scatter-Loading Description File generated by uVision ***
; *************************************************************
;LR即加载域,这个加载域的名字是LR_IROM1,随后为基地址(0x08000000)和最大大小(0x00020000)
LR_IROM1 0x08000000 0x00020000 { ; load region size_region
;ER即运行域,这个运行域的名字是ER_IROM1,随后为基地址(0x08000000)和最大大小(0x00020000)
ER_IROM1 0x08000000 0x00020000 { ; load address = execution address
;将RESET放在最前面,也就是将中断向量表放在最前面
*.o (RESET, +First)
;将 MDK 的一些库文件全部放在根域
*(InRoot$$Sections)
;所有未指定位置的RO数据和XO数据放在此运行域中
;模块名称为.ANY,代表没有指定位置的节,输入节属性为(+RO),代表RO数据
.ANY (+RO)
.ANY (+XO)
}
;这个运行域的名字是RW_IRAM1,随后为基地址(0x20000000)和最大大小(0x00020000)
RW_IRAM1 0x20000000 0x00020000 { ; RW data
;所有未指定位置的RW数据和ZI数据放在此运行域中
.ANY (+RW +ZI)
}
}
分散文件包含一/多个加载域,每个加载区域包含一/多个运行域。每个加载域/运行域范围使用大括号标识。注释使用分号;
标识。
一个加载域包含以下部分:
- 名称
- 基地址
- 属性(可选属性见Load region attributes)
- 最大大小(可选)
- 一/多个运行域
一个运行域包含以下部分:
- 名称
- 基地址(绝对/相对)
- 属性(可选属性见Execution region attributes)
- 最大大小(可选)
- 一/多个输入节(input section)描述
一个输入节描述占一行,包含以下部分:
- 模块名称(object文件(.o)、库文件(.lib)成员名或文件名,可以使用通配符)
- 输入节名称或属性(可选属性见Syntax of an input section description,输入节名称可以使用通配符),可选的属性有RO、XO、RW、ZI等,代表这个模块中的RO、XO、RW、ZI数据
- 符号名称
一个加载域通常至少包含一个ROM和0或多个RAM。因为加载域要保证掉电后也不会丢失,所以加载域必须包含一个ROM并且基地址和最大大小与这块ROM相同。通常不同ROM之间的地址并不连续,因此每个ROM都需要在不同的加载域中,每个加载域中也只有一个ROM。
在工程中使用自定义的分散加载文件(.sct)
在Project – Options for Target ‘xxx’ > Linker 中取消勾选Use Memory Layout From Target Dialog,并在Scatter File中设置自定义的分散加载文件路径即可。
同一加载域下使用多个RAM(=目标(Target)面板中使用额外的RAM)
有时我们需要使用多块内存或同块内存分割,就需要在同一加载域下作为运行域存在。例如STM32H7提供了DTCM(128KiB)和AXI-SRAM(512iKB)内存,分散加载文件编写如下(但此时没有任何数据在AXI-SRAM中):
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
; AXI SRAM 512KB
RW_IRAM_AXI_SRAM 0x24000000 0x00080000 {
}
}
使用.ANY模块名称(=目标(Target)面板中勾选default)
对于没有指定位置的部分可以使用.ANY模块名称表示,并且它可以出现在多个执行域中。例如,数据没有办法在一个RAM中装下,需要装在额外不连续的RAM中,此时就可以在RAM加载域中使用多个.ANY模块名称。例如,让未指定地址的RW和ZI数据同时可以存在于DTCM和AXI-SRAM中:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
; AXI SRAM 512KB
RW_IRAM_AXI_SRAM 0x24000000 0x00080000 {
.ANY (+RW +ZI)
}
}
在有多.ANY时,链接器会优先选择放置在最大的运行域,若放不下才会将放不下的部分放到更小的运行域中。当然我们也可以通过指定.ANY的优先级的方式指定放置的顺序,具体方法请看下节。
.ANY的优先级
在有多.ANY时,链接器会优先选择放置在最大的运行域,我们也可以通过指定.ANY的优先级的方式指定放置的顺序。具体方法为在.ANY后加上一个数字,这个数字就代表了这个.ANY的优先级,优先级是从零开始的正整数,数字越大优先级就越高。例如,我们想让未指定地址的RW和ZI数据优先放在DTCM中,因此我们将DTCM的.ANY优先级设为2,SRAM的.ANY优先级设为1。
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
.ANY2 (+RW +ZI)
}
; AXI SRAM 512KB
RW_IRAM_AXI_SRAM 0x24000000 0x00080000 {
.ANY1 (+RW +ZI)
}
}
加载域中使用UNINIT属性(=目标(Target)面板中勾选No Init)
假如不想让某个段内存中的ZI数据不被初始化,可以使用UNINIT属性(并不会影响RW数据的初始化)。但是有时这个选项并不能完全生效,有时ZI数据仍然会被初始化,具体请见ARM: Uninialized Variables Get Initialized。使用方法为在加载域基地址后加上UNINIT属性。
例如,我们想让DTCM不进行ZI数据的初始化:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 UNINIT 0x00020000 {
.ANY (+RW +ZI)
}
}
指定某个文件中的数据的位置(=文件选项(Options for File)面板中的存储分配(Memory Assginment))
有时我们想将某个文件中的数据放置在特定的位置中,可以显式指定它的位置。使用方法为输入节描述的模块名称使用文件名.o(对象文件)并加上属性(例如RO、XO、ZI、RW等)
例如,我们想将fast.c
文件中的RO数据加载到ITCM中,将fast.c的RW和ZI数据加载到DTCM中运行,并将其他RW和ZI数据加载到AXI-SRAM中,其他RO、XO数据的运行域为ROM:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; ITCM 128KB
RW_IRAM_ITCM 0x00000000 0x00020000 {
fast.o (+RO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
fast.o (+RW +ZI)
}
; AXI SRAM 512KB
RW_IRAM_AXI_SRAM 0x24000000 0x00080000 {
.ANY (+RW +ZI)
}
}
使用多个ROM(多个加载域)(=目标(Target)面板中使用额外的ROM)
由于多个ROM的地址一般并不连续,而加载域的地址空间必须是掉电后不丢失的ROM,因此一般将一个ROM放在一个加载域中,多个ROM就需要多个加载域。这个操作与目标(Target)面板的只读存储区域(Read/Only Memory Areas)中填写额外的ROM相同,填写的基地址和大小将影响生成的加载域和运行域中ROM的基地址和大小。
例如,使用QSPI外接Flash时:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
}
LR_ROM_QSPI 0x90000000 0x00800000 {
ER_ROM_QSPI 0x90000000 0x00800000 {
}
}
例子:将数据下载到片外 SPI Flash 中
这个问题与Keil MDK(ARM编译器)分散加载特性(上):使用GUI控制中相同,只是使用分散加载文件来实现。在分散加载文件添加一下内容:
LR_ROM_SPI 0xC0000000 0x00800000 {
ER_ROM_SPI 0xC0000000 0x00800000 {
gb2312.o (+RO)
}
}
指定了一个名为LR_ROM_SPI的加载域和名为ER_ROM_SPI的运行域,他们实际都是相同的虚拟的ROM,并将字库(编译后名为gb2312.o)的RO数据(实际只包含常量不包含函数)放在这个运行域中。注意我们使用的虚拟的地址,因此不能使用这个地址直接访问SPI Flash中的数据。
例子:使用片外 QSPI Flash 运行代码
这个问题与Keil MDK(ARM编译器)分散加载特性(上):使用GUI控制中相同,只是使用分散加载文件来实现。在分散加载文件添加一下内容:
LR_ROM_QSPI 0x90000000 0x00800000 {
ER_ROM_QSPI 0x90000000 0x00800000 {
extern_function.o (+RO)
}
}
指定了一个名为LR_ROM_QSPI的加载域和名为ER_ROM_QSPI的运行域,他们实际都是相同的虚拟的ROM,并将字库(编译后名为extern_function.o)的RO数据(包含常量和函数)放在这个运行域中。在调用这部分函数/常量之前必须先初始化QSPI。
输入节描述属性可选项
除了前面提到的RO、XO、ZI、RW,还有一些更细分的属性或者同义词可以使用
属性 | 别名 | 说明 |
RO-CODE | CODE | RO数据中的代码部分 |
RO-DATA | CONST | RO数据中的常量部分 |
RO | TEXT | RO数据,同时包含RO-CODE和RO-DATA |
RW-DATA | RO数据中的数据部分 | |
RW-CODE | RO数据中的代码部分 | |
RW | DATA | RW数据,同时包含RW-CODE和RW-DATA |
XO | XO数据 | |
ZI | BSS | ZI数据 |
ENTRY | 包含一个ENTRY点的部分 |
例如,上一节的例子可以等价地写为:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+TEXT)
.ANY (+XO)
}
; ITCM 128KB
RW_IRAM_DTCM 0x00000000 0x00020000 {
fast.o (+RO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
fast.o (+DATA +BSS)
}
; AXI SRAM 512KB
RW_IRAM_AXI_SRAM 0x24000000 0x00080000 {
.ANY (+RW +ZI)
}
}
输入节描述属性伪属性FIRST、LAST
有时我们必须制定放置的顺序,例如我们必须将中断向量表放在地址开始处(一般默认映射到片内Flash的起始地址),此时我们可以使用FIRST伪属性将某个部分放置到最开始的位置。例如,默认生成的分散加载文件就将RESET放在ROM最开始的位置:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
}
而RESET就代表了中断向量表(在启动文件中,RESET这个名字是由AREA后的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
需要注意,一个运行域只能有一个FIRST和LAST伪属性。
修改启动位置(=目标(Target)面板中选择Startup的ROM)
修改启动位置即是调整中断向量表的位置,也是利用输入节描述属性伪属性FIRST,将RESET放置在特定固定位置,具体请见上一节。
在输入节描述的模块选择模式中使用通配符
使用星号*
来指代匹配任何内容,例如
模块选择模式 | 描述 |
* | 匹配任何模块或库 |
*.o | 匹配任何对象模块 |
*armlib* | 匹配 ARM 提供的所有 C 库 |
*cpplib* | 匹配 ARM 提供的所有 C++ 库 |
*math.lib | 匹配任何以 结尾的库路径 math.lib,例如 C:\apps\lib\math\satmath.lib |
需要注意,使用*后则不允许在其他地方使用*(同一属性),若要匹配任意模块且允许在不同运行域中出现请使用.ANY。
例如,将ARM提供的C库加载到ITCM中:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; ITCM 128KB
RW_IRAM_ITCM 0x00000000 0x00020000 {
*armlib* (+RO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
.ANY (+RW +ZI)
}
}
使用自定义的名称来指定位置
我们希望在我们的代码中使用一个名称来标识数据,方便在分散加载文件中拥有同一名称数据的位置。为了达到这个目的可以在代码中使用__attribute__((section("name")))
,并在分散加载文件中指定该名称的数据放置的位置。例如,我们希望将一个变量一定放置在DTCM中,在源文件中:
int variable __attribute__((section(".DTCM")));
在分散加载文件中:
LR_IROM 0x08000000 0x00020000 { ; load region size_region
; on-chip flash 128KB
ER_IROM 0x08000000 0x00020000 { ; load address = execution address
*.o (RESET, +First)
*(InRoot$$Sections)
.ANY (+RO)
.ANY (+XO)
}
; DTCM 128KB
RW_IRAM_DTCM 0x20000000 0x00020000 {
* (.DTCM)
}
; AXI SRAM 512KB
RW_IRAM_AXI_SRAM 0x24000000 0x00080000 {
.ANY (+RW +ZI)
}
}
将数据放置在特定地址
我们也可以直接使用地址直接指定位置,可以使用__attribute__((section(“”.ARM.__at_address”)))或者__attribute__((at(address)))。例如将某个函数放置在0x20000处,在源文件中:
int sqr(int n1) __attribute__((at(0x20000)));
\\ 或者
int sqr(int n1) __attribute__((section(".ARM.__at_0x20000")));
int sqr(int n1)
{
return n1*n1;
}
将一个变量放置在0x8000处:
int variable __attribute__((at(0x8000)));
\\ 或者
int variable __attribute__((section(".ARM.__at_0x8000")));
关于InRoot$$Sections
某些 ARM C 和 C++ 库部分必须放置在根区域中,例如__main.o
、__scatter*.o
、__dc*.o
和*Region$$Table
,这些可以通过InRoot$$Sections
标识。因此需要将InRoot$$Sections
放在根域中。
更多资料
请参考:
ARM Compiler armlink User Guide Version 5.06 – Scatter-loading Features
ARM Compiler armlink User Guide Version 5.06 – Scatter File Syntax