HLS:卷积运算单元设计与SDK测试


一、引言

涉及内容包括:多位宽并行,动态定点数运算,设置LOOP_TRIPCOUNT来性能分析等。


二、概念

1、功能定义。
为了让卷积运算单元更具灵活性,在输入图层的长宽高、卷积核的大小、Stride的大小等很多功能上可设置。
在这里插入图片描述
2、feature的内存排布。
之前的池化模块的feature,是一个四维数组。而卷积核没有分块前,也是四维的。这边按问答方式对一些概念进行整理。
为什么未分块前,卷积核(feature、kernel)为四维的?
从下面这张图上看,卷积核可以表示为2x3x3x3,即:个数x长x宽x通道数,不同的地方将这四个参数的位置可能颠倒了下,但都只是这四个概念。下面对这四个参数做个理解,①个数:表示不同卷积核的数目,对于一张完整的图(RGB都有),要提取里面的不同特征,需要多个不同的卷积核来进行提取,从之前的学习中,可以知道,卷积核本质就是特征feature,用一个特征怎么可能得到不同特征嘛,所以需要多个卷积核。②长x宽:这个为卷积核的size,很好理解,是和局部特征单独运算相关的。③通道数:相同卷积核的数目(权值共享),与一张完整图片中准备卷积处理的通道数相同,一般处理R、G、B通道,一个卷积核的通道数,也就为3了,同时可以得到3张feature map。在下面这张图的例子中,输入6x6,kernel为3x3,输出4x4,首先确定不是same模式,为valid模式,padding为0。只看H,有4-1=(6-3+2xpadding)/stride,则stride为1。通过在通道方向上相乘求累加后,9个点再做一次累加,得到输出feature map上的一个点。
在这里插入图片描述
为什么分块后,卷积核(feature、kernel)为五维的?
从下面这张图上看,可以把kernel上任意个数据表示为kernel: [CHout][Ky][Kx][CHin/K][K]
。CHout表示的是有多少个kernel,Ky和Kx为一个kernel的宽和长,CHin/K为在输入图层方向切了多少个子块,K为每个子块的索引。
因此,本设计中,由一个四维的输入图层feature map和五维的feature(卷积核、权重),得到一个输出的feature map输出图层。
在这里插入图片描述
3、卷积运算的full、same和vaild模式是啥?
这个参考了前人的一篇文章,讲的很好,非常感谢,Add在最后边了。橙色为image,蓝色为kernel。
1°full mode模式:从image和kernel刚相交,就开始做卷积运算。而其余白色的地方全部填0,这样一张橙色的image先补了两行两列的0后,如果stride为1的卷积运算后,图片反而变大了,这模式很少用。
在这里插入图片描述
2°same mode模式:当kernel的中心K与image的边角重叠时,开始做卷积运算。这种模式下,如果stride为1的卷积运算后,输出的feature map尺寸是保持不变的(相当于输入图)。相比full mode,这模式就用的多了。该模式可以在前向传播的过程中,让特征图的大小保持不变,调参师不需要精准计算尺寸的变化,因为根本就没有变化。
由于卷积核的size尺寸一般为奇数,故可以总结一个padding公式,用来计算需要补多少行列的0数据,才能满足same mode模式。具体公式为:padding_x=(kx-1)/2,padding_y=(ky-1)/2。举个例子,这里size为33,那么,padding_x=padding_y=1。
在这里插入图片描述
3°valid mode模式:当kernel全在image里时,进行卷积运算,明显地,会丢掉一部分的数据。
在这里插入图片描述
如何计算卷积运算后的feature map的尺寸大小?
也参考了前人的博文,但网上貌似没有发现推导的过程,举几个例子应该也能总结下,这里也直接作为经验来使用了。
计算的公式为:output_h =(originalSize_h+padding x 2-kernelSize_h)/stride +1。
其中,originalSize_h为原始输入image的H或W,padding为填充的行或列数,kernelSize_h为kernel的size大小,stride为x或y方向上的步长。

