这是迟到很久的卷积电路verilog设计的下篇。。。你看我还有机会吗。。。
上回我们给出系统的层次结构、卷积计算模块以及用于数据缓存的fifo模块,今天我们首先回顾一下上一次的关键内容。
系统结构回顾
RTL代码文件可以分为结构如下所示
~|--top_conv_tb.v|--top_conv.v| |--sram_input.v| |--sram_weight.v| |--sram_output.v |--conv.v |--weight_addr_gen.v |--pixel_addr_gen.v |--conv_calculation.v |--conv_fifo.v |--fifo_wr_control.v
其中top_conv.v为设计的顶层模块,其只有start,reset,clk三个输入端口以及一个输出端口finish,top_conv.v模块负责例化各子模块,完成conv.v, sram_input, sram_onput以及sram_weight之间的连接。
sram_input, sram_onput以及sram_weight三个子模块用于存储输入和输出,直接例化所给代码。
conv模块是实验中需要主要设计的模块,共包含五个子模块,各个子模块的功能概述如下:
•weight_addr_gen模块:用于在每个时钟周期产生当前单元计算卷积所需要的权重在sram中的地址;•pixel_addr_gen模块:用于在每个时钟周期产生当前单元计算卷积所需要的像素值在sram中的地址;•conv_calculation模块:利用得到的像素值和权重完成相乘、累加,并在当前窗口全部像素计算完成后,产生输出请求信号,并输出该窗口的卷积值;•conv_fifo模块:输入数据的sram和计算模块之间的缓冲模块,接受像素数据存储器sram_input和权重数据存储器及sram_weight的输出数据,并向conv_calculation模块输出缓存数据;•fifo_wr_control模块:这一模块较为简单,为一级寄存器,用于调整两个地址产生模块的读请求、读地址信号和写fifo之间的时序。
整合后的系统框图如下所示:
下面就进入到今天的新内容,我们将介绍系统中最后一个稍微麻烦的地址产生模块以及给出最后的结果展示。
地址产生模块
对于权重的地址产生模块较为简单,只需要依次从0到8进行循环即可:
//generate output weight_addralways @(*) beginif (start) begin case(weight_cnt) 4'd0: weight_addr = 0; 4'd1: weight_addr = 1; 4'd2: weight_addr = 2; 4'd3: weight_addr = 3; 4'd4: weight_addr = 4; 4'd5: weight_addr = 5; 4'd6: weight_addr = 6; 4'd7: weight_addr = 7; 4'd8: weight_addr = 8; default:weight_addr = `weight_addr_width'bx; endcaseendelseweight_addr = 0;end
而对于像素的地址产生模块,要相应的复杂一些,这里分两步进行,首先产生每次计算时像素窗口第一个像素(左上角)的地址first_pixel
,然后依次遍历窗口内的各个元素:
//generate the first position(top-left) in the conv windowalways @(posedge clk or negedge reset) beginif (!reset) first_pixel <= `pixel_addr_width'b0;else if (start&&pixel_cnt==8) begin case(first_pixel) 29,61,93,125,157,189,221,253,285,317,349,381,413, 445,477,509,541,573,605,637,669,701,733,765,797, 829,861,893,925: first_pixel <= first_pixel+3; default:first_pixel <= first_pixel+1; endcaseendelse first_pixel <= first_pixel;end//generate output pixel_addralways @(*) beginif (start) begin case(pixel_cnt) 4'd0: pixel_addr = first_pixel + 7'd0; 4'd1: pixel_addr = first_pixel + 7'd1; 4'd2: pixel_addr = first_pixel + 7'd2; 4'd3: pixel_addr = first_pixel + 7'd32; 4'd4: pixel_addr = first_pixel + 7'd33; 4'd5: pixel_addr = first_pixel + 7'd34; 4'd6: pixel_addr = first_pixel + 7'd64; 4'd7: pixel_addr = first_pixel + 7'd65; 4'd8: pixel_addr = first_pixel + 7'd66; default:pixel_addr = `pixel_addr_width'bx; endcaseendelsepixel_addr = 0;end
结果展示
1.波形仿真
我们首先进行波形仿真验证。运行vivado对我们的设计进行仿真验证,波形输出如下图所示:
仿真的关键信号大致可以分为读sram,写fifo,读fifo,卷积计算以及输出这五组,从图中可以看出各个信号的时序满足我们的设计需求,在开始信号start拉高后,conv_calculation模块发出数据请求信号s_read_req,fifo即开始从sram中读取数据。在fifo为非empty的后一个周期,fifo_read信号有效,conv_calculation开始从fifo中读取数据,在开始计算后的第九个周期,计算模块产生当前单元的正确卷积结果并发出完成信号sum_done用于输出请求。
关于计算模块的仿真波形区域放大图,展示如下:
通过sram_output.v中的以下语句,我们将卷积输出结果输出到文本文件"Write_Out_File .txt中。
Write_Out_File =$fopen("Write_Out_File .txt");$fdisplay(Write_Out_File,"%h",s_write_data_b);
Write_Out_File .txt文件共900行,对应于输出图片的900个像素点,安装实验要求,load_txt_to_pic.py将txt中的矩阵结果转化为输出图片并显示。作为参考,我们可以调用python中opencv库中的cv.filter2D函数进行卷积运算,结果如下所示:
可以看出经过conv.v卷积边缘提取后的结果仍能大致看出原有图片轮廓,但是与直接运用python中cv.filter2D相比,有些细节仍然有所丢失。这与我们实验中进行数据定点化处理以及cv.filter2D中卷积计算时的优化处理有关。另外,我们可以看出使用cv.filter2D后,图片的大小仍然保持不变,这是由于在cv.filter2D中进行了一定的填充插值处理。
2.计算资源消耗
•计算延时:整个计算过程开始于start信号拉高的23ns,结束于finish信号拉高的89935ns,在一个时钟周期为10ns的前提下,完成计算共需要8995个时钟周期。•乘法器资源:实验中一共进行了900次乘法运算,但每9个时钟周期内只单独进行一次乘法运算,所以总共需要一个乘法器。•访存次数:访存需要的次数与参考设计中相同,即每次计算一个3*3卷积窗,从input ram和weight ram取数3*3次,并存output到output ram一次。共访问input ram 30*30*3*3次,访问weight ram 30*30*3*3次,访问output ram 900次。总共访问次数为17100次。
vivado综合后的资源使用情况如下:
优化方向
在前面的设计中,我们是按照卷积窗口的移动顺序每次从sram中依次取出数据,对于这样方法,计算一个卷积窗口需要9次访存,总共900个窗口需要900*9
次访存,然而事实上,窗口中的9个weight的数据在计算始终保持不变,而计算相邻窗口时pixel的部分数据也可以重复利用(数据复用),因此我们可以通过减少访问模块外部的sram的次数来提高系统的运行速度。
对于weight来说,如果我们将sram中的数据读取后存储在计算模块内的存储器中,事实上总共只需要一次读weight_sram的操作,从而可以大大减少访问weight时的访存次数。对于pixel来说,相邻的窗口的部分数据读到conv模块后可以进行复用,如下图所示,使用数据复用
也可以减少pixel时的访存次数。
另一种更加普遍的思路是将卷积计算转化为矩阵乘法计算(im2col),对于矩阵乘法运算有大量的优化算法,并可以进一步利用脉动阵列
来实现计算的并行以及数据的重用(谷歌TPU
的基本架构),或者使用加法器和多个并行乘法器组成的加法树完成计算的并行(寒武纪Diannao
的基本架构)。这些优化方法由于时间原因,没有进一步在现有的卷积电路上加以实现,但我们会在后续的推文中给出使用HSL搭建的卷积电路,从中可以明显看出在使用了流水线以及循环展开unroll来获得226倍的加速比。