BlackFin 处理器提高编码效率

当今的数字信号处理器(DSP)的性能、外围设备、功耗和价格的完美结合已经达到了如此之高的吸引力,使许多系统设计工程师期望这些优势超越他们的传统设计方案中使用的处理器。对此一个潜在困难就是设计工程师已经为他们的应用领域开发的大量老的C和C++代码。很明显,这些设计工程师都愿意在DSP平台基础上利用现有的高级代码,同时利用DSP的体系结构特性以获得在先前平台上无法到达的性能。此外,他们还需要一种熟悉的、直观的开发环境,以及一种可选用汇编语言程序提高性能的简单方法。本文将讨论在当今开发环境下对DSP的编程策略和技巧。

HLL与汇编语言——两者的结合可能是最佳的

当同意开发基于DSP的一项任务时,必须要做的一项工作就是确定使用哪一种程序设计方法学。这种选择通常发生在汇编语言和高级语言(HLL)(例如C或C++语言)之间。做出这项决定需要考虑许多因素,因此了解每一种方法所具有的优点和缺点非常重要。

C和C++的优点包括模块性、便携性以及可重复利用性。不仅大部分拥嵌入式程序员拥有使用某种高级语言的经验,而且还有大量的代码基存在,从而可以使用相对简单的方法将它们从现有的微控制器(MCU)或DSP平台移植到新的DSP平台上。由于汇编语言是特殊体系结构的,因此重复利用通常被限制在相同处理器系列的设备中。此外,在一个开发团队内,通常希望各个团队做过不同系统模块的编码,而HLL允许这些具有交叉功能的团队无需了解处理器。

传统的汇编语言由于其神秘的语法和古怪的缩写而长期以来不被人们所看好。然而,在当今,这些因素并不是使用所谓的“代数语法”体系结构的问题。图1所示是使用传统风格和使用代数格式的典型DSP指令实例。很显然后者的结构更加直观。

-1-

<translation of figure1>

图1:传统汇编语法与现代代数语法的比较。在所提供的实例中,r寄存器是数据寄存器,p寄存器是指针寄存器。

操作类型

传统汇编语法

代数汇编语法

移动寄存器内容

mov r7 , r0

r7 = r0

add r0 , r1 , r2

r0 = r1 + r2

sub r3 , r3 , r1

r3 = r3 – r1

从存储器加载到寄存器

lw r5 , p3

r5 = [p3]

将寄存器的内容存到存储器

sw r1 , p0

[p0] = r1

如果输入寄存器相等则条件跳转到_equal,否则跳转到_not_equal

beq r5 , r6, _equal

beq r5 , r6, _not_equal

cc = r5 == r6

if cc jump _location

if !cc jump _location

从存储器和增量指针寄存器加载到寄存器

lw r3 , p5

addi p5 , p5 , 1

r3 = [p5++]

用汇编语言编程一直很困难的一个原因是由于其数据流集中在DSP实际寄存器组、计算单元和存储器之间。用C或C++语言编程,这种操作通常通过使用变量和函数或过程调用出现在更加抽象的处理等级之间,从而使得代码更容易跟随。

当今的C/C++编译器资源相当丰富,许多编译器能够出色地完成将HLL代码编译成接近汇编代码。实际上,仅让编译器优化程序完成其工作通常最佳。然而,实际上编译器所完成的工作受限于工具开发商认为最重要的一些特定的功能。因此,在所有情形下它都无法超越手工汇编代码的性能。

底线是指在为提高DSP执行效率必须优化重要的处理密集代码模块时开发商使用的汇编语言。HLL编译优化转换可以出色地完成该项工作,但它对DSP数据流和计算的直接控制的精心设计是无与伦比的。这就是为什么设计工程师通常结合使用C/C++和汇编语言。HLL适合于控制和基本的数据操作,而汇编语言适合于高效的数字计算。

-2-

适合高效编程的体系结构的特性

为了使汇编语言程序员能够有效地完成工作,极为重要的是了解处理器的结构类型,以便能够区分不适合超高速数字计算的那些DSP处理器。这些适合高效编程的体系结构结构特性包括:

  • 专用寻址模式
  • 硬件环路结构
  • 高速缓冲存储器
  • 每周期多次操作
  • 互锁流水线
  • 灵活的数据寄存器文挡

这些特性在计算效率方面会产生巨大的差别。让我们依次讨论每一种特性。

