HLS第三十二课(codingstyle )

HLS中,C是用来描述硬件的,不是软件编程的,这是基本概念。
下面记录一些常用的C描述技巧。
++++++++++++++++++++++++++++++
移位寄存器的描述。

for(i = N - 1;i > 0;i--){
#pragma HLS unroll
	shift_reg[i] = shift_reg[i - 1];
} 
shift_reg[0] = x;

对于一维向量的操作描述,使用for循环来描述。
为了更精确的描述左移位操作的先后顺序,不再使用i++的方式,而是使用i–。
最后,补充跳出循环边界后,收尾操作。
为了使HLS能够理解移位的并行特点,正确理解设计意图,添加unroll约束,将for完全展平。

verilog中的拼位操作,实际上是被编译器自动进行了按bit展平处理,虽然代码中没有体现这个展平过程,但是我们心里必须知道,是由这个展平过程的。
C语言里,没有拼位操作符,所以,代码中要么手动展平,要么使用for循环描述一系列只有下标不同的类似操作,并用pragma unroll通知编译器完成展平。

+++++++++++++++++++++++++++++++++++++++++++++++
循环起始间隔(II)是另一个重要的性能度量。 它定义为本次循环到下一次循环开始的时钟周期数。 在本例中, 循环II值为1, 这意味着我们可以在每个周期中启动新的迭代循环。

任何for循环都可以进行流水化优化。
我们通过#pragma HLS pipeline在Vivado HLS 中实现。
在大多数情况下, 循环流水线会减少循环的间隔时间, 但不会影响延迟时间。

请注意, 如果没有适当的数组分割, 展开内部循环可能不会提高性能, 因为并发读取操作的数量受到内存端口数量的限制。

有些代码中的内层循环Vivado HLS是无法完全展开的, 因为循环边界不是常量。
这种循环边界不是常量的情况,是需要尽量避免的。

dataflow 指令和pipeline指令都生成能够流水线执行的电路。 关键的区别在于任务流水的粒度不一样。 pipeline 指令构造了一个在循环级别上有效的流水线化的体系结构, 由指令中的II所决定。
dataflow 指令构造了一种体系结构,这些粗粒度操作不是静态调度的, 是通过流水线中的数据握手来动态地控制的。

dataflow 指令必须要有存储器设置以保证在不同进程之间传递数据。 它使用FIFO实现存储。

++++++++++++++++++++++++++++++++++++++++++++
​由于种种原因, 最好使用c++和Vivado HLS模板类
apint<>, ap_uint<>, ap fixed<>, ap_ufixed<>
来表示任意精度数据。

++++++++++++++++++++++++++++++++++++++++++++
当我们选择片上存储器的时候, 需要在嵌入式存储器(例如Block RAM) 或触发器(FF) 之间权衡。
FF的数量通常也限制在大约10万字节左右。
BlockRAM(BRAM) 提供更高的容量, 拥有Mbytes的存储量, 其代价是有限的可访问性。

++++++++++++++++++++++++++++++++++++++++++++
array_reshape和array_partion 都提高了一个时钟周期内可以读取的数组元素个数。
在使用array_reshape 的时候, 所有的元素在变换后的数组中共用同一个地址, 但是array_partition 变换后数组中地址是不相关的。
array_reshape directive 会形成大的存储块, 这样可能更容易高效地映射到FPGA 的资源上。

++++++++++++++++++++++++++++++++++++++++
HLS 中, 通过调用类hls::stream<> 创建FIFO类型的数据结构, 可以很好的进行仿真和综合。
数据通过write() 函数顺序的写入, 通过read() 函数读出。
hls::stream 只能通过引用的方式在函数之间进行传递。
来看一个矩阵块乘的例子。

typedef int DTYPE;
const int SIZE = 8;
const int BLOCK_SIZE = 4;
typedef struct { 
	DTYPE a[BLOCK_SIZE]; 
} blockvec;

typedef struct { 
	DTYPE out[BLOCK_SIZE][BLOCK_SIZE]; 
} blockmat;

void blockmatmul(
	hls::stream<blockvec> &Arows, 
	hls::stream<blockvec> &Bcols,
	blockmat &ABpartial, 
	int it) 
{
#pragma HLS DATAFLOW
	int counter = it % (SIZE/BLOCK_SIZE);
	static DTYPE A[BLOCK_SIZE][SIZE];
	DTYPE AB[BLOCK_SIZE][BLOCK_SIZE] = { 0 };
	
	if(counter == 0){ //only load the A rows when necessary
		loadA: for(int i = 0; i < SIZE; i++) {
			blockvec tempA = Arows.read();

			for(int j = 0; j < BLOCK_SIZE; j++) {
			#pragma HLS PIPELINE II=1
				A[j][i] = tempA.a[j];
			}
		}
	} 
	
	partialsum: for(int k=0; k < SIZE; k++) {
		blockvec tempB = Bcols.read();
		
		for(int i = 0; i < BLOCK_SIZE; i++) {
			for(int j = 0; j < BLOCK_SIZE; j++) {
				AB[i][j] +=A[i][k] * tempB.a[j];
			}
		}
	}
	 
	writeoutput: for(int i = 0; i < BLOCK_SIZE; i++) {
		for(int j = 0; j < BLOCK_SIZE; j++) {
			ABpartial.out[i][j] = AB[i][j];
		}
	}
}