在这里插入图片描述
拿一道面试的题来试试手吧。(卷积向下取整,池化向上取整)
input image:200×200
first layer_conv:kernel size 5×5,padding 1,stride 2
second layer_pooling:kernel size 3×3,padding 0,stride 1
third layer_conv:kernel size 3×3,padding 1,stride 1
output feature map?
答:
1°(200-5+21)/2+1 为99.5,取99。
2°(99-3)/1+1 为97。
3°(97-3+2*1)/1+1 为97。

分块时为什么是沿输入图层的方向,而不是输出图层方向?
为了并行地做子块的矩阵乘累加运算,得到结果(和之前FIR滤波器案例类似)。要时刻把握住,是取出输入feature map的一个子块,和权重的一个子块,来进行相乘累加的操作。


三、程序

1、conv_core.h文件。

#ifndef __CONV_CORE_H__
#define __CONV_CORE_H__

#include <ap_int.h>
#include <iostream>

using namespace std;

#define K 8
// 单个数据是16bit位宽,一个子块数据是16*K位宽,用bus表示。
// 输入图层和权重都是ap_int<16>
typedef ap_int<16>   dtype_dat;
typedef ap_int<16*K> dtype_bus;
// 两个INT16数据相乘,得到的结果会溢出,用32位来存。根据经验,用40位来作为累加的结果位宽。
typedef ap_int<32>   dtype_mul;
typedef ap_int<32*K> dtype_mul_bus;
typedef ap_int<40>   dtype_acc;
// C语言无法传可变索引的数组的,但在硬件内存中,都按照一维分布,故可以把地址传进去。
// 后续得自己计算存放的地址来取数据
// 定点化后的数据由三个小数点的参数来计算得到,INT16,小数点由四位来表示
void Conv(
	ap_uint<16> CHin,  // 输入通道数
	ap_uint<16> Hin,   // 输入特征的高度
	ap_uint<16> Win,   // 输入特征的宽度
	ap_uint<16> CHout, // 输出特征的通道数
	ap_uint<8> Kx,     // 卷积核的宽度
	ap_uint<8> Ky,     // 卷积核的高度
	ap_uint<8> Sx,     // 卷积核扫描时,宽度方向的步进
	ap_uint<8> Sy,     // 卷积核扫描时,高度方向的步进
	ap_uint<1> mode,   // 卷积的模式,valid还是same
	ap_uint<1> relu_en,                // 激活非线性反激活层标志
	dtype_bus feature_in[],            // 输入feature map的数据,由于C中无法使用可变数组,故使用指针,传地址来定位具体数据,四维数组按一维分布
	// 之前池化使用的是固定大小的,可以按照固定数组来传,但这里数组长度是变的
	ap_uint<4> feature_in_precision,   // 输入feature map数据小数点位置
	dtype_bus W[],                     // 输入权重的数据,也是指针索引
	ap_uint<4> W_precision,            // 输入权重数据小数点位置
	dtype_bus feature_out[],           // 输出feature map的数据,也是指针索引
	ap_uint<4> feature_out_precision   // 输出feature map数据小数点位置
	);//mode: 0:VALID, 1:SAME
#endif

2、conv_core.cpp文件。

#include "conv_core.h"

