HDLbits中Conwaylife题目解答,以及从中得到关于HLS的一些思考
导言
笔者最近刚刚刷完HDLbits的所有题目,个人认为其中难度较大的题目,除了状态机的设计外,便是Conwaylife(康威生命游戏),这是一个二维的细胞自动机。这也是为数不多HDLbits中用到for循环语句解答的题,笔者在解题过程中不由得想到了之前利用HLS完成的卷积操作,认为其中有较大的关联性,写下此文用于记录,如果思考有误,也欢迎大家一同探讨。
Conwaylife题目的Verilog HDL解答及思路
首先先贴出题目的中文翻译及笔者的Verilog代码,可以顺利通过仿真,之后简单介绍一下设计思路。
Problem Statement
康威的《生命游戏》是一个二维的细胞自动机。
“游戏”在二维单元格网格上进行,其中每个单元格为1(生存)或0(死亡)。在每个时间步长,每个小区都会根据其具有的邻居数量来更改状态:
0-1个邻居:单元格变为0。
2个邻居:单元格状态不变。
3个邻居:单元格变成1。
4个以上邻居:单元格变为0。
该游戏适用于无限网格。在此电路中,我们将使用16x16的网格。为了使事情变得更有趣,我们将使用16x16的环形面,其侧面环绕在网格的另一侧。例如,角单元格(0,0)有8个邻居:(15,1),(15,0),(15,15),(0,1),(0,15),(1,1) ,(1,0)和(1,15)。 16x16网格由长度为256的矢量表示,其中16个单元格的每一行由子矢量表示:q [15:0]是第0行,q [31:16]是第1行,依此类推。(此工具接受SystemVerilog,因此你可以根据需要使用2D向量。)
qoad:在下一个时钟沿将数据加载到q中,以加载初始状态。
q:游戏的16x16当前状态,每个时钟周期更新一次。
游戏状态应在每个时钟周期前移一个时间步长。
数学家,生命游戏细胞自动机的创造者约翰·康威(John Conway)于2020年4月11日因COVID-19逝世。
module top_module(
input clk,
input load,
input [255:0] data,
output [255:0] q );
reg [15:0] q_2d [15:0]; //2-d q
reg [15:0] q_next [15:0]; //2-d q_next
reg [3:0] sum;
integer i,j;
always@(*)begin
for(i=0;i<16;i++)begin
for(j=0;j<16;j++)begin
if(i==0 && j==0)//左上角
sum=q_2d[15][1]+q_2d[15][0]+q_2d[15][15]+q_2d[0][1]+q_2d[0][15]+q_2d[1][0]+q_2d[1][1]+q_2d[1][15];
else if(i==0 && j==15)//右上角
sum=q_2d[0][0]+q_2d[0][14]+q_2d[15][0]+q_2d[15][14]+q_2d[15][15]+q_2d[1][0]+q_2d[1][14]+q_2d[1][15];
else if(i==15 && j==0)//左下角
sum=q_2d[15][1]+q_2d[15][15]+q_2d[14][0]+q_2d[14][15]+q_2d[14][1]+q_2d[0][0]+q_2d[0][1]+q_2d[0][15];
else if(i==15 && j==15)//右下角
sum=q_2d[15][0]+q_2d[15][14]+q_2d[14][15]+q_2d[14][0]+q_2d[14][14]+q_2d[0][0]+q_2d[0][15]+q_2d[0][14];
else if(i==0)//上边界
sum=q_2d[0][j-1]+q_2d[0][j+1]+q_2d[1][j-1]+q_2d[1][j]+q_2d[1][j+1]+q_2d[15][j-1]+q_2d[15][j]+q_2d[15][j+1];
else if(i==15)//下边界
sum=q_2d[15][j-1]+q_2d[15][j+1]+q_2d[0][j-1]+q_2d[0][j]+q_2d[0][j+1]+q_2d[14][j-1]+q_2d[14][j]+q_2d[14][j+1];
else if(j==0)//左边界
sum=q_2d[i][1]+q_2d[i][15]+q_2d[i-1][0]+q_2d[i-1][15]+q_2d[i-1][1]+q_2d[i+1][0]+q_2d[i+1][1]+q_2d[i+1][15];
else if(j==15)//右边界
sum=q_2d[i][0]+q_2d[i][14]+q_2d[i-1][0]+q_2d[i-1][14]+q_2d[i-1][15]+q_2d[i+1][0]+q_2d[i+1][14]+q_2d[i+1][15];
else //中间元素
sum=q_2d[i-1][j]+q_2d[i-1][j-1]+q_2d[i-1][j+1]+q_2d[i][j-1]+q_2d[i][j+1]+q_2d[i+1][j]+q_2d[i+1][j-1]+q_2d[i+1][j+1];
case(sum)
2:q_next[i][j]=q_2d[i][j];
3:q_next[i][j]=1'b1;
default:q_next[i][j]=0;
endcase
//q_2d = q_next;
end
end
end
always@(posedge clk)begin
if(load)begin
for(i=0;i<16;i++)begin
for(j=0;j<16;j++)begin
q_2d[i][j] <=data[i*16+j];
end
end
end
else
q_2d <= q_next;
end
genvar m,n;
generate
for(m = 0; m < 16; m = m + 1) begin : line_reverse
for(n = 0; n < 16; n = n + 1) begin : list_reverse
assign q[m*16+n] = q_2d[m][n];
end
end
endgenerate
endmodule
该段代码仿真结果正确。书写代码用了一个组合逻辑描述变化规律,一个时序逻辑用于赋值,还有一个generate-for语句用于转换输出格式,个人认为将代码细分的比较合理,类似于三段式状态机的描述方法,便于读者阅读。
1、定义部分:
笔者在这里定义了两个二维的数组q_2d、q_next,用于存储数据的当前时刻与下一时刻的值,存储为二维数组的原因从题目中可以很轻易的得出,该细胞自动机也是二维的,在Verilog可以用这样的语句表达一个二维数组:
reg [15:0] q_2d [15:0]; //2-d q
reg [15:0] q_next [15:0]; //2-d q_next
在Verilog的语法书中是这样解释的,前一个[15:0]表示数据长度16位,后一个则表示共有16个这样的数据,学过硬件的朋友们很容易就可以在脑海中构建二维的数据存储图。在本题中,利用这样的方法将输入的256位数据(0-255)转换为二维数组((0,0),(0,1)······(0,15),(1,0),(1,1)······(15,15)),这样就如同题目表述的意思相同了。
2、组合逻辑部分
在转换为二维数组之后,笔者用了if~else if~else语句来将二维数组分类,方便定义sum记录数据,用于下面判断下一时刻的输出值q_next。当然,由于要判断每一个数据的变换,我们需要定义两个嵌套的for循环遍历整个二维数组。
for(i=0;i<16;i++)begin
for(j=0;j<16;j++)begin
3、时序逻辑部分
在时序逻辑中,如同状态机的时序部分,判断load信号是否有效,进而决定是否载入输入数据,并且每一个时刻刷新一次输出。相对比较简单,这里不再赘述。
4、输出维度转换
这里笔者采用了generate-for语句,将二维数组转换为一维数据。采用这一语句的原因是因为这里存在着复用模块的需求,因此直接再for循环里,对每一个二维数组进行赋值,便可一一对应得到最终的输出。
genvar m,n;
generate
for(m = 0; m < 16; m = m + 1) begin : line_reverse
for(n = 0; n < 16; n = n + 1) begin : list_reverse
assign q[m*16+n] = q_2d[m][n];
end
end
endgenerate
关于generate-for语句的用法,大家可以自行查询,在这里简单说一下,如果有着对于某个模块复用的需求,便可以采用这一语句,个人对于其与for循环语句的理解是。for循环语句无法嵌套模块(例如本题中用的assign),只能进行单纯的赋值运算(在解答代码中也有for循环的使用)。并且generate-for循环语句包含着并行处理的硬件思想,而for循环则是软件中顺序执行的语句。
HLS中卷积操作
在解答此题之后,笔者开始对于这一部分进行了一些思考,该题并不像是利用Verilog编写的题目,如果采用软件模拟的方法,似乎更容易完成细胞自动机,这就让人不由得想到利用高层次语言完成硬件编程,恰巧笔者在之前也有做过类似的项目,利用C语言完成卷积神经网络的前向推导(PL端),通过AXI总线传输数据,再利用PS端调度,输出预测结果,完成数据集的识别,这一项目基于Ultra96 V2板子完成的,其中个人编写的卷积操作如下:
void convolution_c1(
DTYPE X[C1_X_DMNIN][C1_X_DMNIN][C1_N_CHAN],
const DTYPE W[C1_W_DMNIN][C1_W_DMNIN][C1_N_CHAN][C1_N_FILTERS],
DTYPE out[C1_OUT_DMNIN][C1_OUT_DMNIN][C1_N_FILTERS],
const DTYPE bias[C1_N_FILTERS])
{
convolution_c1_label9:for(uint8_t f = 0 ; f < C1_N_FILTERS; f++)
{
convolution_c1_label10:for (uint8_t r = 0; r < C1_OUT_DMNIN ; r++)
{
convolution_c1_label11:for (uint8_t c = 0; c < C1_OUT_DMNIN ; c++)
{
out[r][c][f] = bias[f];
}
}
}
printf("the conv1 output is :");
for(uint8_t f = 0 ; f < C1_N_FILTERS; f++)
{
for (uint8_t r = 0; r < C1_OUT_DMNIN ; r+=STRIDE)
{
for (uint8_t c = 0; c < C1_OUT_DMNIN ; c+=STRIDE)
{
for(uint8_t ch = 0 ; ch < C1_N_CHAN; ch++)
{
for (uint8_t i = 0; i < C1_W_DMNIN ; i++)
{
for (uint8_t j = 0; j < C1_W_DMNIN ; j++)
{
out[r][c][f] = out[r][c][f] + W[i][j][ch][f] * X[r+i][c+j][ch];
}
}
}
printf("%d,",out[r][c][f]);
}
}
}
printf("\n");
}
可以看出,卷积操作由于是在二维图像中进行,也是利用许多for循环嵌套完成的,这一部分的乘加操作在C语言编写中十分简单快捷,六个for循环嵌套便可完成一次卷积操作。
笔者在完成其他部分的函数中,封装IP后在Vivado中调用,成功完成了这一网络的部署,得到输出结果。
一些思考
笔者在对比了两部分内容后,确实觉得在某些问题的求解中,利用C、Python这样的软件语言解决硬件问题时会更加便捷,对于HLS工具的诞生感到钦佩,在一些需要顺序执行语句的问题中,利用HDL语言书写代码怎么写怎么别扭,如果转换编程思维,采用C语言进行编程却能很好的完成任务。掌握软件编程确实是必要的,正如同现在流行的说法“软硬件协同”,这确实也是一名硬件工程师需要掌握的部分。
“软硬件协同这东西只有搞硬件的会搞,搞软件、算法的同学懒得搞这些。”也希望以后可以多多从遇到的一些问题中学习,多多感悟。