1.适用范围
有时候用户希望将不同的代码放在不同存储空间,也就是通过编译器生成的映像文件需要包含多个域,
每个域在加载和运行时可以有不同的地址。要生成这样的映像文件,必须通过某种方式告知编译器相关的地
址映射关系。
在Keil/ADS/IAR等编译工具中,可以通过分散加载机制实现。分散加载通过配置文件实现,这样的文件
称为分散加载文件。本文重点介绍Keil的分散加载文件配置。
2.基础知识
2.1基础概念
要了解分散加载文件前首先需要对以下各个概念进行了解,
Code:为程序代码部分;
RO-Data:表示程序定义的常量及const型数据;
RW-Data:表示已经初始化的静态变量,变量有初值;
ZI-Data:表示未初始化的静态变量,变量无初值;
{ #define DATA (0x10000000) /*RO-Data */
char const GcChar = 5; /*RO-Data */
char GcStr[] = "string."; /*RW-Data */
char GcZero; /*ZI-Data */ }
Keil工程编译完后,查看其的map文件,可得结果如程序清单2.2类似
{
Total Ro Size(Code+RO-Data) 768( 0.75kB)
Total RW Size(RW-Data+ZI-Data) 2060( 2.01kB)
Total ROM Size(Code+RO-Data+RW-Data) 780( 0x76kB) }
由程序清单2.2所示的map文件可看出:
ROM (Flash) Size = Code+RO-Data+RW-Data = 0.76KB;
RAM Size = RW-Data+ZI-Data = 2.01KB
为什么上述的RW-Data既占用Flash又占用RAM,变量不是放在RAM中的么,为什么会占用Flash?因为RW
数据不能像ZI那样"无中生有"的,ZI段数据只要求其所在的区域全部初始化为0,所以只需要程序根据编译器
给出的ZI基址及大小来将相应的RAM清0。但RW段数据却不这样做,所以编译器为了完成所有RW段数据赋值,其
先将RW段的所有初值,先保存到flash中,程序执行时,再flash中的数据搬运到RAM中,所以RW段既占用flash
用占用RAM,且占用的空间大小是相等的。
这里有必要再了解一下,ZI和RW段数据的赋值在一工程中是在什么地方实现的?首先,变量必先要初始化
才能使用,否则初值不正确,而且main()函数后变量已经可以正常使用,那就是说变量的初始化是在之前完成的,
查看这之前的代码只有_main()一个函数,除了赋初值外,还做了什么呢?
_main()函数主要由以下两个部分功能组成,
_main():完成代码和数据的拷贝,并把ZI数据区清零。代码拷贝可将代码拷贝到另一个映射空间并执行,如
将代码拷贝到RAM执行;数据拷贝完成RW段数据赋值;数据区清零完成ZI段数据赋值。以上的代码和分散加载文件密切相关。
_rt_entry():进行STACK和HEAP等的初始化。最后_rt_entry跳进main()函数入口。_rt_entry又将控制权交还给调试器。
3.分散加载文件概述
分散加载(scatter)文件是一个文本文件,它可以用来描述连接器生成映像文件时需要的信息。通过编写一个分散加载文件
来指定ARM连接器在生成映像文件时如何分配Code、RO-Data、RW-Data、ZI-Data等数据的存放地址。如果不用分散加载文件指定,
那么ARM连接器会按照默认的方式来生成映像文件。一般情况下我们不需要使用分散加载文件。但对一些特殊的情况例如需要将
不同的程序代码存储到不同的地址区域时需要修改分散加载文件。
1、如何使用分散加载
连接器的命令行选项提供了一些对数据和代码位置的控制,但要对位置进行全面控制,则需要使用此命令行中的输入内容更
详细的指令。需要或最好使用分散加载描述的情况包括:
2、复杂内存映射
如果必须将代码和数据放在多个不同内存区域中,则需要使用详细指令指定将那些数据放在那个内存空间中。
3、不同类型的内存
驱动系统都包含多种不同的物理内存设备,如闪存、ROM、SDRAM和快速SRAM。分散加载描述可以将代码和数据与最适合的内存类型
相匹配。例如,可以将重点代码放在快速SRAM中以缩短中断等待时间,而将不经常使用的配置信息放在较慢的闪存中。
4、内存映射的I/O
分散加载描述可以将数据节准确放在内存映射中的某个地址,以便能够访问内存映射的外围设备。
5、位于固定位置的函数
可以将函数放在内存中的固定位置,即使已修改并重新编译周围的应用程序。
6、使用符号标识堆和堆栈
链接应用程序时,可以为堆和堆栈位置定义一些符合。
4、 分散加载文件语法
分散加载文件主要由一个加载时域和多个运行时域组成,其大致结构如图:
LOAD_ROM1 0x00000000 <-------------加载时域描述
{
EXEC_ROM1 0x00000000 <-------------运行时域(一个加载时域可包含一个或多个运行时域)
{
*.o(+RO)
}
SRAM 0x10000000
{
*(+RW,+ZI)
}
}
分散加载文件语法结构
4.1 加载时域的描述
加载时域语法格式如4.1所示,其每项含义解释如下:
load_region_name(base_address("+"offset))[attribute_list][max_size]
{
execution_region_description+
}
1. load_region_name:为本加载时域的名称,名称可以按照用户意愿自己定义,该名称只有前31个字符有意义;
2. base_designator:用来表示本加载时域的起始地址,可以有下面两种格式中的一种;
base_address:表示本加载时域中的对象在连接时的起始地址,地址必须是字对齐的;
+offset:表示本加载时域中的对象在连接时的起始地址是在前一个加载时域的结束地址后偏移量offset字节处。
本加载时域是第一个加载时域,则它的起始地址即为offset,offset的值必须能被4整除。
3. attribute_list:指定本加载时域内容的属性,包含以下几种,默认加载时域的属性是ABSOLUTE
ABSOLUTE:绝对地址;
PI:与位置无关;
RELOC:可重定位;
OVERLAY:覆盖;
NOCOMPRESS:不能进行压缩;
4. max_size:指定本加载时域的最大尺寸。如果本加载时域的实际尺寸超过了该值,连接器将报告错误,默认取值为0xFFFFFFFF;
5. execution_region_description:表示运行时域,后面有个+号,表示其可以有一个或者多个运行时域,关于运行时域的介绍看后面。
4.2 运行时域的描述
加载时域语法格式如程序清单4.2所示,
exec_region_name(base_address|"+"offset)[attribute_list][max_size|" "length]
{
input_section_description*
}
1. exec_region_name:为本加载时域的名称,名称可以按照用户意愿自己定义,该名称只有前31个字符有意义;
2. base_address:用来表示加载时域的起始地址,可以有下面两种格式中的一种;
base_address:表示本加载时域中的对象在连接时的起始地址,地址必须是字对齐的;
+offset:表示本加载时域中的对象在连接时的起始地址是在前一个加载时域的结束地址后偏移量offset字节处。offset的值必须能被4整除。
3. attribute_list:指定本加载时域内容的属性:
ABSOLUTE:绝对地址;
PI:与位置无关;
RELOC:可重定位;
OVERLAY:覆盖;
FIXED:固定地址。区加载地址和执行地址都是由基址指示符指定的,基址指示符必须是绝对基址,或者偏移为0.
ALIGNalignment:将执行区的对齐约束从4增加到alignment。alignment必须为2的正数幂。如果执行区具有base_address。则它必须为
alignment对齐。如果执行区具有offset,则连接器将计算的区基址与alignment边界对齐;
EMPTY:在执行区中保留一个给定长度的空白内存块,通常供堆或堆栈使用。
ZEROPAD:零初始化的阶段为0填充块写入ELF文件,因此,运行时无需使用0进行填充;
PADVALUE:定义任何填充的值。如果指定PADVALUE,则必须为其赋值;
NOCOMPRESS:不能进行压缩;
UNINIT:未初始化的数据;
4. max_size:指定本加载时域的最大尺寸。如果本加载时域的实际尺寸超过了该值,连接器将报告错误,默认取值为0xFFFFFFFF
5. length:如果指定的长度为负值,则将base_address作为区结束地址。它通常与EMPTY一起使用,以表示在内存中变小的堆栈。
6. input_section_description:指定输入段的内容。
4.3 输入段描述
输入段语法描述如程序清单
module_select_parrern["("input_section_selector(","input_section_selector)*")"]
("+"input_section_attr|input_section_pattern|input_symbol_pattern)
1. module_select_parrern:目标文件滤波器,支持使用通配符"*"与"?"。其中符合"*"代表零个或多个字符,符合"?"代表单个字符。进行
匹配时所有字符不区分大小写。当module_select_pattern与以下内容之一相匹配时,输入段将与模块选择器模组相匹配。
包含段和目标文件的名称;
库成员名称(不带前导路径名)
库的完整名称(包括路径名)。如果名称包含空格,则可以使用通配符简化搜索。例如,使用*libname.lib匹配C:\lib dir\libname.lib。
2. nput_section_attr:属性选择器与输入段属性相匹配。每个input_section_attr的前面有一个"+"号。如果指定一个模式以匹配输入段
名称,名称前面必须有一个"+"号。可以省略紧靠"+"前面的任何逗号。选择器不区分大小写。可以识别以下选择器:
RO-CODE;
RO-DATA;
RO,同时选择RO-CODE和RO-DATA;
RW-DATA;
RW-CODE;
RW,同时选择RW-CODE和RW-DATA;
ZI;
ENTRY:即包含ENTRY点的段。
可以识别以下同义词:
CODE表示RO-CODE;
CONST表示RO-DATA;
TEXT表示RO;
DATA表示RW;
BSS表示ZI;
可以识别以下伪属性:
FIRST;
LAST;
通过使用特殊模块选择器模式.ANY可以将输入段分配给执行区,而无需考虑其父模块。可以使用一个或多个.ANY模式以任意分配方式填充运行时域。大多数情况下,
使用单个.ANY等效于使用*模块选择器。
5. 分散加载应用实例
5.1 一个普通的分散加载配置
假设,一个Cortex-M3内核的LPC17xx微控制器有Flash、RAM的资源如下:
Flash基址:0x00000000,大小:256KByte
RAM基址:0x10000000,大小:32Kbyte
那么一个分散加载文件应该怎样描述呢?可参考如下:
LR_IROM1 0x00000000 0x00040000{ ;定义一个加载时域,域基址:0x0000000,域大小0x00040000,对应实际Flash的大小
ER_IROM 0x0000000 0x00040000{ ;定义一个运行时域,第一个运行时域必须和加载时域起始地址相同,否则库不能加载到该时域的错误,
;其域大小一般也和加载时域大小相同
*.o(RESET,+First) ;将RESET段最先加载到本域的起始地址外,即RESET的起始地址为0,RESET存储的是向量表
.ANY(+RO) ;加载所有匹配目标文件的只读属性数据,包含:Code、RW-Code、RO-Data。
}
RW_IRAM1 0x10000000 0x00008000{ ;定义一个运行时域,域基址:0x10000000,域大小为0x00008000,对应实际RAM大小
*(+RW +ZI) ;加载所有区匹配目标文件的RW-Data、ZI-Data;这里也可以使用.ANY代替*号
}
}
5.2 多块RAM的分散加载文件配置
还是上述的MCU,假设其增加了另一块RAM,其资源如下:
1、Flash基址:0x00000000,大小:256KByte
2、RAM1基址:0x10000000,大小:32Kbyte
3、RAM2基址:0x2007C000,大小:32Kbyte
如果我想将这两块连续的RAM都使用起来(可使用64Kb RAM)?分散加载文件应该怎样描述?
LR_IROM1 0x00000000 0x00040000{ ;定义一个加载时域,域基址:0x0000000,域大小0x00040000,对应实际Flash的大小
ER_IROM 0x0000000 0x00040000{ ;定义一个运行时域,第一个运行时域必须和加载时域起始地址相同,否则库不能加载到该时域的错误,
;其域大小一般也和加载时域大小相同
*.o(RESET,+First) ;将RESET段最先加载到本域的起始地址外,即RESET的起始地址为0,RESET存储的是向量表
.ANY(+RO) ;加载所有匹配目标文件的只读属性数据,包含:Code、RW-Code、RO-Data。
}
RW_IRAM1 0x10000000 0x00008000{ ;定义RAM1的运行时域,使用.ANY进行随意分配变量,这里不能使用*号代替,*表示匹配所有的目标文件,
.ANY(+RW +ZI) ;这样变量就无法分配到第二块RAM空间了
}
RW_IRAM2 0x2007C000 0x00008000{ ;定义RAM2的运行时域,使用.ANY进行随意分配变量,这里不能使用*号代替,*表示匹配所有的目标文件,
.ANY(+RW +ZI) ;这样变量就无法分配到第二块RAM空间了
}
;如果还有另外多的RAM块,在这里增加新的运行时域即可,格式和RAM2的定义相同
}
如上面所示,确实可以将两块RAM都使用起来,即有64KB的RAM可以使用,但其并不能完全等价于一个64KB的RAM,实际应用可能会碰到如下问题。
如我在main.c文件中声明了1个40KB的数值,
unsigned char GucTest0[40*1024]; /*定义一个40KB的数组*/
//unsigned char GucTest2[20*1024]; /*定义一个20KB的数组*/
//unsigned char GucTest3[20*1024]; /*定义一个20KB的数组*/
如上所示的程序在编译的时候会出现错误,并提示没有足够的空间,为什么?因为数组是一个整体,其内部元素的地址是连续的,不能分割的,但是
在两个不连续的32KB空间中,是没办法配出一个连续的40KB的地址空间,所以编译会提示空间不足,分配40KB数组失败。
还是上述程序,申请两个20KB的数组,编译结果会如何?
编译结果还是会提示空间不足,这是为什么?这里出错的原因其实和上面的原因是相同的,首先,重温一个,.ANY的作用,.ANY是一个通配符,当
其与以下内容之一相匹配时将进行选择。
1、包含段和目标文件的名称;
2、库成员名称(不带前导路径名)
3、库的完整名称(包括路径名)。如果名称包含空格,则可以使用通配符简化搜索。例如,使用*libname匹配C:\lib dir\libname.lib。
后面两个和本次讨论的话题无关,再仔细的看第一个匹配相为:包含段和目标文件,关于这段的解释参考"5.5段在分散加载文件中的应用"小节。
段这里先不用理会,应为这里没有用到段,所以只剩下目标文件。要注意是“目标文件”,不是其他,即是说一个C文件编译后,其所有的变量、代码都会
作为一个整体。所以定义两个20KB和定义了一个40KB,在编译器看来都是一样的,就是这个C文件总共定义了40KB空间,我要用40KB的空间来分配它,因此,
会出现同样的错误。
关于大数组分配的解决方法,有两种,分别是:
1、将数组分开在不同的C文件中定义,避免在同一个C文件定义的数据大小总量超过其中最大的分区。
2、将一个C数组,使用段定义,使其从该C文件中独立除了,这样编译器就不会将它们作为一个整体来划分空间了,示例如下:
#pragma arm section zidata = "SRAM" //在C文件中定义新的段
unsigned char GucTest1[20*1024]; //定义一个20KB的数组
#pragma arm section //恢复原有的段
unsigned char GucTest2[20*1024]; //定义第二个20KB数组,这20KB数组不会和GucTest1作为一个整体来划分空间
5.3 多块Flash的分散加载文件配置
再一下上述的MCU,假其增加多了一块Flash,不是RAM,其资源如下:
1、Flash1基址:0x00000000,大小:256KByte;
2、Flash2基址:0x20000000,大小:2048KByte;
3、RAM基址:0x10000000,大小:32Kbyte;
注意这里多增加的一块的不是RAM,而是Flash,其情况会如何呢?假设其相同,
LR_IROM1 0x00000000 0x00040000{ ;定义一个加载时域,域基址:0x0000000,域大小0x00040000,对应实际Flash的大小
ER_IROM 0x0000000 0x00040000{ ;定义Flash1运行时域
*.o(RESET,+First) ;先加载向量表
.ANY(+RO) ;随意分配只读数据
}
ER_IROM1 0x2000000 0x00200000{ ;定义Flash2运行时域
.ANY(+RO) ;随意分配只读数据
}
RW_IRAM2 0x10000000 0x00008000{ ;定义RAM1的运行时域,使用.ANY进行随意分配变量,这里不能使用*号代替,*表示匹配所有的目标文件,
.ANY(+RW +ZI) ;这样变量就无法分配到第二块RAM空间了
}
}