Vivado HLS中指针作为Top函数参数的处理

指针作为C语言精华,对于软件设计者比较好理解,但是在xilinx vivado HLS高级语言综合的设计中,由于其综合后对应的硬件元素难以用软件的概念解释,常常令程序设计者和VHLS工具使用者头痛。本文采用浅显易懂的描述方式,结合具体的c代码例子,详细描述了常用三种指针的设计类型,以及其作为顶层函数参数时,采用不同的编码风格和HLS约束策略,满足设计者对指针作为RTL接口的需求。

1. 基本指针类型
基本指针类型指的是指针没有运算或者没有多次的存取(读写)。指针作为top函数的参数时,指针综合为wire型或者握手协议类型接口。如下例子1-1:

void pointer_basic (dio_t *d) {
static dio_t acc = 0;
acc += *d;
*d = acc;
}

例子1-1 基本类型指针作为顶层函数参数

在这个例子中,只是简单的读写指针指向的变量值,并没有对指针做偏移或者指针(地址)运算,其接口综合为线型的RTL接口。

2. 指针运算类型。
指针作为top层函数参数,并且函数中有对指针运算时,我们称之为指针运算类型。指针运算常常限制指针可能综合的接口类型。如下例中,指针做了偏移运算用于累加数据,从第二个值开始读出累加,并将每次累加结果写入上一个地址中。

void pointer_arith (dio_t *d) {
static int acc = 0;
int i;
for (i=0;i<4;i++) {
acc += *(d+i+1);
*(d+i) = acc;
}
}

例子1-2 指针运算类型作为顶层函数参数

下面代码例子1-3是这个指针运算类型仿真的testbench。因为函数pointer_arith内部的for循环进行数据累加,testbench通过数组d[5]分配了地址空间并对数组赋值。
int main () {
dio_t d[5], ref[5];
int i, retval=0;
FILE *fp;
// Create input data
for (i=0;i<5;i++) {
d[i] = i;
ref[i] = i;
}
// Call the function to operate on the data
pointer_arith(d);
// Save the results to a file
fp=fopen("result.dat","w");
printf(" Din Dout\n", i, d);
for (i=0;i<4;i++) {
fprintf(fp, "%d \n", d[i]);
printf(" %d %d\n", ref[i], d[i]);
}
fclose(fp);
// Compare the results file with the golden results
retval = system("diff --brief -w result.dat result.golden.dat");
if (retval != 0) {
printf("Test failed!!!\n");
retval=1;
} else {
printf("Test passed!\n");
}
// Return 0 if the test
return retval;
}

例子1-3 指针运算类型作为顶层函数参数的testbench

在C编译环境下仿真上面例子1-3的代码,结果如下:
Din Dout
0 1
1 3
2 6
3 10
Test passed!

指针运算带来的问题是,通常情况下,指针偏移是不规则的,不能按顺序存取指针数据。而Wire,握手类型或者Fifo接口类型没有办法乱序存取数据。

对于wire类型接口来说,当设计本身准备好接收数据时可以读入数据,或者当数据准备好ready时,可以写出数据。对握手和Fifo类型接口,当控制信号允许操作进行时,读入或写出数据。

在上面wire,握手或者FIFO类型接口的情况下,数据从0元素开始,必须按顺序到达(写入)。在指针运算的例子1-2中,第一个数据从索引1开始读入(i从0开始,0+1=1),对应于testbench中数据d[5]的第二个元素。

当这种情况在硬件应用时,需要某种格式的数据索引,这种情况对于wire类型,或者握手类型还是Fifo类型来说,都不支持。像上例1-2指针运算的代码,只能综合成ap_bus接口,因为这种接口带有地址,当数据存取(读写)时,用于对应的数据索引指示。

还有一种方法,代码必须修改成如下例子1-4的风格,用数据array作为接口替代指针。这种方法应用了array作为top层参数时综合成RAM接口(ap_memory)的原理,memory接口可以用地址作为数据的索引并且可以乱序执行,不必顺序存取操作。
void array_arith (dio_t d[5]) {
static int acc = 0;
int i;
for (i=0;i<4;i++) {
acc += d[i+1];
d[i] = acc;
}
}

例子1-4 指针运算类型作为顶层函数参数修改为array

Wire类型、握手类型或Fifo类型接口仅仅可用在数据流方式,因此不能用在与指针运算相关的地方(除非数据从索引0开始并顺序处理)。同时注意,如果想综合为FIFO接口,Fifo接口类型必须是只读或者只写,不能有读又有写操作。