// 输入特征图四维:[C/K][H][W][K]
// 输入权重值五维:[CHout][Ky][Kx][CHin/K][K]
// 输出特征图四维:[C/K][H][W][K],均可以计算出来
void Conv(
	ap_uint<16> CHin,  // 输入通道数(输入图层方向上的数目)(卷积核与特征图参量)
	ap_uint<16> Hin,   // 输入特征的高度(卷积核与特征图参量)
	ap_uint<16> Win,   // 输入特征的宽度(卷积核与特征图参量)
	ap_uint<16> CHout, // 输出特征的通道数(卷积核参量)
	ap_uint<8> Kx,     // 卷积核的宽度
	ap_uint<8> Ky,     // 卷积核的高度
	ap_uint<8> Sx,     // 卷积核扫描时,宽度方向的步进
	ap_uint<8> Sy,     // 卷积核扫描时,高度方向的步进
	ap_uint<1> mode,   // 卷积的模式,valid还是same
	ap_uint<1> relu_en,                // 激活非线性反激活层标志
	dtype_bus feature_in[],            // 输入feature map的数据,由于C中无法使用可变数组,故使用指针,传地址来定位具体数据,四维数组按一维分布
	// 之前池化使用的是固定大小的,可以按照固定数组来传,但这里数组长度是变的
	ap_uint<4> feature_in_precision,   // 输入feature map数据小数点位置
	dtype_bus W[],                     // 输入权重的数据,也是指针索引
	ap_uint<4> W_precision,            // 输入权重数据小数点位置
	dtype_bus feature_out[],           // 输出feature map的数据,也是指针索引
	ap_uint<4> feature_out_precision   // 输出feature map数据小数点位置
	)//mode: 0:VALID, 1:SAME
{
	ap_uint<8> pad_x,pad_y;  // padding数据,表示x和y方向待填补的行列数
	ap_uint<16> CHin_div_K=(CHin+K-1)/K;  // 输入图层方向上,切块后每个的长度
	ap_uint<5> out_truncate;
	out_truncate=feature_in_precision+W_precision-feature_out_precision;

	if(mode==0)  // valid下不进行padding
	{
		pad_x=0;pad_y=0;
	}
	else  // same模式下进行padding,具体padding的数据值,在之前概念部分补充了
	{
		pad_x=(Kx-1)/2;pad_y=(Ky-1)/2;
	}
	// 通过输入的参数数据,计算输出图层的高度和宽度,这个的公式也是固定的
	ap_uint<16> Hout,Wout;
	Wout=(Win+2*pad_x-Kx)/Sx+1;
	Hout=(Hin+2*pad_y-Ky)/Sy+1;

	dtype_acc sum=0;
	dtype_bus out_tp=0;

	// 对输出feature map的数据进行计算
	// 关于是输出方向上需要多少重循环的定位:输出是一个四维的数据,需要前三重做迭代,第四重做处理
	// 一是输出图像的高度,二是输出图像的宽度,三是输入图层方向上切块了多少,四是具体索引到哪个子块
	LOOP_i:for(int i=0;i<Hout;i++)
	{
		// 输出高度和宽度方向上的循环
		LOOP_j:for(int j=0;j<Wout;j++)
		{
			// 输出通道方向上的循环,CHout表示有多少个kernel,是卷积核专门的一个参数,与输入特征图无关
			LOOP_cout:for(int cout=0;cout<CHout;cout=cout+1)
			{
				// 第四维上的数据处理
				// 把卷积核size上一个点,在所有子块上的结果都求出来,再循环size上所有点
				LOOP_ii:for(int ii=0;ii<Ky;ii++)
				{
					LOOP_jj:for(int jj=0;jj<Kx;jj++)
					{
						// CHin_div_K输入方向上切块的循环
						// 输入通道方向上被切块了,需要做一个循环,按输入通道上子块的数目
						LOOP_cin:for(int cin=0;cin<CHin_div_K;cin=cin+1)
						{
							ap_int<16> h=i*Sy-pad_y+ii;
							ap_int<16> w=j*Sx-pad_x+jj;

							dtype_mul_bus tp;
							dtype_bus dat;
							dtype_bus wt;
							// 判断是否在padding的范围内
							// 这段if代码是获取特征图数据和权重数据的
							if(h>=0 && w>=0 && h<Hin && w<Win)
							{
								// 输入数据dat=feature_in[cin][h][w][K]
								// 根据四维数据的这个索引,来算出一维的数据
								// INT16*K feature_in[C/K][H][W]为feature_in的形状,[K]相当于INT16*K了
								// dat=feature_in[cin][h][w]为这个形状里取的索引
								dat=feature_in[cin*Hin*Win+h*Win+w];
								// INT16*K weight[CHout][Ky][Kx][CHin/K]为权重的形状,[K]相当于INT16*K了
								// wt=weight[cout][ii][jj][cin]为这个形状里取的索引
								wt=W[cout*CHin_div_K*Kx*Ky+ii*CHin_div_K*Kx+jj*CHin_div_K+cin];
								//std::cout<<"dat="<<dat<<",W="<<wt<<std::endl;
							}
							else
							{
								dat=0;
								wt=0;
							}
							// 每次取出一个特征图子块和权重子块,来进行累加
							for(int k=0;k<K;k++)
							{
								// 具体子块的累加过程,对应元素相乘
								// tp是一个32*k的数据,位宽32*k
								tp.range(k*32+31,k*32)=(dtype_dat)dat.range(k*16+15,k*16)*(dtype_dat)wt.range(k*16+15,k*16);
							}
							for(int k=0;k<K;k++)
							{
								// 累加,40位的数据,这个累加是移位前的数据
								sum+=(dtype_mul)tp.range(k*32+31,k*32);
								//std::cout<<"sum="<<sum<<std::endl;
							}
							//std::cout<<"sum="<<sum<<std::endl;
							if((cin==CHin_div_K-1) && (jj==Kx-1) && (ii==Ky-1) )
							{
								// 如果使用了非线性激活层,小于0就设为0
								if(relu_en & sum<0)
									sum=0;
								// 数据右移
								// 右移位数为输入feature map小数点位数+权重小数点位数-输出feature map小数点位数
								dtype_acc res=sum>>out_truncate;

								// 移位如果范围,进行前后的钳位
								if(res>32767)
									res=32767;
								else
									if(res<-32768)
										res=-32768;

								dtype_dat res_16=res;
								out_tp.range((cout%K)*16+15,(cout%K)*16)=res;sum=0;
								// feature_out[cout][i][j][cout]
								if( ((cout%K)==K-1) || (cout==(CHout-1)) )
								{
									feature_out[(cout/K)*Wout*Hout+i*Wout+j]=out_tp;out_tp=0;
								}
							}
						}
					}
				}
			}
		}
	}
}

