keil scatter 分散加载

生产的可执行文件

MCU 的存储空间:片内 Flash、片内 RAM。编译完成后,会看到程序占用空间的信息

linking...
Program Size: Code=42116 RO-data=7328 RW-data=2868 ZI-data=128204  

上面提到的 Program Size 包含以下几个部分:

  • Code:代码段,存放程序的代码部分;

  • RO-data:只读数据段,存放程序中定义的常量;

  • RW-data:读写数据段,存放初始化为非 0 值的全局变量;

  • ZI-data:0 数据段,存放未初始化的全局变量及初始化为 0 的变量;

编译完工程会生成一个. map 的文件,该文件说明了各个函数占用的尺寸和地址,在文件的最后几行也说明了上面几个字段的关系:

    Total RO  Size (Code + RO Data)                49444 (  48.29kB)
    Total RW  Size (RW Data + ZI Data)            131072 ( 128.00kB)
    Total ROM Size (Code + RO Data + RW Data)      49604 (  48.44kB)

程序运行之前,需要有文件实体被烧录到 Flash 中,一般是 bin 或者 hex 文件,该被烧录文件称为可执行映像文件。如下图左边部分所示,是可执行映像文件烧录到 STM32 后的内存分布,它包含 RO 段和 RW 段两个部分:其中 RO 段中保存了 Code、RO-data 的数据,RW 段保存了 RW-data 的数据,由于 ZI-data 都是 0,所以未包含在映像文件中。

STM32 在上电启动之后默认从 Flash 启动,启动之后会将 RW 段中的 RW-data(初始化的全局变量)搬运到 RAM 中,但不会搬运 RO 段,即 CPU 的执行代码从 Flash 中读取,另外根据编译器给出的 ZI 地址和大小分配出 ZI 段,并将这块 RAM 区域清零。

为什么上述的 RW-Data 既占用 Flash 又占用 RAM 呢?首先变量必先要初始化才能使用,否则初值不正确,而在 main() 函数后变量已经可以正常使用,那就是说变量的初始化是在之前完成的,查看这之前的代码只有 __main() 一个函数,除了赋初值外,都还做了什么呢?

函数 __main() 主要由以下两部分功能组成,如下所示:

    1. __main():完成代码和数据的拷贝,并把 ZI 数据区清零。代码拷贝可将代码拷贝到另外一个映射空间并执行 (例如将代码拷贝到 RAM 执行); 数据拷贝完成 RW 段数据赋值;数据区清零完成 ZI 段数据赋值。以上的代码和分散加载文件密切相关。

    1. _rt_entry():进行 STACK 和 HEAP 等的初始化。最后 _rt_entry 跳进 main() 函数入口。当 main() 函数执行完后, _rt_entry 又将控制权交还给调试器。

在整个 4GB 的地址空间内

  • 加载时域:程序烧入 Flash 中的状态,

  • 运行时域:是指程序执行时的状态

分散加载文件干什么的

分散加载(scatter)文件是一个文本文件,它可以用来描述ARM连接器生成映像文件时所需要的信息。

如果不用scatter文件指定,那么ARM连接器会按照默认的方式来生成映像文件,一般情况下我们是不需要使用分散加载文件的,但在某些场合,我们希望把某些数据放在指定的地址处,那么这时候scatter文件就发挥了非常大的作用。而且scatter文件用起来非常简单好用。在分散加载文件中可以指定下列信息:

  • 1)各个加载时域(load region)加载时的起始地址(load address)和最大尺寸(max size);

  • 2)各个加载时域的属性。

  • 3)从每个加载时域中分割出来的运行时域。

  • 4)各个运行时域(excution region)的运行起始地址(excution address)和最大尺寸(max size)。

  • 5)各个运行时域的存储访问特性。

  • 6)各个运行时域的属性。

  • 7)各个运行时域中包含的输入段。

一般情况下,我们可以不独自编写分散加载文件,ARM连接器直接按照默认的方式来生成映像文件即可,但是在某些场合,我们希望将某些数据放在指定的位置,此时分散加载文件就发挥了非常发的作用。比如在下面几种情况就充分体现了分散加载文件的优势:

  • 1)复杂内存映射:如果必须将代码和数据放在多个不同的内存区域中,则需要使用详细指令指定将哪些数据放在哪个内存空间中。

  • 2)不同类型的内存:许多系统都包含多种不同的物理内存设备,如闪存、ROM、SDRAM 和快速 SRAM。分散加载描述可以将代码和数据与最适合的内存类型相匹配。例如,可以将中断代码放在快速 SRAM 中以缩短中断等待时间,而将不经常使用的配置信息放在较慢的闪存中。

  • 3)位于固定位置的函数:可以将函数放在内存中的固定位置,即使已修改并重新编译周围的应用程序。

  • 4)使用符号标识堆和堆栈:链接应用程序时,可以为堆和堆栈位置定义一些符号。

