HLS第三十三课(UG871,基于C++Template的工程设计)

HLS第三十三课(UG871,基于C++Template的工程设计),第1张

HLS第三十三课(UG871,基于C++Template的工程设计)

一个可综合的top函数,通常需要调用一些子函数。
这些子函数,被编码为模板函数,在top函数中被具象化为实例函数。
模板函数在H文件中被编码,而不是在CPP文件中。

H文件也是分层次的,
TOP层的H函数,与top函数的CPP文件同名,
在TOP层的H文件中,定义各种常量宏,定义各种typedef,声明TOP函数的函数原型。
在TOP层的H文件中,include所需要用到模板函数所在的H文件。

当然,也可以将H文件的层次展平,
在TOP的H文件中,只定义各种常量和类型。
在各自独立的H文件中,定义所需要的模板函数。
然后,在TOP函数的CPP文件中,依次包含各个H文件。

来看一个例子real2xfft。
+++++++++++++++++++++++++++++++++++++
先来看hls_realfft.h文件

#ifndef _HLS_REALFFT_H_
#define _HLS_REALFFT_H_
	...
#endif

首先是头文件保护宏。

#define DIN_W 16
#define DOUT_W DIN_W

然后是定义常量宏。

typedef ap_fixed din_t;
typedef complex > dout_t;

然后是定义数据类型。

typedef struct {
   ap_uint data;
   ap_uint keep;
   ap_uint<1> last;
} xdma_axis_t;

typedef struct {
   dout_t data;
   ap_uint<1> last;
} xfft_axis_t;

然后是定义结构体类型。这里定义的结构体,是AXIS总线的信号集。
这是HLS中的一个编码技巧,
AXIS总线的定义,要按照信号集,定义为一个结构体,结构体成员的名称,要和AXIS信号集的名称完全一致。

void hls_real2xfft(
      	hls::stream& din,
      	hls::stream& dout
     	);

然后是声明函数原型。

注意这里的知识点,用stream容器封装AXIS结构体。
在HLS中,如果形参是一个结构体变量,那么结构体中的每个成员,都会成为接口中的信号集的一部分。HLS中,stream容器,被理解为一个一维的对象数组
AXIS总线上传输的数据,在HLS里,被理解为一个一维的结构体数组,每个时钟周期采样一次AXIS总线,每一次采样的总线信号,是一个结构体对象,也就是一个stream容器中装载的数组元素。

+++++++++++++++++++++++++++++++++++++++++++
再来看sliding_win.h文件

#ifndef _SLIDING_WIN_H_
#define _SLIDING_WIN_H_

首先是头文件保护宏。

template
void sliding_win_1in2out(
      hls::stream& din,
      T *dout)
{
   	enum {DELAY_LEN = LEN / 2}; // windows overlap by 1/2
   	enum {DELAY_FIFO_DEPTH = DELAY_LEN / 2};

   	T din_val;
   	static ap_shift_reg delay_line;
   	T nodelay[LEN/2], delayed[LEN/2];
   
#pragma HLS ARRAY_PARTITION variable=nodelay,delayed cyclic factor=2
#pragma HLS STREAM depth=DELAY_LEN variable=nodelay
#pragma HLS STREAM depth=DELAY_FIFO_DEPTH variable=delayed
#pragma HLS INLINE
#pragma HLS DATAFLOW

   	// generate delayed and non-delayed streams of packed even-odd pairs
	sliding_win_delay:
   	for (int i = 0; i < LEN / 2; i++) {
	#pragma HLS pipeline rewind
      din_val = din.read();
      nodelay[i] = din_val;
      delayed[i] = delay_line.shift(din_val);
   	}
   
   	// Mux delayed and non-delayed streams to dout (array) streams
	sliding_win_output:
  	for (int i = 0; i < LEN; i++) {
	// want to output two adj samples at a time
	#pragma HLS UNROLL factor=2 
	#pragma HLS pipeline rewind
      T dout_val;
      if (i < LEN / 2) {
         dout_val = delayed[i];
      }
      else {
         dout_val = nodelay[i % (LEN / 2)];
      }
      
      dout[i] = dout_val;
   }
}

