由于嵌入式系统是一个受资源限制的系统,故而直接在嵌入式系统硬件上进行编程显然是不合理的。在嵌入式系统的开发过程中,一般采取交叉平台的开发方法。即在通用PC上编程,然后通过交叉编译和链接,将源程序编成能够在目标平台上运行的二进制代码格式映像。最后将映像下裁到目标平台上的特定位置,由目标板上启动代码(Bootloader)执行这段二行制代码,从而运行起嵌入式系统。
在嵌入系统的开发中,建立起交叉编译环境的机器称为主机(Host),需要运行嵌入式系统的机器环境称为目标板(Target)。如(图1)所示,嵌入式软件的开发和调试过程就通过多种连接方式在两者的互相合作之中完成。
在主机端的开发软件主要包括交叉编译器(compiler)、链接器(linker)和源码级调试工具(debuger)。目标板上开发工具通常会包括动态装载器(loader)、监控诊断软件以及调试代理工具(debug agent)。各种连接方式用于从主机向目标板下载目标程序的映像文件以及调试器和目标板调试代理之间的调试信息。目标程序是由实时操作系统(RTOS)各部件、微内核和应用程序一同编择链接而成的可执行映像。一般,程序员在进行本机主应用开发时无须了解可执行程序是如何被装载入内存中运行又是如何将执行控制权交予此可执行程序的。而嵌入式系统的开发人员在做交叉平台的开发时,需要对目标平台有完全的认识和理解。他必须知道程序的映像文件是如何存储在目标嵌入式系统上的,当系统启动时是如何加载程序映像以及被加载到哪个位置,以及如何与目标系统交互进行调试开发。这些都将直接影响如何进行代码的编写、编译和链接的每个过程。
图1 嵌入式系统开发环境
存在操作系统的情况下,嵌入式系统开发主要分如下几个步骤:建立交叉开发环境,交叉编译和连接,加载映像,联机调试。
1、建立开发环境
在准备开发之前,需要了解在嵌入式编程中使用的交叉开发环境(Cross-development Environment)。交叉开发环境的原理很简单,只是在主机和目标机器体系结构不同的情况下,在主机上开发那些将在目标机器上运行的程序。比如说在x86上开发ARM 目标上运行的程序,就是在x86运行可以将程序编译、连接成ARM可运行代码的编译连接器,并以之编译在x86上编写的代码,就是一种交叉环境开发。
按照发布的形式,交叉开发环境主要分为开放和商用两种类型。开放式交叉开放环境实例主要有gcc,它可以支持多种交叉平台的编译器,由GNU负责维护,使用gcc作为交叉开发平台要遵守General Public License的规定。商用的交叉开发环境主要有Metrowerks CodeWarrior、ARM Software Development Toolkit、SDS Cross compiler、WindRiver Tomado等等。
按照使用方式,交叉开发工具主要分为使用Makefile和IDE开发环境两种类型。使用Makefile的开发环境需要编写Makefile来管理和控制项目的开发,可以自己手写,有时候也可以使用一些自动化的工具。这种开发工具是gcc、SDS Cross Compiler等等。新类型的开发环境一般有一个用户友好的IDE界面,方便管理和控制项目的开发,如Code Warrior抢救无效。有些开发环境既可以用Makefile管理项目,又可以使用IDE,如TomadoⅡ,给使用者很大的余地。
建立交叉开发环境是进行嵌入式系统软件开发的第一步。这里以gcc为例子,如果是在x86的Linux平台上建立一个面向ARM的开发平台,大概的步骤如下所不:
■从http://www.gnu.org/下载需要的文件包,包括binutils、gcc、gilbc、gdb等:
■按顺序对编译器、连接器和函数库进行编译和安装,在配置时需要使用-target=arm-linux选项指定开发环境的目标平台;
在安装结束之后应该有arm-linux-gcc,arm-linux-ar等程序,这些就是交叉开发平台的程序。
2、编译和连接
使用建立好的交叉开发环境完成编译和连接工作,将用户编写的源文件生成可在目标系统上执行的映像文件。程序员使用C/C++源文件和头文件记录程序源代码,部分与处理器相关的代码则会使用相应的汇编代码形式编写。用户编写Makefilr文件用于make工具以方便跟踪源文件的修改情况并在必要时对相应源文件重新编译连接。编译器(compiler) 和汇编器(assembler)将源文件生成包括二进制机器码和程序数据的目标文件。归档工具则将多个目标文件组合成一个库文件。连接器(linker)将目标文件作为输入文件生成可扫行映像或可与其它目标文件再次进行连接的目标文件。连接控制文件的作用就是指示连接器如何合并被连接的目标文件以及如何在目标系统上放置生成的二进制代码数据段。
编译和连接的主要目的就是生成一个大可重定位目标文件、一个可共享的目标文件或一个最终的可执行映像。比如ARM的gcc交叉开发环境中,arm-linux-gcc是编译器,arm-linux-ld是连接器。但并不是说对于一种休系结构只有一种编译连接器,比如说M68K休系结构的gcc编译器而言,就有多种不同的编译和连接器。如果使用COFF的可执行文件格式的话,那么在编译Linux内核时需要使用m68k-coff-gcc和m68k-coff-ld的编译连接器,在编译应用程序时需要使用m68k-coff-pic-gcc和m68k-coff-pic-ld编译连接器。这时因为应用程序代码需要编译成为可重定位代码。这样,虽然因为内核占用位置导致应用程序存放的位置不同,但仍可以使用相对地址运行应用程序。这方面需要注意的问题对各种嵌入式系统面言都是不同的。
在连接过程中,对于嵌入式系统的开发而言,都希望使用较小型的函数库,以使最后产生的可执行代码尽量小。因此在编译中使用的一般是经过特殊定制的函数库。比如使用C做嵌入式开发的人一般使用的几个嵌入式函数库有:uClibc/uClibm,uC-libc/uC-libm和 newlib等。
3、加载映像
生成了目标平台需要的image文件之后,需要从主机端将生成的可执行映像传送到目标系统上去,这个过程被称作加载映像(loading the image)。通常映像可以使用如下方式加载:
※将整个映像写入EEPROM或Flash存储器中。
※通过串口(RS-232)或网络 连接下载目标映像。这需要主机端和目标系统上有数据传送的工具。
※使用硬件调试工具,通过JTAG或BDM接口下载映像。