基于ARM的嵌入式系统程序开发要点(一)

发布时间:2010-3-12 13:17    发布者:蹦蹦
关键词: arm , 开发 , 嵌入式 , 系统程序 , 要点
与传统的4/8位单片机相比,ARM的性能和处理能力当然是遥遥领先的,但与之相应,ARM的系统设计复杂度和难度,较之传统的设计方法也大大提升了。本文旨在通过讨论系统程序设计中的几个基本方面,来说明基于ARM的嵌入式系统程序开发的一些特点,并提出和解决了一些常见的问题。

本文分成几个相对独立的专题陆续刊载。
(一)  嵌入式程序开发基本概念
(二)  系统的初始化过程
(三)  如何满足嵌入式系统的灵活需求
(四)  异常处理机制的设计
(五)  ARM/Thumb的交互工作
(六)  开发高效程序的技巧

1  嵌入式程序开发过程

不同于通用计算机和工作站上的软件开发工程,一个嵌入式程序的开发过程具有很多特点和不确定性。其中最重要的一点是软件跟硬件的紧密耦合特性。

图1是两类简化的嵌入式系统层次结构图。由于嵌入式系统的灵活性和多样性,图1中各个层次之间缺乏统一的标准,几乎每一个独立的系统都不一样。这样就给上层的软件设计人员带来了极大地困难。第一,在软件设计过程中过多地考虑硬件,给开发和调试都带来了很多不便;第二,如果所有的软件工作都需要在硬件平台就绪之后进行,自然就延长了整个的系统开发周期。这些都是应该从方法上加以改进和避免的问题。

1.jpg
图1  两类不同的嵌入式系统结构模型

为了解决这个问题,工程和设计人员提出了许多对策。首先在应用与驱动(或API)这一层接口,可以设计成相对统一的一些接口函数,这对于具体的某一个开发平台或在某个公司内部,是完全做得到的。这样一来,就大大提高了应用层软件设计的标准化程度,方便了应用程序在跨平台之间的复用和移植。

对于驱动/硬件抽象这一层,因为直接驱动硬件,其标准化变得非常困难甚至不太可能。但是为了简化程序的调试和缩短开发周期,我们可以在特定的EDA工具环境下面进行开发,通过后再移植到硬件平台上。这样既可以保证程序逻辑设计的正确性,同时使得软件开发可行甚至超前于硬件开发进程。

我们把脱离于硬件的嵌入式软件开发阶段称之为“PC软件”的开发,可以用图2来示意一个嵌入式系统程序的开发过程。

3.jpg
图2  嵌入式系统程序的开发过程

在“PC软件”开发阶段,可以用软件仿真,即指令集模拟的方法,来对用户程序进行验证。在ARM公司的开发工具中,ADS内嵌的ARMulator和 RealView开发工具中的ISS,都提供了这项功能。在模拟环境下,用户可以设置ARM处理器的型号、时钟频率等,同时还可以配置存储器访问接口的时序参数。程序在模拟环境下运行,不但能够进行程序的运行流程和逻辑测试,还能够统计系统运行的时钟周期数、存储器访问周期数、处理器运行时的流水线状态(有效周期、等待周期、连续和非连续访问周期)等信息。这些宝贵的信息是在硬件调试阶段都无法取得的,对于程序的性能评估非常有价值。

为了更加完整和真实地模拟一个目标系统,ARMulator和ISS还提供了一个开放的API编程环境。用户可以用标准C来描述各种各样的硬件模块,连同工具提供的内核模块一起,组成一个完整的“软”硬件环境。在这个环境下面开发的软件,可以更大程度地接近最终的目标。

利用这种先进的EDA工具环境,极大地方便了程序开发人员进行嵌入式开发的工作。当完成一个“PC软件”的开发之后,只要进行正确的移植,一个真正的嵌入式软件就开发成功了。而移植过程是相对比较容易形成一套规范的流程的,其中三个最重要的方面是:
  ◆ 考虑硬件对库函数的支持
  ◆ 符合目标系统上的存储器资源分布
  ◆ 应用程序运行环境的初始化

2  开发工具环境里面的库函数

如果用户程序里调用了跟目标相关的一些库函数,则在应用前需要裁减这些函数以适合在目标上允许的要求。主要需要考虑以下三类函数:
  ◆ 访问静态数据的函数
  ◆ 访问目标存储器的函数
  ◆ 使用semihosting(半主机)机制实现的函数

这里所指的C库函数,除了ISO C标准里面定义的函数以外,还包括由编译工具提供的另外一些扩展函数和编译辅助函数。