这里,定义了一个模板函数sliding_win_1in2out。
模板函数内,实现了全参数化。
其中,用到了enum,定义常量,这样用enum定义的常量,是推荐的做法,比宏常量更安全。
其他的编码技巧,例如
local cache,上下游任务划分,循环体内分支,等等,不再赘述。

这里重点关注pragma的使用。
这里定义了两个数组,nodelay和delayed,默认情况下,这两个数组,被HLS理解为BRAM,但是这里,明确指定,这两个数组,施加stream约束。从而使得HLS可以正确实现这两个数组。
这个函数,是期望被调用者内联的,所以,施加了inline约束。
函数级,编码风格上,已经按照上下游,进行了任务分块,所以,在函数级使用dataflow约束。

为了增加两个临时数组的访问端口,施加了array_partition约束。

++++++++++++++++++++++++++++++++++++++++
再看看window_fn.h文件。

#ifndef _WINDOW_FN_H_
#define _WINDOW_FN_H_

首先是头文件保护宏。

namespace hls_window_fn {

typedef enum {
   RECT = 0, HANN, HAMMING, GAUSSIAN
} win_fn_t;

// Helper functions
template
double coef_calc(int idx)
{
   double coef_val;

   switch(FT) {
   case RECT:
      coef_val = 1.0;
      break;
   case HANN:
      coef_val = 0.5 * (1.0 - cos(2.0 * M_PI * idx / double(SZ)));
      break;
   case HAMMING:
      coef_val = 0.54 - 0.46 * cos(2.0 * M_PI * idx / double(SZ));
      break;
   case GAUSSIAN:
      const double gaussian_sigma = 0.5;
      double x = (idx - SZ / 2) / (gaussian_sigma * (SZ / 2));
      coef_val = exp(-0.5 * x * x);
      break;
   }
   return coef_val;
}

template
void init_coef_tab(TC *coeff)
{
   for (int i = 0; i < SZ; i++) {
      coeff[i] = coef_calc(i);
   }
};

// The API template functions
template
void window_fn(hls::stream& indata, hls::stream& outdata)
{
   TC coeff_tab[SZ];
   init_coef_tab(coeff_tab);

	apply_win_fn:
   	for (unsigned i = 0; i < SZ; i++) {
	#pragma HLS PIPELINE rewind
      	outdata << coeff_tab[i] * indata.read();
  	}
}

template
void window_fn(TI *indata, TO *outdata)
{
   TC coeff_tab[SZ];
   init_coef_tab(coeff_tab);
#pragma HLS ARRAY_PARTITION variable=coeff_tab cyclic factor=UF

	apply_win_fn:
   	for (unsigned i = 0; i < SZ; i++) {
	#pragma HLS UNROLL factor=UF
	#pragma HLS PIPELINE rewind
      	outdata[i] = coeff_tab[i] * indata[i];
   	}
}

}; // namespace hls_window_fn

这里,有一个C++的编码技巧,就是定义了一个namespace,更好的封装了命名。
使用enum,定义常量,而不是使用宏常量,更加安全。而且使用了typedef,将enum定义为有名称的类型。命名枚举类型,使得常量的用途更具体,更明确。

这里,使用模板参数来指定pragma中需要的参数。

#pragma HLS ARRAY_PARTITION variable=coeff_tab cyclic factor=UF

factor所需要的参数,由UF来指定,而UF,是一个模板参数。

+++++++++++++++++++++++++++++++++++++++
再来看看real2xfft.cpp文件。

#include "hls_realfft.h"
#include "sliding_win.h"

using namespace hls_window_fn;

首先是包含各个需要的头文件。
然后是声明使用的默认命名空间,如果有函数没有显式声明命名空间,则使用默认命名空间。

