ARMC库.docx
《ARMC库.docx》由会员分享,可在线阅读,更多相关《ARMC库.docx(25页珍藏版)》请在冰豆网上搜索。
ARMC库
使用ARM标准C函数库进行嵌入式应用程序的开发
引言
随着对高处理能力、实时多任务、超低功耗等方面需求的增长,高端嵌入式处理器已经进入了国内开发人员的视野,并在国内得到了普遍的重视和应用。
ARM是目前嵌入式领域应用最广泛的RISC微处理器结构,凭借低成本、低功耗、高性能等优点占据了嵌入式系统应用领域的领先地位。
ADS是ARM公司推出的ARM集成开发环境,提供了对C和C++的支持,是目前开发ARM的主要工具。
本文针对日益缩短的嵌入式开发周期,结合ARM系统开发调试经验,对使用ARM标准库进行应用程序开发作了比较系统的分析。
1ARM标准库介绍
ADS提供了ANSIC和C++标准库,本文仅讨论ANSIC库,该库包含下面几个部分:
◇IS0C库标准所定义的函数;
◇在semlhosted环境下用来实现C库函数与目标相关的函数;
◇C和C++编译器要使用的heIper函数。
该库提供的诸如文件输入输出之类的设备,使用了标准的ARMsemihosted执行环境(semihosting是针对ARM目标机的一种机制,它能够根据应用程序代码的输入/输出请求,与运行有调度功能的主机通信,这种技术允许主机为通常没有输入和输出功能的目标硬件提供主机资源)。
ARMulator、Angel和Multi-lCE都支持这个环境,可以使用ADs中提供的开发工具开发应用程序,然后在ARMulator或者是开发板上运行和调试该程序。
如果要使应用系统独立于这个环境,则必须重新实现C库中依赖于这个环境的相关函数,根据用户系统的运行环境对C库进行适当的裁减。
使用ANSI标准C库进行程序开发,不仅可以提高开发效率而且可以增强程序的可移植性。
在程序中使用库函数,必须先建立一个库函数可以执行的环境,这些工作都由库中的函数完成。
当应用程序链接了C库中的函数时,C库中的函数将完成:
◇创建C程序所需的执行环境(建立栈,如果需要创建一个堆,初始化程序使用的部分库);
◇调用main()函数开始执行C程序;
◇支持程序使用的Is0定义的函数;
◇捕获运行时的错误和信号,如果需要,根据错误终止执行或程序退出。
2裁减ARM标准C函数库
标准库中包含了部分依赖于ARMsemihosted执行环境的函数,这部分函数的函数名中包含有单个或两个下划线“-”,需要重新实现这部分函数。
如果在程序中定义这些函数,则编译器就会使用新定义的函数,这个过程称为库函数的裁减。
一般情况下,只需要重新定义很少的几个函数就可以使用C库。
ARM应用系统开始执行用户应用程序,必须先将应用程序加载到执行域,建立应用程序的执行环境。
使用C库时,这些繁琐的工作就大部分由c函数来完成了。
汇编程序完成系统初始化后,跳转到C程序的人口_main()(注意:
不是main(),当C程序中定义了main()主函数时,编译器就会生成_main代码)。
由_main()引导库函数完成C执行环境的初始化,具体过程如下:
◇将非启动代码的RO和RW执行域代码从加载域地址复制到执行域地址;
◇将ZI域清零;
◇跳转到_rt_entry。
调用_main()将大大简化汇编启动代码的编写,汇编代码仅需完成系统硬件的初始化,而没有必要将代码从加载域地址复制到执行域地址,以及ZI域清零等工作。
特别是当使用分布式加载时_main()的作用就更加明显了。
但是_main()并没有建立C库运行必须的环境,这项工作由_rt_entry()完成,主要调用过程为:
◇调用_rt_stackheap_init()建立堆和栈;
◇调用_rt_lib_init()初始化引用的库函数;如果需要,建立main()函数的参数argc和argv等;
◇调用main()函数,执行应用程序,可以应用库函数;
◇用main()函数的返回值作参数调用exit()。
_rt_entry并不是C函数,它是用ARMC库编程的起始点。
_rt_entry不能用C语言宴现,因为这时候堆栈还没有建立,堆栈由_rt_stackheap_init()来建立。
上面简单介绍了C程序使用库函数时的调用过程,由_rt—stackheap_init()建立C库使用的内存模型--堆和栈。
因为ARM库是建立在semihosted执行环境的,它实现的内存模型是基于这个环境的,所以必须修改这个内存模型建立机制。
表1列出了需要重新实现的函数,实现了这些函数,应用程序就可以脱离宿主机环境独立运行了。
其中,必须重新实现的是_userinitial_stackheap(),因为默认的实现是基于semihosted执行环境的,该函数被_n_stackheap_init()调用创建内存模型,其他两个函数没有默认的实现。
实现该函数,必须满足下面的条件:
◇使用不超过96字节的栈空间;
◇除了R12(ip)外不要污染其他寄存器;
◇将堆基址、栈基址、堆边界和栈边界分别存在RO~R3作为返回参数;
◇堆必须保持8个字节对齐。
实现例程如下:
为了提高应用程序开发效率和可移植性,希望在目标系统上使用ARM库提供的标准输人输出库函数。
高层输入输出函数是不依赖于目标系统环境的,但是高层输入输出函数必须调用依赖于目标系统的底层函数,才能实现应用系统的输入输出。
依据目标系统硬件环境重新定义这些底层函数,就可以使用库提供的标准input/output库函数了。
下面以裁减ARM标准库提供的printf系列输出函数为例来作说明。
标准I/O库中最常用的是printf系列函数,包括_printf()、printf()、_fprintf()、fprintf()、vprintf()和vfprintf()。
所有这些函数非透明地使用_FILE,并且仅依赖于fputc()和ferror()两个函数。
函数_printf()和_fprintf()与printf()和fprintf()的区别仅在于前两个函数不能格式化浮点值。
只要定义了自己的_FILE版本和fputc()、ferror()函数,外加定义一个具有FILE类型的_stdout变量,就可以不作任何修改地使用printf系列、fwrite()、fputs()和puts()函数了。
下面给出了具体实现的模板,可以根据实际需要修改。
#include
struct__FILE
{
inthandle;
/*用户需要的任何代码(如果使用文件仅是为了调试使用prinft在标准输出端输出信息,则不需要任何文件处理代码)*/
};
FlLE_stdout;/*FILE在stdio.h中定义为:
typedefstruct_
FILEFILE;*/
intfputc(intch,FILE*f){
/*用户实现的fpute代码。
输出一个字符,可以根据需要实现*/
returnch;
}
intferror(FILE*f){
/*用户实现的ferror代码*/
returnEOF;
}
使用ADS1.2进行嵌入式软件开发
概述
嵌入式应用程序通常都是在样机环境下调试与开发的,这种环境与最终产品之间并不完全相同。
因此,在系统调试阶段就考虑应用程序在最终目标硬件中的运行情况是非常重要的。
本文旨在讨论如何将一个开发/调试环境下的嵌入式应用程序转移到最终独立运行的目标系统中去,并提到了ARMADS1.2开发工具包的一些功能特性及其在这个过程中所起到的作用。
使用ADS开发嵌入式程序时,需要着重考虑以下几个问题:
1.与硬件相关的C语言库函数的使用;
2.某些C语言库函数使用了调试环境中的资源,要把这些使用的资源重定向到目标系统中的硬件上来;
3.可执行映象文件的存储器映射必须根据目标硬件的存储器分布进行裁剪;
4.在主程序执行前,嵌入式应用程序必须先完成系统的初始化。
一个完整的初始化包括用户的启动执行代码和ADS中C库函数的初始化过程。
图1Semihosting的实现举例
图2C语言库函数结构
图3缺省的存储器映射
图4连接器布局规则
缺省的工程项目设置
刚开始一个嵌入式应用软件开发时,ADS用户可能并不完全清楚目标硬件的一些参数指标。
比如有关外设、存储器地址分布,甚至处理器类型等一些细节,可能还没有最终确定。
为了在所有这些细节全部就绪前就能进行软件开发,ADS工具有一套程序构建和调试的缺省设置。
了解这套缺省的工程项目设置方法,对于掌握最终的移植步骤非常有好处。
ADS1.2C语言函数库
Semihosting
在ADS的C语言函数库中,某些ANSIC的功能是由主机的调试环境来提供的,这套机制有一个专门术语叫Semihosting。
Semihosting通过一组软件中断(SWI)指令来实现。
如图1所示,当一个Semihosting软中断被执行时,调试系统先识别这个SWI请求,然后挂起正在运行的程序,调用Semihosting的服务,完成后再恢复原来的程序执行。
因此,主机执行的任务对于程序来说是透明的。
C语言库函数结构
从概念上来讲,C语言库函数可以被分成两部分,一是ANSIC语言规范本身的一部分,一是只受某一特定ANSIC层次支持的函数,如图2所示。
其中一些ANSIC的功能是由主机调试环境调用驱动程序级的函数完成的。
例如,ADS的库函数printf()把输出信息输出到调试器的控制台窗口,这个功能通过调用__sys_write()实现,__sys_write()执行了一个把字符串输出到主机控制台的Semihosting软中断服务程序。
缺省的存储器映射
如果用户在程序编译时没有指定映象的存储器映射分布,ADS将为生成的目标代码和数据分配一个缺省的存储器映射图,如图3所示。
目标印象被连接至地址0x8000,存储和执行区域都位于该地址开始的空间。
RO(只读)部分放在前面,接着是RW(读写)部分,最后是ZI(零初始化)部分。
在ZI部分之上紧跟着HEAP,所以HEAP的确切地址要在连接时才能确定。
STACK的基地址是在应用程序启动时由一个Semihosting操作提供。
这项Semihosting操作返回的地址值视不同调试环境而定:
ARMulator返回配置文件peripherals.ami中的设置值;缺省为0x08000000。
Multi-ICE返回的是调试器内部变量$top_of_memory的值;缺省为0x00080000。
连接器布局规则
连接器对代码和数据在存储器系统中的分配,遵循一套规则,如图4所示。
映象首先按照属性以RO-RW-ZI的次序进行排列,在同一种属性里面代码先于数据。
然后连接器将输入段根据名字的字母顺序进行排列,输入段的名字与汇编代码里面的块名字指示一致(在汇编程序中用AREA关键字)。
在输入段中,来自不同对象的代码和数据放置次序与在连接器命令行中指定的对象文件次序一致。
在需要灵活分配代码和数据放置位置的情况下,建议用户不要简单地依靠这些规则。
后面会介绍一种如何控制代码和数据布局的机制Scatterloading。
图5缺省的ADS初始化过程
图6C库函数重定向
图7scatter文件语法
图8分散加载的简单样例
启动应用程序
大多数嵌入式系统在进入应用主程序之前有一个初始化的过程,该过程完成系统的启动和初始化功能。
缺省的ADS初始化过程如图5所示。
总体上,初始化过程可以分成两部分来看:
_main负责设置运行映像存储器映射;
_rt_entry负责库函数的初始化。
_main完成代码和数据的复制,并把ZI数据区清零。
这一步只有当代码和数据区在存储和运行时处于不同的存储器位置时才有意义。
接着_main跳进_rt_entry,进行STACK和HEAP等的初始化。
最后_rt_entry跳进应用程序的入口main()。
当应用程序执行完时,_rt_entry又将控制权交还给调试器。
函数main()在ADS中有特殊的意义。
当一个程序工程项目中存在main()时,连接器会把_main和_rt_entry中的初始化代码连接进来;如果没有main()函数,初始化过程就不会被连接,结果就会导致一些标准的C库函数无效。
根据目标环境裁减C库函数
缺省状态下C库函数利用Semihotsting机制来实现设备驱动的功能。
但一个真正的嵌入式系统,要使用到具体的外设或硬件独立于主机环境运行。
C库函数重定向
用户可以定义自己的C语言库函数,连接器在连接时自动使用这些新的功能函数。
这个过程叫做重定向C语言库函数,如图6所示。
举例来说,用户有一个I/O设备(如UART)。
本来库函数fputc()是把字符输出到调试器控制窗口中去的,但用户把输出设备改成了UART端口,这样一来,所有基于fputc()函数的printf()系列函数输出都被重定向到UART端口上去了。
下面是实现fputc()重定向的一个例子:
externvoidsendchar(char*ch);
intfputc(intch,FILE*f)
{/*e.g.writeacharactertoanUART*/
chartempch=ch;
sendchar(&tempch);
returnch;
}
这个例子简单地将输入字符重新定向到另一个函数sendchar(),sendchar()假定是一个另外定义的串口输出函数。
在这里,fputc()就好像目标硬件和标准C库函数之间的一个抽象层。
在C语言库函数中禁用Semihosting
在一个独立的嵌入式应用程序中,应该不存在SemihostingSWI操作。
因此,用户必须确定在所有调用到的库函数中没有使用Semihosting。
为了保证这一点,在程序中可以引进一个符号关键字_use_no_semihosting:
在C代码中,使用#prgrama#pragmaimport〈_use_no_semihosting_swi〉
在汇编程序中,使用IMPORT
IMPORT_use_no_semihosting_swi
这样,当有使用SWI机制的库函数被连接时,连接器会进行报错:
Error:
Symbol_semihosting_swi_guardmultiplydefined
为了确定具体是哪一个函数,连接时打开-verbose选项。
这样在结果信息输出时,该库函数上将有一个_I_use_semihosting_swi的标记。
Loadingmembersys_wxit.ofromc_a_un.1.
Definition:
_sys_exit
Reference:
_I_use_semihosting_swi
用户必须要把这些函数定义成自己的执行内容。
有一点需要注意,连接器只能报告库函数中被调用的Semihosting,对用户自定义函数中使用的Semihosting则不会报错。
根据目标硬件定制存储器映射
分散装载(Scatlerloading)
在实际的嵌入式系统中,ADS提供的缺省存储器映射是不能满足要求的。
用户的目标硬件通常有多个存储器设备位于不同的位置,并且这些存储器设备在程序装载和运行时可能还有不同的配置。
Scattertoading可以通过一个文本文件来指定一段代码或数据在加载和运行时在存储器中的不同位置。
这个文本文件scatterfile在命令行中由-scatter开关指定,例如:
armlink_scatterscat.scffilel.ofile2.0
在scatterfile中可以为每一个代码或数据区在装载和执行时指定不同的存储区域地址,Scatlertoading的存储区块可以分成二种类型:
装载区:
当系统启动或加载时应用程序的存放区。
执行区:
系统启动后,应用程序进行执行和数据访问的存储器区域,系统在实时运行时可以有一个或多个执行块。
映像中所有的代码和数据都有一个装载地址和运行地址(二者可能相同也可能不同,视具体情况而定)。
在系统启动时,C函数库中的__main初始化代码会执行必要的复制及清零操作,使应用程序的相应代码和数据段从装载状态转入执行状态。
1.scatter文件语法
scatter文件是一个简单的文本文件,包含一些简单的语法。
My_Region0x00000x1000
{
thecontextofregion
}
每个块由一个头标题开始定义,头中至少包含块的名字和起始地址,另外还有最大长度和其他一些属性选项。
块定义的内容包括在紧接的一对花括号内,依赖于具体的系统情况。
一个加载块必须至少含有一个执行块;实践中通常有多个执行块。
一个执行块必须至少含有一个代码或数据段;这些通常来自源文件或库函数等的目标文件;通配符号*可以匹配指定属性项中所有没有在文件中定义的余下部分。
2.简单分散加载样例
图8所示样例中,只有一个加载块,包含了所有的代码和数据,起始地址为0。
这个加载块一共对应两个执行块。
一个包含所有的RO代码和数据,执行地址与装载地址相同;同时另一个起始地址为0x10000的执行块,包含所有的RW和ZI数据。
这样当系统开始启动时,从第一个执行块开始运行(执行地址等于装载地址),在执行过程中,有一段初始化代码会把装载块中的一部分代码转移到另外的执行块中。
下面是这个scatter描述文件,该文件描述了上述存储器映射方式。
LOAD_ROM0x4000
{
EXE_ROM0x00000x4000;Rootregion
{
*〈+RO〉;Allcodeandconstantdata
}
RAM0x100000x8000
{
*〈+RW,+ZI〉;Allnon-constantdata
}
}
3.在分散文件中放置对象
在大多数应用中,并不是像前例那样,简单地把所有属性都放在一起,用户需要控制特定代码和数据段的放置位置。
这可以通过在scatter文件中对单个目标文件进行定义实现,而不是只简单地依靠通配符。
为了覆盖标准的连接器布局规则,我们可以使用+FIRST和+LAST分散加载指令。
典型的例子是在执行块的开始处放置中断向量表格:
LOAD_ROM0x00000x4000
{
EXEC_ROM0x00000x4000
{
vectors.o〈Vect,+FIRST〉
*〈+RO〉
}
;moreexecregions...
}
在这个scatter文件中,保证了vextors.o中的Vect域被放置于地址0x0000。
4.RootRegion(根区)
根区是一个执行块,它的加载地址与执行地址是一致的。
每个scatter文件至少有一个根区。
分散加载有一个限制:
创建执行块的代码和数据(即完成复制和清零的代码和数据)无法自行复制到另一个位置。
因此,在根区中必须含有下面的部分:
_main.o,包含复制代码/数据的代码;
连接器输出变量$$Table和ZISection$$Table,包含被复制代码/数据的地址。
由于上面两个部分的属性是只读的,因此他们被*〈+RO〉通配符语法匹配。
如果*〈+RO〉被用在了非根区中,则在根区中必须显式地指明另一个RO区域。
下面是一个例子:
LOAD_ROM0x00000x4000
{
EXE_ROM0x00000x4000;rootregion
{
_main.o〈+RO〉;copyingcode
*〈Region$$Tabl0e〉;RO/RWaddressestocopy
*〈ZISection$$Table〉;ZIaddressestozero
}
RAM0x100000x8000
{
*〈+RO〉;allotherROsections
*〈+RW,+ZI〉;allRWandZIsections
}
}
放置堆栈和heap
Scatterloading机制提供了一种指定代码和静态数据布局的方法。
下面介绍如何放置应用程序的堆栈和heap。
*_user_initial_stackheap重定向
应用程序的堆栈和heap是在C库函数初始化过程中建立起来的。
可以通过重定向对应的子程序来改变堆栈和heap的位置,在ADS的库函数中,即_user_initial_stackheap()函数。
_user_initial_stackheap()可以用C或汇编来实现,它必须返回如下参数:
r0:
heap基地址;
r1:
堆栈基地址;
r2:
heap长度限制值(需要的话);
r3:
堆栈长度限制值。
当用户使用分散装载功能的时候,必须重调用_user_initial_stackheap(),否则连接器会报错:
Error:
L6218E:
UndefinedsymbolImage$$ZI$$Limit(referredfromsys_stackheap.o)
*存储器模型
ADS提供了两种实时存储器模型。
缺省时为one-region,应用程序的堆栈和heap位于同一个存储器区块,使用的时候相向生长,当在heap区分配一块存储器空间时需要检查堆栈指针。
另一种情况是堆栈和heap使用两块独立的存储器区域。
对于速度特别快的RAM,可选择只用来作堆栈使用。
为了使用这种two-region模型,用户需要导入符号use_two_region_memory,heap使用需要检查heap的长度限制值。
对这两种模型来说,缺省情况下对堆栈的生长都不进行检查。
用户可以在程序编译时使用-apcs/swst编译器选项来进行软件堆栈检查。
如果使用two-region模型,必须得在执行_user_initial_stackheap时指定一个堆栈限制值。
图9重定向_user_initial_stackheap()
图10基本初始化过程
图11ROM/RAM重定向和映射
表1
系统复位和初始化
目前情况,一般假设程序从C库函数的初始化入口_main开始执行。
实际上,所有的嵌入式程序在启动时都要执行一些系统级的初始化操作。
在此讨论这方面的内容。
初始化过程
图10中显示了一个基于ARM的嵌入式系统的基本初始化过程。
可以看到,在_main之前加入了一个复位处理模块resethandler,它在系统上电复位时立即启动。
标识为$sub$$main的新代码块在进入主程序之前执行。
复位处理模块resethandler通常是一小段汇编代码,在系统复位时执行。
它至少完成应用程序中使用到的所有处理器模式的堆栈初始化工作。
对于含有本地存储器系统的内核(比如含cache的ARM内核),配置工作也必须在这一段初始化过程中完成。
当完成系统初始化之后,通常程序会跳向_main,开始C库函数的初始化过程。
系统初始化过程一般还包括另外一些内容,中断使能等,这些大多安排在C库函数的初始化完成之后执行。
$sub$$main()完成这部分功能。
向量表(vectortable)
所有的ARM系统都有一张中断向量表当出现异常需要处理时,必须调用向量表。
向量表一般要位于0地址处。
表2
表3
表4
表5
表6
表7
表8
表9
表10
存储器配置
*ROM/RAM重定向
当系统启动的时候,为了保证0地址处有正确的启动代码存在,需要非易失性的存储器。
一种简单的方法,就是把系统0x0000开始的一块地址分配给ROM。
其缺点是,由于ROM的访问速度比RAM慢很多,当执行中断响应需要从中断向量表跳转时,会给系统性能带来损失;同时,在