2.1  裁减访问静态数据的函数

库函数里面的静态数据,基本上都是在头文件里面加以定义的。比如CTYPE类库函数,其返回值都是通过预定义好的CTYPE属性表来获得的。比如,想要改变isalpha() 函数的缺省判断,则需要修改对应CTYPE属性表里对字符属性的定义。

2.2  裁减访问目标存储器的函数

有一类动态内存管理函数,如malloc() 等,其本身是独立于目标系统而运行的;但是它所使用的存储器空间需要根据目标来确定。所以malloc() 函数本身并不需要裁减或移植,但那些设置动态内存区(地址和空间)的函数则是跟目标系统的存储器分布直接相关的,需要进行移植。例如堆栈的初始化函数 __user_initial_stackheap(),是用来设置堆(heap)和栈(stack)地址的函数。显然,针对每一个具体的目标平台,该函数都需要根据具体的目标存储器资源进行正确移植。

下面是对示例函数__user_initial_stackheap() 进行移植的一个例子:
__value_in_regs struct __initial_stackheap __user_initial_stackheap(
  unsigned R0, unsigned SP, unsigned R2, unsigned SL)
{
  struct __initial_stackheap config;
  config.heap_base = (unsigned int) 0x11110000;
  // config.stack_base = SP; // optional
  return config;
}

请注意上面的函数体并不完全遵循标准C的关键字和语法规范,使用了ARM公司编译器(ADS或RealView Compilation tool) 里的C语言扩展特性。关于编译器特定的C语言扩展,请参考相关的编译器说明,这里简单介绍函数__user_initial_stackheap() 的功能,它主要是返回堆和栈的基地址。上面的程序中只对堆(heap) 的基地址进行了设置(设成了0x11110000),也就是说用户把0x11110000开始的存储器地址用作了动态内存分配区(heap区)。具体地址的确定是要由用户根据自己的目标系统和应用情况来确定的,至少要满足以下条件:
  ◆ 0x11110000开始的地址空间有效且可写(是RAM)
  ◆ 该存储器空间不与其它功能区冲突(比如代码区、数据区、stack区等)

因为__user_initial_stackheap() 函数的全部执行效果就是返回一些数值,所以只要符合接口的调用标准,直接用汇编来实现看起来更加直观一些:
EXPORT __user_initial_stackheap
__user_initial_stackheap
LDR r0,0x11110000
MOV pc,lr

如果不对这个函数进行移植,编译过程中将使用缺省的设置,这个设置适用于ARM公司的Integrator系列平台。

2.3  裁减使用半主机机制实现的函数

库函数里有一大部分函数是涉及到输入/输出流设备的,比如文件操作函数需要访问磁盘I/O,打印函数需要访问字符输出设备等。在嵌入式调试环境下,所有的标准C库函数都是有效且有其缺省行为的,很多目标系统硬件不能支持的操作,都通过调试工具来完成了。比如printf() 函数,缺省的输出设备是调试器里面的信息输出窗口。

但是一个真实的系统是需要脱离调试工具而独立运行的,所以在程序的移植过程当中,需先对这些库函数的运行机制作一了解。

图3说明了在ADS下面这类C库函数的结构。

4.jpg
图3  C库函数实现过程中的层次调用

如图4中例子所示,函数printf() 最终是调用了底层的输入/输出函数_sys_write() 来实现输出操作的,而_sys_write() 使用了调试工具的内部机制来把信息输出到调试器。

5.jpg
图4  printf() 的调试过程

显然这样的函数调用过程在一个真实的嵌入式系统里是无法实现的,因为独立运行的嵌入式系统将不会有调试器的参与。如果在最终系统中仍然要保留 printf() 函数,而且在系统硬件中具备正确的输出设备(如LCD等),则在移植过程中,需要把printf() 调用的输出设备进行重新定向。

单纯考虑printf() 的输出重新定向,可以有三种途径实现:
  ◆ 改写printf() 本身
  ◆ 改写 fput()
  ◆ 改写 _sys_write()

需要注意的是,越底层的函数,被其他上层函数调用的可能性越大,改变了一个底层函数的实现,则所有调用该函数的上层函数的行为都被改变了。

以fputc() 的重新实现为例,下面是改变fputc() 输出设备到系统串行通信端口的实例:
int fputc(int ch, FILE *f)
{ /* e.g. write a character to an UART */
  char tempch = ch;
  sendchar(&tempch); // UART driver
  return ch; }