void hls_real2xfft(
      hls::stream& din,
      hls::stream& dout)
{
#pragma HLS INTERFACE axis port=dout
#pragma HLS INTERFACE axis port=din
   din_t data2window[REAL_FFT_LEN], windowed[REAL_FFT_LEN];
#pragma HLS ARRAY_PARTITION variable=data2window cyclic factor=2
#pragma HLS ARRAY_PARTITION variable=windowed cyclic factor=2
#pragma HLS STREAM variable=windowed depth=2 dim=1
#pragma HLS STREAM variable=data2window depth=2 dim=1
#pragma HLS DATAFLOW

   sliding_win_1in2out(din, data2window);
   window_fn(data2window, windowed);

	real2xfft_output:
   	for (int i = 0; i < REAL_FFT_LEN; i += 2) {
	#pragma HLS PIPELINE rewind
      dout_t cdata(windowed[i], windowed[i + 1]);
      xfft_axis_t fft_axis_d;
      fft_axis_d.data = cdata;
      fft_axis_d.last = (i == REAL_FFT_LEN - 2) ? 1 : 0;
      dout.write(fft_axis_d);
   	}
}

根据任务划分,所需要的子函数都已经在H文件中定义成模板,在本地,只要具象化一个实例函数,即可实现调用。
TOP函数通常结构比较清晰,并没有多少功能代码,主体部分一般都是在结构化语句块中,对各个子函数进行调用。
对AXIS结构体的填充,是手工完成的,涉及到在合适的时候对last成员的控制。

fft_axis_d.last = (i == REAL_FFT_LEN - 2) ? 1 : 0;

这里,重点关注pragma的使用。
这是TOP函数,所以使用了接口约束,从形参对象中衍生出接口。
这里,对两个stream容器的形参din和dout,施加了axis接口约束。为他们衍生出AXIS接口。AXIS接口的信号集,取决于stream容器中的元素的结构体定义。
函数内,定义了临时数组变量,(temp variable),为了增加对它们的访问端口,使用了array_partition约束。
另外,HLS默认将数组理解为BRAM,这里,使用了stream约束,让HLS能够正确理解数组的存储。
+++++++++++++++++++++++++++++++++++++++++++++++
再看看hls_realfft_test.cpp文件。
由于是CSIM文件,所以这是常规C编程,而不是HLS的C硬件描述。所以没有特殊的编码风格要求,从C编程的角度理解程序即可。

#include 
#include 
#include 
#include 
#include 

#include "hls_realfft.h"
#include "reference_fft.h"

using namespace std;

首先是包含头文件。声明默认命名空间。

#define NUM_TESTS 8

typedef float real32_t;
typedef complex creal32_t;

void signal_gen(din_t *signal, int num_samples);

然后是define宏常量,typedef类型名称,声明子函数的函数原型。

void signal_gen(din_t *signal, int num_samples)
{
   enum {NUM_FREQ = 5};
   struct freq_comp_data {
      double cycles_per_win;
      double phase;
      double amplitude;
   } freq_set[NUM_FREQ] = {
         {497.0, 0.7, 0.8}, {235.0, 1.6, 1.0}, {100.0, 0.0, 0.6},
         {35.0, 0.0, 0.8}, {5.0, 0.0, 0.9}
   };
   static uint64_t t = 0;
   // Generate samples
   for(int i = 0; i < num_samples; i++) {
      double sum_freq = 0.0, sum_ampl = 0.0;
      
      for (int j = 0; j < NUM_FREQ; j++) {
         sum_freq += freq_set[j].amplitude *
            cos(2.0 * M_PI * freq_set[j].cycles_per_win * t / (2 * num_samples));
         sum_ampl += freq_set[j].amplitude;
      }

      din_t sample = ap_fixed(sum_freq / sum_ampl);
      signal[i] = sample;
      t++;
   }
}

子函数定义中,使用enum定义了枚举常量,枚举常量比宏常量更安全。
注意,子函数中定义的枚举常量,只在子函数中可见。

在子函数中,生成多个数据,并输出到signal指定的数组中。

int main(void)
{
	//pre process ----prepare

	// call DUT
  
	//post process ----clean up
   tvin_ofs.close();
   tvout_ofs.close();
   delete [] signal_buf;
   delete [] fft_din;
   delete [] fft_dout;

   cout << "*** TEST COMPLETE ***" << endl << endl;
   return err_cnt;
}