3. 多次读写(存取)指针类型
多次读写指针类型一般用作描述一个数据流方式的接口。
当top层函数参数使用指针,函数体对指针进行多次存取操作时,必须仔细考虑。在同一函数中对一个指针多次的读或者写,就会有多次指针存取发生,从而引起下列问题:

1. 对任何函数指针参数的多次存取要使用volatile限定符。

2. 对于Top层函数,如果要做RTL代码的混合仿真(co-sim),任何这种指针参数必须有这个接口存取次数的详细说明。

3. 确保在综合前验证C功能,确定符合功能要求,保证C模型正确。


如果设计模型要求函数参数指针多次存取,推荐使用数据流模式模型化设计,使用数据流模型可以避免我们将会在下面讨论到的,使用多次读写指针带来的一些问题。

这个章节使用设计例子1-5 糟糕的数据流类型指针(pointer_stream_bad)解释,当多次存取指针时,为什么要使用volatile限定符。同时使用设计例子1-8 好的数据指针类型(pointer_stream_better)来说明,为什么当top层函数参数包含有这种指针接口的设计时,应该用C testbench仿真验证确保设计的行为级模型正确。

在下面的例子1-5中,指针d_i读了4次并且d_o写了2次,设计的本意是存取操作通过fifo接口,综合后的RTL以数据流的方式读入或者写出数据。
void pointer_stream_bad ( dout_t *d_o, din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *d_i;
*d_o = acc;
acc += *d_i;
acc += *d_i;
*d_o = acc;
}

例子1-5 糟糕的数据流指针类型

用于验证的C testbench如下:
int main () {
din_t d_i;
dout_t d_o;
int retval=0;
FILE *fp;
// Open a file for the output results
fp=fopen("result.dat","w");
// Call the function to operate on the data
for (d_i=0;d_i<4;d_i++) {
pointer_stream_bad(&d_o,&d_i);
fprintf(fp, "%d %d\n", d_i, d_o);
}
fclose(fp);
// Compare the results file with the golden results
retval = system("diff --brief -w result.dat result.golden.dat");
if (retval != 0) {
printf("Test failed !!!\n");
retval=1;
} else {
printf("Test passed !\n");
}
// Return 0 if the test
return retval;
}
例子1-6 数据流类型指针testbench

下面让我们详细理解volatile数据标示符。在上面代码例子1-6中,我们的目的是输入指针d_i和输出指针d_o作为RTL接口的FIFO或者握手信号使用,这样可以保证如下功能:

1. 当RTL的d_i端口每次读入时,上行数据输出模块输出一个新数据。

2. 当RTL的d_o端口每次写出时,下行模块接收一个新数据。

然而,标准的C编译器编译时,对每一个指针的多次读写编译为单次读写:就C编译器来说,在函数执行过程中没有指示出d_i数据的变化,而且只于最终d_o写出相关(其它的多次写出在函数完成时被覆盖)。

VivadoHLS与gcc编译器的行为一致,优化这些多次的读和写,最终只执行一次读和写操作。当RTL执行时,在读和写接口上,也仅执行一次读和写操作。

这种设计存在的一个基本问题,是仿真激励testbench和设计不能准确的对设计者希望的RTL接口行为建模:

1. 设计者希望RTL接口在处理时,多次读入和写出数据,能够数据流模式的输入和输出。

2. Testbench只提供了单次的数据输入和返回输出数据。

C仿真显示了如下的结果,展示了每次输入将计算4次,但是一旦写入,4次都是相同的值用于累加操作,而不是4次不同的读入操作。
Din Dout
0 0
1 4
2 8
3 12

这个设计可以通过设置volatile限定符,实现多次RTL接口的读入和写出。如下面代码例子1-7所示:
#include "pointer_stream_better.h"
void pointer_stream_better ( volatile dout_t *d_o, volatile din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *d_i;
*d_o = acc;
acc += *d_i;
acc += *d_i;
*d_o = acc;
}

例子1-7 好的数据流指针类型参数

Volatile限定符限定C编译器和vivado HLS,对指针存取不做任何假设,数据是变化无常的,并且指针的存取不能够优化,对同一指针的多次存取可能读写不同的值,而不是相同不变的值。

上例1-7可以使用上面的仿真testbench,但是volatile限定符阻止了对指针存取的优化,综合后的RTL设计执行4次输入d_i接口的读入和2次输出d_o接口的写出操作。

虽然我们使用了volatile关键字,但testbench和函数的代码风格(同一指针的多次读写)仍然存在问题,testbench不能完全模型化多次不同的读入和写出操作。