这个代码中,使用了多个编码技巧。

通过typedef,将一维数组封装到一个struct中,这样,一维数组被理解为一个元素,然后,可以用stream容器来封装一个结构体元素。
通过typedef,将一个二维数组封装到一个struct中,这样,二维数组被理解为一个结构体对象,这样,二维数组就在后面被理解为一个输出对象。

输入参数使用了stream的具象类,这为模块级的流水化提供了保证。
在函数内,语句块被顺序分为了三大块,这为函数内的任务级的流水化提供了保证。任务从上游到下游,分别是输入缓冲,计算处理,临时结果输出。
这里使用了cache,这是一个良好的代码风格,最小化IO访问。static cache(例如A)用static定义,表示这是在多次调用中可以共享的资源。local cache(例如AB)则没有这个关键字。

先来看第一个任务。
使用了条件显隐,精确控制了cache的装载。
stream容器封装的类型是blockvec,所以,每一次read,都是读取的一个blockvec结构体元素,也就是一个一维数组。读取到一个local cache(例如tempA)中缓冲。
对一维数组的遍历,需要使用一个for循环。将tempA中的数据,导入static cache (例如A)中缓冲。

再来看第二个任务。
在最外层for循环中,每次迭代开始,发起一次read,同样的,是从stream容器中读取的一个blockvec结构体元素,放到一个local cache(例如tempB)中缓冲。
内层,需要一个两层嵌套for循环进行逐点处理。所以这里的循环变量,最能使用的是ij,而最外层反而使用的k。经过内层的这个两层嵌套for循环的逐点处理后,二维数组AB中的每个元素都被更新了一次。在最外层for循环的控制下,每次最外层迭代,AB都会被逐点更新一次。
AB是作为中间结果变量使用的,全部的迭代结束后,AB中的中间结果,就是最终结果。

再来看第三个任务。
用一个两层嵌套for循环,进行逐点处理。将中间结果变量AB中寄存的最终结果,逐点拷贝到输出对象中去。

+++++++++++++++++++++++++++++++++++++++++++++
来看一个直方图的例子。

void histogram(int in[INPUT SIZE], int hist[VALUE SIZE]) 
{
	int val;
	for(int i = 0; i < INPUT SIZE; i++) {
		#pragma HLS PIPELINE
		val = in[i];
		hist[val] = hist[val] + 1;
	}
}

这是原始风格的C描述。
使用了临时变量,也使用了pipeline。但是,代码中仍然存在read after then write。由于内存的重复读写, 系统只能实现II =2的循环。
这是因为在每次迭代循环中我们均需要从hist[]数组中读取数据和写入数据。

改进后的代码如下:

#include ”histogram.h”
void histogram(int in[INPUT SIZE], int hist[VALUE SIZE])
{
#pragma HLS DEPENDENCE variable=hist intra RAW false
	
	int acc = 0;
	int i, val;
	int old = in[0];
	
	for(i = 0; i < INPUT SIZE; i++) {
	#pragma HLS PIPELINE		
		val = in[i];
		
		if(old == val) {
			acc = acc + 1;
		} 
		else {
			hist[old] = acc;
			acc = hist[val] + 1;
		}
		
		old = val;
	} 
	
	hist[old] = acc;
}

这里使用了几个编码技巧。
首先,通过修改代码,解除了hist数组的intraRAW数据依赖关系,
然后,显式约束dependence,告诉HLS编译器,hist数组不存在intra RAW数据依赖关系。

来看看代码如何修改,可以消除intra RAW的数据依赖性?
第一,使用了临时变量,acc,old,val,作为local cache,
第二,使用了条件控制,避免intraRAW,我们知道,影响II的原因,是因为在同一次迭代中,出现了intraRAW,但是严格意义上,只有对同一个数组元素的intraRAW,才是不可实现的。而对于不同数组元素,是不存在intraRAW的,因为可以通过多端口来访问不同的数组元素。
在本例中,if的语句块,只有对local cache的操作,所以不会出现intraRAW,而else的语句块中,hist[old]和hist[val],也不是同一个元素,所以也不会出现intraRAW。其中的关键,就是用val和old作为两个索引坐标,并将old == val作为语句块的控制条件。