3、main.cpp文件。
C仿真是必须的,用来保证C语言这边的设计是没有问题的。

#include "stdio.h"
#include "conv_core.h"

#define IN_WIDTH 10
#define IN_HEIGHT 10
#define IN_CH 1
#define IN_CH_DIV_K ((IN_CH+K-1)/K)

#define KERNEL_WIDTH 5
#define KERNEL_HEIGHT 5
#define X_STRIDE 1
#define Y_STRIDE 1

#define RELU_EN  0
#define MODE     1          //0:VALID, 1:SAME
#define X_PADDING (MODE?(KERNEL_WIDTH-1)/2:0)
#define Y_PADDING (MODE?(KERNEL_HEIGHT-1)/2:0)

#define OUT_CH 1
#define OUT_CH_DIV_K ((OUT_CH+K-1)/K)
#define OUT_WIDTH ((IN_WIDTH+2*X_PADDING-KERNEL_WIDTH)/X_STRIDE+1)
#define OUT_HEIGHT ((IN_HEIGHT+2*Y_PADDING-KERNEL_HEIGHT)/Y_STRIDE+1)

int main(void)
{
	dtype_bus feature_in[IN_CH_DIV_K][IN_HEIGHT][IN_WIDTH];
	dtype_bus W[OUT_CH][KERNEL_HEIGHT][KERNEL_WIDTH][IN_CH_DIV_K];
	dtype_bus feature_out[OUT_CH_DIV_K][OUT_HEIGHT][OUT_WIDTH];
	dtype_dat temp;

	for(int cin=0;cin<IN_CH_DIV_K;cin++)
		for(int i=0;i<IN_HEIGHT;i++)
			for(int j=0;j<IN_WIDTH;j++)
				for(int k=0;k<K;k++)
					if((cin*K+k)<IN_CH)
						feature_in[cin][i][j].range(16*k+15,16*k)=(1<<14);//i*IN_WIDTH+j;
					else
						feature_in[cin][i][j].range(16*k+15,16*k)=0;

	/*打印测试
	for(int cin=0;cin<IN_CH_DIV_K;cin++)
		for(int i=0;i<IN_HEIGHT;i++)
			for(int j=0;j<IN_WIDTH;j++)
				for(int k=0;k<K;k++){
					temp = feature_in[cin][i][j].range(16*k+15,16*k);
					//printf("feature_in[%d][%d][%d][%d] = %d \n", cin, i, j, k, temp);
					std::cout<<"feature_in["<<cin<<"]["<<i<<"]["<<j<<"]["<<k<<"]="<<(dtype_dat)temp<<std::endl;
				}
	*/

	for(int i=0;i<KERNEL_HEIGHT;i++)
		for(int j=0;j<KERNEL_WIDTH;j++)
			for(int cin=0;cin<IN_CH_DIV_K;cin++)
				for(int cout=0;cout<OUT_CH;cout++)
					for(int k=0;k<K;k++)
						W[cout][i][j][cin].range(16*k+15,16*k)=(1<<14);//(i*KERNEL_WIDTH+j);//(cout==0)?(i*KERNEL_WIDTH+j):0;

	/*打印测试
	for(int i=0;i<KERNEL_HEIGHT;i++)
			for(int j=0;j<KERNEL_WIDTH;j++)
				for(int cin=0;cin<IN_CH_DIV_K;cin++)
					for(int cout=0;cout<OUT_CH;cout++)
						for(int k=0;k<K;k++){
							temp = W[cout][i][j][cin].range(16*k+15,16*k);
							std::cout<<"W["<<cin<<"]["<<i<<"]["<<j<<"]["<<k<<"]="<<(dtype_dat)temp<<std::endl;
						}
	*/


	for(int cout=0;cout<OUT_CH_DIV_K;cout++)
		for(int i=0;i<OUT_HEIGHT;i++)
			for(int j=0;j<OUT_WIDTH;j++)
				feature_out[cout][i][j]=0;

	printf("1234\n");

	Conv(IN_CH,IN_HEIGHT,IN_WIDTH,OUT_CH,
			KERNEL_WIDTH,KERNEL_HEIGHT,X_STRIDE,Y_STRIDE,MODE,RELU_EN,
			&feature_in[0][0][0],14,
			&W[0][0][0][0],14,
			&feature_out[0][0][0],10
		);//mode: 0:VALID, 1:SAME

	for(int i=0;i<OUT_HEIGHT;i++)
		for(int j=0;j<OUT_WIDTH;j++)
			for(int cout=0;cout<OUT_CH_DIV_K;cout++)
			{
				std::cout<<"OUT["<<cout<<"]["<<i<<"]["<<j<<"]="<<(dtype_dat)feature_out[cout][i][j].range(15,0)<<std::endl;
			}

	return 0;
}