main函数就是testbench。
TB分为多个阶段。
准备数据,调用DUT,输出数据,比对结果。
下面分别说明。
++++++++++++++++++++++++++++++++++++
pre process ----prepare

	//pre process ----prepare
	int err_cnt = 0;
   	short din_val = 0;
   	
  	din_t * const signal_buf = new din_t [REAL_FFT_LEN];
   	hls::stream frontend_din("fe_din");
   	hls::stream frontend_dout("fe_dout");
   	
   	dout_t * const fft_din = new dout_t [REAL_FFT_LEN/2];
   	dout_t * const fft_dout = new dout_t [REAL_FFT_LEN/2];
   	hls::stream backend_din("be_din");
   	hls::stream backend_dout("be_dout");
   	
   	ofstream tvin_ofs("realfft_fe_tvin.dat");
   	tvin_ofs.fill('0');
   	ofstream tvout_ofs("realfft_be_tvout.dat");
   	tvout_ofs.fill('0');

主要是准备数据,为后续的调用DUT准备好数据环境。
这里的技巧是,C++的 new array (new[]),常址指针(const pointer)。

对于din,
首先new了一个din_t类型的数据,用一个指针signal_buf 作为句柄来控制这个数组。
注意,这里使用了常址指针,指针只能作为句柄使用,不能修改这个指针,再指向别的数据元素。
然后创建了一个stream的具象化对象frontend_din。这是一个一维向量,元素类型为din_t。
然后创建了一个stream的具象化对象frontend_dout。这是一个一维向量,元素类型为xfft_axis_t。

对于dout,
首先new了两个dout_t类型的数据,用一个指针作为句柄来控制这个数组。
然后创建了一个stream的具象化对象backend_din。这是一个一维向量,元素类型为xfft_axis_t。
然后创建了一个stream的具象化对象backend_dout。这是一个一维向量,元素类型为dout_t。

对于output file,
创建了两个ofstream对象,用来作为FILE的控制对象。
调用了ofstream的方法fill,对文件初始化。

++++++++++++++++++++++++++++++++++++++++++++++++
call DUT
用一个for循环,执行了多次test task。

	// call DUT
for (int i = 0; i < NUM_TESTS; i++) {
      // Generate a new set of samples
      signal_gen(signal_buf, REAL_FFT_LEN / 2);
      
      // Put samples into fronted DUT input stream
      for (int j = 0; j < REAL_FFT_LEN / 2; j++) {
         frontend_din << signal_buf[j];
         // Capture input TVs for system level (Vivado XSIM) RTL simulation
         tvin_ofs.width(DIN_W / 4);
         tvin_ofs << hex << ap_uint(signal_buf[j].range(DIN_W - 1, 0)) << endl;
      }
      
      // Frontend DUT - applies a window function to data pairwise
      // output is formatted for XFFT AXIS input (complex TDATA w/ TLAST)
      hls_real2xfft(frontend_din, frontend_dout);
      
      // Put output of FE through reference FFT - HW will use Xilinx FFT IP
      for (int j = 0; j < REAL_FFT_LEN / 2; j++) {
         xfft_axis_t windowed_samples = frontend_dout.read();
         fft_din[j].real(windowed_samples.data.real());
         fft_din[j].imag(windowed_samples.data.imag());
      }
      
      fft_rad2_dit_nr(fft_dout, fft_din, REAL_FFT_LEN / 2, false);
      
      // convert ref FFT floating point output into fixed-point for backend
      for (int j = 0; j < REAL_FFT_LEN / 2; j++) {
         xfft_axis_t fft_axis_out;
         fft_axis_out.data.real(fft_dout[j].real());
         fft_axis_out.data.imag(fft_dout[j].imag());
         fft_axis_out.last = j == REAL_FFT_LEN / 2 - 1 ? 1 : 0;
         
         backend_din << fft_axis_out;
      }
      
      // Backend DUT
      hls_xfft2real(backend_din, backend_dout);

      for (int j = 0; j < REAL_FFT_LEN / 2; j++) {
         dout_t dout = backend_dout.read();
         float re = dout.real().to_float();
         float im = dout.imag().to_float();
         real32_t mag = sqrt(re * re + im * im);

         // write output test vector
         ap_uint<2*DOUT_W> tv_dout = (dout.imag().range(DOUT_W - 1, 0), dout.real().range(DOUT_W - 1, 0));
         tvout_ofs.width(2 * DOUT_W / 4);
         tvout_ofs << hex << tv_dout << endl;

         // Printout information for each bin
         if (i == NUM_TESTS - 1) {
            printf("%4d:t{ %9.6f, %9.6f }; mag = %8.6fn", j, re, im, mag);
         }
      }
      
      fflush(stdout);
      cout << endl;
}