专用寻址模式

允许处理器在单周期内访问多个数据字需要完全灵活的地址产生方式。除了需要更大的以DSP为中心的16 bit和32 bit边界的访问尺寸外,还需要字节寻址以达到最有效的处理。这是非常重要的,因为一些普通应用(例如许多基于视频的系统)都是按照8 bit数据操作。当处理器访问限制在单一边界内时,处理器可能需要额外的周期来掩蔽掉相关的位(bit)。

另外一种有利的寻址能力是“循环缓冲”。该特性必须直接由处理器支持,无需专门的软件管理开销。循环缓冲允许程序员定义存储器中的缓冲器并且自动跨越它们。一旦缓冲器设置好,则无需专门的软件交互操作数据。地址发生器处理非同式跨幅,更重要的是可以处理图2所示的“环绕式”特性。如果没有这种自动地址产生,程序员必须手动跟踪缓冲器,从而浪费了宝贵的处理周期。

-3-

<translation of figure  2>

Address=地址

1st Access=1st访问

2nd Access=2nd访问

3rd Access=3rd访问

4th Access=4th访问

5th Access=5th访问

  • 基地址和起始索引地址=0x0
  • 索引地址寄存器I0指向地址0x0
  • 缓冲器长度L=44

(11个数据元素×4字节/元素)

  • 修改寄存器 M0=16

(4个数据元素×4字节/元素)

实例代码:

R0 = [I0++M0]; //R0 = 1 & I0 执行后指向0x10

R1 = [I0++M0]; //R1 = 5 & I0 执行后指向0x20

R2 = [I0++M0]; //R2 = 9 & I0 执行后指向0x04

R3 = [I0++M0]; //R3 = 2 & I0 执行后指向0x14

R4 = [I0++M0]; //R4 = 6 & I0 执行后指向0x24

图2:循环缓冲实例

用于高效信号处理运算〔例如快速傅立叶变换(FFT)和离散余弦变换(DCT)〕的一种重要寻址模式是比特翻转。顾名思义,“比特翻转”就是将二进制地址中的比特翻转。也就是说将权值最小的比特与权值最大的比特交换位置。由基为2的蝶形所要求的数据顺序是“已翻转比特”的顺序,因此比特翻转索引用来组合FFT级。我们可以计算软件中的这些比特翻转索引,但是这样做效率非常低。图3所示是比特翻转地址流实例。

-4-

<translation of figure 3>

Address LSB=地址LSB

Input buffer=输入缓冲器

Bit-reversed buffer=比特翻转缓冲器

实例代码:

LSETUP(起始, 终止)LC0=P0;       //循环数P0=8

起始:R0 = [I0] || I0 += M0(BREV);   //I0指向输入缓冲器,在比特翻转过程中自动增加

终止:[I2++] = R0;                 //I2指向比特翻转缓冲器

图3:硬件比特翻转机理

硬件循环结构

循环在通信处理算法中是一项很重要的特性。有两个与循环有关的关键特性能够改进多种算法的性能。第一个特性称作“零开销硬件循环”。随着寻址能力的提高,循环结构可以在硬件中实现。此外,当该项功能用软件实现时,相关的开销可减小到实时处理预算。零开销循环允许程序员通过设置计数值并且定义循环边界初始化循环。处理器将继续执行循环直到计数结束。

零开销循环是大多数处理器必不可少的一部分,但“硬件循环缓冲器”实际上可以提高循环结构的性能。它们用作循环中所执行指令的一种高速缓冲存储器。例如,在第一次执行完一个循环之后,可将该指令保存在循环缓冲器中,从而无需在每一次循环都反复地重取相同指令。这样可以通过将循环指令保存在能够在单周期内访问的缓冲器中来节省大量的周期数。该特性不需要程序员进行额外的设置,但是需要知道循环缓冲器的尺寸以合理地选择循环大小。

高速缓冲存储器

-5-

通常典型的DSP具有少量的快速、内置存储器。MCU通常可以访问大量的外部存储器。分层存储器体系结构将这两种方案的优点结合在一起,从而可提供几种等级具有不同性能的存储器。对于需要最高确定性的应用,可以在单个核心时钟周期内访问内置SRAM。对于具有大代码尺寸的系统,可提供大量、较长等待延迟的片内和片外存储器。

