VLSI Final Project:小型卷积核单元设计 项目总结
0 项目背景
VLSI流程课程在期末要求完成一个project,来让我这样的小白简单走一遍VLSI的设计流程有个概念。作业内容是实现一个简单的小型卷积核,给定输入的数据后能计算并返回相应正确的卷积运算结果。整个project主要分功能仿真,然后是综合,再综合后仿真,形式验证。我个人任务主要是根据需要设计并完成verilog部分工作,队友完成了后续的综合、后仿真、形式验证等内容,所以我对verilog内容熟悉一些,也主要记录这一块的内容与感受。
作为一个芯片、verilog等知识点的小白,边学边写,并在deadline截至前两天快速迭代代码版本着实是非常刺激。趁自己还记着点project细节,以及纪念一波撰写第一个小型verilog任务、实现了本project的前仿、综合、后仿、形式验证等环节,我记录一下此project的思路以及相关的基础知识。
1 项目要求
设计简单的卷积核运算模块,限定了模块的输入端口、输出端口,在保证正确性的前提下能完整的通过后续的dc过程、后仿与形式验证等过程。在同等情况下, 卷积核单元的面积、速度、功耗将成为评判性能优劣的标准。(我并没有考虑这玩意儿( ̄▽ ̄)")
2 常规概念
作为一个IC设计小白,有一些基础知识与概念得去了解以后才能实施后续的模块设计。简单把相关内容分为verilog与IC流程相关吧。
2.1 Verilog语法
用程序的形式 “写出” 一个芯片 来, 那肯定是要用一个 “能够描写电路工作逻辑”的程序语言 来写才行吧!HDL,(Hardware Description Language),就是专门写电路运行逻辑的编程语言,比C语言更底层一丢丢,是所有能干这个事情的程序语言的大类名称。其中有两个很有名的常用语言,一个是VHDL,但是以前美国军方用的,咱不提他;另一个是Verilog语言,好,俺用的也是这个。所以Verilog语言属于HDL。
我们用Verilog劈里啪啦写了一个project以后,这些代码文件后缀是.v格式的。
verilog的语法内容和C语言有很多是相像的,在学习的时候对照一下有那些相同点和不同点即可快速入门。在课程上有老师多次提到,在网络上也有别的博主说过:撰写verilog的时候,心中要有电路!起初我觉得这是一句废话,我写c语言的时候脑子也会有算法的运行逻辑,这不是很正常的吗?随着这个project的撰写推进,直到后续的dc环节、后仿真环节时,我才意识到这句话内部的真正含义:要想你你写的verilog模块得是真实、有效、能流片的芯片逻辑,必须要心中有“电路”而不是仅仅有逻辑而已!!!——verilog代码能运行,和能产生相对应的电路(即可综合)是两码事!( ̄▽ ̄)"
- 相同点:
- 注释、标识符命名规范、运算符(逻辑与或非、转义符等,但是verilog没有自增运算符,即‘i++’)
- 常用的基本语句块,如if-else、for语句、while. 但是
{}
变成了begin
和end
.
- 不同点:(仅写了部分,)
- 所有.v文件写的时候要先声明模块名字
module xxx(x..x); endmodule
,这并不等价于c语言里的函数的概念,而是和matlab的所有.m文件的模块化一个意思。也可以理解为一个“类”,后续可以用这个module的名字进行实例化不同个数的模块。 - 对于模块的参数,要在后文中写明是输入类型(input)还是输出类型(output),并声明其类别(是reg类还是wire等,不写就默认是wire类型).
- 常量的定义语法不同,verilog中写法是
parameter xx=...
- verilog不怎么使用for语句,尽量少用,因为这样会让后续生成的电路比较复杂,一般都是无脑写出来(没错,是你想那样)。
- verilog的switch和if-else语句,一定要把每个分支都考虑到位,switch要养成写default的习惯,不然后续综合出的电路多出来一堆latch的时候,有你好受的( ̄▽ ̄)".
- verilog里最大的两个多出来的部分,一个是alway语句块,一个是assign赋值语句,这个学习一下即可。
- verilog中多出来两个重要的变量类型,分别是reg和wire类型。要搞清楚它们之间的联系与区别,不然最基础的代码逻辑都没法写,这是俩verilog中最最常用也是最最核心的两个变量类型。一般来说,对于一个module的输入输出端口可以都是wire类型。但是在实例化模块的时候要区别对待:把形参是wire类型的输入端口变量用实参是reg类型的变量代进去。
- veriog的过程赋值语句中分成了两种,一种是和c语言一样的自上而下顺序执行的语句:阻塞赋值(
=
);另一种是语句块里的内容同时执行的语句:非阻塞赋值(<=
).
- 所有.v文件写的时候要先声明模块名字
在完成本项目的过程中,我起初并没有care这两种赋值手段的区别,完全按照c语言的编码思路。我当时捉摸着只要数据变量之间不存在dependency的话,在语句块中使用阻塞赋值和非阻塞赋值没啥区别。但是后来发觉,verilog在撰写模块的时候不用非阻塞赋值没法综合,也就是后续的环节走不下去,写的代码虽然功能上正确也能仿真出正确的结果和波形,但是无法生成相应的现实对应的电路图,那肯定不行啊( ̄▽ ̄)"。(很好理解,电路中的变化都是随着时钟的每一个上升沿或下降沿的cycle进行变化的,而各个部分都是同时发生变化而不是顺序串行变化的,所以在写时序逻辑代码的时候要用非阻塞赋值才能对应电路而不能用阻塞赋值)。所以在小白写verilog语句的时候,我觉得最重要的两个知识点,就是完全搞明白阻塞赋值versus非阻塞赋值,以及always模块的一些关键特性与执行逻辑(多个always是并行之类的)。
这一部分呢,可以看这份博客,对于阻塞赋值和非阻塞赋值有一些更好的理解:
Verilog十大基本功0(阻塞赋值与非阻塞赋值)—— CSDN Times_poem
Verilog HDL 阻塞和非阻塞赋值的理解(2) —— CSDN ShareWow丶
2.2 Testbench的概念
以前没写过verilog,接触早先的quartus9.1时也没有接触过testbench,所以对这个概念有点迷茫。实际上,在撰写verilog代码的时候,另一个有一定工作量的活儿就是写testbench。
- 如何理解testbench?
- 在最简单的C程序中,我们完全可以让我们需要运算、处理的数据内容、最初的数据信息在main()函数中、或某个自定义函数中进行初始化、引入。
- 但verilog中并不行。用verilog写出来的程序模块只能进行“对数据的加工和处理”而不能进行初始化工作,因为整个模块(对应到现实中,就是这个模块对应的实例化出的电路)只要通电了以后就会不断的运行与工作,哪有什么“初始化”的概念。唯一一个相似的概念是——“复位”。但这并完全等价于初始化。
- 可能读者会对什么情况下,数据需要进行初始化没有概念?举个例子,就是时钟信号 c l k clk clk, 这个一位的信号会隔固定的时间从0翻转到1,又过一会儿翻转回0. 因此,需要在某个地方声明时钟信号的翻转频率,这就是最简单的初始化语句(虽然会永远执行,但第一次执行时间是模块最开始的时候进行的)。
- 在verilog中,其实存在一个用于初始化的语法——语句块:
initial begin ... end
,这句话的内容只会在模块被创建出后执行一次。但是!在综合环节中,verilog的模块时不能使用这句话的( ̄▽ ̄)",用了IDE也不会报错(会不会来着,忘记了),因为语法上没有问题,但是后续dc环节就无法综合出电路了… - 所以,为了进行输入数据的传递、初始化信号的传入,我们就需要写在一个专门的文件中——就是testbench。而testbench也是一个.v文件,只不过它不会参与后续芯片的后仿、综合等过程,仅仅是起到传输输入信号、数据的文件,所以是测试代码正确性、RTL波形仿真中不可缺少的伴随文件。而在testbench中啥都可以写,只要逻辑和语法对就行,所以可以使用intial语句块、以及时间延迟的 ‘#’ 符号等对数据进行处理。
2.3 基本的数字逻辑电路的知识
本科要有学过的底子,有个大概就行,不用很好。
学期初我一直不理解TTL和CMOS的区别。本科的时候只讲了TTL没有讲MOS,而读研讲MOS不讲TTL,但是它们都有类似的截止区、放大区、饱和区,工作特性类似,确实不同的结构。那它们有什么区别呢?为什么都是用CMOS进行集成电路设计呢?在网上搜了半天,没有得到想要的答案,结果翻书的时候,书上提到了并给出了总结:
- TTL和CMOS集成电路的制造工艺不同,但是逻辑功能和应用上没有大区别,多相同。
- ELC最快、TTL中等、CMOS最慢;
- ELC抗干扰能力最弱、TTL中等、CMOS最好;
- 功耗ELC最高、TTL中等、CMOS最低。
而我们上课常提到的MOS反相器,又称为非门。在MOS集成电路中,各逻辑门都是由反相器(非门)构成的。所以它的重要性如此之高。
还有一个要注意:在搞VLSI的时候,常常会看到有老师、博客会说 ”不要生成latch,要用FF代替latch“ 的话,那么什么是 ”latch“ 什么是 ”FF“ 呢,这个要注意一下。可以在网上搜一下,这个还是蛮多的。
2.4 VLSI的基础知识补充
不像C语言中点击“编译并运行”就能看出结果是否够正确,因为我们verilog写代码的目的是写电路、写芯片,所以代码最后能跑出正确的结果也并不一定说明代码能用——因为可能产生不了对应电路、性能太辣鸡等等,所以RTL级别的验证只是最最基础的而已,之后还需要子走别的流程。
-
前仿,又名功能仿真,是针对RTL的功能验证。信号的跳变是瞬时完成的,所以比较理想,是看看功能上是否正确的。
-
综合,是将RTL代码生成对应的电路图(门级网表),你可以展开看。
- 生成文件: 网表文件.v和SDF文件.sdf.
-
后仿,又称门级仿真,又称动态时序仿真,就是综合以后,在前仿的基础上加入一些约束、单元延时等信息以后,验证设计的时序和功能是否正确,可以说是更贴近现实生活中的电路(上下信号跳变有延迟了)。后仿又分为,综合后仿真和布局布线后仿真。
- 所需文件:上述的网表文件.v和SDF文件.sdf,综合时所用工艺库db(其内包含对应的标准单元工艺库.v文件),和所用的testbench.
-
形式验证(Formality),是一种静态的验证手段(动态和静态的区别在后文),根据电路结构静态地判断两个设计在功能上是否等价,常用来判断一个设计在修改前和修改后其功能是否保持一致。不需要testbench,但是必须有一个参照设计和一个待验证的设计,比如说你的verilog的HDL代码和生成的网表,看看两者是否在功能上一致,一致就说明ok。因为形式验证是在数学逻辑上进行比对的,所以贼快!但是它不考虑timing,所以要和STA结合使用。
-
因为后仿太慢了太慢了, 受不了,所以有些人会不跑后仿,用STA+Formality的形式来替代后仿。但STA只检查边沿timing,Formality只看register和combination的抽象功能,所以呢会有些不完备。后仿在下面三种情况是必要的:异步逻辑设计部分、ATPG向量验证和初始化状态验证。另外,后仿产生的VCD文件还可以做功耗分析。
-
IC时序验证的两种方法:动态时序分析和静态时序分析。
- 动态时序分析,就是仿真。通过testbench来看时许和功能是否一致,但是不能保证100%的覆盖率,而且需要各种各样的testbench,而且到了门级的仿真会很慢很慢!
- 静态时序分析(STA),只分析时序不管功能验证,不需要testbench,能100%覆盖所有的路径!但是只能对同步电路进行分析,而不能对异步电路进行时序分析。缺点,是不能完整把所有影响延时的因素给包含进去,所以用STA工具导出关键路径后再门级仿真来确定时序正确。
此处我参考了博客:前仿后仿与形式验证——CSDN fgupupup
2.5 Verilog哪些行为能或不能的概念
在用verilog进行编写逻辑、完成一个project时,一个很坑的地方就在于:一件事情,你能不能做,IDE有时候并不会告诉你、编译器也不会告诉你… 很多合乎程序语言逻辑的行为,其实在当下环节中是不被允许的,但是相关的软件并没有告知你的功能( ̄▽ ̄)"。而这些事情,十足的影响到编写verilog的工作体验,对萌新和小白(我)也非常不友好,前期没有意识到、做足功课,就会导致后期对project进行大改。
举几个在本次project中,我多次热泪盈眶的例子,都是符合verilog语法但是后面才知道又不能做的几件事(不能做是因为后续的环节会失败,但是在撰写前面环节的内容的时候不知道,后期才明白前面写的不对…)。比如说:
-
阻塞语句和非阻塞语句
-
这个之前提到过了,大家可以在这个博客内容中看看,我觉得写的不错,就不赘述了:
-
总而言之呢,就是在一般情况下,组合逻辑中才使用阻塞赋值(C语言中的顺序执行),时序逻辑中才使用非阻塞赋值。
-
那对于我这样的废物小白和已经把数电模电还给老师的萌新,不理解什么情况下算“时序逻辑”咋办? 唔,我个人的理解是:在module中一般情况统统全用非阻塞赋值,养成这个好习惯就行,在testbench中使用阻塞赋值( ̄▽ ̄)".
-
-
同时呢,使用阻塞赋值还会有一些坑,比如“引入信号变化竞争带来的不可预测结果”:
-
不同的always之间,虽然理论上是并行执行的,但实际上还是要考虑顺序的,因为阻塞赋值会放大信号竞争的情况,不同的always顺序,会导致不同的波形图。如下图:
-
顺序①:
结果这样的:
因为逻辑顺序是先判断 j j j 是否赋值,然后再去修改 i i i. 所以这个波形图里面, j j j 的变化就晚了一个周期。到了第5个cycle才是1. -
顺序②:
结果这样的:
因为逻辑顺序先修改 i i i,然后判断 j j j 是否赋值. 所以这个波形图里面, j j j 的变化就正常,或说早了一个周期。到了第3个cycle就是1了.
-
结论: 当有多个always语句块,且有变量之间相互依赖关系时,波形仿真结果会因为竞争出现多种可能的情况,在这个例子中是按照always出现的顺序进行优先执行的(本例子是在vivado下的仿真结果,不同的软件可能结果不同,比如vcs等) 这个问题带来的缺陷是,以后思考逻辑的时候还要去思考这个时序问题如何协调,如何协调每个变量的赋值顺序,以及因为思考不到位出现的一系列隐性bug。
-
解决方案: 上述问题的根本原因,是因为使用阻塞赋值的实时赋值,从而因为赋值顺序的微观上差异(竞争),导致运算逻辑变得玄学,受always的定义顺序而限定。所以改成使用“非阻塞赋值” 以后,不管是上述那种写法,都将是在第5 cycle以后才会是1(变成了①的波形)!即变量依赖关系全部以滞后一个周期来进行展现!不管是哪种写法,全部变成了图①的结果!
-
-
习惯使用非阻塞赋值后,虽然数cycle的时候要注意一下什么时候要晚一周期,但是再也不用担心cycle数跳来跳去的情况了。
-
-
用intial语句
- 写verilog时,每个模块我各自写了个intial进行初始化——✔️——逻辑上没错.
- 我拿着这些模块去dc(综合生成电路)——❌——想综合电路的话,intial只能在testbench用了,“综合”时并不支持模块内使用intial语句块.
-
使用二维数组甚至多维数组
- 因为我使用reg来模拟自己的memory,理所当然的就想到用reg类变量开一坨数组来存放数据。但是在verilog中,一个数据很大概率上本身就是某种意义上的数组,例8位数据
t
e
m
p
temp
temp 就是
reg [7:0] temp;
而占据了多位数据。所以如果我想开一个在C语言中的int数组,开100的大小,那么在verilog中就是一个32位但是列数是100列的二维数组了:reg [7:0] temp [99:0];
. - 但是!Verilog中的数组变量,不能作为传入传出的端口数据,只能成为中间变量在module内部用用。
- 其次,Verilog中的数组大小虽然用起来好像是无限的,但是 根据我个人惨痛的教训,在后续dc的compile时(综合阶段),如果你的memory使用的reg数组太大,compile的时间会增加2.5倍!就会卡在compile阶段,让你怀疑是不是代码写错了——即reg数组的电路综合成本是很大的。 但是这些在verilog代码的撰写阶段,并没有sei会告诉你( ̄▽ ̄)"。
- 为具体说明dc环节对reg数组大小的敏感,我统计了一下不同reg数组大小时,dc的compile阶段增加的时间情况(我只统计了个大概):
- 对照组:开150长度25位的reg数组,使用dc阶段的compile时间是18s;
- 开200长度25位的reg数组(1.33倍对照组的memory大小),使用dc阶段的compile时间是31s(1.72倍对照组时间);
- 开500长度25位的reg数组(3.33倍对照组的memory大小),使用dc阶段的compile时间是185s(10.28倍对照组时间);
- 开1000长度25位的reg数组(6.67倍对照组的memory大小),使用dc阶段的compile时间是870s(48.33倍对照组时间);
- 3600长度25位的reg数组,( ̄▽ ̄)"我没试了,估计要3个小时…
- 上面这个对比,说明reg用的越多,这个dc的compile时间是至少是平方倍数增加呀,所以说verilog撰写过程中,对于数组的使用还是要非常谨慎。不然逻辑上、功能上、波形仿真上都是没有问题的,但是后续环节可能会直接裂开,导致前功尽弃。
- 同时,还要追加一下出现上述情景的背后原因,不一定对,是我向学长请教后的可能性猜测。当我声明了一个数组,对其初始化操作并不会增加dc的compile时间,但当我使用这个数组的时候,也就是赋值操作的时候数组出现在了等号的右边,对数组寻址的过程会导致上述的compile时间的显著变化结果。这可能是因为数组的索引过程,在dc环节电路映射时,本质上是用MUX进行多选,但是数组的大小会直接影响到每次多选器MUX的大小,就会导致电路中的MUX非常大且复杂,从而影响了dc的效率。所以呢,撰写verilog的时候,一要将时序逻辑功能和相应的电路图心中有联系;二是要理解映射后的电路复杂性是如何的。(当然,第二步要多多积累经验了( ̄▽ ̄)"…)
- 因为我使用reg来模拟自己的memory,理所当然的就想到用reg类变量开一坨数组来存放数据。但是在verilog中,一个数据很大概率上本身就是某种意义上的数组,例8位数据
t
e
m
p
temp
temp 就是
-
在多个always块中修改同一个变量
- 在构思一些功能的时候,我很当然的想到,在一个always块中将某个控制信号变成1,另一个always信号进行监听,随之在下一个cycle将控制信号恢复成0.
- 很可惜,这个实现思路在逻辑、语法、波形仿真上是对的——✔️
- 但这个写法是后续的电路综合环节,跑dc时不被允许的——❌——没啥为什么,就是不给你用。所以如果后续要跑综合,要将某变量的修改都放到同一个always里面,用不同的if-else块进行执行。
- 但是一个always进行变量修改,另一个always中对变量进行读和使用是没有关系滴~
3 Verilog实现思路
3.1 如何进行第一个verilog仿真?
小白如何使用vivado或quartus进行HDL代码的波形仿真呢?这部分内容可以参考我写的这篇博客:
【Chips】如何启动第一个Quartus/Vivado下的Verilog仿真过程 —— CSDN 仰天倀笑
3.2 Project题意与要求
根据Project的要求,实现的小型卷积运算单元要:
- 运算正确
- 模块端口限定:
- 控制端口要含有如开始和结束信号;
- 卷积的通道数channel和卷积的kernel个数(即输出通道数),比如 卷积是一张 32通道的fmap, 有8个kernel,每个kernel各有32个通道,那么 c h a n n e l N u m b e r = 32 channelNumber=32 channelNumber=32 而 k e r n e l N u m b e r = 8 kernelNumber=8 kernelNumber=8 ;
- 卷积核的大小固定为 4 × 4 4 \times 4 4×4, 传入的 fmap 的大小也固定为 64 × 64 64 \times 64 64×64, 因此卷积结果大小也固定为 61 × 61 61 \times 61 61×61.
- 数据端口仅 8个8位的fmap数据线、8个8位的kernel数据线、2个25位的卷积结果输出线;
- 数据传输控制信号要有:读kernel数据使能、读fmap数据使能和发结果数据使能;
- testbench的数据传入顺序可以自己定。
3.3 版本一代码设计逻辑
-
版本一代码特色:
- 仅8条数据线;
- 通篇使用阻塞赋值;
- 无读写控制信号,由counter计数器进行控制逻辑;
- 卷积结果存储面积以一个完整fmap卷积结果为单位 ( 61 × 61 61 \times 61 61×61);
- 双存储memory空间轮流使用;
- 输出数据线只有两根(后面都保持只有两根)。
-
同步时序的逻辑设计:
在testbench和卷积模块topModule之间,利用基于时钟周期clk的计数器counter,进行不同时序下的逻辑功能执行,分别是:
-
testbench准备工作好了,就将控制信号置1, 表示卷积可以开始;
-
版本1的代码审题审错了 ,不知道kernel和fmap的数据线是分开的各8条 ,我以为一共只有8条。所以逻辑写成共用8条数据线,先发送一个kernel,再发送相关的fmap数据。
-
topModule发现卷积开始信号为1时,就开始接收数据并开始卷积计算。
-
-
topModule计算功能逻辑:
- 数据线有8根,所以每次能拿8个不同的数据。一个完整的单通道的kernel是 4 × 4 4 \times 4 4×4 大小,所以需要2个cycle就取完一次卷积运算的卷积核。
- 第一次卷积运算也要取完整的 4 × 4 4 \times 4 4×4 fmap数据,所以也要2个cycle。 而之后,卷积核每一次右移是读入新的4个数据,但是数据总线有8根,有点浪费;所以逻辑改成**:后续每个cycle卷积核在fmap上右移两列,完成2次卷积核运算,**并累加在卷积结果的memory上。
- 由上述提到,每个cycle,我们都会执行两次卷积CONV,因此整个电路中拥有两个卷积模块,并行运行。
- 当一行卷积结束以后,卷积进入下一行的开头进行运算,此时又要读取前 4 × 4 4 \times 4 4×4 大小的fmap数据,需要2个cycle,此处要停一下,并不会计算卷积。
- 当所有行卷积都结束以后,说明一个channel的卷积结果结束,进行下一个channe的卷积运算。那么就需要重新读取卷积核的内容,也就是下一个channel的kernel的数据,又需要2个cycle读取 4 × 4 4 \times 4 4×4 的kernel数据。
- 当所有channel卷积结果都累加在memory上以后,就将当前的memory输出,即当前kernel的卷积结果输出;并开始从下一个kernel的首个channel开始读取卷积核以及fmap的数据。
- 由于输出卷积结果的时候,是在当前fmap的多个channnel的卷积值都累加完毕以后才进行的,输出结果的时候不能memory就不能进行下一个kernel的计算了,会导致计算资源的闲置。因此,决定采用双 61 × 61 61 \times 61 61×61 大小的memory空间,一个用于输出卷积结果,一个用于卷积计算,实现并行化。
- 当最后一个kernel的卷积结果都结束时,将模块结束使能信号置1.
- 卷积累加后的ReLU函数在输出时用if句追加。
-
时钟周期计数器的安排:
-
由于全project的模块执行逻辑由计数器决定,因此计数器的各数值对应的运行逻辑要提前规划清楚,来确定卷积模块的执行行为与数据传送逻辑。在整个project中,一共有cycleCounter、rowCounter、channelCounter、kernelCounter计数器,分别统计卷积过程中当前的相对周期数、行数、通道数、kernel数。其中cycleCouter到达一定数值,就会rowCounter增加并复位cycleCounter;rowCounter到达一定数值就会使channelCounter增加并复位rowCounter;以此类推。
-
在每个channel的第一行( r o w C o u n t e r = = 0 rowCounter==0 rowCounter==0)时,要读取kernel数据。
-
在每个kernel的最后一个channel运算结束时,输出当前kernel的卷积结果。并复位所有memeory的值及相关信号。
-
对于cycleCounter的运行逻辑如下:(数据线8根)
- cycleCounter: 0~1, read 4 × 4 4 \times 4 4×4 kernel数据.
- cycleCounter: 2-3, read 4 × 4 4 \times 4 4×4 fmap数据. 计算一次卷积CONV.(当前行的首个 4 × 4 4 \times 4 4×4 fmap数据)
- cycleCounter: 4-33, read 两列 4 × 1 4 \times 1 4×1 的fmap数据. 计算两次卷积CONV. (重复30次,则单行卷积运算结束)
- 进入下一行,并复位 c y c l e C o u n t e r = 2 cycleCounter=2 cycleCounter=2 (并不是0,因为kernel数据不需要重复读取),且增加rowCounter进入下一行,后续逻辑类似。
- 对于memory翻转切换使用的逻辑,以及卷积运算单元的结果取回逻辑(需要一个cycle计算,所以取回会滞后一个cycle),在此略过不谈(单位卷积运算单元结果取回逻辑和版本二有少量区别,与版本二不同,版本一比较辣鸡)。
-
-
出现的一些基础问题:
- 对于寄存器如memory的初始化行为使用不成熟。在此版本中采用的是使用 intial 语句进行初始化。实际上这样的方式在功能级是正确的,但是在dc综合阶段是不被允许的,因此后续版本有优化。
- 寄存器使用大、电路也贼大。
- ①是我们的卷积计算保存的是 61 × 61 61 \times 61 61×61 的寄存器数组,对于电路而言太大了,更何况电路要基于数组下标生成索引用的MUX电路,会导致电路更大而大;
- ②因为此版本中使用了两个memory实现计算与结果输出的并行,那就会导致寄存器数量翻倍,恶化了情况。
- 代码复杂。
- ①第一次写verilog,对于always语句块以及相关逻辑不够清楚,使得写出来的代码又臭又长;
- ②代码实现逻辑基于计数器counter,好几个不同职责的counter在不同阶段复位为0的过程显得极其混乱。
- ③模块使用了双卷积计算单元convModule,而单cycle要执行两次卷积的数据移位过程,增加代码复杂性。而卷积计算单元的结果要在下个周期取得,因此卷积计算单元的取回也需要额外的逻辑处理。
- ④要实现上述的双寄存器翻转切换使用的逻辑,加入的判断逻辑会进一步增加复杂量;
-
程序结果:
- 功能正确——✔️
- 不可综合——❌
3.4 版本二代码优化逻辑
-
版本二代码特色:
- 更加符合题意,补全了 读写控制信号;
- 补全数据线:8条fmap数据线以及8条kernel数据线;
- 通篇使用非阻塞赋值;
- 卷积结果存储memory优化,从双memory切换使用,改成仅使用单 61 × 61 61 \times 61 61×61大小;
-
修改代码的初衷:
- 更符合题目要求(控制信号与数据线的数目);
- 版本一的代码跑不了dc(综合)。因为我使用的阻塞赋值,所以需要优化。(当然,后续还发现memory使用过大)。
-
功能逻辑的修改与调整:
- 首先,为了增强各模块的独立性,也是题目的要求,需要顶层topModule模块需要向testbench”接收数据“的控制型号,化”被动型接受数据“为‘主动性接收数据”的形式。同时,在发送输出卷积结果时,也需要将“发送数据”的使能信号置1.
- 可综合verilog代码要求,是时序逻辑代码用非阻塞赋值,而组合逻辑代码使用阻塞赋值。我写的verilog要想能综合成电路,那当然得符合时序逻辑。因此,版本一通篇的阻塞赋值代码是没办法综合的。我之前想当然决定“功能正确就能产生对应电路”的想法明显是错误的。
- 由于题目要求,fmap的数据线与kernel数据线是独立开来的各8条。之前自己的审题不清,给自己带来了额外的修改工作量,以及对计数器的时序逻辑的大修改,也是没办法的事情。
- 第二次审视自己的memory使用代码,显得非常多余。为什么要等待 61 × 61 61 \times 61 61×61 卷积值累加结果都计算完毕了才开始传输结果呢?这样确实需要两块memory才能实现计算与结果输出的并行。但是若在最后一个channel的卷积值累加完毕就立马进行输出,实现累加与输出的并行,那么当 61 × 61 61 \times 61 61×61 的卷积值累加结果都计算完时,前 61 × 60 + 60 61 \times 60 + 60 61×60+60 的卷积结果也都传输完毕了,就不需要两个memory了,节约了电路空间也简化了代码逻辑。( ̄▽ ̄)"
-
代码修改的实现细节:
- 增加了8条kernel数据线,使得计数器的counter安排上出现了修改,并微调了单位卷积运算结果的获取逻辑。
- 将代码修改为非阻塞赋值,使代码中对于counter的修改会在下个cycle中得到体现,而当前cycle使用的counter值并不会变,这在并行的always语句块中对同counter的访问时需要强烈注意。
- 由于将代码修改为非阻塞赋值,对于控制信号的修改,testbench也只能在下个时钟上升沿检测到并采取行动,因此相关控制信号的置位与复位需要提前进行!
- 对于cycleCounter的运行逻辑如下:(数据线8根以及kernel数据线8根)
- cycleCounter: 0~1, read 4 × 4 4 \times 4 4×4 kernel数据. 也同时 read 4 × 4 4 \times 4 4×4 fmap数据. 并计算一次卷积CONV.(当前行的首个 4 × 4 4 \times 4 4×4 fmap数据)
- cycleCounter: 2, 取上次卷积结果CONV,并read 两列 4 × 1 4 \times 1 4×1 的fmap数据, 计算此两次卷积CONV.
- cycleCounter: 3-31, 取上两次卷积结果CONV,并read 两列 4 × 1 4 \times 1 4×1 的fmap数据. 计算两次卷积CONV. (重复29次)关读取fmap数据信号(因为下cycle不计算只负责回收此行最后卷积结果,故关信号防止漏了数据。而要切记:此处为非阻塞赋值,下cycle才起效)
- cycleCounter: 32, 取上两次卷积结果CONV.
- cycleCounter: 33, 开读取fmap数据信号(切记:非阻塞赋值,下cycle起效;其实是可以 c y c l e C o u n t e r = 32 cycleCounter=32 cycleCounter=32合并的,但是写错了就懒得改了…( ̄▽ ̄)")
- 进入下一行,并复位 c y c l e C o u n t e r = 0 cycleCounter=0 cycleCounter=0 (此处是0,因为fmap数据已经与kernel数据并行读取了,而kernel数据是否读取是由控制信号控制的而不仅仅是cycleCounter了,可以放心了),且增加rowCounter进入下一行,后续逻辑类似。
- 对于输出结果的逻辑部分,即检测当前channelCounter是否是最后一个通道,如果是,就将控制信号打开,并每个cycle累加完卷积结果就立马输出,这样就不需要两个memory了。
- kernel数据的读取使能信号以及其他控制信号的何时置1与复位的逻辑类似,在此略。不然写得就有点乱了。仅写了 fmap数据的读取信号的置1与复位逻辑供理解。核心特点是:要提前置1或复位,因为非阻塞赋值和时钟上升沿的关系,testbench下个cycle才能检测到。
-
程序结果:
- 功能正确——✔️
- 依然不可综合——❌——( ̄▽ ̄)"
3.5 版本三代码优化逻辑
-
版本三代码特色:
- 一切为代码可综合服务!
- 卷积结果存储memory再次优化,从reg类型的 61 × 61 61 \times 61 61×61大小的memory优化成 61 61 61 大小的memory;
- 将always逻辑优化;
- 用testbench传入的初始控制信号来代替intial进行模块的初始化工作
- 单元卷积计算模块,增加上一个卷积累加结果作为进位端口,省去累加的cycle。
-
修改代码的初衷:
- 正如我上文的VLSI基本概念篇幅内所言,电路是否可综合真的和verilog代码功能是否正确没关系,所以即使上述代码版本都正确,依然无法成功用dc将verilog进行综合。
- 有些不可综合的原因dc是会告诉你的,譬如:
- 不能在一个always块中,对同一个变量又进行阻塞赋值又进行非阻塞赋值;
- 不能在多个always块中,对同一个变量进行赋值;
- 不能再一个always中,又使用一个变量(如时钟clk)的上升沿又使用它的下降沿;
- 不能在模块中使用intial模块(所以前面说要想办法删去intial模块,只能在testbench中使用,因为testbench不会被跑dc综合);
- 其他记不住了( ̄▽ ̄)",可以参考这两篇博客:
- 但最骚的是,有些不可综合的原因dc是不会告诉你的。譬如,我的版本二代码完成上述修改以后,程序在进去dc后就一直处于compile阶段卡死了,30分钟后都没反应。于是我就开始debug,将代码全删去,再一行一行加进来,看看加到哪一行的时候dc会卡死,说明这行verilog代码有问题。 功夫不负有心人( ̄▽ ̄)",我发现是我的memory开太大了…
- 大伙儿可以回到上述的 【2.5 Verilog哪些行为能或不能的概念】中提到的,“dc对memory” 敏感度。 也就是说是我们的memory使用太大了,导致compile的时间预计要3个小时~( ̄▽ ̄)",这就很离谱,小作业要compile这么久谁顶得住?既然 61 × 61 61 \times 61 61×61 的memory太大了,就必须想办法精简!
-
功能逻辑的修改与调整:
-
一,代码中不能添加intial模块。但是我又需要初始化工作:如控制信号的初始化,memory的初始化等等。如何实现?我们这个卷积核的project的启动是根据testbench传入的开始使能信号触发的,因此,我们可以让开始信号为0的时候作为判断条件写一个always进行初始化,代替intial语句块,同时让testbench保持开始信号为0一段时间,确保模块的初始化工作能够完成。
-
二,优化存储空间。
-
原来的卷积运算逻辑是:以完整的fmap为一个单位进行卷积计算并累加——对于 61 × 61 61 \times 61 61×61 的所有卷积结果,把所有channel的卷积值累加上去,输出掉;再下一个kernel。
-
现在可以更改为:以fmap的一行为单位进行卷积计算并累加——对于每一行的 61 61 61 个的卷积结果,把所有channel的卷积值累加上去,输出掉;再下一行fmap;所有行结束了再下一个kernel。
-
-
三,进一步提升卷积结果取回速度。之前的逻辑是:卷积单元运算,运算结果取回,运算结果累加并发送;可以对卷积计算单元进行补充,将之前的卷积结果作为“来自低位的进位”,就可以实现类似“全加器”比“半加器”多出来的优势,能够直接再卷积单元里把累加过程给完成了,节省一个cycle。
-
-
代码修改的实现细节:
- cycleCounter的逻辑细节基本和版本二一致,但是将在当前channel的行尾卷积完成后,将“进入下一行”变成“进入下一个channel”,并复位 c y c l e C o u n t e r = 0 cycleCounter=0 cycleCounter=0; 只有当前channel是最后一个通道,完成当前行的卷积结果输出后,才会增加rowCounter进入下一行,其他逻辑大致不变。
- 其他逻辑修改在此不赘述。
-
程序结果:
- 功能正确——✔️
- 终于 可综合——✔️——( ̄▽ ̄)" Nice!
4 运行性能
虽然verilog迭代了好几个版本,但是第三版的verilog代码是写的相对简洁而干净, 我很满意。没有用什么switch和多余的if-else结构,所以dc也没有生成冗余的latch等电路。虽然功耗、性能一般,但是我个人觉得还行~( ̄▽ ̄)"
-
总计算周期数
- 总计算周期数 = 单行卷积数×channel数×行数×kernel数,(需额外1cycle 以模块初始化,未算入)
- 8 channels 8 kernels: 34 × 8 × 61 × 8 = 132736 34 \times 8 \times 61 \times 8 = 132736 34×8×61×8=132736 cycles
- 32 channels 32 kernels: 34 × 32 × 61 × 32 = 2123776 34 \times 32 \times 61 \times 32 = 2123776 34×32×61×32=2123776 cycles
- 总计算周期数 = 单行卷积数×channel数×行数×kernel数,(需额外1cycle 以模块初始化,未算入)
-
Area 面积
- Power 功耗
- Timing 时延
关键路径所需的时间4.97ns.
-
计算(A x P x {1/f} x N) PPA
74683 × 6.6635 × 4.5 n s × 132736 = 2.973 × 1 0 8 ( μ m 2 × m W / M H z ) 74683 \times 6.6635 \times 4.5ns \times 132736 = 2.973 \times 10^8 (\mu m^2 \times mW / MHz) 74683×6.6635×4.5ns×132736=2.973×108(μm2×mW/MHz)
-
Formality 形式验证
5 dc(综合)后、 综合后仿真的结果与感悟
虽然成功跑完综合,但是后仿就没有成功了。没有debug,不知道问题出在哪里。也许是因为testbench设置的时间不够…整个程序我个人感悟最大的是verilog代码的迭代部分,而后续dc综合、dc后仿真以及形式验证是伙伴捣鼓出来的,理解不能( ̄▽ ̄)",抱了一波大腿~
dc的界面好丑啊…当时看到的quartus觉得好丑,看到vivado眼前一亮,看到dc又觉得好丑… 也许这就是垄断吧,希望日后能有国产的相关产业链,软件能更加人性化的同时也能更好看一点~( ̄▽ ̄)" 不能作为程序员就对美感失去追求,是吧~
6 Code
时隔好久,神奇的发现自己没上传代码… ( ̄▽ ̄)"
代码见此处: 21-06-VLSI-CONV_RTL ,第一次写verilog,技术含量很有限,可以给和我一样的 verilog 入门小白做一个参照~