ug902-Chapter 3:High-Level Synthesis Coding Styles

文章目录

  • Unsupported C Constructs
    • System Calls
    • 动态内存使用
    • 指针的局限性
    • 递归函数
      • 标准模板库
  • C Test Bench
    • Productive Test Benches
    • Design Files and Test Bench Files
    • Combining Test Bench and Design Files
  • Functions
    • 内联函数
    • Impact of Coding Style
  • RTL Blackbox
  • Loops
  • Arrays
    • Array Accesses and Performance
    • Arrays on the Interface
    • Array Initialization
  • Data Types
  • C builtin Funtions
  • Hardware Efficient C Code
  • C++ Classes and Templates
  • Assertions
  • SystemC Synthesis

本章说明如何将C,C ++和SystemC的各种结构综合到FPGA硬件实现中。
重要!本指南中使用的术语“C代码”是指用C、c++和SystemC编写的代码,除非另有特别说明。

Unsupported C Constructs

虽然Vivado HLS支持广泛的C语言,但是有些构造是无法综合,或者会导致设计流中出现错误。
可以综合:
•C函数必须包含设计的全部功能。
•通过对操作系统的系统调用无法执行任何功能。None of the functionality can be performed by system calls to the operating system.?
•C构造必须具有固定或有限的大小。
•这些结构的实现必须明确。

System Calls

系统调用不能被综合。例如printf()和fprintf(stdout)。通常,对系统的调用不能被合成,应该在合成之前从函数中删除。其他类似调用的例子有getc()、time()、sleep(),它们都调用操作系统。

当执行合成时,Vivado HLS定义宏__SYNTHESIS__。 这允许__SYNTHESIS__宏从设计中排除不可综合的代码。
NOTE:仅在要合成的代码中使用__SYNTHESIS__宏。 请勿在测试平台中使用此宏,因为C仿真或C RTL协同仿真不会遵循该宏。

CAUTION!您不能在代码或编译器选项中定义或取消定义_ synthesis__宏,否则编译可能会失败。
__ SYNTHESIS __ 例子
宏__SYNTHESIS__用于确保在合成过程中忽略不可合成的写文件操作。

#include "hier_func4.h"
int sumsub_func(din_t *in1, din_t *in2, dint_t *outSum, dint_t *outSub)
{
*outSum = *in1 + *in2;
*outSub = *in1 - *in2;
}
int shift_func(dint_t *in1, dint_t *in2, dout_t *outA, dout_t *outB)
{
*outA = *in1 >> 1;
*outB = *in2 >> 2;
}
void hier_func4(din_t A, din_t B, dout_t *C, dout_t *D)
{
dint_t apb, amb;
sumsub_func(&A,&B,&apb,&amb);
#ifndef __SYNTHESIS__
FILE *fp1; // The following code is ignored for synthesis
char filename[255];
sprintf(filename,Out_apb_%03d.dat,apb);
fp1=fopen(filename,w);
fprintf(fp1, %d \n, apb);
fclose(fp1);
#endif
shift_func(&apb,&amb,C,D);
}

__SYNTHESIS__宏是一种方便的方法,可以在不从C函数中删除代码的情况下,排除不可合成的代码。 使用这样的宏意味着用于仿真的C代码和用于综合的C代码不同了。

CAUTION! 如果使用宏来改变C代码的功能,它会在C模拟和C合成之间产生不同的结果。这些代码中的错误本来就很难调试。不要使用宏来改变功能。

动态内存使用

系统中任何管理内存分配的系统调用(例如malloc(),alloc()和free())都使用操作系统内存中存在的资源,这些资源在运行时创建和释放:为了能够综合硬件实现,设计必须完全独立,并指定所有必需的资源。