在这种情况下,执行了4次读入,但4次读入的是相同的数据。同时,执行两次独立的写出,每次写出正确的数据,但是testbench仅仅抓到最好一次读出的数据。中间的读写过程可以通过使能cosim_design的create a trace file选项,通过保存RTL仿真过程中dump波形文件查看。

上面的例子也能应用wire型的约束作为接口,如果约束为FIFO接口,Vivado HLS将会产生一个RTL的testbench用于数据流方式,每次读入一个新数据。但是由于从testbench中不能读入可用的新数据,RTL仿真验证将会失败。这是由于testbench不能正确模型化多次的读写导致的。

一种比较好的方法是使用HLS::stream<>模型化的数据流接口。
与软件不同,硬件系统本身的并行允许利用数据流方式,当数据连续的输入设计中处理同时连续的输出数据,RTL设计在完成处理现存的数据之前,能够接收新的数据。

上面的例子说明,当用软件模型化一个存在的硬件应用时(模型化实际硬件的并行和数据流方式),模型化数据流对软件来说是很至关重要的事情。

这里有几个方法可以考虑:
1. 简单的增加volatile限定符,如同上例中一样。Testbench如果不能模型化读入和写出想要的数据,RTL仿真时使用原C代码的testbench可以导致仿真失败,但是通过查看trace file波形文件,可以看到正确的读写操作执行。

2. 修改代码显式的表示模型化读入和写出。见下面的代码。

3. 修改代码,使用数据流接口类型。数据流方式的数据类型允许精确模型化使用数据流的硬件。
下面修改的代码例子1-8确保4次从testbench读入希望的值,并写2次输出。由于指针存取是串行执行并从位置0开始,在综合时,会产生数据流方式接口。

void pointer_stream_good ( volatile dout_t *d_o, volatile din_t *d_i) {
din_t acc = 0;
acc += *d_i;
acc += *(d_i+1);
*d_o = acc;
acc += *(d_i+2);
acc += *(d_i+3);
*(d_o+1) = acc;
}

例子1-8 多次存取指针参数函数优化

修改Testbench模型化函数的实际运行,读入4次不同的值。新的testbench例子1-9仅模型化了单次的处理,模型化多次处理,需要增加输入数据集,并且多次调用这个函数。
int main () {
din_t d_i[4];
dout_t d_o[4];
int i, retval=0;
FILE *fp;
// Create input data
for (i=0;i<4;i++) {
d_i[i] = i;
}
// Call the function to operate on the data
pointer_stream_good(d_o,d_i);
// Save the results to a file
fp=fopen("result.dat","w");
for (i=0;i<4;i++) {
if (i<2)
fprintf(fp, "%d %d\n", d_i[i], d_o[i]);
else
fprintf(fp, "%d \n", d_i[i]);
}
fclose(fp);
// Compare the results file with the golden results
retval = system("diff --brief -w result.dat result.golden.dat");
if (retval != 0) {
printf("Test failed !!!\n");
retval=1;
} else {
printf("Test passed !\n");
}
// Return 0 if the test
return retval;
}

例子1-9 多次存取指针参数函数的testbench

用例子1-9的testbench验证算法,输出下面的结果,显示单次处理有二个输出,第一个输出是前2次的读入数据进行累加的值;第二个输出是,前2个输入的累加值,再加上后2次读入数据的累加结果。
Din Dout
0 1
1 6
2
3

从最终的仿真结果可以看出,当函数接口使用多次指针存取时,与RTL仿真结果一致。

4. VHLS中指针作为top函数参数的总结
对于基本指针类型,即函数体只进行一次的指针读写操作,指针综合为wire类型或者握手信号类型接口。
指针运算类型,是指函数体对指针进行偏移操作,通常情况下,由于指针偏移不是顺序执行的,综合为ap_bus类型接口。但当指针偏移是完全的顺序执行时,可以综合约束为FIFO接口类型。如果设计接口需要ap_memory类型,可以将带有指针运算的指针类型改写为array型,array名等同于数组首地址,经过这样的修改,带有指针运算的指针类型综合为memory接口。

多次存取同一指针变量时,称此指针变量为多次存取指针类型。当多次存取指针作为顶层函数参数时,对其处理有三种方法。首先,如果设计本意的操作是数据流方式操作,即每次对指针的读入或者写出可能是不同的值,对这个指针变量增加volatile限定符即可,但需要testbench确保正确模型化实际数据输入输出操作。其次,修改代码风格,显式的表示多次指针存取的地址。最后,对于数据流方式操作的指针类型,直接使用VHLS提供的数据类型streaming接口的c++模板类,hls::stream<>类型。


你可能感兴趣的:(Vivado,HLS)