这种分层存储器体系结构本身仅仅是发挥一定的作用,由于当今的高速处理器本身都愿意以非常低的速度高效运行,因为大的应用程序只能装在较慢的外部存储器中。此外,程序员*手动将关键代码移入和移出内部SRAM。但是,通过将数据和指令高速缓冲存储器加到该体系结构中后,外部存储器更易于管理。高速缓冲存储器可以减少将指令和数据移入处理器内核的手工操作。通过无需考虑进入内核的数据和指令流管理而大大简化了编程模式。

图4示出在需要指令时将其从外部存储器移入指令的典型存储器配置。指令高速缓存通常采用了某种类型的最近最少使用(LRU)算法,从而确保更常用的指令代替较少使用的指令。该图也示出将一些内置数据存储器配置为高速缓存和部分SRAM的能力可以优化性能。直接存存储器访问(DMA)控制器能够直接控制内核,同时当需要表中数据时将其读入数据高速缓存。

-6-

<translation of figure 4>

图4:可配置的高速缓存和存储器结构可以优化数据移动

Instruction Cache=指令高速缓存

Way 1=通路1

Way 2=通路2

Way 3=通路3

Way 4=通路4

Data Cache=数据高速缓存

Data SRAM=数据SRAM

Buffer 1=缓冲器1

Buffer 2=缓冲器2

Buffer 3=缓冲器3

On-chip memory: Smaller capacity but lower latency=内置存储器:减小容量,但降低等待时间

High bandwidth cache fill=宽带宽高速缓存填充

High-speed DMA=高速DMA

Large External Memory=大容量外部存储器

Func_A=函数A

Main()=主函数()

Table=表

Data=数据

High-speed peripherals=高速外围设备

Off-chip memory: Greater capacity but larger latency=片外存储器:大容量,但提高等待时间

Once cache is enabled and the DMA controller is configured, the programmer can focus on core algorithm development.

=一旦高速缓存允许并且DMA控制器配置完毕,则程序员即可集中精力于内核算法的开发。

每周期多次操作

通常以每秒执行多少百万条指令(MIPS)来衡量处理器。然而,对于当代的处理器这可能会引起人们的误解,因为人们困惑于究竟是什么构成一条指令。例如,曾经被保留用在高成本并行处理器中的多发布指令目前也可用在低成本、定点处理器中。除了在每个核心处理器周期内完成多条ALU或MAC操作外,在相同周期内还可完成额外的数据处理和数据存储。该存储器通常划分为子若干个存储器组,这些存储器组可以被内核双向访问并且被DMA控制器随机访问。鉴于上述的基于硬件的寻址算法的分解方法,很明显其可以在单个周期内完成许多操作。

图5示出多操作指令的一个实例。如图所示,在一个周期内除了完成两条独立的MAC操作外,在相同的处理器时钟周期内还完成了数据读取和数据存储。

-7-

<translation of figure 5>

multiplication R0.H*R2.H=相乘R0.H*R2.H

accumulation to A1=加到A1上

store to R1.H=存储到R1.H

multiplication R0.L*R2.H=相乘R0.L*R2.H

accumulation to A0=加到A0上

store to R1.L=存储到R1.L

store of two registers R1.H and R1.L to memory for use in next instruction

=使用下一条指令将R1.H和R1.L两个寄存器的内容存储到存储器

Increment pointer register I1 by 4 bytes=递增指针寄存器增加4个字节

Memory=存储器

load of two 16-bit registers R2.H and R2.L from memory for use in next instruction

=使用下一条指令从存储器内容加载两个16 bit寄存器R2.H和R2.L

Decrement pointer register I1 by 4 bytes=递增指针寄存器减少4个字节

图5:Blackfin多发布指令在单周期内完成几次操作

互锁流水线

随着处理器速度的增加,从整个电路级来看处理流水线必定会变得更深。了解这一点非常重要,因为当需要汇编语言编程时,流水线可能会使编程更具有挑战性。然而,有些处理器具有“互锁”流水线。这意味着当完成汇编语言编程时,程序员不必手动调度或跟踪通过流水线的数据和指令。处理器会自动处理这些时序的事情。

灵活的数据寄存器文档

最后,另外一个补充特性是通用数据寄存器集。在传统的定点DSP中,字长通常是固定的。然而,具有能够用作一个32 bit字(例如R0)或两个16 bit字(例如分别用于低8 bit和高8 bit字的R0.L和R0.H)的数据寄存器是非常有利的。在双MAC系统中,这允许在单周期内操作四个16 bit数据。

