HLS学习笔记
1. 什么是HLS
HLS是一种高级综合技术,它允许开发人员使用高级语言(如C、C++和SystemC)来描述数字电路的行为和功能,然后将其转换为硬件电路实现。这种转换过程是自动完成的,因此开发人员无需手动编写硬件描述语言(HDL)。
HLS的主要目的是简化FPGA设计流程,提高设计效率和设计质量。通过使用高级语言来描述电路,开发人员可以更快速地进行设计和调试,同时也可以更容易地对电路进行修改和优化。此外,HLS还可以自动生成优化后的硬件电路,从而提高性能和资源利用率。
2. HLS开发流程
HLS开发流程大致可以分为以下几个步骤:
- 设计和实现HLS模块的功能和行为。这一步通常使用C/C++等高级语言来描述电路行为,包括输入/输出端口、计算逻辑和状态机等。
- 使用HLS工具将高级语言代码转换为硬件电路实现。这一步通常由HLS工具自动完成,但也需要开发人员进行一些配置和调整,以便获得所需的性能和资源利用率。
- 对生成的硬件电路进行验证和调试。这一步通常需要使用仿真工具对硬件电路进行验证,以确保其与高级语言代码的行为一致,并进行调试以解决任何问题。
- 将生成的硬件电路与其他硬件/软件组件进行集成和部署。这一步通常需要将生成的硬件电路与其他硬件/软件组件进行集成,以实现最终的系统功能。
3. HLS基本语法
HLS使用特定的语法和指令来描述电路行为和特性。下面是一些基本的HLS语法和指令:
3.1. #pragma HLS
#pragma HLS是HLS中最常用的指令之一,用于控制HLS工具生成的硬件电路的特性和行为。该指令的语法如下:
#pragma HLS directive_name [directive_options]
其中,directive_name是具体的指令名称,directive_options是指令的参数。
以下是一些常见的#pragma HLS指令:
- #pragma HLS interface:定义模块的输入/输出端口类型和属性。
- #pragma HLS array_partition:将数组分区为多个部分,以优化资源利用率和性能。
- #pragma HLS pipeline:将循环展开为流水线以提高性能。
- #pragma HLS inline:将指定的函数内联以提高性能。
- #pragma HLS reset:定义复位信号的属性和行为。
- #pragma HLS latency:定义操作的延迟,以便HLS工具可以更好地优化电路。
3.2. HLS数据类型
HLS支持C/C++语言中的大多数基本数据类型,例如int、float、double等。此外,HLS还支持以下数据类型:
- ap_int和ap_uint:分别表示带符号整数和无符号整数。
- ap_fixed和ap_ufixed:分别表示带符号和无符号的固定点数。
- ap_vector和ap_matrix:分别表示向量和矩阵。
3.3. HLS模块定义
HLS模块定义类似于C/C++函数定义,包括模块名称、输入/输出端口、模块属性等。例如:
void my_module(
int input1,
int input2,
int output1,
int output2
) {
#pragma HLS interface ap_ctrl_none port=return
#pragma HLS interface ap_none port=input1
#pragma HLS interface ap_none port=input2
#pragma HLS interface ap_none port=output1
#pragma HLS interface ap_none port=output2
// 模块实现逻辑
}
上述代码中,my_module是一个HLS模块,有两个输入端口input1和input2,以及两个输出端口output1和output2。#pragma HLS interface指令用于定义端口的属性和类型,例如ap_ctrl_none表示该模块不包含任何控制信号,ap_none表示该端口不进行数据流控制。
3.4. 数组分区
HLS中的array_partition是一种用于在FPGA或ASIC中实现数组分区的指令。它允许将一个数组划分为多个小数组,每个小数组都可以独立地存储在FPGA或ASIC的不同部分,以实现并行化操作。这样可以提高设计的并行性,从而提高性能。使用 array_partition 可以帮助设计人员更好地利用硬件资源,提高设计效率。例如:
#define N 1024
int data[N];
#pragma HLS array_partition variable=data complete dim=1
上述代码中,data是一个长度为1024的数组。#pragma HLS array_partition指令用于将该数组分为多个部分,并指定每个部分的类型。complete表示将数组完全分区,dim=1表示将数组按行进行分区。
在HLS的array_partition指令中,dim参数是用于指定分区的维度的。它指示分区应该应用于哪个维度。对于一维数组,dim参数应该设置为1,而对于二维数组,dim参数应该设置为0或1。如果设置为0,则分区将应用于第一维,而如果设置为1,则分区将应用于第二维。例如,考虑以下代码段:
int A[4][8];
#pragma HLS array_partition variable=A dim=1 complete
这将A数组的第二维(即列)分为8个小数组,每个小数组包含4个元素。因此,每个小数组都可以并行地存储在FPGA或ASIC的不同部分,以实现并行化操作。
除了dim参数之外,array_partition指令还支持以下参数:
- variable: 指定要分区的数组变量名称。
- factor: 指定要分区的因子数。例如,#pragma HLS array_partition variable=A factor=2 将数组A分为两个块。
- type: 指定分区的类型。可选的类型包括block、cyclic、complete和none。block类型指定分区为块状分区,其中每个块包含连续的元素。
- cyclic类型指定分区为循环分区,其中每个块包含间隔相等的元素。
- complete类型指定分区为完全分区,其中所有分区包含相等数量的元素。
- none类型指定不进行分区。
- dim: 指定要应用分区的维度,可以是0或1,具体含义在前面回答中已经介绍。
这些参数可以组合使用以实现各种不同的分区方案,以便更好地利用FPGA或ASIC上的并行计算资源。
3.5. 流水线优化
HLS支持将循环展开为流水线,以提高性能。例如:
#define N 1024
int data[N];
void my_module(int *input, int *output) {
#pragma HLS interface ap_ctrl_none port=return
#pragma HLS interface ap_none port=input
#pragma HLS interface ap_none port=output
#pragma HLS pipeline II=1
for (int i = 0; i < N; i++) {
output[i] = input[i] * 2;
}
}
II=1表示该循环的迭代间隔为1,即将该循环展开为一个流水线。HLS工具将在流水线上并行执行多个操作,以提高性能。
#pragma HLS pipeline是在HLS(高层次综合)中使用的一种指令,用于指示编译器将循环展开为流水线。通过使用#pragma HLS pipeline,可以将循环分为多个阶段,从而允许每个阶段在不同的时钟周期内并行执行,从而提高系统的性能。
使用#pragma HLS pipeline指令可以有效地利用FPGA或ASIC上的并行计算资源,从而实现更高的时钟频率和更快的数据处理速度。它通常用于加速循环密集型的计算和数据处理任务,如图像处理、信号处理和矩阵计算等。
#pragma HLS pipeline指令的语法如下:
#pragma HLS pipeline [N]
其中N是可选参数,指示流水线的并行度。默认情况下,流水线的并行度等于1,即每个阶段在一个时钟周期内执行。如果指定了N,则表示将流水线分为N个阶段,从而允许每个阶段在不同的时钟周期内并行执行。通常,N的值应该是2的幂,以便更好地利用硬件并行计算资源。
除了N参数之外,#pragma HLS pipeline还支持一些其他的可选参数,包括:
- II:II代表“间隔间隔”(Iteration Interval)的缩写,是在HLS(高层次综合)中使用的一个指令参数,用于指示循环迭代之间的最小间隔。通过指定II参数,可以帮助编译器更好地理解循环的数据依赖关系,从而生成更高效的硬件设计。
- 在流水线化的循环中,每个迭代可以分为多个阶段,这些阶段可以在不同的时钟周期内并行执行。如果循环迭代之间的间隔太小,就可能会导致不同的阶段之间存在数据依赖关系,从而导致流水线暂停等性能问题。因此,可以使用II参数指定循环迭代之间的最小间隔,以便在硬件设计中解决这些问题。
- II的值应该是一个正整数,表示相邻两次迭代之间的最小间隔,通常以时钟周期为单位。例如,如果II=2,则表示每两次迭代之间必须间隔两个时钟周期,即第一个迭代在时钟周期1执行,第二个迭代在时钟周期3执行,第三个迭代在时钟周期5执行,以此类推。
- enable_flush:指示是否应该在输入结束时强制冲洗流水线,以避免在下一个输入时出现数据冲突。
- ii_reduction:指示是否应该将内部循环展开为累加器,从而进一步减少延迟和提高时钟频率。
下面是一个使用#pragma HLS pipeline指令的例子:
#pragma HLS pipeline II=1
for (int i = 0; i < N; i++) {
// ...
}
在上面的例子中,#pragma HLS pipeline指令将循环展开为流水线,并将流水线分为II=1个阶段,每个阶段在一个时钟周期内执行。通过流水线化,该循环可以更快地执行,并且可以在不同的时钟周期内并行执行。
3.6. 组合逻辑优化
HLS支持将指定的函数内联以提高性能。例如:
int add(int a, int b) {
return a + b;
}
int sub(int a, int b) {
return a - b;
}
int calc(int a, int b) {
return add(a, b) * sub(a, b);
}
void my_module(int *input, int *output) {
#pragma HLS interface ap_ctrl_none port=return
#pragma HLS interface ap_none port=input
#pragma HLS interface ap_none port=output
#pragma HLS inline
for (int i = 0; i < N; i++) {
output[i] = calc(input[i], input[i+1]);
}
}
上述代码中,calc函数内部调用了add和sub函数。#pragma HLS inline指令用于将calc函数内联,即将其实现逻辑直接嵌入到my_module中,以避免在运行时频繁调用函数。
在HLS(高层次综合)中,inline是一种指令,用于告诉编译器在编译时将函数调用内联展开,而不是生成实际的函数调用。
使用inline指令可以有效地减少函数调用带来的开销,因为它可以将函数体的代码直接插入到调用该函数的代码中,从而避免了函数调用的开销(如函数调用和返回操作以及参数的复制等)。这在一些高频率的数据通路设计中非常有用。
下面是一个使用inline指令的例子:
inline int add(int a, int b) {
return a + b;
}
int main() {
int x = 10, y = 20;
int z = add(x, y); // 实际上是将add函数的代码插入到这里
return 0;
}
在上面的例子中,inline指令告诉编译器将add函数的代码内联到调用它的地方,而不是生成实际的函数调用。这样,当程序运行时,将不会存在真正的函数调用,而是在main函数中直接执行add函数的代码,从而避免了函数调用的开销。
#pragma HLS inline是在HLS中使用的指令,用于指示编译器在编译时对特定函数进行内联展开优化。使用#pragma HLS inline可以告诉编译器将指定的函数内联展开,从而加速系统的性能。
#pragma HLS inline指令的语法如下:
#pragma HLS inline
int add(int a, int b) {
return a + b;
}
int main() {
int x = 10, y = 20;
int z = add(x, y); // 实际上是将add函数的代码插入到这里
return 0;
}
在上面的例子中,#pragma HLS inline指令告诉编译器将add函数内联展开,从而加速系统的性能。
3.7. 一些基本概念
- iteration latency: 从第一个输入到第一个输出的时间
- iteration interval(II): 每两次输出/输入之间的时间间隔
- total latency: 总时间,total latency = iteration latency + II * (number of items - 1),II可以改善total latency但不影响iteration latency
3.8. 完整示例
3.8.1. 矩阵乘法
以下是一个完整的HLS示例,展示了如何使用HLS实现一个矩阵相乘的功能。
#include <hls_stream.h>
#include <ap_axi_sdata.h>
#define N 32
#define M 32
#define P 32
typedef ap_axiu<32,1,1,1> data_t;
void matrix_multiply(
data_t A[N*M],
data_t B[M*P],
data_t C[N*P]
) {
#pragma HLS interface ap_ctrl_none port=return
#pragma HLS interface axis port=A
#pragma HLS interface axis port=B
#pragma HLS interface axis port=C
data_t a;
data_t b;
data_t c;
hls::stream<data_t> stream_A;
hls::stream<data_t> stream_B;
hls::stream<data_t> stream_C;
// 将输入数据流转换为流对象
for (int i = 0; i < N*M; i++) {
#pragma HLS pipeline II=1
a = A[i];
stream_A.write(a);
}
for (int i = 0; i < M*P; i++) {
#pragma HLS pipeline II=1
b = B[i];
stream_B.write(b);
}
// 矩阵乘法计算逻辑
for (int i = 0; i < N; i++) {
for (int j = 0; j < P; j++) {
#pragma HLS pipeline II=1
int sum = 0;
for (int k = 0; k < M; k++) {
a = stream_A.read();
b = stream_B.read();
sum += a.data * b.data;
}
c.data = sum;
c.keep = 15;
c.strb = 15;
c.last = (i == N-1 && j == P-1) ? 1 : 0;
stream_C.write(c)
}
}
// 将输出流转换为输出数据流
for (int i = 0; i < N*P; i++) {
#pragma HLS pipeline II=1
c = stream_C.read();
C[i] = c;
}
}
在上述示例中,我们定义了一个名为matrix_multiply
的函数,该函数用于计算两个矩阵的乘积,并将结果存储在输出矩阵中。我们使用了#pragma HLS interface
指令指定了函数的输入输出接口,以及数据类型。然后我们定义了一些流对象,用于在HLS中处理数据流。
在函数实现中,我们首先将输入的数据流转换为流对象,然后执行矩阵乘法计算。在矩阵乘法计算中,我们使用三重嵌套循环遍历矩阵,以计算矩阵乘积的每个元素。内部的循环使用#pragma HLS pipeline
指令进行流水线操作。最后,我们将输出流转换为输出数据流,并将其返回。
这个示例演示了HLS如何使用流水线和流对象来高效处理数据流,以及如何使用#pragma HLS interface
指令来指定HLS代码的接口和数据类型。对于HLS的新手来说,这个示例可以作为一个非常好的学习范例。
3.8.2. 函数调用和循环
下面我们再来一个简单的示例,演示如何在HLS中使用函数调用和循环。
假设我们有一个函数compute,它的作用是对一个数组进行一系列计算,并返回计算结果。现在我们想在HLS中使用这个函数,以便可以在FPGA上进行硬件加速。下面是示例代码:
#define N 1024
void compute(int A[N], int B[N], int C[N]) {
for (int i = 0; i < N; i++) {
B[i] = A[i] + 1;
}
for (int i = 0; i < N; i++) {
C[i] = B[i] * 2;
}
}
void top_function(int A[N], int C[N]) {
#pragma HLS interface ap_fifo port=A
#pragma HLS interface ap_fifo port=C
int B[N];
compute(A, B, C);
}
在上述示例中,我们定义了一个名为compute的函数,用于对一个数组进行一系列计算。然后我们定义了另一个函数top_function,它使用#pragma HLS interface指令指定了函数的输入输出接口和数据类型。在top_function中,我们首先声明一个数组B,用于存储compute函数的计算结果。然后我们调用compute函数,将输入数组A、输出数组C和中间数组B作为参数传递给该函数。最后,我们将C数组作为输出返回。
这个示例演示了如何在HLS中使用函数调用和循环。在HLS中,我们可以像在C语言中一样使用函数和循环来实现算法,从而更容易地将现有的软件算法移植到硬件中。同时,我们也可以使用#pragma HLS interface指令来指定函数的接口和数据类型,从而更方便地与其他硬件模块集成。
3.8.3. 流水线和并行化指令
接下来,我们将介绍如何在HLS中使用流水线和并行化指令来实现加速。
流水线是一种常见的并行化技术,它将一个任务分解为多个阶段,每个阶段可以并行执行。例如,在对一个长数组进行操作时,我们可以将其分为多个段,每个段可以并行处理。在HLS中,我们可以使用#pragma HLS pipeline指令来实现流水线加速。下面是一个示例代码:
#define N 1024
void top_function(int A[N], int C[N]) {
#pragma HLS interface ap_fifo port=A
#pragma HLS interface ap_fifo port=C
#pragma HLS pipeline
int B[N];
for (int i = 0; i < N; i++) {
#pragma HLS unroll
B[i] = A[i] + 1;
}
for (int i = 0; i < N; i++) {
#pragma HLS unroll
C[i] = B[i] * 2;
}
}
在上述示例中,我们使用#pragma HLS pipeline指令来指定函数中的循环可以流水线并行执行。在每个循环中,我们还使用#pragma HLS unroll指令来指定循环可以展开为多个迭代,以进一步提高并行度。这个示例演示了如何使用流水线和并行化指令来实现加速。
最后,我们需要注意一些常见的优化技巧,以进一步提高HLS的性能。例如,我们可以使用#pragma HLS array_partition指令将数组分区,以充分利用FPGA的存储资源。我们还可以使用#pragma HLS dataflow指令来实现数据流架构,从而更好地利用FPGA的并行性能。此外,我们还可以使用#pragma HLS stream指令来指定数据流接口,以优化数据传输和缓存。这些优化技巧可以进一步提高HLS的性能和效率。
4. HLS高级语法
在HLS中,还有一些高级特性,如定点数和浮点数支持,数据重用和代码生成等。我们在下面继续介绍一些高级特性和应用示例。
4.1. 定点数和浮点数支持
在FPGA中,通常使用定点数来表示数字,因为它可以更有效地利用硬件资源和提高运算速度。在HLS中,我们可以使用ap_fixed和ap_ufixed类型来表示定点数,它们具有定点位和整数位。例如:
#include "ap_fixed.h"
typedef ap_fixed<16, 8> fixed_point;
上述代码定义了一个16位定点数,其中8位为小数位,另外8位为整数位。我们还可以使用ap_float类型来表示浮点数,它具有单精度和双精度浮点数。例如:
#include "ap_float.h"
typedef ap_float<32> single_float;
typedef ap_float<64> double_float;
4.2. 数据重用
在HLS中,我们可以使用数据重用技术来减少计算量和存储器带宽。数据重用指的是在计算过程中多次使用同一数据,而不是每次都从存储器中读取数据。在HLS中,我们可以使用#pragma HLS array_reshape指令和#pragma HLS data_pack指令来实现数据重用。例如:
#define N 1024
void top_function(int A[N], int B[N]) {
#pragma HLS interface ap_fifo port=A
#pragma HLS interface ap_fifo port=B
int C[N];
for (int i = 0; i < N; i++) {
#pragma HLS pipeline II=1
#pragma HLS unroll factor=4
C[i] = A[i] + B[i];
}
#pragma HLS array_reshape variable=C complete dim=1
#pragma HLS data_pack variable=C
for (int i = 0; i < N/4; i++) {
#pragma HLS pipeline II=1
int sum = C[i*4] + C[i*4+1] + C[i*4+2] + C[i*4+3];
B[i] = sum;
}
}
在上述示例中,我们使用#pragma HLS array_reshape指令将数组C从一维数组重塑为二维数组,以利用数据重用。然后,我们使用#pragma HLS data_pack指令对数组C进行数据打包,以减少存储器带宽。最后,我们将数组C用于计算,并使用流水线和并行化指令来实现加速。
4.3. #pragma HLS interface
#pragma HLS interface是在HLS(高层次综合)中使用的一个指令,用于定义模块的接口,包括输入、输出端口和数据类型等。通过使用#pragma HLS interface,可以使HLS编译器了解模块的接口和数据类型,并生成相应的硬件设计。
具体而言,#pragma HLS interface指令的作用是将模块的输入和输出端口映射到HLS的IO接口上,从而将模块与外部环境连接起来。此外,还可以使用该指令指定输入和输出端口的数据类型、数据位宽、信号是否阻塞等属性,以便在硬件设计中生成相应的电路。
以下是一个使用#pragma HLS interface指令的简单例子:
#pragma HLS interface ap_ctrl_none port=return
#pragma HLS interface axis port=input
#pragma HLS interface axis port=output
void my_module(stream<data_t> &input, stream<data_t> &output) {
// ...
}
在上面的例子中,#pragma HLS interface指令定义了一个名为my_module的模块,该模块有两个输入输出端口input和output,数据类型为stream<data_t>,其中data_t是一个自定义的数据类型。第一行的ap_ctrl_none指示该模块不需要控制器端口,port=return表示该模块的返回值映射到HLS的返回端口上。
需要注意的是,#pragma HLS interface指令只是定义模块的接口,不会生成模块的实现。模块的实现应该在其他地方定义,例如在C或C++源代码中。在模块实现的代码中,需要使用与接口定义中相同的数据类型和端口名,以便与HLS生成的电路连接起来。同时,还需要使用HLS指令来指示编译器如何将模块转换为硬件电路,例如使用#pragma HLS design和#pragma HLS pipeline等指令。
#pragma HLS interface指令用于定义模块的接口和属性,以下是常用的参数:
- ap_none、ap_hs、ap_stable、ap_vld:指定输入/输出端口的数据类型。
- port=:指定模块的返回值端口名,必须在模块的接口中指定。
- axis、s_axilite、m_axi、s_axi等:指定输入/输出端口的接口类型,例如AXI总线、轴接口等。
- register、noflip、flatten等:指定输入/输出端口的属性,例如数据是否寄存器存储、是否需要数据翻转等。
以下是参数类型和接口协议对应图
图中的“ D”表示“ default”,表示 Vivado HLS 工具默认综合出来的接口。“ S”表示“ support”,表示 Vivado HLS 工具支持综合出来的接口。
4.4. #pragma HLS design
#pragma HLS design指令用于指示编译器如何将模块转换为硬件电路。以下是常用的参数:
- flatten:将模块展开为更简单的逻辑结构。
- pipeline:使模块内部的循环/分支语句并行执行,提高性能。
- inline:将模块内的函数调用展开为实际的函数代码。
- dataflow:指示编译器使用数据流编程模型进行优化。
- latency:指定模块的延迟目标。
- parallel:指定模块的并行度目标。
使用示例:
#pragma HLS design flatten
#pragma HLS design pipeline II=2
在使用#pragma HLS design时,需要将其放置在模块定义之前,并且模块定义必须在同一个文件中。在模块实现的代码中,可以使用该指令来指示编译器对模块进行优化,例如将模块展开为更简单的逻辑结构、并行执行循环/分支语句、调用函数等。同时,还可以使用其他HLS指令来进一步控制优化策略,例如#pragma HLS loop_tripcount、#pragma HLS dependence等。
4.5. 流式处理器
- 流式处理器:流式存储器(streaming memory)是一种用于解决数据存储和读取延迟问题的技术。使用流式存储器可以减少在高级综合(HLS)中数据存储和读取的延迟,从而提高设计的吞吐量和性能。
- 以下是使用流式存储器的一些技巧:
- 数据缓冲区:在HLS设计中,使用流式存储器可以将数据存储在缓冲区中,以便稍后读取。这可以减少存储和读取数据的延迟,从而提高设计的性能。
- 数据分块:使用流式存储器可以将数据分成块,每个块都可以作为单独的流进行处理。这可以帮助优化流水线设计,减少存储和读取数据的延迟。
- 数据流合并:在某些情况下,可以将多个数据流合并为一个数据流。这可以减少存储和读取数据的延迟,同时提高设计的性能。
- 数据流分裂:类似于数据流合并,数据流分裂可以将一个数据流分成多个数据流进行处理。这可以帮助优化流水线设计,减少存储和读取数据的延迟。
- 内存分配:在HLS设计中,可以使用内存分配技术来管理流式存储器。这可以减少存储和读取数据的延迟,同时提高设计的性能。
- 总之,使用流式存储器可以帮助优化HLS设计,减少数据存储和读取的延迟,从而提高设计的性能和吞吐量。
以下是一个简单的HLS流式处理器的例子,该流式处理器使用Pipelining和Loop Unrolling优化策略来提高性能。
#include <hls_stream.h>
// 定义流式处理器
void stream_processor(hls::stream<int>& in_stream, hls::stream<int>& out_stream, int num_elements) {
#pragma HLS interface ap_ctrl_none port=return
#pragma HLS interface axis port=in_stream
#pragma HLS interface axis port=out_stream
int buffer[4];
// 循环展开初始化,实现4元素缓冲区
for (int i = 0; i < 4; i++) {
#pragma HLS unroll
buffer[i] = 0;
}
// 流式处理器核心部分
for (int i = 0; i < num_elements; i++) {
#pragma HLS pipeline II=1
int input_data = in_stream.read();
// 向缓冲区添加新数据
buffer[0] = buffer[1];
buffer[1] = buffer[2];
buffer[2] = buffer[3];
buffer[3] = input_data;
// 计算输出
int output_data = buffer[0] + buffer[1] + buffer[2] + buffer[3];
// 输出到流
out_stream.write(output_data);
}
}
上述例子中,stream_processor函数实现了一个简单的流式处理器。它的输入是一个整数流,输出也是一个整数流。在函数内部,使用了Pipelining和Loop Unrolling优化策略,将处理逻辑拆分为多个阶段,并使每个阶段都可以并行执行。同时,通过循环展开的方式实现了一个4元素的缓冲区,以加速计算过程。在处理完所有输入数据之后,该函数会将输出流中的数据写回到主存中。
需要注意的是,在使用HLS开发流式处理器时,还需要考虑数据流的稳定性和流量控制等问题。为了确保稳定性,可以使用ap_stable等数据类型来声明输入/输出端口;而为了控制流量,可以使用ap_fifo等缓冲区类型来调整输入/输出流的大小。
4.6. #pragma HLS dataflow
#pragma HLS dataflow数据流指令: 将设计的功能划分成不同的阶段,让HLS能够对不同的功能阶段进行优化,提高综合后的效果。
#include "hls_stream.h"
void adder(hls::stream<int> &in, hls::stream<int> &out, int val) {
for (int i = 0; i < 10; i++) {
int x = in.read();
out.write(x + val);
}
}
void multiplier(hls::stream<int> &in, hls::stream<int> &out, int val) {
for (int i = 0; i < 10; i++) {
int x = in.read();
out.write(x * val);
}
}
void top(hls::stream<int> &in, hls::stream<int> &out, int val) {
#pragma HLS dataflow
hls::stream<int> intermediate;
adder(in, intermediate, val);
multiplier(intermediate, out, val);
}
4.7. #pragma HLS reset variable
指定变量在每个时钟周期的初始值。
int counter = 0;
void my_counter(bool reset, int &count) {
#pragma HLS reset variable=counter
if (reset) {
counter = 0;
} else {
counter++;
}
count = counter;
}
5. HLS技巧
下面我将为你介绍一些HLS的最佳实践,帮助你更好地使用HLS进行硬件电路设计。
- 了解HLS的限制和优化
虽然HLS可以让你使用高级编程语言进行硬件电路设计,但是它仍然有一些限制。例如,HLS无法处理动态内存分配,因此你需要手动分配所有的内存。另外,HLS通常无法处理递归函数,因为递归需要使用栈来保存函数调用的状态,而栈需要使用大量的存储器资源。
此外,为了获得最佳的性能和资源利用率,你需要使用HLS的一些优化选项,例如循环展开、分区、指令重排等。这些优化选项可以根据你的算法和目标设备进行调整,因此你需要了解HLS的优化选项和如何使用它们。 - 将复杂的算法拆分成简单的模块
HLS最适合处理简单的、高度并行的算法。因此,当你使用HLS进行硬件电路设计时,应该尽可能将复杂的算法拆分成简单的模块。每个模块都应该尽可能独立,以便并行化处理。 - 优化内存访问
在HLS中,内存访问通常是性能和资源利用率的瓶颈。因此,你需要优化内存访问以获得最佳的性能和资源利用率。一种常见的优化方法是使用局部变量和数组,这可以减少对存储器的访问次数。另外,你还可以使用流缓冲区来减少存储器的访问次数。 - 使用硬件加速器IP核
HLS提供了许多硬件加速器IP核,例如FFT、卷积等。这些IP核已经经过优化,可以在FPGA上实现高性能的硬件电路。当你需要实现这些算法时,应该首先查看HLS提供的IP核,以便快速实现高性能的硬件电路。 - 进行性能分析和调试
最后,当你使用HLS进行硬件电路设计时,需要进行性能分析和调试,以确保硬件电路的性能和正确性。HLS提供了许多调试和性能分析工具,例如波形查看器、性能分析器等。你可以使用这些工具来定位和解决问题,从而加快硬件电路设计的开发速度。
希望这些最佳实践可以帮助你更好地使用HLS进行硬件电路设计。
以下是一些在使用HLS进行高层次综合时可能有用的技巧:
- 分区(partitioning):使用HLS时,将大型数组分成较小的块可以增加并行性,从而提高性能。
- 流水线(pipelining):将操作流水线化可以减少操作的延迟,并在FPGA上实现更高的时钟频率。
- 循环展开(loop unrolling):展开循环可以减少循环迭代的次数,并增加并行性。
- 优化数据类型:HLS支持多种数据类型,包括固定点数和浮点数。选择合适的数据类型可以减少逻辑资源使用和运行时间。
- 优化存储:使用合适的存储器类型可以减少存储器使用和延迟。例如,使用流式存储器(streaming memory)可以减少数据存储和读取的延迟。
- 优化数据流:使用HLS时,将操作转换为数据流可以提高性能,因为它允许并行操作。
- 使用pragma指令:HLS支持pragma指令,这些指令可以告诉HLS如何执行某些操作。pragma指令可以优化代码,从而提高性能。
- 调整数据结构:合适的数据结构可以减少逻辑资源使用和运行时间。例如,使用链表(linked list)可以减少数据移动的延迟。 - 使用代码重构:HLS生成的代码可能不是最优的。通过重构代码,可以手动优化代码,从而提高性能。
- 合适的编译器指令:编译器指令可以告诉编译器如何优化代码。使用合适的编译器指令可以提高性能。
6. 总结
在本教程中,我们介绍了HLS的基本概念和编程模型,包括HLS的主要特性、编程模型和调优技巧。我们还提供了一些示例,展示了如何使用HLS实现不同的应用程序,并介绍了HLS中的一些高级特性,如定点数和浮点数支持、数据重用和代码生成等。通过学习本教程,您将能够掌握HLS的基本概念和编程技巧,以便使用HLS来实现高效的FPGA设计。
HLS是一个强大的工具,可以帮助我们将软件算法转换为硬件电路,从而在FPGA上获得高性能的加速效果。在学习HLS时,我们需要掌握C/C++编程语言和FPGA体系结构知识,并掌握HLS中常见的优化技巧和指令。同时,我们也需要有耐心和恒心,逐步积累经验和技能,从而成为一名优秀的HLS工程师。
7. 资料来源
ChatGPT,对没错哈哈哈哈,全部是ChatGPT生成的,我只是知识的搬运工 >。<