每一次的测试任务,由一次迭代来完成。
在循环体内,
首先调用子函数,生个一个激励数据的数组,数据存放在signal_buff中。
然后,在一个for循环内,依次读取数据,写入stream具象化对象frontend_din中。同时,将激励数据用方法range截位处理后,类型强转为ap_uint类型。并将转换后的临时结果,输出到FILE中。
这里的FILE,是用tvin_ofs对象控制的。
然后,调用DUT,hls_real2xfft函数。输出结果存放在frontend_dout这个stream具象化对象中。
然后,在一个for循环中,依次读取frontend_dout对象中的元素,分离出real和image,并分别存入fft_din的元素的real成员和imag成员中。
然后,调用fft_rad2_dit_nr函数,输出结果存放在fft_dout数组内。
然后,在一个for循环中,依次将用ft_dout的元素的real成员和imag成员,填充fft_axis_out的real成员和imag成员,填充动作,通过调用成员函数real和imag完成。填充完成后,将fft_axis_out的数据,写入backend_din这个stream具象化对象中。
然后调用DUT,hls_xfft2real函数。输出结果放在backend_dout对象中。
然后,进行结果输出,结果比对计分。
在一个for循环中,依次读取backend_dout对象的元素,分离出real和imag,计算mag,将dout的imag和real分别截位处理,再拼位处理,并赋值给ap_uint类型的tv_out。将tv_out输出到tvout_ofs对象所控制的FILE中。
最后,将cout中的字符串刷新到屏幕上。

+++++++++++++++++++++++++++++++++++++++++++++++
post process ----clean up

	//post process ----clean up
   tvin_ofs.close();
   tvout_ofs.close();
   
   delete [] signal_buf;
   delete [] fft_din;
   delete [] fft_dout;

   cout << "*** TEST COMPLETE ***" << endl << endl;
   return err_cnt;

主要是进行收尾工作。收尾工作按照rollback的方式进行处理。
首先是关闭FILE。调用ofstream的方法close,关闭FILE。
然后是释放HEAP。由于之前使用new[]创建了多个数组,所以,要配对使用delete[],将之前创建的数组清除。
最后,打印console,返回比较结果。

+++++++++++++++++++++++++++++++++++++++++++++++
再来看看xfft2real.cpp文件。
它只是简单的封装了一个具象化函数。因为TOP函数必须是常规函数,不能是模板函数的具象化函数。

#include "xfft2real.h"

// This is the top-level (for HLS) function for the Real FFT backend processing
void hls_xfft2real(
      hls::stream& din,
      hls::stream& dout)
{
#pragma HLS INTERFACE axis port=dout
#pragma HLS INTERFACE axis port=din
#pragma HLS DATA_PACK variable=dout
#pragma HLS DATAFLOW

   // Template functions cannot be the top-level for HLS...
   xfft2real(din, dout);
}

这里使用了一些必须的pragma。
本函数遵循了良好的代码风格,
例如,使用stream的具象类型的对象引用作为形参对象引用,
从形参上衍生出AXIS接口,
对形参对象使用data_pack约束,但是这是不推荐的代码风格。
在函数级使用dataflow约束。
++++++++++++++++++++++++++++++++++++++++++++++
来看看xfft2real.h文件。

#ifndef _XFFT2REAL_H_
#define _XFFT2REAL_H_

首先是头文件保护宏。

#include "hls_realfft.h"

然后是包含需要的头文件。