综合之前必须从设计代码中删除内存分配的系统调用。由于动态内存操作用于定义设计的功能,因此必须将其转换为等效的有界表示形式。
下面的代码示例演示如何将使用malloc()的设计转换为可综合的版本,并重点介绍两种有用的编码样式技术:

  • 该设计不使用__SYNTHESIS__宏。
    用户定义的宏NO_SYNTH用于在可合成版本和不可合成版本之间进行选择。这样可以确保在C中模拟相同的代码,并在Vivado HLS中进行合成。
  • 使用malloc()的原始设计中的指针无需重写即可使用固定大小的元素。
    可以创建固定大小的资源,并且可以简单地使现有指针指向固定大小的资源。此技术可以防止对现有设计进行手动重新编码。

NO_SYNTH

#include "malloc_removed.h"
#include 
//#define NO_SYNTH
dout_t malloc_removed(din_t din[N], dsel_t width) {
#ifdef NO_SYNTH
long long *out_accum = malloc (sizeof(long long));
int* array_local = malloc (64 * sizeof(int));
#else
long long _out_accum;
long long *out_accum = &_out_accum;
int _array_local[64];
int* array_local = &_array_local[0];
#endif
int i,j;
LOOP_SHIFT:for (i=0;i<N-1; i++) {
if (i<width)
*(array_local+i)=din[i];
else
*(array_local+i)=din[i]>>2;
}
*out_accum=0;
LOOP_ACCUM:for (j=0;j<N-1; j++) {
*out_accum += *(array_local+j);
}
return *out_accum;
}

因为这里的代码更改会影响设计的功能,所以Xilinx不建议使用
__ synthesis__宏。Xilinx建议您执行以下步骤:
1.将用户定义的宏NO_SYNTH添加到代码中并修改代码。
2. 开启宏NO_SYNTH,执行C仿真并保存结果。
3. 禁用宏NO_SYNTH,并执行C模拟来验证结果是否相同。
4. 在禁用用户定义的宏的情况下执行合成。
这种方法可确保通过C仿真验证更新后的代码,然后再合成相同的代码。 与C中对动态内存使用的限制一样,Vivado HLS不支持(用于综合)动态创建或销毁的C ++对象。 这包括动态多态和动态虚函数调用。
以下代码无法综合,因为它在运行时创建了一个新函数。

Class A {
public:
	virtual void bar() {â¦};
};

void fun(A* a) {
	a->bar();
}

A* a = 0;
if (base)
	a = new A();
else
	a = new B();
foo(a);

指针的局限性

Vivado HLS不支持一般的指针类型转换,但支持本机C类型之间的指针转换。
Vivado HLS does not support general pointer casting, but supports pointer casting between native C types.
指针数组
Vivado HLS支持用于综合的指针数组,前提是每个指针都指向标量或标量数组。指针数组不能指向其他指针。
函数指针
不支持函数指针。

递归函数

递归函数不能综合。
这适用于可以形成无限递归的函数,其中无限:?

unsigned foo (unsigned n)
{
	if (n == 0 || n == 1) return 1;
	return (foo(n-2) + foo(n-1));
}

Vivado HLS不支持尾部递归,其中函数调用数量有限

unsigned foo (unsigned m, unsigned n)
{
if (m == 0) return n;
if (n == 0) return m;
return foo(n, m%n);
}

在c++中,模板可以实现尾部递归。接下来介绍c++。

标准模板库

许多c++标准模板库(STLs)包含函数递归并使用动态内存分配。因此,STLs不能被合成。使用STLs的解决方案是创建一个具有相同功能的本地函数,该函数不表现出递归、动态内存分配或动态创建和销毁对象的这些特征。

Note::支持标准数据类型(如std::complex)的合成。

C Test Bench

任何模块合成的第一步都是验证C函数是否正确。C函数的执行速度比RTL模拟快几个数量级。在综合之前使用C语言开发和验证算法比在RTL上开发效率更高。

  • 利用C语言开发时间的关键是有一个测试平台,它根据已知的良好结果来检查函数的结果。因为算法是正确的,所以任何代码更改都可以在合成之前进行验证。
  • Vivado HLS重用C测试平台来验证RTL的设计。使用Vivado HLS时不需要创建RTL测试工作台。如果测试平台检查来自顶层函数的结果,则可以通过仿真验证RTL。

