目录:
我发现点下面的链接会跳到一个不知道是谁的优快云下面需要付费下载,这个很迷惑,麻烦自行复制下面的链接。
Github:https://github.com/MasLiang/CNN-On-FPGA
那个不知道是谁的链接:https://download.youkuaiyun.com/download/weixin_42138780/18551586
没有下载不让举报,有办法的朋友麻烦举报一下
写在最前面的闲话:
前面我们用了很大的篇幅来说如何去做前期准备,实际上,在FPGA实现CNN最难的地方也就在于前期的设计与规划,后面的Verilog部署那就是体力活+经验活了,拼排流水的功底。所以做好前期准备是非常重要的。这个系列的博文由于是为了给新手一个入门的思路,因此很多细节都没有考虑,真正的循环如何去展开如何去并行,可以看看论文,推荐一篇2017年的很经典的论文《Optimizing Loop Operation and Dataflow in FPGA Acceleration of Deep Convolutional Neural Networks》,发表在FPGA2017,非常详细的把CNN的循环结构与循环优化进行的讲解。
下面进入正题
首先我们来讨论数据的性质。
- 参数包括weight、bias,如果存储空间足够(比如这个demo),可以将这些做成coe文件存进片上,如果空间不足,需要先写入flash,上电后将参数存入DDR,之后的时间就从DDR读取参数。
- 输入数据两种可能,一种是在做research的时候只想看一帧的运行,那么就只需将这一帧的数据像参数一样处理,而如果想做成实时系统,输入数据有其他的输入来源比如摄像头等,就不需要一开始存进去,而是需要开辟一块儿缓存空间。
- 层间数据,这部分肯定是需要开辟缓存空间的,上一层的输出缓存进去,再读取数据作为下一层的输入。
根据数据的性质,存储模块分成这样几种:
- 只需要用于读取的模块
- 在运行过程中需要不断的写入与读取的模块
- 在初始化阶段需要写入,在运行过程中只需要读取的模块
对于第一种情况,实际上就是在片上存储空间足够的时候,我们使用COE文件将数据存储在片上BRAM中,在使用的时候去读取。这种情况我们使用BRAM例化一个单口ROM。
对于第二种情况,有两种可能,一种是我们片上资源充裕,使用BRAM例化双口RAM,另一种是调取片外DDR。
对于第三种情况,与第二种情况类似,不过是写入是一次性的,从flash写入双口RAM或者DDR当中。
对于我们这个demo,就不考虑DDR和Flash写入的问题了(这部分也不是很复杂,网上都有各种各样的教程),所有能写死的数据全都用COE文件写死,缓存也都使用BRAM来例化双口RAM。
下面给出BRAM例化为单口RAM的过程:
1、选择Block Memory Generator IP核,这个在Vivado里面应该是可以用AXI4协议的。
2、选择单口RAM
3、设置大小,这里的位宽根据每个数据的位宽大小来这顶,而深度根据数据量来设定。
4、添加COE文件,要注意的是,COE文件位宽和数据个数要与前面设置的位宽和深度相对应、。
例化双口RAM的过程如下:
1、还是先选择IP核
2、选择例化为双口RAM(这里有两种双口RAM,一种是两个口都可以读写,另一种是一口读一口写,我们只需要后面这种)
3、我们可以很明确的看到这里需要设置读和写两个口的位宽,而由于他们公用同样的存储空间,仅需要在读口设置深度来确定空间大小即可。
4、同样的这里可以设置初始化的COE文件。
现在我们来考虑读写时序。
首先是按顺序读取输入数据,我们前面曾经说过这个demo给每个卷积操作只分配1个DSP单元,因此我们一次只能读取一个数据进行计算。这个很好理解,就是简单的按照卷积的顺序进行。下图给出了数据读取顺序。(当然有些人的习惯是按照列来进行,都一样的)
到程序里面最粗暴的写法就是行列转地址的方式,demo在这里。这种方式是设置行和列变量,通过所在的行列位置,来计算在存储空间中的地址,边界情况、时序等就比较考察verilog的功底了,这就是一个细心的体力活,不做多叙述。
下面我们来看进一步的,在前面我们说,这个demo中会使用多组卷积并行的方式。我们以第一层为例,假设是4组卷积并行,那么数据读取的顺序如下图所示:
我们需要同时输出4个数,可是ROM或者RAM的一个口只能在一个时钟内读取一个地址的数据,那怎么办呢~这里提供几种方法:
最简单粗暴的显然是我搞四个一毛一样的ROM或者RAM,读的时候从四个里面每个输出一个数就好了。这个时候需要算好了我们还剩下多少空间可供我们使用。
稍微复杂一些的就做两个一毛一样的ROM或者RAM,然后存储数据的时候,一个地址对应两个数,这样我可以一次性读取横着的两个数,再分割,下一次的时候,把右边的数交给左边,右边读取新的数据。
更复杂的那就是自己根据数据的读取顺序来设计存储顺序,比如卷积展开成矩阵乘法等,这样可以把离散的地址变成连续的地址,就可以在一个时钟内读取。
我们demo中使用最简单粗暴的方法,抱歉,有资源就是可以为所欲为[斜眼笑],如果资源不够多,就暂时不要去实现多组卷积并行的方式,反正目的也是熟悉整个开发流程。
输入数据是这样的,其他的层间数据也是一个道理,就不再多说。
至于参数,相对于数据更加简单,和数据是一样的读取方式,但是不需要滑动,就是这些参数一直在循环读取。
而这里最重要的是两种数据的对齐,需要同时开始读取,按照一样的顺序进行读取。
下面来分析写时序。写时序实际上是一样的道理,也是使用行列位置转地址的方式进行,只不过使用的端口变成了写入端口。
数据读写模块就到这里,实现很简单,主要在于时序的对齐。下一节将介绍卷积模块的设计。