当设计人员试图从算法中获得最佳性能但软件方法没有选择时,可以尝试通过硬件/软件重新分区进行加速。 FPGA 便于软件模块和硬件模块的交换,而无需更改处理器或进行板级更改。 本文介绍如何使用FPGA实现算法的硬件加速。
如果您想从代码中获得最佳性能,方法包括优化算法、使用查找表而不是算法、将所有内容转换为本机字大小、使用注册变量、展开循环,甚至可能使用汇编代码。 如果所有其他方法都失败了,您可以迁移到更快的处理器,使用不同的处理器架构,或者将代码分成两部分并在两个处理器上并行运行。 然而,如果有一种方法可以将这些对时间要求严格的代码段转换为运行速度提高 5-100 倍的函数调用,并且这种方法是可用于软件开发的标准工具,该怎么办? 可信吗? 现在,利用可编程逻辑作为硬件加速的基础可以使这成为现实。
图 1:具有自定义指令的可配置处理器架构。
低成本可编程逻辑在嵌入式系统中越来越普遍,为系统设计人员提供了无需对处理器或架构进行重大更改即可实现更高性能的选择。 可编程逻辑将计算密集型功能转换为硬件加速功能。 从软件角度来看,这只是对自定义硬件模块进行函数调用,但运行速度比通过汇编语言优化或将算法转换为查找表的相同代码要快得多。
1.硬件加速
首先,我们来探讨一下什么是硬件加速,以及将算法实现为自定义指令与使用硬件外围电路之间的区别。 硬件加速是指用硬件模块代替软件算法,充分利用硬件固有的快速特性。 从软件角度来看,与硬件加速模块的接口就像调用函数一样。 唯一的区别是该函数驻留在硬件中并且对于调用函数是透明的。
根据算法的不同,执行时间最多可加快 100 倍。 硬件执行各种操作的速度要快得多,例如执行复杂的数学函数、将数据从一个地方移动到另一个地方以及多次执行相同的操作。 在本文后面,我们将讨论一些通常在软件中完成的操作,这些操作可以通过硬件加速实现显着的性能改进。
如果您在系统设计中使用 FPGA,则可以在设计周期中随时添加定制硬件。 设计人员可以立即编写软件代码,并在最终确定之前在硬件部分上运行它。 此外,可以采用增量方法来确定代码的哪些部分应该用硬件而不是软件来实现。 FPGA供应商提供的开发工具可以实现硬件和软件之间的无缝切换。 这些工具可以生成总线逻辑和中断逻辑的HDL代码,并可以根据系统配置定制软件库和文件。
2. RISC 和一些 CISC
精简指令集计算 (RISC) 架构的目标之一是保持指令简单,以便它们能够运行得足够快。 这与复杂指令集计算 (CISC) 架构形成鲜明对比,复杂指令集计算 (CISC) 架构通常执行指令的速度不快,但每条指令执行更多处理。 这两种架构都被广泛使用并且各有优点。
如果能根据具体应用,将RISC的简单、速度与CISC的强大处理能力结合起来,岂不是两全其美? 事实上,这正是硬件加速的作用。 添加为应用程序定制的硬件加速模块可以提高处理能力并降低代码复杂性和密度,因为硬件模块取代了软件模块。 可以这么说,您正在用硬件来换取速度和简单性。
定制指令及硬件外围电路方法
硬件加速模块有两种实现方式。 一种是自定义指令,几乎可以在每个可配置处理器中实现,这是使用可配置处理器的主要优点。 如图 1 所示,自定义指令被添加为算术逻辑单元 (ALU) 的扩展。 处理器只知道像其他指令一样的自定义指令,包括拥有自己的操作码。 对于C代码,宏是自动生成的,因此使用自定义指令与调用函数相同。
如果一条自定义指令需要几个时钟周期才能完成并连续调用,则可以采用流水线方式实现。 这会在每个时钟周期产生一个结果,但有一些初始延迟。
硬件加速模块的另一种实现方式是硬件外围电路。 在这种方法中,数据不会传递给软件功能,而是写入存储器映射的硬件外围电路。 计算是在CPU外部完成的,因此CPU可以在外围电路工作时继续运行代码。 事实上,代替软件算法的只是普通的硬件外围电路。 与自定义指令的另一个区别是,硬件外围电路可以访问系统中的其他外围电路或存储器,而无需CPU干预。
根据硬件需要做什么、如何工作以及需要多长时间,您可以决定自定义指令还是硬件外围电路更合适。 对于可以在几个周期内完成的操作,自定义指令通常更好,因为它们产生的开销更少。 对于外围电路来说,一般需要多条指令来写入控制寄存器、状态寄存器和数据寄存器,并需要一条指令来读取结果。 如果计算需要多个周期,最好实现外围电路,因为它不会影响CPU管道。 或者,您可以实现前面描述的流水线定制指令。
另一个区别是自定义指令需要有限数量的操作数并返回结果。 操作数根据处理器指令集架构的不同而变化。 对于某些操作来说,这可能看起来很麻烦。 另外,如果需要硬件对内存或内存中的其他外围电路进行读写,则必须使用硬件外围电路,因为自定义指令无法访问总线。
图 2:16 位 CRC 算法的硬件实现。 ()
3. 选择代码
当您需要优化 C 代码以满足某些速度要求时,您可能需要运行代码克隆工具或亲自检查代码以了解代码的哪一部分导致系统停顿。 当然,这需要熟悉代码才能知道瓶颈在哪里。
即使找到了瓶颈,优化它仍然是一个挑战。 一些解决方案使用本机字大小的变量、具有预先计算值的查找表以及通用软件算法优化。 这些技术可以使执行速度提高数倍。 优化 C 算法的另一种方法是用汇编语言编写它们。 在过去这种做法可以实现非常好的改进,但是今天的编译器在优化C算法方面已经做得很好,所以这种性能的改进是有限的。 如果需要显着提高性能,传统的软件算法优化技术可能还不够。
然而,硬件实现的算法比软件实现的功能强大 100 倍也就不足为奇了。 那么,您如何决定将哪些代码转移到硬件实现呢? 不要将整个软件模块转换为硬件,而是选择在硬件中运行速度特别快的操作,例如将数据从一个位置复制到另一个位置、繁重的数学运算以及任何多次运行的循环。 如果一个任务由多个数学运算组成,您还可以考虑在硬件中加速整个任务。 有时,仅加速任务中的一项操作就足以满足性能要求。
4. 示例:CRC算法的硬件加速
由于计算量大且重复,循环冗余校验(CRC)算法或任何“校验和”算法是硬件加速的不错选择。 接下来我们将讨论如何通过CRC算法的优化过程来实现硬件加速。
首先,使用传统的软件技巧来优化算法,然后转向自定义指令来加速算法。 我们将讨论不同实现方法的性能比较和权衡。
CRC算法可用于验证数据在传输过程中是否被损坏。 这些算法很受欢迎,因为它们具有很高的错误检测率,并且由于在数据信息中添加了 CRC 校验位,因此不会对数据吞吐量产生太大影响。 然而,CRC 算法比一些简单的校验和算法具有更大的计算要求。 尽管如此,提高的错误检测率使得该算法值得实施。
一般来说,发送端对要发送的报文进行CRC算法,并将CRC结果添加到报文中。 消息的接收端对包含CRC结果的消息进行相同的CRC操作。 如果接收端的结果与发送端的结果不同,则说明数据已损坏。
CRC 算法是涉及二进制模除 (-2) 的密集数学运算,即数据消息的余数除以 16 或 32 位多项式(取决于所使用的 CRC 标准)。 此操作通常通过 XOR 和移位的迭代过程来实现,这相当于使用 16 位多项式时每个数据字节有数百条指令。 如果发送数百个字节,计算量可能会达到数万条指令。 因此,任何优化都会显着提高吞吐量。
代码清单 1 中的 CRC 函数有两个参数(消息指针和消息中的字节数),并返回计算出的 CRC 值(余数)。 尽管该函数的参数是字节数,但计算是逐位执行的。 该算法效率不高,因为所有操作(AND、移位、XOR 和循环控制)都必须逐位执行。
列表 1:逐位执行的 CRC 算法的 C 代码。
typedef unsigned char crc;
#define WIDTH (8 * sizeof(crc))
#define TOPBIT (1 << (WIDTH - 1))
crc crcSlow(unsigned char const message[], int nBytes)
{
crc remainder = 0;
for (int byte = 0; byte < nBytes; ++byte)
{
remainder ^= (message[byte] << (WIDTH - 8));
for (unsigned char bit = 8; bit > 0; bit--)
{
if (remainder & TOPBIT)
{
remainder = (remainder << 1) ^ POLYNOMIAL;
}
else
{
remainder = (remainder << 1);
}
}
}
return (remainder);
}
4.1 传统软件优化
图3:带有CRC外围电路和DMA的系统模块示意图。
让我们看一下如何使用传统的软件技巧来优化 CRC 算法。 由于 CRC 运算中的操作数之一多项式(除数)是常数,因此可以预先计算字节宽 CRC 运算的所有可能结果并将其存储在查找表中。 这样就可以通过读查找表动作逐字节执行操作。
使用该算法时,这些预先计算的值需要存储在内存中。 您可以选择 ROM 或 RAM,只要在开始 CRC 计算之前对存储器进行初始化即可。 查找表有 256 个字节,表中的每个字节位置都包含一个 CRC 结果,总共给出 256 个可能的 8 位消息(无论多项式大小如何)。
清单2显示了使用查找表方法的C代码,包括生成 table()中的值的代码。
crc crcTable[256];
void crcInit(void)
{
crc remainder;
for (int dividend = 0; dividend < 256; ++dividend)
{
remainder = dividend << (WIDTH - 8);
for (unsigned char bit = 8; bit > 0; bit--)
{
if (remainder & TOPBIT)
{
remainder = (remainder << 1) ^ POLYNOMIAL;
}
else
{
remainder = (remainder << 1);
}
}
crcTable[dividend] = remainder;
}
}
crc crcFast(unsigned char const message[], int nBytes)
{
unsigned char data;
crc remainder = 0;
for (int byte = 0; byte < nBytes; ++byte)
{
data = message[byte] ^ (remainder >> (WIDTH - 8));
remainder = crcTable[data] ^ (remainder << 8);
}
return (remainder);
}
整个计算被简化为一个循环,其中包含两个 XOR、两个移位操作以及每字节(而非位)两个加载指令。 基本上,您正在用查找表存储空间来换取速度。 该方法比逐位计算方法快9.9倍,对于某些应用来说已经足够了。 如果需要更高的性能,可以尝试编写汇编代码或增加查找表大小以挤出更多性能。 但是,如果需要20倍、50倍甚至500倍的性能提升,则应该考虑使用硬件加速来实现算法。
表1:不同大小的数据模块下CRC算法测试的比较结果。
4.2 采用定制指令方式
CRC 算法由连续的 XOR 和移位操作组成,并且可以用很少的逻辑轻松地在硬件中实现。 由于该硬件模块只需要几个周期来计算CRC,因此使用自定义指令来实现CRC计算比使用外围电路更好。 此外,系统中不需要涉及任何其他外围电路或存储器。 只需一个微处理器即可支持自定义指令,通常是可配置微处理器。
当在硬件中实现时,该算法应一次执行 16 或 32 位计算,具体取决于所采用的 CRC 标准。 如果使用CRC-CCITT标准(16位多项式),最好一次执行16位计算。 如果使用8位微处理器,效率可能不是很高,因为需要额外的周期来加载操作数值并返回CRC值。 图 2 显示了在硬件中实现 16 位 CRC 算法的内核。
信号 msg(15..0) 一次一位移入 XOR/移位硬件。 清单 3 显示了一些用于在 64KB 数据块上计算 CRC 的 C 代码示例。 此示例适用于 Nios 嵌入式处理器。
清单 3:使用自定义指令进行 CRC 计算的 C 代码。
unsigned short crcCompute(unsigned short *data_block, unsigned int nWords)
{
unsigned short* pointer;
unsigned short word;
word = nm_crc (0xFFFF, 1);
for (pointer = data_block; pointer < (data_block + nWords); pointer ++)
word = nm_crc(*pointer, 0) return (word);
}
int main(void)
{
#define data_block_begin (na_onchip_memory)
#define data_block_end (na_onchip_memory + 0xffff)
unsigned short crc_result;
unsigned int data_block_length = (unsigned short *)data_block_end - \
(unsigned short *)data_block_begin + 1;
crc_result = crcCompute((unsigned short *)data_block_begin, data_block_length);
}
当使用自定义指令时,用于计算 CRC 值的代码是函数调用或宏。 当为 Nios 处理器实现自定义指令时,系统构建工具会生成一个宏。 本例中为(),可用于调用自定义指令。
在开始 CRC 计算之前,需要先初始化自定义指令内的 CRC 寄存器。 加载初始值是CRC标准的一部分,并且对于每个CRC标准都是不同的。 接下来,循环将为数据模块中的每 16 位数据调用 CRC 自定义指令。 这种自定义指令实现比逐位实现快 27 倍。
4.3 CRC外围电路方法
如果将CRC算法实现为硬件外围电路,并使用DMA将数据从存储器传输到外围电路,则速度可以进一步提高。 这种方法将消除处理器为每次计算加载数据所需的额外周期。 DMA 可以在该外围电路完成先前的 CRC 计算的时钟周期内提供新数据。 图3所示为采用DMA和CRC外围电路实现加速的系统模块示意图。
在 64KB 数据模块上,使用具有 DMA 的定制外围电路可实现比逐位计算的纯软件算法快 500 倍的性能。 要知道,随着数据模块大小的增加,使用DMA获得的性能也随之增加。 这是因为设置 DMA 只需很少的开销,并且设置后 DMA 运行速度极快,因为它可以在每个周期传输数据。 因此,如果只有几个字节的数据,使用DMA并不划算。
这里讨论的所有算法均使用 CRC-CCITT 标准(16 位多项式),并在 FPGA 的 Nios 处理器上实现。 表 1 显示了各种数据长度的测试比较结果,以及大致的硬件使用情况(FPGA 中的内存或逻辑单元)。
可以看出,算法使用的硬件越多,算法速度越快。 这是用硬件资源换取速度。
5、FPGA的优点
当使用基于 FPGA 的嵌入式系统时,不必在设计周期开始时就为每个模块选择硬件或软件。 如果在设计过程中需要一些额外的性能,可以利用 FPGA 中的现有硬件资源来加速软件代码的瓶颈部分。 由于 FPGA 中的逻辑单元是可编程的,因此可以针对特定应用定制硬件。 因此,只需使用所需的硬件即可,无需进行任何板级更改(前提是FPGA中的逻辑单元足够)。 设计人员无需切换到新处理器或编写汇编代码即可做到这一点。
使用具有可配置处理器的 FPGA 获得设计灵活性。 设计人员可以选择如何在软件代码中实现每个模块,例如使用自定义指令或硬件外围电路。 此外,通过添加定制硬件,可以实现比现成微处理器更好的性能。
另外要知道的是,FPGA 拥有充足的资源,可配置处理器系统可以充分利用这一资源。
算法可以用软件或硬件来实现。 出于简单性和成本的原因,通常使用软件来实现大多数操作,除非需要更高的速度来满足性能目标。 软件可以优化,但有时这还不够。 如果需要更高的速度,使用硬件加速算法是一个不错的选择。
FPGA 可以更轻松地相互交换软件和硬件模块,而无需更改处理器或进行板级更改。 设计人员可以在速度、硬件逻辑、内存、代码大小和成本之间进行权衡。 FPGA 可用于设计定制嵌入式系统,以添加新功能并优化性能。