NOTE:要向测试工作台提供输入参数,选择Project → Project Settings,单击Simulation,,然后使用Input Arguments选项。测试工作台不能要求执行交互式用户输入。Vivado HLS GUI没有命令控制台,在执行测试工作台时不能接受用户输入。
Xilinx建议将用于合成的顶级函数与测试工作台分开,并使用头文件。下面的代码示例展示了函数hier_func调用两个子函数的设计:
• sumsub_func performs addition and subtraction.
• shift_func performs shift.
数据类型在头文件(hier_function .h)中定义,它也被描述为:

#include "hier_func.h"
int sumsub_func(din_t *in1, din_t *in2, dint_t *outSum, dint_t *outSub)
{
*outSum = *in1 + *in2;
*outSub = *in1 - *in2;
}
int shift_func(dint_t *in1, dint_t *in2, dout_t *outA, dout_t *outB)
{
*outA = *in1 >> 1;
*outB = *in2 >> 2;
}
void hier_func(din_t A, din_t B, dout_t *C, dout_t *D)
{
dint_t apb, amb;
sumsub_func(&A,&B,&apb,&amb);
shift_func(&apb,&amb,C,D);
}

顶级函数可以包含多个子函数。综合只能有一个顶级函数。若要综合多个函数,请将它们组为单个顶级函数。
合成函数hier_func:

  1. 将上面示例所示的文件作为设计文件添加到Vivado HLS项目中。
  2. 将顶层函数指定为hier_func。

综合之后:

  • 将顶级函数(上面示例中的A、B、C和D)的参数合成为RTL端口。
  • 顶层中的函数(上面示例中的sumsub_func和shift_func)被合成为层次结构块。
    上面示例中的头文件(hier_function .h)展示了如何使用宏,以及typedef语句如何使代码更具可移植性和可读性。后面的部分将展示typedef语句如何允许在最终的FPGA实现中对变量的类型和位宽进行细化,从而实现区域和性能的改进。
#ifndef _HIER_FUNC_H_
#define _HIER_FUNC_H_
#include 
#define NUM_TRANS 40
typedef int din_t;
typedef int dint_t;
typedef int dout_t;
void hier_func(din_t A, din_t B, dout_t *C, dout_t *D);
#endif

本例中的头文件包含一些设计文件中不需要的定义(如NUM_TRANS)。这些定义由测试工作台使用,测试工作台还包括相同的头文件。

下面的代码示例显示了第一个示例中显示的设计的测试工作台。

#include "hier_func.h"
int main() {
// Data storage
int a[NUM_TRANS], b[NUM_TRANS];
int c_expected[NUM_TRANS], d_expected[NUM_TRANS];
int c[NUM_TRANS], d[NUM_TRANS];
//Function data (to/from function)
int a_actual, b_actual;
int c_actual, d_actual;
// Misc
int retval=0, i, i_trans, tmp;
FILE *fp;
// Load input data from files
fp=fopen(tb_data/inA.dat,r);
for (i=0; i<NUM_TRANS; i++){
fscanf(fp, %d, &tmp);
a[i] = tmp;
}
fclose(fp);
fp=fopen(tb_data/inB.dat,r);
for (i=0; i<NUM_TRANS; i++){
fscanf(fp, %d, &tmp);
b[i] = tmp;
}
fclose(fp);
// Execute the function multiple times (multiple transactions)
for(i_trans=0; i_trans<NUM_TRANS-1; i_trans++){
//Apply next data values
a_actual = a[i_trans];
b_actual = b[i_trans];
hier_func(a_actual, b_actual, &c_actual, &d_actual);
//Store outputs
c[i_trans] = c_actual;
d[i_trans] = d_actual;
}
// Load expected output data from files
fp=fopen(tb_data/outC.golden.dat,r);
for (i=0; i<NUM_TRANS; i++){
fscanf(fp, %d, &tmp);
c_expected[i] = tmp;
}
fclose(fp);
fp=fopen(tb_data/outD.golden.dat,r);
for (i=0; i<NUM_TRANS; i++){
fscanf(fp, %d, &tmp);
d_expected[i] = tmp;
}
fclose(fp);
// Check outputs against expected
for (i = 0; i < NUM_TRANS-1; ++i) {
if(c[i] != c_expected[i]){
retval = 1;
}
if(d[i] != d_expected[i]){
retval = 1;
}
}
// Print Results
if(retval == 0){
printf( *** *** *** *** \n);
printf( Results are good \n);
printf( *** *** *** *** \n);
} else {
printf( *** *** *** *** \n);
printf( Mismatch: retval=%d \n, retval);
printf( *** *** *** *** \n);
}
// Return 0 if outputs are corre
return retval;
}