输入feature map打印出来的数据,可以看出,只有当具体的高度i和宽度j发生变化时,才有数据,其他都为0。而weight数据全都是16384,这里就不贴出来了。
在这里插入图片描述
验证数据。
在这里插入图片描述


四、优化

1、第一次没加任何优化。
综合结果是直接报错了。原因是之前几个数组采用的是指针来索引的,HLS工具并没能很好知道这几个数组在内存中的具体位置和大小。
在这里插入图片描述
回想之前HP口那一次lab(池化),那里为了给自己的模块说明,数据是来源于DDR的具体哪个位置,使用了m_axi和offset=slave的方式。
在这里插入图片描述

	#pragma HLS INTERFACE m_axi depth=99999 port=feature_in offset=slave
	#pragma HLS INTERFACE m_axi depth=99999 port=W offset=slave
	#pragma HLS INTERFACE m_axi depth=99999 port=feature_out offset=slave

更改完,也确实可以综合过了,但Latency为“?”,这个是没有控制循环次数造成的。

2、循环次数设置。
按照所写的main函数,来对模块的循环变量进行控制。
在这里插入图片描述
3、端口部分优化。
除了feature in、feature out和weight数据,其他的都得CPU来进行配置,因此都是用GP口的s_axi。但feature in、feature out和weight数据得用HP,来与DDR内存进行交互。另外,单总线有一个缺陷,就是feature in和weight数据,得两个周期才能读取出来,但axi接口上有一个bundle捆版的操作,就是一个模块,可以产生两个接口。

	#pragma HLS INTERFACE s_axilite port=return
	#pragma HLS INTERFACE s_axilite port=feature_out_precision
	#pragma HLS INTERFACE s_axilite port=feature_in_precision
	#pragma HLS INTERFACE s_axilite port=Sy
	#pragma HLS INTERFACE s_axilite port=Kx
	#pragma HLS INTERFACE s_axilite port=Win
	#pragma HLS INTERFACE s_axilite port=Sx
	#pragma HLS INTERFACE s_axilite port=Hin
	#pragma HLS INTERFACE s_axilite port=W_precision
	#pragma HLS INTERFACE s_axilite port=relu_en
	#pragma HLS INTERFACE s_axilite port=Ky
	#pragma HLS INTERFACE s_axilite port=CHin
	#pragma HLS INTERFACE s_axilite port=mode
	#pragma HLS INTERFACE s_axilite port=CHout

	#pragma HLS INTERFACE m_axi depth=100 port=feature_in offset=slave bundle=bus1
	#pragma HLS INTERFACE m_axi depth=25 port=W offset=slave bundle=bus2
	#pragma HLS INTERFACE m_axi depth=100 port=feature_out offset=slave bundle=bus1

