嵌入式软件移植
嵌入式软件与通用软件的不同在于,嵌入式软件高度依赖于目标应用的软硬件环境。软件的部分功能函数是汇编语言编写的,与处理器高度相关,可移植性差。
在嵌入式应用软件开发中,系统通常对正确性和实时性有较高要求,因此往往需要借助执行效率较高的汇编语言来实现关键功能。然而,这种方式往往会导致软件的可移植性显著降低。另一方面,一个已经稳定运行的嵌入式软件或其部分子程序,由于其经过多次应用、测试与维护,具备较高的可靠性,在后续类似应用领域的开发中往往具备较高的复用价值。基于原有稳定代码进行移植,能够有效缩短开发周期、提升开发效率并降低开发成本。正因如此,在嵌入式软件开发过程中,必须高度重视应用软件的可移植性与可重用性。
不过,可移植性和可重用性的程度应该根据实际的应用情况来考虑。由于嵌入式应用软件有许多自身的特点,如果追求过高的可移植性和可重用性,可能会降低应用软件的实时性能,并增加软件的代码量。嵌入式应用软件的开发可以分为无操作系统和有操作系统两种情形。与之相对应,在移植嵌入式软件的时候,也可以分无操作系统的软件移植和有操作系统的软件移植种情形。对于后者,又可以细分为两种方式:一是把操作系统和应用软件作为一个整体进行移植;二是把应用软件移植到一个新的操作系统上。
无操作系统的软件移植:
在一些嵌入式系统中,软件的开发直接基于硬件平台进行,并未采用操作系统。当处理器或其他硬件设备发生变更时,原有的应用软件往往需要移植到新的硬件平台上才能继续运行。通常这类嵌入式系统的应用软件结构相对简单,代码规模较小。若开发时采用的是与处理器架构紧密相关的汇编语言,则在移植时将面临很大困难,甚至几乎不可行。此时的移植工作近乎于重新开发一套软件——尽管在数据结构和算法层面上仍有部分可复用性,但此类情况不在本文讨论范围之内。
基于层次化的嵌入式应用软件可分为两层结构,I/O模块属于设备驱动程序层,它以嵌入式硬件为运行平台,实现了各种I/O设备的输入/输出功能,并向上层的应用软件提供相应的I/O接口函数API,如控制功能、数据读写等。这些接口函数被设计成与硬件无关的,因此在移植系统时,只需要重新编写与处理器有关的IO模块即可,不需要修改该模块的API。移植的工作量主要体现在I/O的编码工作上。
可以对上述软件结构作进一步的细化设计,如图所示。在这种体系结构中,在处理器的硬件层之上添加了一个硬件抽象层,这层软件把不同类型的硬件进行了封装和抽象。对于I/O模块,它不再是直接面对处理器硬件,而是基于硬件抽象层。也就是说,它被设计为与硬件无关的,这样=移植的工作量也进一步减少。
有操作系统的移植:
在本文中,所讨论的基于操作系统的软件移植,是指将操作系统及其上运行的应用软件视为一个整体,并将其迁移至新的嵌入式硬件平台的过程。
嵌入式软件的体系结构通常可分为四个层次:设备驱动层、操作系统层、中间件层和应用软件层。在将整个嵌入式软件系统移植到新硬件平台时,实际需要修改的,主要是与硬件直接相关的部分,而操作系统内核、中间件及应用程序等通常无需改动。当然,在某些采用单体式架构的嵌入式操作系统中,设备驱动程序被集成在内核内部,此时则需要将这部分代码从内核中分离出来并进行相应修改。
总体而言,系统移植过程中真正需要移植的内容主要包括:引导加载程序(BootLoader)、设备驱动程序,以及操作系统中与处理器架构紧密相关的代码。
为了提高可移植性,BootLoader 通常被划分为 stage1 和 stage2 两个部分。与 CPU 体系结构密切相关的代码(例如设备初始化代码)通常放置在 stage1 中,并使用汇编语言实现;而 stage2 则采用 C 语言编写。在移植过程中,主要工作量集中在 stage1 部分,通常需要根据目标硬件重新编写该阶段代码。
一般来说,嵌入式操作系统在设计时已充分考虑到可移植性,因此其移植相对较为简便。以 uC/OS-II 为例,为便于移植,该系统的大部分代码采用标准 C 语言编写,无需修改。仅少数直接操作 CPU 寄存器的代码需针对具体处理器进行调整,这部分通常使用汇编语言实现。
如图所示,uC/OS-II 操作系统的代码可分为三个主要部分:第一部分为与处理器无关的代码,包括任务管理、调度、内存管理、信号量、邮箱、消息队列等功能;第二部分是与系统配置相关的代码,应用程序开发者可通过修改配置文件对内核进行裁剪,以选择所需的系统服务;第三部分为与处理器体系结构相关的代码,主要包括 OS_CPU.H、OS_CPU_A.ASM 和 OS_CPU_C.C 三个文件。在移植 uC/OS-II 时,主要需要修改的正是这三个文件。
OS_CPU.H:该文件包括三部分的内容,首先是一个符号常量,用来设定处理器的栈的增长方向;其次是3个宏定义,用来关闭和打开中断;最后是10个数据类型的定义,用来定义与编译器无关的数据类型。在操作系统内部,只使用这些数据类型,而不是标准C的数据类型。
OS_CPU.ASM:用汇编语言编写OS_CPU.ASM文件中的四个与处理器相关的函数,包括任务切换、时钟中断服务程序等。
OS_CPU_C.C:用C语言编写OS_CPU_C.C文件中的十个与操作系统相关的函数数,一般只需要改写其中的一个函数,即任务堆栈的初始化函数。
应用软件的移植:
嵌入式应用软件的移植,是指将应用软件从一个嵌入式操作系统平台迁移至另一个操作系统平台的过程。
实现一个应用软件通常涉及两个关键方面:
(1)采用某种编程语言进行开发,例如汇编语言、C 语言或 C++ 语言;
(2)该软件需在某一特定平台上运行,该平台通常为操作系统,如 Windows XP、Linux 等。
值得注意的是,某些软件系统(如 Java)本身兼具编程语言与运行平台的双重身份。因此,在进行应用软件移植时,既要考虑编程语言的差异,也需兼顾运行平台的变化。
在个人计算机环境中,应用软件的运行平台相对集中,主要可分为 Windows 系列和 UNIX 系列两大类,每一类平台都提供各自的应用编程接口(API)。而在嵌入式系统中,编程语言带来的障碍通常较小,这是因为大多数嵌入式开发采用可移植性较好的 C 语言。
嵌入式应用软件的移植,指的是将应用软件从一个嵌入式操作系统平台迁移至另一个操作系统平台的过程。然而,在运行平台方面,嵌入式操作系统的种类极为丰富,目前已有数以百计、各具特色的产品。从理论上讲,每种操作系统都会定义各自的一套 API 接口函数,这导致在嵌入式平台上进行应用软件移植的难度相对较大。
为提高嵌入式应用软件的可移植性,在软件开发过程中应遵循以下原则:
(1)在软件设计上,采用层次化与模块化设计。层次化指软件在纵向结构上分层,下层为上层提供服务,上层调用下层功能。每一层应具备清晰的接口与明确的功能,分层数量也应适中。层次化设计的优势在于,系统移植时通常只需改动底层软件,而上层可保持不动。模块化则体现在整体架构与同一层的内部结构中。与层次化不同,模块之间通常相互独立,实现上不互相依赖。良好的模块化设计便于对软件模块进行裁剪和更新。
(2)在软件体系结构上,可在操作系统与应用软件之间引入一个虚拟机层(也称为操作系统抽象层),用于封装通用的、共性化的操作系统 API 接口。应用程序不直接调用具体操作系统的 API,而是通过该虚拟层提供的接口进行开发。在移植时,仅需针对新操作系统平台重新实现该抽象层,其余代码无需变动。定义虚拟层时应综合考虑多种嵌入式操作系统的特性,尽量遵循 POSIX 等标准接口规范。
(3)在功能服务调用方面,应尽量使用可移植的函数,如标准 C 库函数或自定义函数,避免直接调用与特定操作系统紧密相关的 API。
(4)在数据类型方面,由于 C 语言的数据类型依赖于机器字长和编译器,可采用宏定义方式统一封装一套可移植数据类型,并在应用程序中完全使用这些定义类型,而非直接使用原生 C 类型。例如,可使用
INT32U
表示无符号 32 位整型,针对具体编译器可定义为:#define INT32U unsigned int
(5)将不可移植的代码局部化。若不可移植的代码分散于软件各处,移植时将难以全面识别和修改,且容易引入错误。因此,应通过宏和函数等形式将非移植代码分类集中在少数几个文件中。这样既便于定位和修改,也提高了移植的效率和可靠性。
(6)提高代码的可重用性。在开发过程中应有意识地提升代码复用程度,不断积累可重用软件资源。例如,通过更合理的函数抽象,增强模块的独立性,保证功能专一、接口简洁,从而提升代码的通用性和可移植性。
本文内容参考书籍:嵌入式系统设计师教程 第2版