如今的数字信号处理器(DSP)在性能、外围设备集成、功耗和成本方面都具备很多优势,很多系统设计人员希望在原有系统设计中利用DSP获得比传统处理器更好的效益。但其中一个潜在障碍就是为应用开发的大量遗留C/C++语言代码。显然,工程师们希望能够在DSP平台上最大程度地利用原有高级语言代码,同时充分利用DSP的结构特点,达到原平台无法企及的高性能。此外,设计人员还需要一个熟悉、直观的程序开发环境和一种简单的方法,用于有选择地进行汇编语言的例行程序。本文将介绍在当前的开发环境下为DSP编程的策略与技巧,其中以ADI公司推出的Blackfin
媒体处理器系列作为示例。
高级语言与汇编语言:两者的结合是最佳办法
在进行一项基于DSP的项目开发时,必然要面临的一个问题就是选择使用何种编程方法。选择的结果通常都是在汇编语言与高级语言如C或C++之间选择其中一种。而在选择过程中往往需要考虑许多其它的因素,因此,在选择之前了解这两种语言的长处与不足是十分重要的。
C/C++的好处包括模块化、可移植性以及可复用性。此外,不仅大多数的嵌入式程序设计员使用过这种高级语言,而且已经存在大量的代码基础,可以通过一种相对简单的方法将这些代码从原来的微控制器或DSP移植到新的DSP平台中。而汇编语言是针对特定体系结构的,因此代码重用仅限于同一系列的处理器。此外,一个系统开发项目组通常划分成不同的开发小组,分别负责不同的系统模块,采用高级语言可以使这些功能交叉的开发小组不必知道各自的处理器平台。
传统的汇编语言因为难懂的语法以及奇怪的首字缩写而长期受到贬低。而现在这些因素在采用称作"代数语法"的结构中已不成什么问题。图1中所给出的示例就是将典型的DSP指令分别以传统的格式和代数格式表示时的对比。从图中可以清楚地看出后者的结构要比前者更加直观。
使用汇编语言编程困难的原因之一,就是它专注于DSP寄存器组、运算单元与存储器之间的数据交流。而在C/C++高级语言中,这一过程通常是通过调用变量、函数以及子程序的方法在一个更加抽象的层面来完成的,因此使得编程更为简单。
如今,C/C++编译器所包含的内容十分丰富,其中许多功能可以完成将高级语言代码编译为严密的汇编语言代码。事实上,编译过程中最好的方法就是通过编译器中的优化程序完成任务。但工具开发人员认为最重要的一系列功能,将影响编译器的性能。因此,高级语言代码不可能在所有方面都超过手工的汇编语言代码。
程序开发人员通常只是在需要优化重要的密集型数据处理代码程序块时才会使用汇编语言,以提高程序在DSP上的运行效率。尽管高级语言编译器在程序优化转换方面做的很好,但在对DSP数据流与运算进行直接、仔细的控制时仍然存在不足之处。这也是许多程序设计员经常将C/C++
等高级语言与汇编语言结合使用的原因。高级语言在程序控制以及基本的数据处理方面有着不错的表现,而汇编语言则在高效的数学运算与速度最为关键的中断服务例程方面体现出明显的优势。
高效编程的结构特点
汇编程序员要使编写的程序高效运行,就必须要了解DSP与未针对超高速数据处理进行优化的普通处理器的区别。这些结构特点包括:
特殊的寻址方式
硬件循环结构
可缓冲的存储器
单循环执行多个操作
互锁流水线
灵活的数据寄存器文件
这些结构特点可以在提高计算效率方面起到十分大的作用。下面逐个讨论这些特点。
特殊的寻址方式
如果要求处理器在一个单循环中访问多个数据字,那么就需要处理器在地址生成方面具有完全的灵活性。除了在16位与 32位范围内的以DSP为主的访问大小之外,需要使用字节编址的方式才能达到最高效率的数据处理。这一点十分重要,因为在一些通常的应用中,包括许多以视频为基础的系统,都是以8位数据方式工作的。当存储器的访问被局限在单一的范围内时,处理器就需要额外的循环用于屏蔽相关的位。
寻址方式的另一个好处就是采用了"循环缓冲"功能。这一功能必须是由DSP在不借助任何专门软件管理而直接支持的。程序设计员可以利用循环缓冲功能在存储器中定义缓冲区,程序执行时会自动跳过这一段。当缓冲区建立后,也无需专门的软件管理这段数据。地址生成器不仅会处理不一致的跳跃,而且更重要的是它能够如图2中所示具备"环绕式处理程序"功能。如果没有这种自动生成地址的功能,程序员将不得不人工跟踪缓冲区,这样就会浪费大量宝贵的处理周期。
一种基本的、用于高效率信号处理操作(如快速傅立叶变换与离散余弦变换)的寻址方式是位反转技术。单从字面上理解,"位反转"就是要按照二进制地址将位反转。即把最不重要的位与最重要的位进行位置交换。基2蝶形运算所需的数据排序是按照"位反转"的顺序,因此在进行快速傅立叶变换阶段需要用到位反转索引。利用软件可以计算出这些位反转索引,但这种做法的效率十分低。图3中给出的是位反转地址流程示例图。
硬件循环构造
在通信处理算法中,循环是十分关键的功能。对于大多数算法而言,有两种与循环相关的功能可以提高算法的性能。第一种被称之?quot;零开销硬件循环"。利用寻址功能,循环构造通过硬件来实现。当然,这一功能也可以通过软件来实现,此时相关的开销则会影响到实时处理的性能。程序设计员通过"零开销循环"对循环进行初始化,其方法就是建立一个计数值并定义循环范围。处理器将不断地执行这一循环直至达到这一计数值。
大多数DSP都支持"零开销循环",但"硬件循环缓存"能够真正提高循环结构的性能。它们用作存放循环中所执行指令的一个高速缓冲存储器。例如,在循环执行了第一次之后,指令可以暂时存放在循环缓冲器中以备下次使用,从而在整个循环过程中就无需每次重取相同的指令。将循环中的指令存放在一个整个循环过程都能访问到的缓存器中,这样就能极大地节省循环次数。虽然这一功能无需程序设计员另外进行设定,但程序员必须了解缓存器的大小,这样才能正确地选择相应的循环大小。
可缓冲的存储器
标准的数字信号处理器通常都有少量片上高速存储器。微控制器通常能够访问较大的外部存储器。分层式存储器结构则汇集了这两种方法的优点,提供了几种具有不同性能层次的存储器。对于最需要决定的应用,片内的SRAM可以在每个核心时钟周期内完成一次访问。而对于那些代码量更大的系统,则可使用容量更大、等待时间稍长、片上或片外的存储器。
就其本身而言,这种分层式结构的作用只是相对的,因为当今的高速处理器只是以较慢的速度有效地运行,因为大型的应用只配备有速度相对较慢的外部存储器。此外,程序设计员也不得不手工地将重要代码从内置式SRAM中移进移出。然而,如果在结构中增加了用于存放数据与指令的高速缓冲存储器,外存储器就变得更加易于管理了。高速缓存可以减少用手工方式移动指令与数据进出处理器内核的次数。这样程序设计员就无需考虑进入处理器内核数据与指令流程的管理,从而极大地简化了编程模式。
图4是一个标准的存储器配置,其中的指令可以根据需要从外存储器中调入。指令高速缓存通常与一些最近最少使用(LRU)算法一起使用,这样就能够确保那些经常使用的指令取代那些较少使用的指令。从图中可以看出:通过配置象高速缓存这样的片上存储器以及SRAM等存储器,还可以优化处理器的性能。DSP控制器能够直接向内核写入内容,而来自表中的数据则可以根据需要被调入数据高速缓存。
每个循环执行多个操作
处理器的衡量标准通常是每秒所能执行的百万条指令数(MIPS)。然而,对于现在的处理器而言,这一标准则会由于组成每条指令含混的内涵而引起误解。例如,过去因用于高端并行处理器而保留的多事件指令现在仍然用于低成本的定点处理器。在每个核心处理器周期内,除了执行多ALU/MAC操作之外,多余数据的载入与存储操作也可以在同一周期内完成。存储器通常被分成几个子存储空间,这样它就能够被内核或DMA控制器进行双重访问了。正如前面所述的基于硬件的寻址计算中进行的分析那样,在一个单周期内完成多项操作是显而易见的。
图5中描述的是多操作指令示例。如图中所示,在同一个处理器时钟周期内,除了进行两个 MAC 操作之外,还完成了一次取数据和存数据的操作。
互锁流水线
随着处理器的速度不断提高,处理器的处理流水线也应该随着整体性能的提高而不断加深。理解这点十分重要,因为在需要使用汇编语言时,流水线可能会使编程更加具有挑战性。而现在一些处理器已经使用了互锁流水线。这就意味着,在使用汇编语言编程中,程序设计员无需人工安排或跟踪数据与指令的流向,因为这些工作将全部由处理器进行自动处理。
灵活的数据寄存器组
最后,数字信号处理器的另一项功能就是通用数据寄存器组。对于传统的数字信号处理器而言,字长通常是固定的。而如果数据寄存器既能被看作是一个32位字(如R0),也能被看作是两个16位字(R0.L
与 R0.H,分别用于高和低的一半),其优点十分明显。在双MAC系统中,这样就允许在一个时钟周期内进行四个16位数据操作。
编程代码对比与分析
上述介绍的结构框架是DSP高效编程的基础。如果程序设计员能够充分利用处理器的所有功能,许多常见的数学算法可以极为快速的完成。下面挑选出一些常用的算法,并介绍它们在DSP中的用法。需要注意的是,当程序员需要在汇编水平上检查代码的高效性时,如今经优化的DSP编译器同样采用了很多汇编程序设计员使用的规则。下面的示例使用的是Blackfin处理器汇编语言。
标量积
标量积是在测定两个矢量正交性时的一种十分有用的操作。大多数的C语言程序设计员都会对下列这个标量积运用十分熟悉:
short dot(short a[], short b[], int size) {
int i;
int output = 0;
for(i=0; i<size; i++) {
output += (a[i] * b[i]);
}
return output;
下面是汇编语言代码的主体部分:
//P0=loop count, I0 & P1 are address registers
A1 =A0 =0; //A0 & A1 are accumulators
LSETUP(loop1,loop1)LC0 =P0;
//Set up hardware loop starting at label loop1:
loop1: A1 += R1.H * R0.H , A0 += R1.L * R0.L || R1 = [ P1++ ] ||
R0 = [ I0 ++ ] ;
利用下面介绍的几项数字信号处理器结构功能,将有助于编程。
通过使用硬件循环缓冲器与循环计数器,则无需在每次反复操作结尾时执行跳转指令。 由于标量积是一个累加的和,它是通过一个循环来实现的。为了执行循环中的下一次反复操作,许多RISC微控制器都是在每次反复操作结尾使用一条跳转指令。汇编程序中为LSETUP
指令,这是执行一个循环所需的唯一指令。
多事件指令允许在一个时钟周期内执行指令和两次数据访问。在每次反复操作中,值 a[i] 与 b[i] 都一定会被读取,然后相乘,并最后重新写回到变量输出的运行总和中。在大多数的微控制器平台中,这一过程需要使用四条指令。从汇编语言代码中的最后一行可以看出,这些操作可以在一个时钟周期内完成。
并行ALU操作允许两个16位指令可以同时执行。汇编语言代码表明两个累加单元(A0 与 A1)在每次反复操作中都会被用到。这样就能够将反复操作的次数减少50%,从而有效地将执行时间缩短了一半。
FIR
有限脉冲响应滤波器(FIR)是一个与卷积操作一样常用的滤波器程序结构。简单的C 语言命令与标量积十分相似:
// sample the signal into a circular buffer
x[cur] = sampling_function();
cur = (cur+1)%TAPS; // advance the cur pointer in a circular fashion
// perform the multiply-addition
y = 0;
for (k=0; k<TAPS; k++) {
y += h[k] * x[(cur+k)%TAPS];
}
FIR的核心部分用汇编代码表示出来之后与标量积的格式十分相似。事实上,DSP相同的功能也被用于实现执行算法的最高性能。在本例中,信号采样存贮在寄存器R0中,系数则存贮在寄存器R1中。
// P0 holds # of filter taps
R0=[I0++]||R1=[I1++]; // set initial values for R0 and R1
A1=A0=0; // zero the accumulators
LSETUP(loop1,loop1)LC0 =P0; // configure inner loop
loop1: A1+=R0.L*R1.L, A0+=R0.H*R1.H || R0 = [I0++] ||
R1 = [I1++]; // compute
除了具有上述标量积的功能之外,上例中的FIR算法还使用了循环缓存器。
通过循环缓存器则无需使用明显的模运算。在C语言代码片断中, % (模数)运算符提供了一种用于循环缓冲的机制。如汇编核心程序所示,该模运算符在循环内部并没有转换为一条另外的指令。取而代之的是,数据地址生成寄存器I0
与 I1 在循环外进行了设置,并且自动返回系数缓存器边界的开始位置。
FFT(快速傅立叶变换)
快速傅立叶变换是许多信号处理算法的核心部分。它的特点之一就是输入矢量按照时间顺序排序,而输出矢量则是按照"位反转"的顺序。大多数传统的通用型处理器都要求程序设计员执行一个单独的程序,用于将经位反转的输出矢量复原。在数字信号处理器平台中,位反转已经被设计在寻址部分中了。
在执行快速傅立叶变换过程中,通过位反转寻址则无需使用单独的位反转程序。允许硬件对快速傅立叶变换算法中的输出矢量自动进行位反转,这样程序设计员就不用另外编写应用程序,从而提高了处理器的性能。
除了上述介绍的指令结构之外,象 Blackfin这样的处理器 还另外包括一些专用的指令集用于支持大范围的应用。这些指令的作用是将处理器的处理能力进一步扩展到其它一些算法,如Viterbi,
Huffman编码以及许多其它的位处理程序。
至此,可以清楚地认识到:在确定一个基于DSP应用的编程方案时,有许多需要考虑的内容。使用C 或C++ 这类带强大编译器与优化程序功能的高级语言可以快速地开发出各种产品,但使用手工编程的汇编语言则是在处理器以外获取额外性能的最佳方法。当然,采用汇编语言的前提是选择一种在结构上基本支持高效编码的处理器。
|