4、关于latency和II的优化。
这个需要非常理解HLS工具,HLS工具在处理for循环的时候,会做一个Loop Flattening的操作,就是外层的若干个循环中,若没有语句截断,就当成一个for循环,并把Loop的次数给计算出来。在代码设计时,就需要很注意了,如果把语句放在不同的for循环层,会导致出现:消耗周期=(内层Loop次数 x II+latency)x 外层Loop次数。因此,需要做一件事,就是把外层for循环的语句,全部搬移到内部趣,再通过if语句来限制啥时候触发。代码中的这句,就是做了这个工作。

if( (cin==CHin_div_K-1) && (jj==Kx-1) && (ii==Ky-1) )

5、关于feature out数据的优化。
feature out是一个axi的接口,输出数据不能使用range的方式来输出,这样会导致CPU先将最完整的数据读取出来,再使用截断的方式改写后写回(以前学嵌入式的时候,好像是位带的操作)。这操作,直接导致输出的latency变长。


五、测试

1、实验平台搭建。
使用到了两个HP口,具体平台如下。
在这里插入图片描述
实验环境,用了Zynq这块板子。
在这里插入图片描述
2、数据预备测试。
把feature_in、feature_out和weight数据先存储到内存中,并作打印测试先。结果和HLS的C仿真是一样的,没啥问题。另外,SDK的%d可以直接打印short、int等不同类型,不用单独使用hd来打印,这和c++的输出流很像。

// 初始化feature_in数据
    for(i=0; i<1; i++)
    	for(j=0; j<10; j++)
    		for(k=0; k<10; k++)
    			for(v=0; v<8; v++)
    				if((i*8+v)<1)
						feature_in[i][j][k][v]=(1<<14);//i*IN_WIDTH+j;
					else
						feature_in[i][j][k][v]=0;

    for(i=0; i<1; i++)
		for(j=0; j<10; j++)
			for(k=0; k<10; k++)
				for(v=0; v<8; v++)
					xil_printf( "the input_buffer is [%d][%d][%d][%d] : %d \n\r" , i,j,k,v, feature_in[i][j][k][v]);

    // 初始化weight数据
    for(i=0; i<1; i++)
		for(j=0; j<5; j++)
			for(k=0; k<5; k++)
				for(v=0; v<1; v++)
					for(w=0; w<8; w++)
						weight[i][j][k][v][w]=(1<<14);

    for(i=0; i<1; i++)
		for(j=0; j<5; j++)
			for(k=0; k<5; k++)
				for(v=0; v<1; v++)
					for(w=0; w<8; w++)
						xil_printf( "the weight is [%d][%d][%d][%d][%d] : %d \n\r" , i,j,k,v,w, weight[i][j][k][v][w]);

测试的结果。
在这里插入图片描述
通过debug的方式,检查寄存器配置的值。
在这里插入图片描述
3、SDK部分测试代码。
可能部分有些更改,详细可看工程。

#include <stdio.h>
#include <stdlib.h>
#include "platform.h"
#include "xil_printf.h"
#include "xparameters.h"
#include "xparameters_ps.h"
#include "xil_io.h"
#include "xConv.h"
#include "xConv_hw.h"
#include "sleep.h"