Productive Test Benches

Design Files and Test Bench Files

因为Vivado HLS重用C测试工作台进行RTL验证,所以需要在将测试工作台和任何相关文件添加到Vivado HLS项目时将它们表示为测试工作台文件。与测试工作台相关联的文件是:

  • 被测试工作台访问
  • 测试台正常运行所必需的

此类文件的示例包括测试平台示例中的inA.dat和inB.dat数据文件。必须将这些作为测试工作台文件添加到Vivado HLS项目中。

在Vivado HLS项目中识别测试工作台文件的需求并不要求设计和测试工作台位于单独的文件中(尽管推荐使用单独的文件)。

下面的示例重复了来自C Test Bench章节的相同设计。惟一的区别是,为了区分示例,将顶级函数重命名为hier_func2。
使用相同的头文件和测试工作台(除了从hier_func到hier_func2的更改之外),
Vivado HLS中将函数sumsub_func综合成顶级函数所需的更改是

  • 将sumsub_func设置为Vivado HLS项目中的顶级函数。
  • 将下面示例中的文件同时添加为设计文件和项目文件。sumsub_func(函数hier_func2)之上的层现在是测试工作台的一部分。它必须包含在RTL仿真中。

尽管没有在main()函数中显式实例化sumsub_func函数,但是其余的函数(hier_func2和shift_func)确认它的操作是正确的,因此它是测试工作台的一部分。

#include "hier_func2.h"
int sumsub_func(din_t *in1, din_t *in2, dint_t *outSum, dint_t *outSub)
{
*outSum = *in1 + *in2;
*outSub = *in1 - *in2;
}
int shift_func(dint_t *in1, dint_t *in2, dout_t *outA, dout_t *outB)
{
*outA = *in1 >> 1;
*outB = *in2 >> 2;
}
void hier_func2(din_t A, din_t B, dout_t *C, dout_t *D)
{
dint_t apb, amb;
sumsub_func(&A,&B,&apb,&amb);
shift_func(&apb,&amb,C,D);
}

Combining Test Bench and Design Files

您还可以将设计和测试工作台包含到单个设计文件中。下面的示例具有与整个C测试工作台相同的功能,只是所有内容都在一个文件中捕获。函数hier_func被重命名为hier_func3,以确保示例是惟一的。

IMPORTANT! 如果测试工作台和设计在一个文件中,则必须将该文件作为设计文件和测试工作台文件添加到Vivado HLS项目中。

Functions

顶层函数综合后成为RTL设计的顶层。在RTL设计中,子函数被综合为blocks
IMPORTANT! The top-level function cannot be a static function.
综合后,设计中的每个功能都有自己的综合报告和RTL HDL文件(Verilog和VHDL)。

内联函数

可以选择内联子函数,以将其逻辑与周围功能的逻辑合并。 虽然内联函数可以带来更好的优化,但也可以增加运行时间。 更多逻辑和更多可能性必须保留在内存中并进行分析。
TIP:Vivado HLS可能会自动内嵌一些小功能。 要禁用小功能的自动内联,请将该功能的inline指令设置为off。
如果函数被内联,则该函数没有报告或单独的RTL文件。 逻辑和循环在层次结构中与其上层函数合并。

Impact of Coding Style