一些需要关注的点:

  • 1)编译后输出的映像文件中各段是首尾相连的,中间没有空闲的区域,他们的先后关系是根据链接时参数的先后次序决定的armlinker -file1.o file2.o …

  • 2)scatter用于将编译后的映像文件中的特定段加载到多个分散的指定内存区域。

  • 3)两类域(region):执行域(execution region)和加载域(load region)。

  • 4)加载域,该映像文件开始运行前存放的区域,即当系统启动或加载时应用程序存放的区域。

  • 5)执行域,映像文件运行时的区域,即系统启动后,应用程序进行执行和数据访问的存储器区域,系统在实时运行时可以有一个或多个执行块。

  • 6)scatter本身并不能对映像实现“解压缩”,编译器读入scatter文件之后会根据其中的各种地址生成启动代码了,实现对映像的加载,而这一段代码就是 *(InRoot$$Sections) 它是 __main() 的一部分。这就是在汇编启动代码的最后跳转到 __main() 而不是跳向 main() 的原因之一。

  • 7)起始地址与加载域重合的执行域称为root region,*(InRootSections) 必须放在这个执行域中,否则链接的时候会报错。*(+RO) 包含了 *(InRootSections),所以如果在 rootregion 中用到了 *(+RO) 就可以不再指定 *(InRootSections)

语法

注解:

  1. ROM_LOAD是加载域。这里只有一个,也可以有多个(rom地址不连续的情况)

  2. ROM、SRAM、SDRAM1、SDRAM2是执行域,有多个。第一个执行域必须和加载域地址重合,因为ARM的复位地址就是加载域的起始地址(有bootstrap的话加载域址就是bootstrap执行完后的跳转地址)

  3. vectors.o (+RO, +FIRST) 中断向量表放在最开头

  4. ROM 0x00000000 0x003FFFFF; 加载域名 起始地址 最大允许长度;‘最大允许长度’也可以省略,但缺点是编译器不会检查段是否溢出和别的段重叠了。‘起始地址’= +0表示紧接着上一段开始的连续地址。

    • (InRoot$$Sections)是复制代码的代码

  5. UNINT关键字表示不进行初始化清零

值得注意的是:在一个scatter文件中,同一个.o文件不能出现2次,即使是在2个不同的加载域中也不可以,否则会报错:Ambiguous selectors found for *.o,错误的例子: LOAD1 0x00000000 { EXE1 { Init.o } }

LOAD2 0xFFFF0000 { EXE2 { Init.o } }

结构

分散加载文件一般由1个加载时域和1到多个运行时域组成(当然也可以包含2个以上的加载时域)

具体来说,在scatter文件中可以指定下列信息:

  • 各个加载时域的加载时起始地址、最大尺寸和属性;

  • 每个加载时域包含的输出段;

  • 各个输出段的运行时起始地址、最大尺寸、存储访问特性和属性;

  • 各个输出段中包含的输入段。