代码比较和分析

上述的体系结构框架是高效DSP编程的基础。如果程序员能够利用所有潜在的处理器特性,则可以非常快地完成许多通用的数字运算算法。下面是选出的一些通用算法,描述了如何在DSP上执行它们。请注意,当需要在汇编级别上检验代码效率时,当代优化的DSP编译器可以使用与处理汇编编程器相同的规则。为说明这一点,在本例中采用了Blackfin处理器汇编语言。

-8-

点积

点积或者标量积在度量两个向量的正交性时是很有效的操作。大多数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 ++ ] ;

下面几点说明了简化这种紧凑编码的DSP体系结构特性。

硬件循环缓冲器和循环计数器在每次迭代的末端不需要跳转指令。由于点积是乘积总和,因此其可在一次循环中完成。许多精简指令集计算机(RISC)的MCU在每次迭代的末端都使用跳转指令以处理循环的下一次迭代。该汇编程序所示是LSETUP指令,它是执行循环所需的唯一指令。

多发布指令允许在相同的周期内执行多条指令和两次数据访问。在每一次迭代时,必须先读取a[i]和b[i]值,然后相乘,最后写回到可变输出的运行总和中。在许多MCU平台上,这实际上等于四条指令。汇编代码的最后一行示出在一个周期内可执行的所有操作。

并行ALU操作允许同时执行两条16 bit指令。汇编代码示出在每次迭代中使用的两个累加器单元(A0和A1)。这将迭代次数减少了50%,从而有效地将原来的执行时间减少了一半。

FIR算法

有限脉冲响应(FIR)滤波器是一种等价于卷积操作很常见的滤波器结构。A通过C操作看起来非常类似于点积:

// 将信号取样到循环缓冲器中

x[cur] = sampling_function();

cur = (cur+1)%TAPS; // 在循环中增加cur指针

// 完成乘加

y = 0;

for (k=0; k<TAPS; k++) {

  y += h[k] * x[(cur+k)%TAPS];

}

使用汇编编写的FIR内核的重要部分表明了一种类似于点积的格式。实际上,使用相同DSP特性提供算法执行的最佳性能。在这个特定的例子中,取样值存储在R0寄存器中,同时系数存储在R1寄存器中。

                                         

                                                              //  P0 存有滤波器抽头

R0=[I0++] || R1=[I1++];                               // 设置R0和R1的初始值

A1=A0=0;                                           // 将累加器置零

LSETUP (loop1, loop1) LC0 = P0; // 设置内部循环

loop1: A1+=R0.L*R1.L, A0+=R0.H*R1.H || R0 = [I0++] || R1 = [I1++];  // 计算

除了具有所描述的用于点积的特性外,上面所示的FIR算法也可使用循环缓冲。

循环缓冲器不需要直接取余运算。在C代码片段中,%(取余)运算符可提供一种用于循环缓冲的机理。正如汇编内核中所示,这些取余运算符不能被译为内循环中的其它指令。相反,数据地址发生器寄存器I0和I1可在外循环中配置以在循环到达系数缓冲器边界时能够自动返回起始处。

FFT算法

快速傅立叶变换(FFT)是许多信号处理算法不可缺少的一部分。其特性之一是输入向量按照连续时间顺序排列,但输出按比特翻转顺序排列。大多数传统的通用处理器要求程序员实现单独的程序以整理比特翻转输出。在DSP平台上,比特翻转可用于寻址引擎。

比特翻转寻址在实现FFT时不需要单独的比特翻转程序。其允许硬件自动将FFT算法的输出进行比特翻转而无需程序员编写额外的程序,从而改进了性能。

除了上面所提到的指令外,有些处理器也包括额外一套专用指令以支持多种应用。这些指令的目的就是进一步提高对算法的处理能力,例如维特比算法、Huffman编码以及许多其它比特操作程序。

显然,当为一个基于DSP的应用定义编程策略时需要考虑很多因素。在大多数情况下使用带有编译和优化能力很强的C或C++语言可产生鲁棒性的结果,但是手工汇编通常是获得处理器额外性能的最佳方法。然而,这种尝试必须在完全掌握了促进高效编码的体系结构之后方可以采用。

上一篇:ARM DSP库CMSIS-DSP的使用——以STM32F4浮点FFT为例 [原创]


下一篇:js JSON对象属性