template
void xfft2real(
      hls::stream& din,
      hls::stream& dout)
{
   	enum {REAL_SZ = (1 << LOG2_REAL_SZ)};
   	TI descramble_buf[REAL_SZ/2];
#pragma HLS ARRAY_PARTITION block factor=2 variable=descramble_buf
#pragma HLS INLINE // into a top-level DATAFLOW region

   // N-pt twiddle factor table used to descramble real from complex
   const complex twid_rom[REAL_SZ/2] = {
#include "w_rom_1k_init.txt"
   };

	realfft_be_buffer:
   	for (int i = 0; i < REAL_SZ / 2; i++) {
	#pragma HLS PIPELINE rewind
      xfft_axis_t tmp = din.read();
      ap_uint dst_addr = i;
      
      if (BITREV)
         dst_addr = dst_addr.range(0, LOG2_REAL_SZ - 2);
         
      descramble_buf[dst_addr] = tmp.data;
   }
   
	realfft_be_descramble:
   	for (int i = 0; i < REAL_SZ / 2; i++) {
	#pragma HLS PIPELINE 
	
      TI y1 = descramble_buf[i];
      TO cdata;
      
      if (i == 0) {
         cdata = TO((y1.real() + y1.imag()), (y1.real() - y1.imag()));
      } 
      else {
         TI y2 = conj(descramble_buf[(REAL_SZ / 2) - i]);
         
         // calculate: f = (y1 + y2) / 2; g = j*(y2 - y1) / 2
         TI f(( (y1.real() + y2.real()) / 2), ((y1.imag() + y2.imag()) / 2));
         TI g((-(y2.imag() - y1.imag()) / 2), ( (y2.real() - y1.real()) / 2));
         TO wg = TO(twid_rom[i]) * TO(g);
         
         cdata = f + wg;
      }
      
      dout << cdata;
   }
}

这个模板函数,最后是要在TOP函数中被封装的,所以,这里使用了inline,可以在TOP函数中被提级处理。
注意,这里有一个良好的编程技巧。文本分离(text separation)。

const complex twid_rom[REAL_SZ/2] = {
#include "w_rom_1k_init.txt"
   };

对于数组的初始化,是使用文本来完成的。如果没有文本分离,那么每次修改数组初始化文本,都要在H文件的对应位置去修改,但是如果使用了文本分离,那么只需要修改TXT文件即可,H文件可以不动。

+++++++++++++++++++++++++++++++++++++++++++++++
补充:
常址指针和指常指针的区别:
常址指针,一般也简称常指针,形如

din_t * const signal_buf= new dout_t [REAL_FFT_LEN/2];

它表示指针本身是常数,不能被修改,所以必须定义时立即赋值。
常址指针用作句柄,可以修改所指向的对象的内容,但是只能固定的某个对象,不能修改句柄,使句柄指向别的对象。

指常指针,一般也成为常量指针,或常数指针,或只读指针,形如

const char* str1 = "hello world";

它表示指针作为句柄,所指向的对象的内容只读不写。也就是说,句柄所控制的对象,位于常数存储区。
常量指针能够被修改,用来控制另外一个对象,但是,只能再次指向另外一个常量对象。指针始终是只读的。

++++++++++++++++++++++++++++++++++++++++
为什么不推荐使用data_pack?
默认情况下,
如果形参使用了结构体对象类型struct,那么HLS将解构这个结构体,将每个成员单独生成一个port,
这样,对于具有N个成员的结构体类型,解构后,就会存在独立的N个port,
这样的好处是,如果形参是一个结构体对象数组,那么解构后,有N个数组。

如果使用了data_pack,那么HLS将不会解构结构体,相反,将结构体实现为一个位向量,每个成员,占用这个位向量的一段,
那么,如果结构体封装了一个包含N个成员的数组,那么,这个位向量将会变的巨大,甚至不可综合。

谨慎使用data_pack,如果是比较小的位向量,那么可以使用data_pack,如果是比较大的位向量,则不要使用data_pack。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5099851.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-11-16
下一篇 2022-11-17

发表评论

登录后才能评论

评论列表(0条)

保存