#define DDR_BASEARDDR      XPAR_DDR_MEM_BASEADDR + 0x10000000

int main()
{
    init_platform();

    int  i,j,k,v,w;
    //int  rev;
    int  data,state;
    short feature_in[1][10][10][8];
    short weight[1][5][5][1][8];
    short feature_out[1][10][10][8];

    XConv xconv;
    XConv_Config *ConfigPtr;

    xil_printf("feature_in address is %p \n\r", feature_in);
    xil_printf("weight address is %p \n\r", weight);
    xil_printf("feature_out address is %p \n\r", feature_out);

    print("Hello World\n\r");

    // 关闭cache,保证内存缓存读写的一致性
    Xil_DCacheDisable();

    // 初始化feature_in数据
    for(i=0; i<1; i++)
    	for(j=0; j<10; j++)
    		for(k=0; k<10; k++)
    			for(v=0; v<8; v++)
    				if((i*8+v)<1)
						feature_in[i][j][k][v]=(1<<14);//i*IN_WIDTH+j;
					else
						feature_in[i][j][k][v]=0;
/*
    for(i=0; i<1; i++)
		for(j=0; j<10; j++)
			for(k=0; k<10; k++)
				for(v=0; v<8; v++)
					xil_printf( "the input_buffer is [%d][%d][%d][%d] : %d \n\r" , i,j,k,v, feature_in[i][j][k][v]);
*/

    // 初始化weight数据
    for(i=0; i<1; i++)
		for(j=0; j<5; j++)
			for(k=0; k<5; k++)
				for(v=0; v<1; v++)
					for(w=0; w<8; w++)
						weight[i][j][k][v][w]=(1<<14);
/*
    for(i=0; i<1; i++)
		for(j=0; j<5; j++)
			for(k=0; k<5; k++)
				for(v=0; v<1; v++)
					for(w=0; w<8; w++)
						xil_printf( "the weight is [%d][%d][%d][%d][%d] : %d \n\r" , i,j,k,v,w, weight[i][j][k][v][w]);
*/

    for(i=0; i<1; i++)
		for(j=0; j<10; j++)
			for(k=0; k<10; k++)
				for(v=0; v<8; v++)
					feature_out[i][j][k][v]=0;


    ConfigPtr = XConv_LookupConfig(XPAR_CONV_0_DEVICE_ID);
    state = XConv_CfgInitialize(&xconv, ConfigPtr);

    // 判断初始化pool模块是否成功
	//state = XConv_Initialize(&xconv, XPAR_CONV_0_DEVICE_ID);
	if(state != XST_SUCCESS)
	{
		print("XConv_Initialize fail!!\n\r");
		return XST_FAILURE;
	}

	//XConv_DisableAutoRestart(&xconv);
    // 众多参数配置,得参照slave的优化和C仿真时Conv调用传入的形参
    // 设置通道数
    XConv_Set_CHin_V(&xconv, 1);
    // 设置输入特征的高度
    XConv_Set_Hin_V(&xconv, 10);
    // 设置输入特征的宽度
    XConv_Set_Win_V(&xconv, 10);
    // 设置输出特征的通道数
    XConv_Set_CHout_V(&xconv, 1);
    // 设置卷积核的宽度、高度
    XConv_Set_Kx_V(&xconv, 5);
    XConv_Set_Ky_V(&xconv, 5);
    // 设置卷积核扫描水平与竖直的步进
    XConv_Set_Sx_V(&xconv, 1);
    XConv_Set_Sy_V(&xconv, 1);
    // 设置卷积的模式
    XConv_Set_mode_V(&xconv, 1);
    // 设置是否要relu非线性激活层
    XConv_Set_relu_en_V(&xconv, 0);
    // 设置feature_in、feature_out和weight的地址,以及各自小数点的位置
    XConv_Set_feature_in_V(&xconv, (u32)feature_in);
    XConv_Set_feature_in_precision_V(&xconv, 14);
	XConv_Set_W_V(&xconv, (u32)weight);
	XConv_Set_W_precision_V(&xconv, 14);
	XConv_Set_feature_out_V(&xconv, (u32)feature_out);
	XConv_Set_feature_out_precision_V(&xconv, 10);

    // 启动电路,将ap_start置为1,开始计算。

	XConv_EnableAutoRestart(&xconv);
    XConv_Start(&xconv);
    print("Test Start!!!\n\r");

    // 数据输出
	// 判断计算完成的条件是ap_done为1,当不是1时说明还尚未完成,就一直读取判断,直至算完。
/*	data  = XConv_IsDone(&xconv);
	while(data != 1)
	{
		data  = XConv_IsDone(&xconv);

	}*/
	sleep(20);
	print("Test Done!!!\n\r");

	for(i=0; i<1; i++)
		for(j=0; j<10; j++)
			for(k=0; k<10; k++)
				//for(v=0; v<8; v++)
					xil_printf( "the feature_out is [%d][%d][%d][2] : %d \n\r" , i,j,k, feature_out[i][j][k][2]);

    cleanup_platform();
    return 0;
}

