一个可综合的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_fixeddin_t; typedef complex > dout_t;
然后是定义数据类型。
typedef struct { ap_uintdata; 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_
首先是头文件保护宏。
templatevoid 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 templatedouble 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 complexcreal32_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::streamfrontend_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"
然后是包含需要的头文件。
templatevoid 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 complextwid_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。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)