编码风格对函数的主要影响是对函数参数和接口的影响。
如果正确确定函数的参数大小,则Vivado HLS可以在设计中传播此信息。没有必要为每个变量创建任意的精确类型。在下面的例子中,两个整数相乘,结果仅使用低24位。

#include "ap_cint.h"
int24 foo(int x, int y) {
int tmp;
tmp = (x * y);
return tmp
}

此代码综合后,结果是一个32位乘法器,输出被截断为24位。
如下面的代码示例所示,如果输入的大小正确设置为12位类型(int12),则最终的RTL使用24位乘法器。

#include "ap_cint.h"
typedef int12 din_t;
typedef int24 dout_t;
dout_t func_sized(din_t x, din_t y) {
int tmp;
tmp = (x * y);
return tmp
}

对于两个函数输入使用任意精度类型就足以确保Vivado HLS使用24位乘法器创建设计。 12位类型通过设计传播。 Xilinx建议正确设置层次结构中所有函数的参数大小。
通常,当变量直接从函数接口驱动时,特别是从toplevel函数接口,它们会阻止一些优化的发生。这种情况的典型情况是将输入用作循环索引的上限。

RTL Blackbox

RTL blackbox允许将现有的RTL IP集成到HLS设计中,从而产生可以在HLS设计流中运行的设计。RTL blackbox允许将现有的RTL IP集成到HLS设计中,从而产生可以在HLS设计流中运行的设计。RTL IP可以用于sequential,pipeline, or dataflow region。
将RTL IP集成到HLS需要以下文件:

  1. Blackbox描述文件
  2. RTL IP文件
  3. RTL的C实现

将RTL IP集成到HLS设计中:

  1. 创建RTL IP的C implementation function。
  2. 在HLS设计中调用C implementation function。
  3. 使用必要的字段创建一个JSON文件。RTL Blackbox JSON文件提供了一个示例JSON文件和关于该格式的信息。
  4. 将JSON文件添加到script.tcl中。使用add_files选项的tcl文件。add_files –blackbox my_file.json。
  5. 运行HLS设计流程;i.e., Csim, synthesis, and cosim。

要求和限制

  • 在HLS内部,RTL blackbox支持仅限于c++。
  • 在HLS内部,RTL blackbox无法连接到top-level interface I/O signals。
  • 在HLS内部,RTL blackbox不能直接充当DUT(Device-Under- Test被测试设备)。
  • 在HLS内部,RTL blackbox不支持struct or class type interfaces。
  • 在HLS内部,RTL黑盒支持以下接口协议:…
  • 提供给HLS的RTL IP文件应该是Verilog (.v)。
  • RTL IP模块必须有一个唯一的时钟信号和一个唯一的高电平复位信号。
  • RTL IP模块必须有一个CE信号,用于启用或停止RTL IP。
  • RTL IP必须使用ap_ctrl_chain协议。有关更多信息,请参见块级I/O协议。

JSON file limitations:
• The c_function name field must be consistent with the C function model.
• The rtl_top_module_name must be consistent with the c_function_name.
• Unused c_parameters fields should be deleted from the template.
• Every c_parameter field should be associated with a rtl_port field.
Note: All other HLS design restrictions still apply when using the RTL blackbox.

JSON File Format
下表描述了JSON文件格式:
。。。

Loops

循环提供了一种非常直观和简洁的方式来捕获算法行为,并且经常用在C代码中。 综合功能很好地支持循环:循环可以流水线化,展开,部分展开,合并和展平。
优化可以有效地展开,部分展开,展平和合并,从而对循环结构进行更改,就像更改了代码一样。 这些优化可确保在优化循环时仅需要有限的编码更改。 某些优化只能在某些条件下应用。 可能需要更改一些编码。
RECOMMENDED:避免将全局变量用于循环索引变量,因为这会抑制某些优化。

Arrays

