基于DSP混合编程关键问题的研究
摘 要: 本文介绍了C语言与汇编语言混合编程的规则,讨论了混合编程的一般方法。并在此基础上从工程实际的角度对混合编程的几项关键问题作了深入的论述。
现代电子设备中,对数据处理能力的要求日益提高。数字信号处理器(DSP),由于具有先进的体系结构,强大的信号处理能力而得到了蓬勃的发展。在本文所涉及的容错供配电系统设计中,采用了TMS320LF240xA系列,它作为TI公司DSP向传统的单片机领域渗透的产品,在控制领域得到了广泛的应用。
DSP具有的优势允许它面向更加复杂的应用。对于这类系统,采用嵌入式实时操作系统极大的改善了系统的设计与运行。它不仅提高了开发的效率,而且提高了程序运行的效率,稳定性,以及扩展性等。本设计中采用了mC/OS-II v2.62。该内核已历经近十年的应用与改进,在实用性与可*性方面得到了广泛的好评。开放源代码的优势也使它尤其适合设计中自检测,以及故障诊断与定位的要求。
C与汇编混合编程的一般规则
C语言的mC/OS-II内核带来了编程方式选择的问题。一方面,采用C语言设计效率较高,利于实现模块化、组态化的设计目标。另一方面,信号采集处理频率较高,如400Hz交流信号、频率信号和分辨率较高的开关量信号;同时,程序运行既要符合mC/OS-II时钟节拍,也要满足数据总线上大小周期的定义。这导致对定时的要求较高,对中断程序设计要求严格。因此混合编程是合理的编程方式。
一般混合编程方法
混合编程的一般方法有三种:一是对C程序编译后形成的汇编程序进行手工的修改与优化。二是直接在C语言程序中嵌入汇编语句。三是分别编写C程序和汇编程序,再独立编译成目标代码模块,再进行链接。
第一种编程方式要求对汇编与C语言都极其熟悉,并且这样的编程方式对程序的可读性和扩展性的负面影响比较大。第二种方法适用于语句执行频率非常高,并且C编程与汇编编程效率差异较大的情况,例如进入中断的通用中断子程序等。第三种方式是混合编程最常用的方式之一。在这种方式下,C程序与汇编程序均可使用另一方定义的函数与变量。
变量定义的规则与数组、指针的使用
变量定义是混合编程的基本问题。C程序与汇编程序定义的变量相互之间可以进行访问。在汇编程序中定义时,需要在变量前加下划线“_”,然后再用.global定义为全局变量。在C程序中则需要说明为extern变量。
C语言中常用的数组与指针也可以很方便地在汇编程序中定义并互相访问。如例,在汇编语言中用.usect定义占用6*32个字的全局变量_AC_RSLT,在C程序中被说明成一个6*32的16位无符号数的二位数组。当然,在汇编程序中,数据的存放格式需要满足C对二维数组数据存放格式的要求。即在存储器中,从低地址到高地址,二维数组的内容从第一行起各行依次放入存储器中。实际上,这个数组定义也可以看作是指针定义。
汇编程序:
_AC_RSLT .usect “ADDATA”, (6*32)
.global _AC_RSLT
C程序:
extern INT16U AC_RSLT[6][32];
( extern INT16U *AC_RSLT; )
依此类推,只要具有明确的数据存放格式,C语言中各种类型的变量在汇编语言中都可以方便地定义和使用。
函数调用的一般规则
C程序中调用汇编函数
C程序中调用的汇编函数,在汇编中其名称以程序标号的形式出现。程序标号作为操作数用.global进行定义。并且同变量一样,在前面加下划线“_”。汇编函数也可以给调用者传递返回值,它实际上是通过被写入累加器返回的。汇编函数一般具有如下例的形式:
_calcu:
POPD *+ 将返回地址压入任务栈
SAR AR0, *+ 存储调用者的帧指针
SAR AR1, *
LAR AR0, #01h
LAR AR0, *0+,AR2 堆栈指针复制到AR0并创建局部帧
SETC SXM 注意: 改变了状态寄存器
MAR *, AR2
LAR AR2, #0FFFDh AR2=-3
MAR *0+
LACC * 访问调用者传递的参数
...
MAR *, AR3
LACL * 传递返回值
MAR *, AR1
SBRK #02h
LAR AR0, *-
PSHD *
RET
汇编程序中调用C函数
汇编程序中调用C函数相对比较简单,编译器已经提供了相当完善的支持。函数在C中需要用extern进行定义,函数名加“_”后在汇编程序中用.ref说明为外部标号。在调用函数之前应手工编程将参数以逆序写入到当前运行任务所使用的任务堆栈中,压栈之前堆栈指针可不作调整。被调用的C函数即可正常访问调用者传递的参数,函数调用完毕后需要调整堆栈指针,清除函数调用中参数所占用的堆栈空间。C函数的返回值可以通过访问累加器获得。
参数的访问与堆栈指针的调整
如果希望被C程序调用的汇编函数含有参数,那么就必须按照编译器对含参数的C函数的参数处理原则,利用任务堆栈指针访问参数。其方式如上例中所示。在DSP中,堆栈指针的调整借助辅助寄存器(ARx或者XARx)实现。如例,通过AR2实现了带符号数的补码运算,这样可以根据需要移动堆栈指针,实现对函数参数的访问,此外,调用者与被调用函数的局部数据结构的创建与释放也是通过堆栈指针实现的。值得一提的是,在μC/OS-II移植中任务调度与中断部分程序需要注意因参数而产生的堆栈指针调整,应用任务的参数定义也必须与之保持一致。
混合编程中程序运行环境的改变与保持
不管采用哪种混合编程方式,运行环境的改变与保持对程序运行正常与否有着重要影响。正常的运行环境不仅关系到DSP状态寄存器的各状态位,也关系到编译器的函数调用规则,寄存器和堆栈的使用规则。尤其在使用第三方提供的函数时,应该特别注意这个问题。在允许的情况下函数调用前后也可采用保存与恢复现场的手段。这样做不利于提高耦合性和减少冗余,但有利于增加可移植性。此外,对运行环境的关注也有利于提高程序的扩展性。
mC/OS-II及应用程序在DSP上运行关键问题的解决 堆栈的使用。I$$SAVE,I$$REST函数
函数调用是否正常,任务调度与中断是否正常,在很大程度上都取决于堆栈的使用正确与否。对于240xA系列, TI提供了保存与恢复堆栈的函数I$$SAVE、I$$REST,这两个函数都位于rts2xx.lib库中,所在的源文件是saverest.asm。它们在发生任务调度和中断的时候使用,保存或恢复了所有编译器所使用的寄存器以及硬件堆栈。I$$SAVE函数的代码如下:
I$$SAVE:
MAR *, AR1 AR1存放堆栈指针
ADRK #1h
SST #1, *+ 保存状态寄存器
SST #0, *+
SACH *+, 0 保存ACC内容
SACL *+, 0
CLRC OVM
SPM
SPH *+ 保存PREG
SPL *+
MPY #1h 保存TREG
SPL *+
SAR AR0, *+ 保存辅助寄存器
SAR AR2, *+
SAR AR3, *+
SAR AR4, *+
SAR AR5, *+
SAR AR6, *+
SAR AR7, *+
POPD * I$$SAVE的返回
地址压入任务堆栈
LACC *, 0
RPT #6h 硬件堆栈其它内容
压入任务堆栈
POPD *+
BACC
与I$$SAVE函数相反,I$$REST恢复各寄存器与硬件堆栈的值。但是现场恢复的顺序与保存有所不同,其中利用了TREG将PREG的低16位弹出堆栈,再弹出TREG本身,这里弹栈的过程相对于压栈而言不是顺序的。另外,I$$SAVE函数将8级硬件堆栈全部压入任务栈,但栈顶内容被其下的内容覆盖,同时最后使用了跳转指令,因此调用I$$SAVE函数必须使用CALL指令,而I$$REST函数只需恢复除了原栈顶以外其它7级硬件堆栈的内容。
AR1自始至终存放着堆栈指针。在I$$SAVE与I$$REST函数的首尾对堆栈指针作了调整。两个函数对指针的调整是相互对应的,确保了堆栈指针位置正确。
在现场保存要求较高情况下,这两个函数可以用修改库文件的方法改进,添加某些用户希望在调度与中断过程中保存的量,特别是对于大型嵌入式软件中具有类似C语言volatile属性的量。当堆栈空间紧张时,也可以用同样的方式去除某些不必保存的量。
对操作系统的移植而言,堆栈在初始化中必须模仿被中断时保存堆栈的情形,只是在任务返回地址处不使用硬件堆栈中的值,而是使用任务的入口地址。
某些DSP并不具有硬件堆栈,因而保存与恢复寄存器的方式有较大的不同,给编程带来了更大的灵活性。他们通常采用了TRAP与IRET这样的保存恢复现场的指令,在执行这些指令前需要对堆栈指针进行调整。
程序及数据的ROM初始化
混合编程的程序在DSP中的固化涉及到程序是否能够彻底脱机运行。其内容包括设置微处理器/微控制器位,合理的编写.cmd文件等。而程序固化的关键问题是如何在程序存储器中分配存储空间给常量和用const关键字定义的静态、全局变量。
上述这些量被分配到存储空间中的.const段。它在DSP中的初始化一般有三种方式,一种是去除const关键字,在程序中赋初值使用。并且需要在.cmd文件中将.cinit段分配到程序区FLASH存储空间,然后在编译器的编译选项中选中“-C”,即ROM初始化。第二种方法不对定义作修改,.const段保存在FLASH存储器中,数据不向数据存储器移动,程序运行时直接在程序存储空间中访问这些量。由于C语言缺乏访问程序区数据的有效手段,因此这些语句只能使用汇编语言编写。由于在每一处访问这些常量时都必须使用这些语句,因此这样编写程序改动量较大。
第三种方法较之前两种,运用起来要方便得多。它不需要修改常量定义,也不必编写专门的程序。主要的工作是修改.cmd文件并对工程中使用的库文件作简单的修改,修改工作量小而且集中,极大地方便了程序的编写。同前两种方式不同,这种方式需要设置.const段的装载地址和运行地址。其表达式如下。
.const : LOAD = FLASH PAGE 0 , RUN = EXT PAGE 1
{
__const_run = .; /* 获取运行地址 */
*(.c_mark) /* 标志装载地址 */
*(.const) /* 分配.const段 */
__const_length = .- __const_run; /* 计算段长 */
}
采用了装载地址与运行地址分离的方式后,为了使程序正常运行,在初始化时,需要将.const段的内容从装载地址拷贝到运行地址内。这段程序可以在编译时由编译器自动生成。这还需要对软件所使用的库文件作简单的修改。该库名称即是rts.lib(表示不同类型的DSP,有2xx,25,50等等)。修改该库的方法是将源文件从库中提取出来,作修改,编译后再替代原有的文件。具体的操作如下:先执行DOS命令:
dspar -x rts.src boot.asm
这句的功能是从rts.src文件中提取出boot.asm文件。这个rts.src即是rts.lib的源文件。在boot.asm文件中能找到CONST_COPY这个标志量,为了实现所需要的功能它应被赋值为1。对boot.asm文件的编辑完成之后,就可以将其编译生成目标文件,执行语句:
dspa -v boot.asm
其中对于不同的DSP需要使用不同的参数,对于240xA来说,应该使用2xx来代替“”。语句执行完后会生成boot.obj文件。再执行语句:
dspar -r rts.lib boot.obj
这时它就替换了库里的同名文件。在编译时编译器就会自动增加拷贝.const段到数据空间的语句。这种方法不必修改程序,代价是牺牲了一定数据存储空间,时间开销主要出现在初始化中。这是最经济实用的方法之一。
使用代码工具
各种各样的代码生成工具可以大大增加编程的灵活性。除了前面提到的dspar与dspa两个指令外,C文法分析程序dspac.exe对.C文件进行文法分析,生成.IF中间文件,dspopt.exe对.IF文件进行优化,生成.OPT文件。此外,还有其它多种功能不同的代码工具。对于应用环境苛刻的程序,它们有利于分析如何对其进行优化。
混合编程的实际运用及结论
本项目全面采用了C与汇编混合编程,采用这种方式是为了提高程序的运行效率,节省空间。本项目软件包括模拟量采集与计算,频率采集与计算,开关量、数字量采集与控制,总线通讯,历史数据备份,BIT等诸多功能;程序运行一方面依据mC/OS-II提供的时钟节拍并考虑数据采集分辨率的要求,另一方面需要兼顾总线上的大小定时周期。如果全部采用C语言编程,那么16路交流模拟量以16个点采样并计算时CPU资源紧张,而使用C与汇编混合编程,CPU利用率有超过5%的降幅。每个周期内CPU仍然有一定的空闲时间。这其中,数**算函数也使用了TI提供的由汇编程序生成的库。
我们的实践证明,采用混合编程的软件更加契合一般嵌入式系统对时间和空间的严格约束。设计良好的混合编程软件既能有效地满足嵌入式系统对功能与性能的需求;同时,它也可以为程序的扩展和移植预留足够的空间。混合编程是嵌入式系统软件最优化的重要途径。
|