引子
某些单片机拥有多个ROM与RAM,例如STM32H750就有ITCM、DTCM、SRAM1~SRAM4等多块内存,并且他们的地址并不连续,同时某些外设也支持地址映射,例如QSPI,从而访问外部Flash的储存空间,如何管理这些地址成了一个问题。有时我们希望将某些代码加载到内存运行,以提升某些关键操作的速度。为了解决这些问题,就需要使用ARM编译器的分散加载(Scatter-loading)特性。在IAR中是由链接器配置文件(Linker configuration file(.icf))实现类似功能。
分散加载
利用分散加载特性可以控制链接器(Linker)将镜像(Image)的不同部分放入不同位置,具体而言,分散加载可以控制:
- 映射的存储空间(RAM、ROM、Flash)的位置和大小
- 某个/些函数或变量(包含标准库)的位置
- 未指定位置的代码/数据段的存储空间的优先级
- 空白区域的位置和大小(通常用于堆栈)
定义
数据类型
RO(Read-Only)数据
RO 数据是只读的数据,通常为代码或者常量。
XO(eXecute-Only)数据
XO是只可执行的数据,用于保护代码不被读出,通常保存在XOM中,在简单需求下一般不会出现XO数据,本文不会具体涉及这一部分。具体请见
eXecute-Only-Memory (XOM) – arm community
Execute-only memory – arm Developer
Building applications for execute-only memory – arm Developer。
RW(Read-Write)数据
RW数据为可读可写的数据,通常为全局有初值的变量。
ZI(Zero-Initialized)数据
ZI 是零初始化的数据,一般为没有初值而被默认初始化的全局变量。
加载域Load Region(LR)、运行域Execution Region(ER)
由于断电时RAM无法使用,因此RW数据等在断电时需要在ROM中储存,在上电后再加载到RAM中供程序使用,因此前后存在两个地址,一个是在ROM中的地址,一个是加载到RAM中程序真正使用的地址。这前后两个状态分别为加载域和运行域。
加载域可以认为是在单片机运行之前的状态,因此没有任何内容是保存在RAM中的,而是将数据保存在ROM中,但它依旧包含了程序运行所需要的所有信息(RO、XO、RW数据),由于ZI不实际填充数据,因此加载域中不保存ZI数据。
运行域可以认为是单片机单片机开始运行时的状态,通常它将加载域中的RW数据复制到RAM中,并对ZI数据进行默认初始化,整个过程如下图所示:
运行域的操作结束后,程序就可以开始运行了。
使用目标(Target)面板设置分散加载
若程序较为简单,可以使用目标(Target)面板(Project – Options for Target ‘xxx’ > Target)可视化设置(注意,需要在Project – Options for Target ‘xxx’ > Linker中勾选Use Memory Layout From Target Dialog才能使Target面板中的设置生效),如下图所示:
只读存储区域(Read/Only Memory Areas)控制ROM相关的设置,其中可以设置ROM的起始地址(Start)和大小(Size),选中默认(default)则数据没有指定位置时会存放到这些区域,启动位置(Startup)则指定程序最开头部分(中断向量表, 复位)的位置。
读写存储区域(Read/Write Memory Areas)控制RAM相关的设置,默认、起始地址、大小的意义与Read/Only Memory Areas相同,不初始化(NoInit)勾选后不进行默认初始化,即ZI区域不初始化为0。
控制文件、文件夹的分散加载
在一个文件或文件夹上右键,选择文件选项(Options for File)或组选项(Options for Group),在属性(Properties)下的存储分配(Memory Assginment)可以控制RO、RW、ZI数据运行域的位置,如下图所示:
其中Code/Const对应RO数据,Zero Initialized Data对应RW数据、Other Data对应RW数据,可以选择目标(Target)面板中设置的ROM或RAM,选择<default>则在默认ROM/RAM中。
RO数据的选项中包含了ROM和RAM的选择,也就是说可以直接在ROM中直接运行,也可以把代码加载到RAM中运行,需要注意,如果选择ROM,则这部分RO的加载域和运行域都是这个ROM,但是选择RAM则加载域时在默认ROM中,运行域再加载到选择的RAM中。
对于ZI数据和RW数据,只能选择某个RAM作为运行域的位置,加载域还是在默认ROM中。
改变了Memory Assignment的默认设置后,对应的文件/文件夹左下角会出现一个雪花(我猜的,我也不知道这是个啥标志)标志(修改属性(Properties)下的任何属性为非默认后都会出现这个标志)。
例子
例子:将代码加载到RAM中运行
问题:通常情况下,代码加载域和执行域的地址相同且都在ROM中,在程序运行时直接访问ROM取代码。但是由于ROM的访问速度比RAM慢,因此我们想将对速度要求高的关键代码加载到RAM中运行。
解决方案:将关键代码的文件/文件夹的Code/Const项选择一个RAM,即这些文件的RO数据的运行域就变为RAM了。需要注意,在系统上电后RO数据必须是可以访问的才可以加载到RAM中(例如片内Flash),对于片外的Flash,由于并没有初始化,访问片外Flash是无效的,因此需要在初始化后手动将RO数据加载到RAM中才可以使用。
例子:将数据下载到片外SPI Flash中
问题:需要将字库、资源等文件下载到片外SPI Flash中,并且提供了SPI Flash的下载算法。
解决方案:由于SPI Flash一般没有地址访问的方法,因此我们可以使用一个虚拟的未使用的地址,这个地址指示为了匹配对应的SPI Flash下载算法,并没有实际意义,例如,下图所示的SPI Flash下载算法的地址设置为9000 0000H – 90FF FFFFH(Project – Options for Target ‘xxx’ > Utilities 在这个面板下有Use Target Driver for Flash Programming下的Setting)。
在Target面板中添加对应的地址,地址范围需要在算法的地址算法之内。
将要下载的数据所在文件/文件夹的Code/Const选择为虚拟地址的ROM中,例如上图为ROM2。下载程序时则会将数据下载到Flash中,通常数据文件只需要下载一次,因此不需要下载这些RO数据时可以将文件设置为不包含在目标构建中(一个文件或文件夹上右键,选择文件选项(Options for File)或组选项(Options for Group),在属性(Properties)下取消勾选Include in Target Build)从而阻止这些数据参与构建,之后会在文件/文件夹的右下方出现一个红色警告标志。
注意,如果有多个RO数据,使用文件/文件夹属性并不能有效控制他们之间的实际顺序,若要指定他们的具体位置,请看Keil MDK(ARM编译器)分散加载特性(下):使用分散加载文件(.sct)控制。如何将文件转换为C语言中的数组不是本文内容,请看Keil MDK下载资源数据(还没写)。
例子:使用片外QSPI Flash运行代码
问题:QSPI拥有地址空间,在配置好后可以直接通过地址访问,并且速度快,常用于扩展ROM空间,因此片内储存空间不够时将代码储存在片外的QSPI Flash中,并直接在片外QSPI Flash中运行代码。
解决方案:将储存在片外QSPI Flash的文件/文件夹的Code/Const项选择片外QSPI Flash,即这些文件的RO数据的加载域和运行域就变为片外QSPI Flash了。需要注意,在QSPI初始化前访问QSPI地址是无效的,因此在QSPI初始化前不可以使用储存在片外QSPI Flash的RO数据(例如使用某个函数)。
尾声
在需求较为简单时,使用Keil MDK提供的GUI即可实现简单的分散加载。实际上,工程会根据这些GUI中的设置生成一个分散加载文件(.sct),而这个文件才直接控制链接器分散加载,如果要实现更为复杂在GUI中设置不了的功能,就需要直接编写这个分散加载文件,具体请看Keil MDK(ARM编译器)分散加载特性(下):使用分散加载文件(.sct)控制。