代码中的函数sendchar() 假定是系统的串口设备驱动函数。只要新建函数fput() 的接口符合标准,经过编译链接后,该函数实现就覆盖了原来缺省的函数体,所有对该函数的调用,其行为都被新实现的函数所重新定向了。

3  Semihosting 机制

上面提到许多库函数在调试环境下的实现都调用了一种叫作semihosting(半主机)的机制。Semihosting具体来讲是指一种让代码在 ARM 目标上运行,但使用运行了ARM 调试器的主机上I/O 设备;也就是让ARM 目标将输入/ 输出请求从应用程序代码传递到运行调试器的主机的一种机制。通常这些输入/输出设备包括键盘、屏幕和磁盘I/O。

半主机由一组已定义的SWI 操作来实现,如图5所示。库函数调用相应的SWI(软件中断),然后调试代理程序处理SWI 异常,并提供所需的与主机之间的通讯。多数情况下,半主机SWI 是由库函数内的代码调用的。但是应用程序也可以直接调用半主机SWI。半主机SWI 的接口函数是通用的。当半主机操作在硬件仿真器、指令集仿真器、RealMonitor或Angel下执行时,不需要进行移植处理。

6.jpg
图5  semihosting的实现过程

使用单个SWI 编号请求半主机操作。其它的SWI 编号可供应用程序或操作系统使用。用于半主机的SWI号是:
  在ARM 状态下:0x123456
  在Thumb 状态下:0xAB

SWI 编号向调试代理程序指示该SWI 请求是半主机请求。要辨别具体的操作类型,用寄存器r0 作为参数传递。r0 传递的可用半主机操作编号分配如下:
  ◆ 0x00~0x31:这些编号由ARM 公司使用,分别对应32个具体的执行函数。
  ◆ 0x32~0xFF:这些编号由ARM 公司保留,以备将来用作函数扩展。
  ◆ 0x100~0x1FF:这些编号保留给用户应用程序。但是,如果编写自己的SWI 操作,建议直接使用SWI指令和SWI编号,而不要使用半主
         机SWI 编号加这些操作类型编号的方法。
  ◆ 0x200~0xFFFFFFFF:这些编号未定义。当前未使用并且不推荐使用这些编号。半主机SWI使用的软件中断编号也可以由用户自定
         义,但若是改变了缺省的软中断编号,需要:
  ◆ 更改系统中所有代码(包括库代码)的半主机SWI 调用
  ◆ 重新配置调试器对半主机请求的捕捉与响应这样才能使用新的SWI 编号。

4  应用环境的初始化和根据目标系统资源进行的移植

在下一期中介绍应用环境和目标系统的初始化。(更多关于ARM的详细技术资料,请访问:http://www.arm.com/arm/documentation?OpenDocument

引证文献

   1. 谭云福.程志刚.孙会珺 基于ARM的嵌入式系统的初始化引导程序 [期刊论文] -河北省科学院学报2006(04)
   2. 孟凌凌 基于嵌入式系统技术的桩基础检测仪的研制 [学位论文] 硕士2006
   3. 连迅 基于ARM微处理器的玻璃缺陷在线检测系统 [学位论文] 硕士2006
   4. 杨志强 嵌入式系统设计与发展 [期刊论文] -青海师范大学学报(自然科学版)2005(03)
   5. 刘志勇 基于ARM的无线视频传输硬件系统的初步研究与开发 [学位论文] 硕士2005
   6. 田劲华 基于USB Host和ARM技术的大容量数据采集系统的研究 [学位论文] 硕士2005
   7. 金志强 基于ARM的嵌入式控制系统硬件平台设计 [学位论文] 硕士2005
   8. 王雷 基于ARM微处理器的应用研究 [学位论文] 硕士2005
   9. 信俊昌 基于ARM的具有网络功能的嵌入式平台的设计与实现 [学位论文] 硕士2004

作 者:ARM中国 费浙平  
刊 名:单片机与嵌入式系统应用 2003(8)
本文地址:https://www.eechina.com/thread-9274-1-1.html     【打印本页】

本站部分文章为转载或网友发布,目的在于传递和分享信息,并不代表本网赞同其观点和对其真实性负责;文章版权归原作者及原出处所有,如涉及作品内容、版权和其它问题,我们将根据著作权人的要求,第一时间更正或删除。
您需要登录后才可以发表评论 登录 | 立即注册

厂商推荐

相关视频

关于我们  -  服务条款  -  使用指南  -  站点地图  -  友情链接  -  联系我们
电子工程网 © 版权所有   京ICP备16069177号 | 京公网安备11010502021702
快速回复 返回顶部 返回列表