在每次迭代的开始阶段,读取in的元素,寄存到val中,
然后判断val和old是否相同,如果相同,则只操作acc,
如果不相同,则将acc中的计数,写入old索引的数组元素中。然后将val索引的元素的值导入acc,然后将acc加一。
在每次迭代的最后收尾阶段,更新old,将本次输入的val更新到old中,为下一次迭代准备好数据上下文环境。
全部迭代结束后,进行最后的收尾工作,将最后的acc写入最后的old索引的元素。由于最后的收尾是在for循环之外,所以,不会影响for循环的II。

+++++++++++++++++++++++++++++++++++++++++++++++
HLS中, 有多种编码方式可以将代码中的数组接口综合成AXIS流接口,
一种是使用接口约束,
另一种是使用 hls::stream<> 方式显式建立总线流接口。

HLS包含 hls::linebuffer<> 和 hls::window buffer<> 类, 它们可以简化窗口缓冲区和行缓冲区的管理。
但是我们也可以手写一些简约的代码,用来描述行缓冲和窗口。

如果不使用linebuffer,那么window的设置,代码如下:

row_loop: for (int row = 0; row < MAX_HEIGHT; row++) {
	col_loop: for (int col = 0; col < MAX_WIDTH; col++) {
	#pragma HLS pipeline
		
		for (int i = 0; i < 3; i++) {
			for (int j = 0; j < 3; j++) {
				int wi = row + i − 1;
				int wj = col + j − 1;
				
				if (wi < 0 || wi >= MAX_HEIGHT || wj < 0 || wj >= MAX_WIDTH) {
					window[i][j].R = 0;
					window[i][j].G = 0;
					window[i][j].B = 0;
				} else
					window[i][j] = pixel_in[wi][wj];
				}
			}
		}
		
		if (row == 0 || col == 0 || row == (MAX_HEIGHT − 1) || col == (MAX_WIDTH − 1)) {
			pixel_out[row][col].R = 0;
			pixel_out[row][col].G = 0;
			pixel_out[row][col].B = 0;
		} else
			pixel_out[row][col] = filter(window);
		}
	}
}

在外层的两层嵌套for循环中,对图像进行逐点处理。
在循环体内,开始阶段,就是设置window,window是一个二维数组,所以设置window需要用到一个两层嵌套的for循环,进行逐点处理。
由于存在对输入的形参数组pixel_in的反复读取,所以这个代码的II是很高的。

改进的方法,就是使用linebuffer。

rgb_pixel window[3][3];
rgb_pixel line_buffer[2][MAX WIDTH];

#pragma HLS array_partition variable=line_buffer complete dim=1
row_loop: for (int row = 0; row < MAX_HEIGHT; row++) {
	col_loop: for (int col = 0; col < MAX_WIDTH; col++) {
	#pragma HLS pipeline

		for(int i = 0; i < 3; i++) {
			window[i][0] = window[i][1];
			window[i][1] = window[i][2];
		}
		window[0][2] = (line_buffer[0][col]);
		window[1][2] = (line_buffer[0][col] = line_buffer[1][col]);
		window[2][2] = (line_buffer[1][col] = pixel_in[row][col]);

		if (row == 0 || col == 0 ||
								row == (MAX_HEIGHT − 1) ||
								col == (MAX_WIDTH − 1)) {
			pixel_out[row][col].R = 0;
			pixel_out[row][col].G = 0;
			pixel_out[row][col].B = 0;
		} else {
			pixel_out[row][col] = filter(window);
		}
	}
}

同样的,在外层的两层嵌套for循环中,对图像进行逐点处理。
在逐点处理的每次迭代的开始阶段,就是设置window,window是一个二维数组,所以设置window需要用到一个两层嵌套的for循环,进行逐点处理。但是这里,我们选择的设计风格是手工展平。
设置window分两个stage,
stage1,move window to eject oldest data,这个操作,是在一个一层for循环中逐行完成的,在每一行中,移动窗口,覆盖掉最旧的数据。
stage2,update window from newest data,这个操作,这里是逐行手工完成的。这里涉及到两个动作,一个是update window,一个是update linebuffer。
需要遵循“先取用再覆盖”的原则,防止数据丢失。
所以,首先用oldest linebuffer的对应column的数据,更新窗口,
然后,再用newer linebuffer的对应column的数据,更新older linebuffer的对应column的数据,并同时更新窗口,
依次向下处理,更新各个linebuffer,并同时更新window。
最后,将本点数据,即newest data 更新到newest linebuffer的对应column,并同时更新窗口。

在大多数情况下, 计算缓冲区窗口只是输入图像的一个区域。 然而, 在输入图像的边界区域, 滤波器
进行计算的范围将超过输入图像的边界。

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值