4、测试结果。
出现一个问题,数据不在k为0时有,而是k为2时才有数据,这是和C仿真冲突的,已经做了下面一些尝试:调试发现,问题关键在于XConv_IsDone函数没有成功返回1,即模块功能没正常完成。
1°调整了conv模块时钟为20MHz,不行。
2°generator output再生成比特流,不行。
3°使用单总线验证,降低效率来验证,同样问题,不行。
4°更改了feature和权重数据存储的方式,不行。
5°尝试了Disable模块后再配置,不行。

在这里插入图片描述

5、猜测与后续验证。
1°内存问题,使用了双HP口,如果模块想同时对DDR访问,是不是有些细节没有注意到。可用单端验证过,这问题双HP口应该可以处理,那内存上的问题,可能是地址与存储形式上,需要后续验证。
2°Conv模块配置问题,这部分也是还没验证的,对比了很多文章里的配置,基本上都是一个套路,可依旧不行。要说细节,就是双HP口的模块配置,会不会需要打开interrupt这东西,也需要后续验证,不过得先理解下按几个interrupt是用来干啥的。
就很无语,C语言只有那几行代码,多维数组测试也没啥问题,但数据发生了漂移,而且前面有几个是错的,但后面基本都是对的,前面做的几次测试,也都是这个结果,除了能排除薛定谔的Conv现象外,这个Conv模块是没啥问题的,Bug集中在“SDK测试的配置中”。算了去南亭吃碗面,今天天气太好了,适合出去玩,打球啥的,后续再填这个坑。


六、补充

一些关键时间点的记录:

2021年04月13日:卷积的一些概念。
2021年04月14日:动态定点数概念、网络定点化概念、权重和特征的排布方式、子块间并行、卷积运算单元需支持哪些参数(卷积规模等)、代码的理解。
2021年04月20日:分析模块需要如何工作,约束优化等。
2021年04月21日早上:板级测试,但失败了,原因是数据前几个出错,并且发生漂移。

参考的资料:
为什么卷积核是四维的:https://blog.csdn.net/qq_30763385/article/details/103094391。
卷积的三种模式:https://blog.csdn.net/leviopku/article/details/80327478
面试中常考的feature map的大小计算:https://zhuanlan.zhihu.com/p/49913137
feature map大小计算方法:https://blog.csdn.net/qq_28424679/article/details/78665273

后续可能做的工作:(非常的多)
1、填坑调Bug。
2、卷积相关部分的加深学习。
3、该实验在PynqZ2上的验证。这得等有Pynq板卡后了。
4、HLS工具中Analysis的学习。是一个分析综合后latency和II的组件,值得学习。
5、C语言部分的提高。C语言长期不用带来的后遗症,后续回看朱老师的课,并研究下算法,卷积相关的Lab难点在于多重for循环设计与多维数组。

Some Notes:
1、多重for循环的一个技巧。如果想把外层循环的多条语句放到内部,只需要剪切后,在外面套一层if来判断,就行了。
2、关于程序下载的一个点。之前好像application是默认有勾上的,这次不知为啥,生成的平台没有勾上,下载程序后,啥反应都没有,主要原因就是elf程序没有下载到CPU里边。
在这里插入图片描述

  • 14
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学不懂啊阿田

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值