在讨论编码风格如何影响合成后数组的实现之前,有必要讨论这样一种情况,即在执行合成之前,比如在C模拟期间,数组可能会引入问题。
如果指定一个非常大的数组,可能会导致C仿真耗尽内存并失败,
如下面的示例所示:

#include "ap_cint.h"
int i, acc;
// Use an arbitrary precision type
int32 la0[10000000], la1[10000000];
for (i=0 ; i < 10000000; i++) {
acc = acc + la0[i] + la1[i];
}

仿真可能会因为耗尽内存而失败,因为数组放在内存中存在的堆栈上,而不是由OS管理的堆上,并且可以使用本地磁盘空间来增长。
这可能意味着设计在运行时耗尽内存,某些问题可能会使此问题更有可能发生

  • 在pc上,可用内存通常小于大型Linux机器,可用内存也可能更少。
  • 如上所示,使用任意精度类型可能会使这个问题变得更糟,因为它们比标准C类型需要更多的内存。
  • 使用在c++和SystemC中发现的更复杂的定点任意精度类型可能会导致更大的问题,因为它们需要更多的内存。

在C/C++代码开发中,改善内存资源的标准方法是使用linker选项增加堆栈的大小,例如下面的选项显式地设置堆栈大小-Wl,–stack,10485760。这可以在Vivado HLS中应用,方法是ProjectSettings → Simulation → Linker flags,或者也可以作为Tcl命令的选项提供:

csim_design -ldflags {-Wl,--stack,10485760}
cosim_design -ldflags {-Wl,--stack,10485760}

在某些情况下,计算机可能没有足够的可用内存,增加堆栈大小也无济于事。
解决方案是使用动态内存分配进行仿真,而使用固定大小的数组进行综合,如下面的示例所示。这意味着所需的内存是在堆上分配的,由OS管理,并且可以使用本地磁盘空间来增长。
代码这样更改并不理想,因为仿真的代码和综合的代码是不同的,但有时这可能是推进设计过程的惟一方法。如果这样做了,请确保C测试工作台覆盖了访问数组的所有方面。cosim_design执行的RTL模拟将验证内存访问是否正确。

#include "ap_cint.h"
int i, acc;
#ifdef __SYNTHESIS__
// Use an arbitrary precision type & array for synthesis
int32 la0[10000000], la1[10000000];
#else
// Use an arbitrary precision type & dynamic memory for simulation
int32 *la0 = malloc(10000000 * sizeof(int32));
int32 *la1 = malloc(10000000 * sizeof(int32));
#endif
for (i=0 ; i < 10000000; i++) {
acc = acc + la0[i] + la1[i];
}

Note:Only use the __ SYNTHESIS __ macro in the code to be synthesized. Do not use this macro in the test bench, because it is not obeyed by C simulation or C RTL co-simulation.
Arrays通常在合成后被实现为memory(RAM、ROM或FIFO)。顶级函数接口上的数组被综合为访问外部存储器的RTL端口。
在设计内部,小于1024的阵列将被合成为SRL。根据优化设置,大于1024的阵列将被合成为块RAM、LUTRAM、UltraRAM。
与loops一样,数组是一种直观的编码结构,因此经常在C程序中出现。与loops一样,Vivado HLS也包含优化和指令,可以应用这些优化和指令在RTL中优化它们的实现,而不需要修改代码。

数组在RTL中产生问题的情况包括:

  • 数组访问常常会造成性能瓶颈。当作为内存实现时,内存端口的数量限制了对数据的访问。 如果不仔细执行数组的初始化,则会导致RTL中的重置和初始化时间过长。
  • 必须注意确保只需要读访问的数组在RTL中实现为rom。

Vivado HLS支持指针数组。每个指针只能指向标量或标量数组。

Note:数组必须有大小。例如,支持有大小的数组,例如:数组[10];。但是,不支持无大小的数组,例如:Array[];。

Array Accesses and Performance

Arrays on the Interface

Array Initialization

Data Types

C builtin Funtions

Hardware Efficient C Code

C++ Classes and Templates

Assertions

SystemC Synthesis

你可能感兴趣的:(HLS)