(赛诺微医疗科技(浙江)有限公司 北京分公司,北京 100012)
在现代计算机系统中,操作系统使用内存管理单元(MMU)为每个进程提供虚拟地址空间,MMU负责虚拟地址到物理地址的转换。物理地址对应真实内存,所有进程共享物理内存。进程在设计时都假定其是系统中唯一的代码,可以使用任何地址空间。出于对功耗和面积的考虑,嵌入式系统中多数微控制器(MCU)并没有MMU单元,例如基于ARM Cortex-M架构的系列处理器。
使用μCLinux可以在不具有MMU的MCU中动态加载程序。这引出一个问题:为什么不推荐μCLinux?首先,μCLinux对RAM的需求在兆字节级别,这对于许多小型系统来说过于庞大。此外,μCLinux需要一台Linux计算机来编译程序。最重要的一点是,已有的MCU程序要运行于μCLinux下,需要重写代码并更换开发环境。所以考虑到工作量及硬件成本,μCLinux并不是最佳选择。
本文讨论一种无需MMU即可实现系统运行时动态地添加、更新和删除应用程序的方法。本方法在工程实践中取得了良好的效果。
为便于阐述,给定如下硬件平台及软件开发环境。
假设有如下硬件系统:处理器基于Cortex-M内核且没有MMU单元;处理器通过扩展总线与外部SRAM相连;通过SDIO接口与外部SD卡相连;通过SPI总线与外部SPI-Flash相连。硬件平台框架示意图如图1所示。
图1 硬件平台框架示意图
软件开发使用IAR Embedded Workbench for ARM Version:6.30。
假设有5个可以正常工作于前述硬件平台的独立应用程序,这些程序使用IAR开发,其名称分别为app1~app5。现在,把app1装入SPI-FLASHA,把app2装入SPI-FLASHB,把app3~app5装入SD卡(基于FAT文件系统)。
设计一个程序管理器,可以按需动态装载上述任意应用程序并执行。应用程序执行完毕或用户中断应用程序后,系统控制权可以返回程序管理器,以便后续加载应用程序。
程序管理器与程序加载器都可以称为loader,但两者的工作机制有很大不同。一般来说,loader的工作主要是初始化硬件并加载目标程序,例如,Cortex-A系列MPU通过前级loader初始化外部SDRAM并加载uboot,进而由uboot加载Linux。这里的loader和uboot都可以看作程序加载器,两者的共同点也很明显,即加载目标程序后,目标程序即开始运行,控制权交给目标程序。除非系统复位,否则,控制权永远不会返回到loader。
相比而言,程序管理器除了加载目标文件并引导其运行外,还必须具有控制权接管和多次加载目标文件的能力。这一点,有点操作系统任务调度管理的意味。
基于ARM Cortex-M系列的处理器,解决应用程序加载问题的一种常见方法是在编译时将固定地址分配给应用程序。例如,应用程序A加载于地址0x2 1000处,应用程序B加载于地址0x2 2000处。这类处理方法有两个明显的不足:应用程序的扩展规模受限;系统运行时不能动态地添加、更新和删除应用程序。预先分配地址技术无法实现应用程序的动态管理。
针对不具有MMU单元的MCU,动态管理应用程序可以使用以下几种方法:在程序管理器中实现ELF分析器;使用可重定位代码编译,使用程序管理器转化二进制程序的内存位置;使用位置无关代码(PIC)进行编译,并在载入应用程序时调整程序管理器的全局偏移寄存器。
使用ELF语法分析器的缺点是:程序管理器需要比其它方法进行更多的处理,意味着程序管理器将占用大量的程序存储空间。
使用可重定位代码和内存位置转化技术比构建ELF解析器简单,并且性能比位置无关的代码略好。
位置无关代码是一个很好的解决方案,但由于使用全局偏移的间接层而略微降低了性能。由于ARM指令集针对PIC操作进行了优化,大多数代码的运行开销可低至几乎没有额外运行成本。
本文所实现的动态程序管理方法基于位置无关代码(PIC)技术,PIC技术允许将代码放在任何地址。在PIC代码中,所有分支和跳转目标都基于PC相对偏移;所有对数据部分的引用都通过全局偏移寄存器(GOR)进行间接寻址。
本文所设计的程序管理器(loader)使用PIC技术来加载多个应用程序。程序管理器根据SRAM的真实地址在加载应用程序时更新全局偏移寄存器GOR中的基地址。GOR本身是一个寄存器,其值在切换至应用程序代码之前由程序管理器设置。
应用程序动态管理,需要结合处理器的架构特点并充分利用编译器的多种机制才能实现。针对本文的硬件平台及开发环境,对系统资源做如下划分:loader程序的存储位置及运行位置均位于MCU片内Flash;应用程序存储于外部存储器,运行于外部SRAM;全局栈区及loader程序所使用的内存变量位于MCU片内RAM;应用程序使用的内存变量位于外部SRAM。
4.2.1 应用程序设计
结合Cortex-M系列MCU的架构特点,配合IAR的相关机制,在应用程序设计中完成以下工作:
(1)源代码修改
① 用汇编语言编写一个名为ropi_rwpi_header.s的模块,并将其加入应用程序工程文件。该模块主要内容如代码段1所示,用于记录应用程序的关键信息,如ROM起始地址,RO、RW容量,程序入口偏移等。这些信息由汇编文件指导IAR编译器自动产生。
【代码段1】
…
DATA
ropi_rwpi_header:
DC32ROM_address ; 编译时ROM 地址,定义于链接脚本icf文件中
DC32ROPI$$Length; 包含本头模块的RO字节数
DC32RWPI$$Length; 程序RW字节数
DC32main - . - 4; 代码起始至main函数的偏移
END
② 修改应用程序的启动文件。在启动文件头部引入main符号,并将NVIC向量表的第二项,即系统上电入口地址修改为main,如代码段2所示。
【代码段2】
…
SECTION.intvec:CODE:ROOT(2)
EXTERNmain
PUBLIC __vector_table
DATA
__intial_spEQU0x20000400
__vector_table
DCD __intial_sp
DCDmain
…
③ 在应用程序main函数中新增三个函数调用语句。即SystemInit、__iar_data_init3和nvic_update。其中,SystemInit是CMSIS内建函数,用于设置处理器时钟系统;__iar_data_init3是IAR内建函数,用于运行时数据初始化;nvic_update需要自行编写,用于更新向量表入口并设置处理器向量表偏移寄存器VTOR。nvic_update依据loader在加载应用程序时传入的程序基地址,将NVIC向量表中的地址增加相应的偏移。需要注意的是,NVIC中的第一个32位数据是编译时的栈顶,该数据无需任何操作,忽略即可。
(2)编译器设置及链接脚本修改
① 在IAR工程设置C/C++ Compiler选项的Code页面中,依照图2完成设置,指示编译器产生PIC代码。
图2 指示IAR编译器生成PIC代码
② 在Linker选项的Library页面中,依照图3所示将入口符号设置为ropi_rwpi_header。
图3 修改程序入口符号
③ 在Linker选项的Config页面中,依照图4示例将.intvec start及ROM地址设置为0x00。其它参数对PIC代码没有意义,忽略即可。本步骤也可在icf文件中将__ICFEDIT_region_ROM_start__和__ICFEDIT_intvec_start__定义为0x00实现,效果相同。
图4 修改起始地址
④ 参照代码段3的示例内容修改IAR应用工程的icf链接脚本,强制编译器在目标文件头部加入ropi_rwpi_header模块。
【代码段3】
…
define exported symbol ROM_address = __ICFEDIT_region_ROM_start__;
…
define block RO with alignment = 4, fixed order{
ro object ropi_rwpi_header.o,
ro section .intvec,
ro, ro data
};
"ROM":
place in ROM_region{
block RO };
define movable block RW with alignment = 8, fixed order, static base{
rw,
block HEAP
};
"RAM":
place in RAM_region { block RW };
原有应用程序工程完成上述步骤后,编译即得到可动态管理的应用程序。从上述步骤可以看出,本方法不需要对已有代码进行任何重写,只需简单的处理即可。
4.2.2 Loader程序设计
基于前述资源划分,结合Cortex-M系列MCU的架构特点,配合IAR的相关机制,需要在loader程序设计中完成以下工作:
(1)源代码修改
① 在loader程序的启动文件中增加外部SRAM初始化函数,完成外部SRAM初始化;
② 加载应用程序时,从SPI-Flash或SD卡读入目标应用程序。例如从SD卡读入目标文件可使用fopen类函数和“rb”参数完成;
③ 解析目标文件的头部信息区,得到目标文件的入口偏移、目标程序大小及全局变量容量等信息;
④ 依据头部信息区代码容量分配程序基址。此步骤有两种方法:动态管理和静态管理。动态管理是依据目标程序容量申请堆空间,并将外部存储器中的应用程序读入堆起始地址;静态管理是人为将外部SRAM划分成程序区及数据区。静态管理的好处是,避免了多次加载不同应用程序时造成的内存碎片;
⑤ 依据头部信息区数据容量分配数据基址。此步骤有两种方法:动态管理和静态管理。动态管理是依据目标程序全局变量容量申请堆空间,并将配分的起始地址写入GOR;静态管理则是将人为划分的数据区起始地址写入GOR。静态管理的好处是避免了多次加载不同应用程序时造成的内存碎片;
⑥ 将步骤④ 得到的起始地址加上头信息区的偏移值后赋值给函数指针;
⑦ 通过固定内存区域或以函数参数的方式将起始地址传入对应函数,以便应用程序修改NVIC向量偏移及设置VTOR;
⑧ 调用函数指针。
至此,应用程序开始运行。如果应用程序设计是可返回的或可通过命令终止的,则当应用程序结束后系统控制权将自动返还至loader,以便系统进行后续程序管理。
上述步骤的代码举例请参阅代码段4,内容仅供参考。
【代码段4】
…
typedef int function(void);
typedef function * function_p;
…
struct ropi_rwpi_header_layout{
uint ROM_address;
size_t code_bytes;
size_t data_bytes;
size_t start_offset;
};
…
//数据基址寄存器
static __no_init char* rwpi_data @ R9
…
//程序执行函数
void execute(char * program_image){
int status = 0;
struct ropi_rwpi_header_layout ropi_rwpi_header;
…
//从SD卡读取应用程序文件
FILE* f = fopen(program_image, "rb");
fread((char*) &ropi_rwpi_header, 1, sizeof(ropi_rwpi_header), f);
//为PIC代码动态申请空间并拷贝代码
unsigned char* ropi_code = malloc(ropi_rwpi_header.code_bytes - sizeof(ropi_rwpi_header));
fread(ropi_code, 1, ropi_rwpi_header.code_bytes- sizeof(ropi_rwpi_header), f);
fclose(f);
//动态申请RAM并写入基址寄存器
rwpi_data = malloc(ropi_rwpi_header.data_bytes);
function_p application = (function_p)(ropi_code + ropi_rwpi_header.start_offset);
status = application(ropi_code); // 应用程序执行完毕或被用户终止返回
…
free(rwpi_data);
free(ropi_code);
…
}
(2)编译器设置及链接脚本修改
修改loader程序的链接器脚本文件,将堆区置于外部SRAM。
依据上述方法,可将一个普通loader程序转换为动态程序管理器loader。将编译后的loader下载至MCU片内程序存储器,将编译得到的应用程序写入SPI-Flash或SD卡。启动系统,即可按需动态加载、删除或更新应用程序。
参考文献
[1] 唐思超.嵌入式系统软件设计实战--基于IAR Embedded Workbench[M].北京:北京航空航天大学出版社,2010.
[2] Arm.Cortex-M3 Technical Reference Manual[EB/OL].[2018-02].www.arm.com.
[3] IAR Systems.ARM IAR Assembler Reference Guide[EB/OL].[2018-02].www.iar.com.
[4] IAR Systems.IAR Development Guide-Compiling and Linking[EB/OL].[2018-02].www.iar.com.