加载时域

  • load_region_name:加载时域的名称,名称可以按照用户意愿自己定义,该名称中只有前 31 个字符有意义。它仅仅用来唯一的标识一个加载时域,而不像运行时域的名称除了唯一的标识一个运行时域外,还用来构成连接器连接生成的连接符号。

  • #start_address + #offset:用来表示本加载时域的起始地址

    • start_address:表示本加载时域中的对象在连接时的起始地址,地址必须是字对齐的;

    • offset:连接时的起始地址相对于上一加载时域的偏移地址,4 Byte 对齐。本加载时域是第一个加载时域,则它的起始地址即为 offset。

  • attribute:指定本加载时域的属性,默认加载时域的属性是ABSOLUTE。

    • PI – 位置无关属性。

    • RELOC – 重定位。

    • OVERLAY – 覆盖。

    • ABSOLUTE – 起始地址由[#start_address + #offset]

  • max_size:指定本加载时域的最大尺寸。如果本加载时域的实际尺寸超过了该值,连接器将报告错误,默认取值为 0xFFFFFFFF

  • execution_region_description:运行时域,可以有多个运行时域。

运行时域

  • execution_region_name:为运行时域的名称,名称可以按照用户意愿自己定义,该名称中只有前 31 个字符有意义。它除了唯一的标识一个运行时域外,还用来构成连接器生成的连接符号。

  • #start_address + #offset:用来表示本运行时域的起始地址:

    • start_address:表示本运行时域中的对象在连接时的起始地址,地址必须是字对齐的;

    • offset:表示本运行时域相对前一个运行时域结束地址的偏移量。

  • attribute:指定本加载时域内容的属性,包含以下几种, 默认加载时域的属性是ABSOLUTE。

    • PI – 位置无关属性。

    • RELOC – 重定位。

    • OVERLAY – 覆盖。

    • ABSOLUTE – 起始地址由 start_address 指定(默认属性)。

    • FIXED – 固定地址。此时该域加载时域地址和运行时域地址是相同的,而且都是通过 start_address 指定的,而且 start_address 必须是绝对地址或者 offset 为 0 。

  • max_size:指定本运行时域的最大尺寸。如果本运行时域的实际尺寸超过了该值,连接器将报告错误,默认取值为 0xFFFFFFFF。

  • length:如果指定的长度为负值,则将 start_address 作为区结束地址。它通常与 EMPTY 一起使用,以表示在内存中变小的堆栈。

输入段描述

  • module_select_pattern:目标文件滤波器,支持使用通配符 *?

    • * 匹配任意字符,*.ANY

    • ? 匹配单个字符

    • 进行匹配时所有字符不区分大小写。

  • input_section_attr:属性选择器与输入段属性相匹配。每个 input_section_attr 的前面有一个“+”号。如果指定一个模式以匹配输入段名称,名称前面必须有一个“+”号。可以省略紧靠“+”号前面的任何逗号。 选择器不区分大小写(可以识别的为属性First、Last)。

    • 通过使用特殊模块选择器模式.ANY ,可以将输入段分配给执行区,而无需考虑其父模块。可以使用一个或多个.ANY 模式以任意分配方式填充运行时域。在大多数情况下,使用单个.ANY 等效于使用*模块选择器。

应用举例

1

以 ST 的 Cortex-M4 核的低功耗 STM32L476VC 芯片为例,其资源如下:

  • Flash 基地址:0x08000000,小为 256kB(0x00040000)

  • RAM 基地址:0x20000000,大小为 96kB(0x00018000)

如上分散加载文件所示:它包含1个加载时域,3个运行时域。其第一个运行时域与加载时域的基地址一致(在嵌入式系统中,必须首先加载中断向量表,且必须与加载时域的基地址保持一致,否则编译时会报错)。

SECTION_APP_INFO:用户自定义的一块区域,记录终端的版本、升级等相关信息,它是一块固定的位置,紧随中断向量变之后。它为一个结构体形式。

类型声明为:

实现为

以属性FIXED实现一个加载域多个执行域的情况

下面的这种情况是对中分散加载文件的略微修改,它将第 2 个加载时域 (ER_IROM2) 的基地址固定为了0x08000400 。这样为中断向量表预留的空间大小为 0x400 字节,冗余的部分以 0x00 填充(这样写会导致生成的映像文件变大)。对应的 .bin 文件如下图所示:

下面的这种情况也是是对6.1中分散加载文件的略微修改,它将第2个加载时(ER_IROM2 )的基地址固定为了0x08000400 ,第3个加载域(ER_IROM23)的基地址固定为了0x08000800,这样为中断向量表和SECTION_APP_INFO均预留的空间大小为0x400字节,冗余的部分以0x00填充。

多块 RAM 的分散加载文件配置

还是上述的 MCU,假设其增加了另外一块 RAM,其资源如下:

  • 1)Flash基地址:0x08000000,小为256KB

  • 2)RAM基地址:0x10000000,大小为24KB(0x6000)

  • 3)RAM基地址:0x20000000,大小为24KB(0x6000)

若想将两块RAM都使用起来(可使用48KB),那么分散加载文件的写法应该如下:

以上的分散加载机制确实可以使两个RAM空间(48KB)都使用起来,但是它并不等同于一个48KB的RAM。在实际应用中定义一个30KB 的数组 unsigned char test[30*1024]; ,编译会出错,提示没有足够空间。

改成在同一个文件内定义两个 15KB 的数组,任会出错,因为.ANY 是按文件名匹配的。

解决方法,定义在两个文件内,或指定段。

多块 Flash 分散加载文件配置

假设有一个MCU,它有两块独立的Flash,一个RAM,资源分配如下:

  • (1)Flash1 基址: 0x00000000,大小:256 KB(0x00040000);

  • (2)Flash2 基址: 0x20000000,大小:2048 KB(0x00200000);

  • (3)RAM 基址: 0x10000000,大小:32 KB(0x00008000);

如果这么写会出错

正确的编写方式应该如下:

这样写能够解决双Flash加载的问题,但是同样有一个问题,那就是,编译时会生成2份bin文件,需要分别两次烧录代码。

一些补充

  • 第一个运行时域存放的代码不会进行额外拷贝

    • 因为分散加载文件有一项很强大的功能,就是可以将 Flash 的代码拷贝到 RAM 中运行,这一段拷贝代码就存在于 __main() 函数中,但拷贝代码不能拷贝自身,所以必须规定有一个运行时域中存放的代码是不会被拷贝的,这个指的就是第一个运行时域。

    • 一段代码必须先完成拷贝,才能被执行。换句理解就是拷贝代码前包括自身的所 有代码都不能拷贝,也就是说这些代码全部都必须放在第一个运行时域中。

  • 规定其余运行时域中存放的代码均会被拷贝

    • 一个加载时域,只需要一个不拷贝的运行时域即可。所以规定其余所有的运行时域中的代码均会被拷贝。

  • 第一个运行时域的基址必须与加载域基址相同

    • 为了保证第一个运行时域的代码能够被正确存储和执行,因此要求第一个运行时域的基址必须和加载时域的基址相同。

关于 __main() 做的事情:此函数为编译器自动创建,编译器发现定义了 main() 就会自动创建 __main(),此函数为 C 库函数。程序 reset 运行后,执行到这里,主要有两个比较大的行为

  • __scatterload() 负责把 RW/RO 段从装载域地址复制到运行域地址,并完成 ZI 运行域的初始化工作

  • __rt_entry() 负责初始化堆栈,完成库函数初始化,最后自动跳转 main() 函数

一个实际的例子

参考资料

最后更新于