2 嵌入式系统开发相关技术
相对于在Windows环境下的开发应用程序,嵌入式系统开发有着很多的不同。不同的硬件平台和操作系统带来了许多附加的开发复杂性。
2.1 嵌入式开发过程
在嵌入式开发过程中有宿主机和目标机的角色之分:宿主机是执行编译、链接、定址过程的计算机;目标机指运行嵌入式软件的硬件平台。首先须把应用程序转换成可以在目标机上运行的二进制代码。这一过程包含三个步骤:编译、链接、定址。编译过程由交叉编译器实现。所谓交叉编译器就是运行在一个计算机平台上并为另一个平台产生代码的编译器。常用的交叉编译器有GNU C/C++(gcc)。编译过程产生的所有目标文件被链接成一个目标文件,称为链接过程。定址过程会把物理存储器地址指定给目标文件的每个相对偏移处。该过程生成的文件就是可以在嵌入式平台上执行的二进制文件。
嵌入式开发过程中另一个重要的步骤是调试目标机上的应用程序。嵌入式调试采用交叉调试器,一般采用宿主机-目标机的调试方式,它们之间由串行口线或以太网或BDM线相连。交叉调试有任务级、源码级和汇编级的调试,调试时需将宿主机上的应用程序和操作系统内核下载到目标机的RAM中或直接烧录到目标机的ROM中。目标监控器是调试器对目标机上运行的应用程序进行控制的代理(Debugger Agent),事先被固化在目标机的Flash、ROM中,在目标机上电后自动启动,并等待宿主机方调试器发来的命令,配合调试器完成应用程序的下载、运行和基本的调试功能,将调试信息返回给宿主机。
2.2 向嵌入式平台移植软件
大部分嵌入式开发人员选用的软件开发模式是先在PC机上编写软件,再进行软件的移植工作。在PC机上编写软件时,要注意软件的可移植性,选用具有较高移植性的编程语言(如C语言),尽量少调用操作系统函数,注意屏蔽不同硬件平台带来的字节顺序、字节对齐等问题。以下是我们在移植协议栈过程中的一些体会。
2.2.1 字节顺序
字节顺序是指占内存多于一个字节类型的数据在内存中的存放顺序,通常有小端、大端两种字节顺序。小端字节序指低字节数据存放在内存低地址处,高字节数据存放在内存高地址处;大端字节序是高字节数据存放在低地址处,低字节数据存放在高地址处。基于X86平台的PC机是小端字节序的,而有的嵌入式平台则是大端字节序的。因而对int、uint16、uint32等多于1字节类型的数据,在这些嵌入式平台上应该变换其存储顺序。通常我们认为,在空中传输的字节的顺序即网络字节序为标准顺序,考虑到与协议的一致以及与同类其它平台产品的互通,在程序中发数据包时,将主机字节序转换为网络字节序,收数据包处将网络字节序转换为主机字节序。
2.2.2 字节对齐
有的嵌入式处理器的寻址方式决定了在内存中占2字节的int16、uint16等类型数据只能存放在偶数内存地址处,占4字节的int32 、uint32 等类型数据只能存放在4的整数倍的内存地址处;占8字节的类型数据只能存放在8的整数倍的内存地址处;而在内存中只占1字节的类型数据可以存放在任意地址处。由于这些限制,在这些平台上编程时有很大的不同。首先,结构体成员之间会有空洞,比如这样一个结构:
typedef struct test{
char a;
uint16 b;
}TEST
结构TEST在单字节对齐的平台上占内存三个字节,而在以上所述的嵌入式平台上有可能占三个或四个字节,视成员a的存储地址而定。当a存储地址为偶数时,该结构占四个字节,在a与b之间存在一个字节的空洞。对于通信双方都是对结构成员操作的,这种情况不会出错,但如果有一方是逐字节读取内容的(通信协议大都如此),就会错误地读到其它字节的内容。其次,若对内存中数据以强制类型转换的方式读取,字节对齐的不同会引起数据读取的错误。因为假如指针指在基数内存地址处,我们想取得占内存两个字节的数据存放在uint16型的变量中,强制类型转换的结果是取得了该指针所指地址与前一地址处的数据,并没有按照我们的愿望取该指针所指地址与后一地址处的数据,这样就导致了数据读取的错误。
解决字节对齐有许多方法,比如可以在GCC的项目管理文件MakeFile中增加编译选项--pack-struct;但这种方法只能去除结构中的空洞,并不能解决强制类型转换引起的错误。为了增强软件的可移植性以及和同类其它平台产品的互通性,我们在收数据包处增加了拆包的函数,发数据包处增加了组包的函数。这两个函数解决了字节序的问题,也解决了字节对齐的问题。即组包时根据参数中的格式字符串将内存中的不同数据类型的某段数据放在指定地址处,组成包发给下层;拆包时,根据参数中的格式字符串将收到的内存中的数据存放在不同类型的变量或结构成员中。在函数中针对不同的数据类型作不同的处理。
2.2.3 位 段
由于位段的空间分配方向因硬件平台的不同而不同,对X86平台,位段是从右向左分配的;而一些嵌入式平台,位段是从左向右分配的。分配顺序的不同导致了数据存取的错误。解决这一问题的一种方法是采用条件编译的方式,针对不同的平台定义顺序不同的位段;也可以在前面所述的两个函数中加上对位段的处理。
2.2.4 代码优化
嵌入式系统对应用软件的质量要求更高,因而在嵌入式开发中尤其须注意对代码进行优化,尽可能地提高代码的效率,减少代码的大小。虽然现代C和C++编译器都提供了一定程度的代码优化,但大部分由编译器执行的优化技术仅涉及执行速度和代码大小的平衡,不可能使程序既快又小,因而必须在编写嵌入式软件时采取必要的措施。
(1)提高代码的效率
①switch-case 语句。在程序中经常会使用switch-case语句,每一个由机器语言实现的测试和跳转仅仅是为了决定下一步要做什么,就浪费了处理器时间。为了提高速度,可以把具体的情况按照它们发生的相对频率排序。即把最可能发生的情况放在第一,最不可能发生的情况放在最后,这样会减少平均的代码执行时间。
② 全局变量。使用全局变量比向函数传递参数更加有效率,这样做去除了函数调用前参数入栈和函数完成后参数出栈的需要。当然,使用全局变量会对程序有一些负作用。
(2)减小代码的大小
嵌入式系统编程应避免使用标准库例程,因为很多大的库例程设法处理所有可能的情况,所以占用了庞大的内存空间,因而应尽可能地减少使用标准库例程。
(3)避免内存泄漏
用户内存空间(堆)为RAM中全局数据和任务堆栈空间都分配后的剩余空间,为了使程序能有足够的内存运行,必须在申请的内存不用后及时地将其释放,以确保再次申请时能有空间。如果程序中存在内存泄漏(即申请内存后没有及时释放)的情况,程序最终会因为没有足够的内存空间而无法运行。