由于当前嵌入式设计对处理器的快速处理能力和低功耗等方面的要求越来越高,因此高端嵌入式处理器得到了更多的重视和应用。ARM是一种RISC结构微处理器,与其他类型的处理器相比,具有低成本、低功耗、高性能等优点,所以在嵌入式领域的应用中处于领先地位。嵌入式系统离不开嵌入式应用程序,嵌入式应用程序开发的最终目的,是要将程序运行于独立的目标系统。基于ARM的嵌入式开发同样也是先使用开发工具在宿主机上开发程序,然后再将程序移植到目标硬件环境中运行;但随着嵌入式系统复杂度的增加,代码从开发环境到实际运行环境的移植工作变得越来越困难,往往会出现开发环境下运行正常的代码移植到目标硬件上无法工作的情况。本文针对上述问题,讲述在ADS1.2开发环境下使用ARM标准C库函数的应用程序上电执行过程,以及嵌入式系统脱离宿主机实现系统独立运行的方法。
1程序的上电执行过程
研究程序上电执行过程前有必要先了解ADS提供的运行时库。ADS提供了ANSI C和C++两种运行时库,由于C++运行时库中函数的执行不依赖于运行环境,因此只讨论与运行环境相关的ANSI C库。该库包含3个部分:
◆ ISO C库标准所定义的函数;
◆ 在semihosting环境下运行的ARM C库函数;
◆ C和C++编译器要使用的helper函数。
ISO C库标准所定义的函数与C和C++编译器使用的helper函数的执行效果都不依赖运行环境。这里要特别强调的是,在semihosting环境下运行的ARM C库函数是与运行环境相关的。semihosting环境是一种调试手段,它的原理就是利用MultiICE等工具捕捉目标环境运行过程中产生的值为0x123456的SWI中断,然后向上位机的调试软件AXD发送对应的调试信息。最终在目标板上运行的代码是无法提供semihosting环境的,所以在semihosting调试环境下运行的代码在下载到目标板脱离宿主机运行之前,需要对一些在semihosting环境下运行的库函数重定向。要重新实现semihosting环境下用到的库函数首先要了解ARM调用库函数的上电执行过程。
使用ARM标准C库的程序上电执行过程如图1所示。
图1使用ARM C库的程序上电执行过程
① 启动程序(start.s)完成系统硬件初始化后,调用系统库函数__main(ADS系统函数)完成以下工作:
◆ 将非启动代码的RO和RW执行域代码从加载域地址复制到执行域地址;
◆ 将ZI域清零;
◆ 跳转到__rt_entry。
② 程序跳转到__rt_entry后进行以下工作:
◆ 调用__rt_stackheap_init()(ADS系统函数)建立堆和栈;
◆ 执行_rt_stackheap_init()中会调用_user_initial_stackheap ()取用户设定的堆和栈的基地址;
◆ 建立堆和栈后调用_rt_lib_init()初始化引用的C库函数;
◆ C库函数初始化后调用用户的main()函数,执行含有库函数的应用程序。
③ 执行完用户代码,退出应用程序。
其中__main的代码搬移功能,用户可以在启动程序(start.s)里自己实现,然后调用__rt_entry进入后面的初始化过程。但是,实际工程中一般不这样实现,大多数工程师会直接调用__main实现代码的自动搬移。
在程序调用函数_rt_lib_init()时,系统会初始化用到的库函数,其中就包括semihosting环境下的某些函数,对这些函数的重定向是解决脱机问题的关键。此外,还需要定义符合实际硬件条件的内存模型。
2如何在目标板上独立运行
为了实现应用程序独立在目标板上运行,必须对ARM C库部分函数进行裁剪。操作过程可以分为两步:
① 定义自己的内存模型。缺省的内存模型是基于semihosting环境的,定义自己的内存模型需要重新实现函数_user initial_stackheap()。实现方式可以采用汇编方式或者C方式,实现方法包括单区模型和双区模型。单区模型是heap和stack共用一块存储区。该存储区大的顶端地址作为stack基地址,向下生长;小的底端地址作为heap基地址,向上生长。双区模型是heap和stack分别开辟存储区。用作heap的存储区的小的底端地址作为heap基地址,向上生长;用作stack的存储区的大的顶端地址作为stack基地址,向下生长。
② 重定向依赖于目标硬件的ANSI C库函数。重定向的方法就是用户自己实现调用到的库函数的功能。如果用户没有重新实现这些库函数,则程序在semihosting环境中可以执行正常,下载目标板独立运行时程序将无法返回或者执行错误。这些经常会用到的需要重定向的函数罗列如下:clock()、_clock_init()、 time()、 remove()、 rename()、 system()、 getenv()、 _getenv_init()。其中,clock()和_clock_init()两个函数必须同时被重新定义。
其他一些库函数(如对heap的一系列初始化函数)用它缺省实现就可以,当然用户也可以自己实现;另外一些依赖于semihosting环境的库函数(如输入输出函数printf和scanf),用户用到时必须重新实现,没用到时不必实现。
在使用标准库函数开发程序过程中,用户是无法判断哪些函数是运行在semihosting环境下的函数。下面介绍一种方法检测用户程序中是否应用了基于semihosting环境的库函数。
在C语言模块中,用#pragma import〈__use_no_semihosting_swi〉语句或在汇编模块中用IMPORT __use_no_semihosting_swi语句,并且在命令行中加入verbose选项。这样,当有使用依赖于semihosting环境的库函数被连接时,连接器会进行报错:
Error:Symbol_semihosting_swi_guardmultiplydefined
在结果信息输出时,该库函数上将有一个_I_use_semihosting_swi的标记。带有这个标记的函数都是执行过程中使用semihosting环境的函数,用户必须要把这些函数定义成自己的执行内容。
3总结
本文通过嵌入式系统调用ARM标准C库函数的上电执行过程,分析了嵌入式应用程序的开发环境和实际运行环境差异,从而找到了实际环境下程序会运行不正常的原因。此外,文中还讲述了嵌入式系统脱离宿主机实现系统独立运行的方法,以及如何判断应用程序是否使用了基于semihosting环境库函数的方法。对嵌入式系统开发,特别是由开发环境到实际硬件环境的代码移植工作具有一定的借鉴意义。