Start_Sig 和 Done_Sig 是仿顺序操作中,是模块象征性的信号,如果掌握它们利用Verilog HDL模仿仿顺序操作再也不是梦。Start_Sig 和Done_Sig 顾名思义就是模仿顺序操作语言中的“函数调用”和“函数返回”指令。如果从某个角度来看前者是启动信号,后者是反馈信号。
如果作为初学者,允许在不了解它们的情况下也可以使用它们。相反的,如果想进一步的深入,就不得不对 Start_Sig 和 Done_Sig 时序之间的协调性去探讨。
Start_Sig 和 Done_Sig 当两个模块互相作用的时候,这两个信号的时序会如此协调与和谐,笔者也觉得很不可思议。笔者还记得在写这一本《Verilog HDL 建模技巧 · 仿顺序操作 思路篇》笔记的时候,也是低级建模的初创之期,当时的笔者对时钟和步骤的掌握还不成熟,笔者既然会创作出这样的东西,只能说是奇迹或者是神明帮助 ...
接下来,需要实验一的帮助,在这里笔者重新粘贴 multiplier_module.v 和 multiplier_module.vt 相关的步骤,刷新刷新已经发霉的脑袋,同时也使得读者省去翻页的麻烦。
在这里我们仅需要 multiplier_module.v 和 multiplier_module.vt 相关的几个步骤而已。
Done_Sig 和 Start_Sig 时序的协调性不是什么复杂的东西,但是对于新手而言是一件苦差事。上面的仿真图是实验一的仿真结果,这一张图已经说明了一切。.vt 文件代表实验一的 multiplier_divider_module.vt ,亦即激励文件。.v 文件代表实验一的 multiplier_divider_module.v,亦即乘法模块。
在T0的时候 .vt 文件是步骤0(亦即i=0),把Start_Sig 拉高,发送“决定”乘数和被乘数。但是这时候 .v 文件判断的 Start_Sig 是该时间点的过去值,结果是逻辑0,所以 .v 文件没有被启动。
当T1的时候 . vt文件还是停留在步骤0,Start_Sig 依序被拉高。但是这时候 .v 文件在该时间点判断的 Start_Sig 的过去值不再是逻辑0,而是逻辑1,结果 .v 被启动,计入步骤0,亦即乘法模块的初始化步骤。
当时钟是 T2 的时候,.vt 文件没有变化。反之是 .v 文件完成初始化,进入步骤1 .v 文件已经完成第一次的乘法运算操作。所以在T2时间点,未来的 Product值是10。
在T3 的时候 .vt 文件还是老样子。.v 文件已近完成第二次的乘法运算操作。所以在T3时间点,未来的Product值是20。
在T4的时候 .vt 文件还是还是老样子。.v 文件这时候已经完成乘法运算操作“决定”
为了判断,乘法器时候已经完成乘法运算操作,使用了一个时钟来判断,在T4的未来,没有任何改变,只不过 .v 内部的步骤i递增了,进入产生完成信号的步骤。
在T5的时候 .vt 文件还是还是还是老样子。.v 文件这时候已近进入步骤2,对于 .v 文件来说,在步骤2是决定“拉高”完成信号。所以在T5时间点,未来的Done_Sig值会是逻辑1。
在T6的时候 .vt 文件不是老样子了,但是它察觉到在该时间点的 Done_Sig 过去值是逻辑1,所以它会“决定”拉低 Start_Sig 并且进入下一个步骤。所以在 T6时间点 Start_Sig 的未来值会是逻辑0。在同一个时间,.v 文件处于步骤3,然而步骤4的“决定”是拉低 Done_Sig 并且步骤回到0。所以在T6时间点 Done_Sig 的未来值会是逻辑0,并且步骤会回到0。
当时间是T7的时候,.vt 文件会进入步骤1,它会重新“决定”拉高Start_Sig ,和发送乘数和被乘数 ... 上述的动作会在一次的重复 ......
===================================================================
Done_Sig 和 Start_Sig 顾名思义就是控制信号的一员,故架在仿顺序操作上的它们,可以视为是两个模块之间沟通的桥梁吧!?笔者也是因为这个“桥梁”,才会继续深入模块之间的控制或者协调作用。事实上Done_Sig 和 Start_Sig 之间的协调作用,只要掌握好使用办法,不明白时钟和步骤的相互作用也不要紧。但是,想要深入了解 Verilog HDL语言的话,这一步不能跳过。
在这里稍微反思一下流水操作和仿顺序操作的差别吧。在前章笔者说过,流水操作是“永远向前走”操作方式,因为它只要将完成的工作丢个下一方,用不着像仿顺序操作那样,为了与其他模块沟通,需要配备控制信号。流水操作在某种程度上,比仿顺序操作单调多了,这同时也使得流水操作模块之间的沟通难度。
笔者不得不否认,仿顺序操作在《Verilog HDL那些事儿》中出现最多了,所以Start_Sig 和 Done_Sig 信号的身影也频频出现。因为实验占大部分都是和“控制,驱动”有关。“控制,驱动”最基本的就是模块之间沟通,为了能发挥最大的效果,所以控制信号才被需要。
(嗯,大脑很累吧 ... 比起在仿真结果中分析输出结果,故在仿真结果中分析控制信号的相互作用,更是累人。忍耐吧,年轻人!跑起来吧,年轻人!
为了写这章笔记,笔者真的花了不少前期的准备。还记得在 《Verilog HDL 那些事儿》中,笔者留下的问题“为什么会发生FIFO,信息卡住现象”... 那时候的笔者,真的没有能力解决 ,其中的原因是:
第一,笔者还没有达到目前的能力(还没有了解时钟和步骤)。
第二, 没有深入了解FIFO。
虽然笔者没有找到问题的原因,但是笔者已经找到解决的方法,这个方法就是使用手动建立的同步FIFO来替代Quartus II 生成的FIFO(Quartus II生成的FIFO是异步FIFO)。
同步FIFO实际上就是一个被加工后的移位寄存器,用文字来表达可能会抽象一点,稍微用借用画图的力量:
在上图中,四个小格子就是深度为4的移位寄存器,右边的字母表示数据,左边的代码代表“写操作”的“决定”,下边是移位寄存器的数据存入数目,最右边是当前的时间(当前的时间点)。该操作是使用“时间点”的概念来解读。我们先从写操作开始:
在T0的时候,移位寄存器的当前数据数目是0,然而在该时间点所做的“决定”是“写入数据”。假设 Write_Req 从初始的时候就一直被拉高,那么在T0的未来,Count的值会递增为1,然后移位寄存器的第一个格子就会读入A。
在T1的时候,由于T0的决定,移位寄存器的当前数据数目是1,然而第一个格子的数据是A。然后在T1依然重复同样的“决定”。假设Write_Req 从初始的时候就一直被拉高,那么在T1的未来,Count的值会递增为2,然后A会向第二个格子移动,则第一个格子会读入数据B。
在T2的时候,由于T1的决定,移位寄存器的当前数据数目是2,然而第一个格子的数据是B,第二个格子则是A。然后在T2重复同样的“决定”。假设 Write_Req 从初始的时候就一直被拉高,那么在T2的未来,Count的值会递增为3,然后A会向第三个格子移动,B会向第三个格子移动,则第一个格子会读入数据C。
在T3的时候,由于T2的决定,移位寄存器的当前数据数目是3,然而第一个格子是C,第二个格子是B,第三个格子则是A。然后在T3重复同样的“决定”。假设 Write_Req 从初始的时候就一直被拉高,那么在T3的未来,Count的值会递增为4,然后A会向第四个格子移动,B会向第三个格子移动,C会向第二个格子移动,则第一个格子会读入数据A。
在T4的时候,由于T3的决定,移位寄存器的当前数据数目是4,然而第一个格子是D,第二个格子是C,第三个格子是B, 第四个格子则是A。然后在T4重复同样的“决定”。假设 Write_Req 从初始的时候就一直被拉高,由于写操作的“决定”有条件约束,就是 Count < 4, 所以T4的未来,没有任何改变。
在T5的时候,由于受到写操作的条件约束,所以移位寄存器的结果和T4的时候一模一样。数据存入数也是4个。
啊~写操作已经被笔者玩厌了,借来执行读操作看看。
在T6的时候,由于T5没有任何“决定”,所以T6保持T5未来的结果。再重复说明一下,移位寄存器的状况。目前的输入存入数依然保持4个。然而 T6 做了读操作的“决定”。假设从T5开始Read_Req 一直被拉高,T6的未来会是,数据A会被读出,然后Count的值会递减为3。
在T7的时候,由于T6的“决定”,移位寄存的第四个格子已经被读出,第三个格子寄存B,第二个格子寄存C,第一个格子寄存D 。目前的输入存入数是3个。然而 T7 重复一样的读操作“决定”。假设从T5开始Read_Req 一直被拉高,T7的未来会是,数据B会被读出,然后Count的值会递减为2。
在T8的时候,由于T7的“决定”,移位寄存的第三格子已经被读出,第二个格子寄存C,第一个格子寄存D 。目前的输入存入数是2个。然而 T8重复一样的读操作“决定”。假设从T5开始Read_Req 一直被拉高,T8的未来会是,数据C会被读出,然后Count的值会递减为1。
在T9的时候,由于T8的“决定”,移位寄存的第二个格子已经被读出,第一个格子寄存D 。目前的输入存入数是1个。然而 T9 重复一样的读操作“决定”。假设从T5开始Read_Req 一直被拉高,T9的未来会是,数据D会被读出,然后Count的值会递减为0。
在TA的时候,由于T9的“决定”,移位寄存的第一个格子已经被读出,。目前的输入存入数是0个。如果T9 重复一样的读操作“决定”,会由于读操作“决定”的条件约束,Count > 0, 那么TA的未来褒词不变。
在TB的时候,由于TA的“决定”受到条件约束的影响,所以TB的未来也会保持不变。
嗯~读操作笔者也一样玩厌了,这时候来刺激一点的。如果写操作和读操作同时发生的话 .... 那么我们必须乘坐时光机(最近很流行的非主流新闻)回到T8 ......
时间回到T8,这时候所做的“决定”是同时读写操作(同时读写操作的“决定”就像是读操作和写操作的总和,其中Count相互抵消)。那么在T8的未来,C会被读出,D会移入第二个格子,E会被读入第一个格子,Count保持不变。
如果我们又坐时光机(据说时光电视,也称为时光窥视机已经发明了)回到T6,然后执行同样的决定(同时读写操作),看看会发生怎样的结果 ...
上图是在T6的时候,四个格子中的字母是 D, C, B, A,而Count是4。T6所“决定”的操作是“读写同时执行”,所以在T6的未来,亦即T7之际,A从移位寄存器中被读出,B,C,D相续移位,然而第一个格子读入E,并且Count保持不变。
同步FIFO的操作有,读操作,写操作,和读写同时操作,这三个操作而已。同步FIFO操作的原理很简单,但是要从外部调用同步FIFO,实现这些操作,就必须伤一点脑筋。
典型的FIFO控制信号有,Write_Req, Read_Req, Full_Sig 和 Empty_Sig,每一个信号的功能如命名般一样,但是这些控制信号,仅适合异步的FIFO而已。如果“死马当活马”把它用在同步FIFO的身上,调用就会发生问题。
第3~13行,是FIFO的输入输出,其中(12~13行)是Full_Sig 和 Empty_Sig,它们的驱动条件在 74~75行。Full_Sig 拉高的条件就是“Count 等价于深度”,然而Empty_Sig 的拉高条件就是“Count等价于零”的时候。第17~21行是仿真输出。
在28行声明了常量DEEP为4,也就说笔者打算建立深度为4的同步FIFO。
在32行,声明了位宽为8个字(words)为5,也就是位宽为8,深度为5的存储器。笔者建立同步FIFO都有一个坏习惯,第0个深度的储存器是视为不见,目的只有一个就是为了方便同步FIFO的设计。具体的原因往下看,就会明白了。
第33~34行声明的Count寄存器和Data寄存器,前者用来计数数据存入数,后者用来驱动FIFO_Read_Data输出(73行)。
第37~45初始化的动作,这一步对于FIFO来说是最重要的。如果建立的同步FIFO深度不多的话,可以考虑类似方式,将所有相关的寄存器都初始化为0。反之,如果同步FIFO的深度的数目很大的话,必须考虑利用 .mif 文件来初始化了。(具体的方法请参考VerilogHDL 那些事儿 - 第五章。)
第54~63行是对同步FIFO的写操作。第64~68行是对同步FIFO的读操作。第46~53行是读同步FIFO同时读写操作。有一个重点不得不注意是,同时读写操作,读操作和写操作的优先级,同时读写操作的优先级永远都是最高的。
在这里,第74~75行的Full_Sig 和 Empty_Sig驱动条件看是很有道理,很有逻辑。其实这是初学者对FIFO的幻想。如果用于异步FIFO的话,74~75行的驱动条件绝对没有错误,但是把它们用于同步FIFO的话,这就大错特错,具体的原因看了仿真结果就会知道。
.vt 文件的风格还是一如既往一样清一色。
69~97行的动作是,先写一个数据,然后同时间写一个数据再读一个数据,最后读一个数据。为简单起见,笔者把每一个动作都消耗两个时钟。
71~75行的动作是在步骤0拉高 Write_Req 然后发送数据 8'd5, 然后在步骤1拉低 Write_Req。79~87行的动作是在步骤2同时拉高Write_Req 和 Read_Req 并且写入数据 8'd6, 然后再步骤3同时拉低 Write_Req 和 Read_Req 。91~95行的动作是在步骤4拉高Read_Req ,然后在步骤5拉低 Read_Req。
99~115行执行69~97的动作,但是时钟消耗是一个时钟而已。在步骤6拉高 Write_Req 并且写入数据 8'd100。步骤7的动作是同时拉高Write_Req 和 Read_Req 并且写入数据 8'd33。步骤8的动作是拉高 Read_Req 。
69~115(步骤0~8)只是单纯的测试,读操作,写操作,和同时读写操作而已。真正的压轴好戏现在才开始。
在111~115行(步骤8)的结果,我们忘记了把 Read_Req 拉低,当步骤进入步骤9(119~120行)的时候。这时候的动作虽然是写操作,写入数据 8'd99, 由于上一个步骤忘记拉低了 Read_Req ,所以这一个步骤的动作等于同时读写操作(意外开始发生了)。
121~122行(步骤9)是单纯的读操作而已(Write_Req被拉低了),然后在127~128行(步骤10)只是单纯的拉低Read_Req。
第132~138行是用来测试,使用Full_Sig 和 Empty_Sig 控制信号的结果。在步骤12会陆续向FIFO写入数据,直到Full_Sig 信号拉高为止。反之步骤13会陆续从FIFO读取数据,直到Empty_Sig 拉高为止(意外也开始发生了)。
这些动作到底会产生怎样的结果,我们从仿真结果中拭目以待。
T0的时候,也是 .vt 文件的步骤0开始。数据8'd5“决定”被写入,和Write_Req“决定”被拉高。所以在T0的未来,FIFO_Write_Data的未来值是8'd5, Write_Req会被拉高。
(注意:由于初始状态,FIFO为空Empty_Sig是拉高状态。)
T1的时候 .vt 进入步骤1,Write_Req “决定”被拉低。在同一个时间 .v 检测 Write_Req 的过去值是逻辑1,所以 .v “决定”写操作,FIFO_Write_Data 的过去值会被写入FIFO的第一格子。T1的未来,Empty_Sig 会拉低,Write_Req会拉低。
(注意:T1未来的SQ_rS1。)
T2的时候 .vt 是步骤2,Read_Req 和 Write_Req“决定”拉高,然后 FIFO_Write_Data “决定”写入数据 8'd6。这个时间点Empty_Sig和 Full_Sig 的过去值都是被拉低,所以 .v 就乖乖的待命。
T3的时候 .vt 进入步骤3,它“决定”拉低 Read_Req 和 Write_Req。在同一个时候 .v 文件检测到,在该时间点 Empty_Sig 和 Full_Sig 都是拉高状态,所以 .v 决定“同时读写操作”。T3的未来,Empty_Sig 和 Full_Sig 被拉低,FIFO第一个格子的数据,亦即8'd5会被读出,然后FIFO_Write_Data 在T3的过去值,亦即8'd6会被写入FIFO的第个一格子。(注意:同时读写操作,使得Count不变和第一个格子的数据 8'd5被读出的同时,数据8'd6别写入。)
T4的时候 .vt 进入步骤4,它“决定”拉高 Read_Req。在同一个时候 .v 在这个时间点,检查不到 Read_Req 还是 Write_Req 过去值被拉高(逻辑)所以 .v“决定”沉默。
在T4的未来,Read_Req被拉低而已。
T5的时候 .vt 进入步骤5,它“决定”拉低Read_Req。在同一个时候 .v 在这个时间检查到 Read_Req 的过去值是被拉高,所以它“决定”读操作。在T5的未来,Read_Req 被拉低,FIFO第一个格子的数据 8'd6 会被读出。(注意:由于FIFO已经空了,所以Empty_Sig 会在这个时间的未来被拉高。则Count也成为0。)
T6~T8重复T0~T5的动作,只是时钟消耗从2个变成1个。
T9的时候。在T8之际,也是在 .vt 的步骤8,它的“决定”使得Read_Req 在T8的未来,它还是保持拉高状态。在T9的时候,也是 .vt 步骤9 ,它“决定”拉高Write_Req。在同一个时候 .v 检测到,在该时间点 Read_Req 的过去值是被拉高,所以 .v “决定”读操作。在T9的未来,Read_Req 保持不变,Write_Req 被拉高,而FIFO会从第一个格子中把数据8'd33 吐出来。
(注意:这时候的FIFO已经为空,所以Empty_Sig会被拉高)。
T10的时候,意外发生了。在这个时候 .vt 进入步骤10,它“决定”拉高 Read_Req。在同一个时间 .v 检查到在改时间的 Write_Req 和Read_Req 的过去同时被拉高,但是
FIFO已经为空了,所以同时读写操作“决定”不成立,反而写操作的“决定”成立。
在T10的未来,Read_Req 被拉高,数据8'd99 被写入FIFO的第一个格子。
( 注意:T10的未来里Empty_Sig 被拉低了。)
T11的时候 .vt 进入步骤11,它“决定”拉低 Read_Req。在同一个时间 .v 检测到在该时间点 Read_Req 的过去值是被拉高,所以它“决定”读操作。在T11的未来,Read_Req 被拉低,数据8'd99 被读出。
嗯!暂时冷静一下脑袋吧。在这里我们只是讨论, 同步FIFO的读操作,写操作,和读写操作而已,我们还没有进入控制信号 Empty_Sig 和Full_Sig 的应用(不要被笔者吓到,真正的好戏现在才要开始。)
事实上,在T10之际的意外是决定不会发生的,因为在T10和之前的操作我们忽略了控制信号,所以才会发生这样的意外。FIFO在实际的调用中,如果好好的使用控制信号,这样的囧境((╯□╰))是绝对不会发生。
在这里,笔者重新粘贴仿真结果,为了避免翻页的麻烦。
在T12也是 .vt 进入步骤12的时候。它“决定”不停的向FIFO写入数据,直到Full_Sig拉高。在T13,T14,T15和T16的“决定”之间,数据的写入还成功,但是在T16的“决定”之后,意外又发生了。
在T16这个时间点的过去,事实上同步FIFO已经饱了,但是同步FIFO所遵守的是“时间点”的概念,所以Full_Sig 的拉高是发生在T16的未来。可是在这一个瞬间,由于 .v 对Full_Sig 判断失误,在这个时间点它继续“决定”执行写操作 ...
在T17的时候 .v 检测到 Write_Req 被拉高,但是它已经无法继续“吸收”数据了,所以数据8'd104 会作废,并且消失在这个世界中。在同一时间 .vt 依然持续在步骤16,但是它检测到在 T17 该时间 Full_Sig 的过去值是拉高状态,所以它“决定”停止写操作。
在T18~T22之间 .v 陆续“决定”从FIFO读出数据,结果数据都成功被读出。
从仿真结果中,我们可以知道几个个事实。
第一,FIFO的调用绝对需要控制信号,不然会发生像在T10的意外。
第二,Full_Sig 和 Empty_Sig 是绝对不适合同步FIFO,而是适合异步FIFO,这话要怎么说呢?
我们知道同步FIFO,它的行为时遵守“时间点”的概念,“决定”的结果是出现在该时间点的过去或者未来。然而控制信号Full_Sig 和Empty_Sig 在“时间点”的概念之中,发生了排斥,导致了写操作失败(Full_Sig拉高迟了一个时钟)。
异步FIFO,它的基本结构就是组合逻辑(异步FIFO的详细结构自己谷歌一下),然而我们知道组合逻辑的操作是无视“时间点”的概念,所以控制信号Full_Sig 和 Empty_Sig 产生的是“即时结果”,亦不会慢了半拍。
同步FIFO和异步FIFO,虽然它们只是相差了一个字,它们实际的结果是全然不同。
Quartus II 所生成的是异步FIFO,但是它可以兼容为同步FIFO,这也使得初学者们对Quartus II 的 FIFO 的ip 产生的一种幻想 ...
现在稍微把头脑冷静一下,在《Verilog HDL 那些事儿》中FIFO的目的是用来缓冲信息和独立化接口模块。然而同步FIFO完全可以胜任这一点,为何还要执着于异步FIFO呢?但是余下的问题是控制信号 Full_Sig 和 Empty_Sig 同步FIFO用不了,我们需要其他的控制信号。
实际上 Full_Sig 和 Empty_Sig 同步FIFO不是用不了,只要写操作和读操作均用两个时钟的话,这个问题可以漂亮的解决。如果这样,会使得同步FIFO的调用特别别扭。
在最后,我们还知道一个事实,同步FIFO的调用绝对需要控制信号,不然的话会发生像在T10的意外。
实验十七我们留下了这样一个问题:“同步FIFO不适合Empty_Sig 和 Full_Sig,那么有什么样的控制信号适合它呢?”
在这里笔者稍微刷新一下 Empty_Sig 和 Full_Sig 在同步FIFO的作用。Empty_Sig 和 Full_Sig 都是FIFO典型的控制信号,它们用来判断FIFO的状态。如果Empty_Sig 拉高,这表示FIFO已经为空。反之Full_Sig拉高的话,则表示FIFO已经为满。但是Empty_Sig 和 Full_Sig 在同步FIFO的中,尤其是在写操作中,出现了慢一拍的情况。
为了解决一个问题,我们需要适合同步FIFO的控制信号。
Empty_Sig 和 Full_Sig 都有一个共同点,它们均是反馈FIFO中的“空格数目”。那么,笔者用一个更傻瓜的想法,如果直接引出 FIFO中的“空格数目”的话 ...
在这个实验中,我们放弃了 Empty_Sig 和 Full_Sig,取而代之的是 Left_Sig 。作用如名字般,该信号用来反馈出 FIFO 目前的“空格数目”。
同样是深度为4的同步FIFO。但是在12行中Left_Sig 取代了 Empty_Sig 和 Full_Sig。在63行是 Left_Sig 输出信号的驱动条件“反馈出FIFO目前的空格数目”。其余的地方和实验十七的没有什么两样。
上面是激励文件。第56~80行(步骤0~7)不适用控制信号的条件下,前四个时钟写入4个数据,亦即 8'd1, 8'd2, 8'd3, 8'd4。然后后四个时钟陆续读出FIFO的数据。
第84~86行(步骤8)是利用控制信号,对FIFO陆续写入数据,直到if条件满足。
第88~90行(步骤9)同样是利用控制信号,对FIFO陆续读出数据,直到if条件满足。
步骤10~15的操作,主要是把同步FIFO想象为,它处在中间被调用。它同时被一方写,又被一方读。每一个步骤中的操作都是用控制信号。在步骤10~13被想象为A方向同步FIFO写入数据,然而在步骤12~15被想象为B方从同步FIFO读取数据。
94~134行的动作如下:
步骤10,A对FIFO写入数据8'd5(94~96行)。
步骤11,A对FIFO写入数据8'd6(98~100)
步骤12,A对FIFO写入数据8'd7(105~106),同时B对FIFO读出数据(108~109)。
步骤13,A对FIFO写入数据8'd8(118~119),同时B对FIFO读出数据(121~122)。
步骤14,B对FIFO读出数据(128~130)。
步骤15,B对FIFO读出数据(132~134)。
在这里,可能读者会产生疑问:“为什么if 的条件是 Left_Sig >= 1 或者 Left_Sig <= 1”
这不是笔者随便填的,具体答案请往仿真结果看。
上图是仿真结果。从T0~T7是 .vt 的步骤0~7,它的操作是写入四个数据,然后读出4个数据,没有是用控制信号。
在T8的时候 .vt 是步骤8,它“决定”陆续写入数据,直到if条件成立。在T8~T11利用控制信号(Left_Sig)对FIFO的写入还蛮Okay的。那么重点来了,在T12的时候 .vt 还是保持在步骤8,检测在该时间点 Left_Sig 的过去值是1,这时候 if 条件成立了 ( Left_Sig <= 1 ),它“决定”拉低Write_Req。所以,在T12的未来 Write_Req 被拉低。
在T3的时候 .vt 进入步骤9,它“决定”陆续读出数据,直到if条件成立。在T13~16利用控制信号从FIFO读取数据的工作还蛮顺利的。但是在T17,这时候 .vt 还保持在步骤9,它检测在该时间点 Left_Sig 的过去值是3,所以 if条件成立(Left_Sig >= 3)并且它“决定”拉低Read_Req。在同一个时候 .v 检查到在该时间 Read_Req 的过去值是被拉高状态,所以它“决定”读取数据。所以,在T17的未来Read_Req被拉低,并且数据8'd8被读出。
.vt 接下来的动作可以想象有两方同时对同步FIFO的调用,一方是写操作,另一方是读操作,它们均利用控制信号。
在T18的时候 .vt 是步骤10,亦即A方对FIFO写操作。A方先检测在该时间 Left_Sig的过去值是4,if条件成立(Left_Sig >= 1),它“决定”拉高 Write_Req,并且“决定”写入数据 8'd5。在同一个时候 .v 检测在该时间点的 Write_Req 和 Read_Req 的过去值,它们均为拉低状态,结果 .v 安静的等待。所以在T18的未来,Write_Req 被拉高,数据8'd5发送在FIFO_Write_Data。
在T19的时候 .vt 是步骤11,亦即A方对FIFO写操作。A方先检测在该时间 Left_Sig的过去值是3,if条件成立(Left_Sig >= 1),它“决定”拉高 Write_Req,并且“决定”写入数据 8'd6。在同一个时候 .v 检测在该时间点的 Write_Req 过去值是1 ,它“决定”写入在该时间点FIFO_Write_Data的过去值,亦即8'd5。所以在T19的未来,Write_Req 持续被拉高,数据8'd6被发送,数据8'd5被读入FIFO。
在T20的时候 .vt 是步骤12,亦即A方对FIFO写操作,B方对FIFO读操作。A方检测到在该时间点 Left_Sig 的过去值是3,if条件成立它“决定”拉高 Write_Req并且“决定”写入数据 8'd7。B方检测到在该时间点Left_Sig 的过去值是3,if条件成立(Left_Sig <= 3),它“决定”拉高Read_Req。
在同一个时候 .v 检测到在该时间点 Write_Req的过去值是拉高状态,所以它“决定”将数据8'd6读入。所以在T20的未来,Write_Req持续被拉高,Read_Req被拉高,数据8'd7被发送,数据8'd6被读入。
在T21的时候 .vt 是步骤13,亦即A方对FIFO写操作,B方对FIFO读操作。A方检测到在该时间点 Left_Sig 的过去值是2,if条件成立它“决定”拉高 Write_Req并且“决定”写入数据 8'd8。B方检测到在该时间点Left_Sig 的过去值是2,if条件成立(Left_Sig <= 3),它“决定”拉高Read_Req。
在同一个时候 .v 检测到在该时间点 Write_Req和 Read_Req的过去值均被拉高,所以它“决定”将数据8'd7读入,将8'd5读出。所以在T21的未来,Write_Req持续被拉高,Read_Req持续被拉高,数据8'd7被发送,数据8'd6被读入。
在T22的时候 .vt 进入步骤14,亦即A方结束写入操作,B方继续对FIFO读操作。A方“决定”拉低Write_Req(注:.vt文件的129行)。B方检测到在该时间点Left_Sig 的过去值是2,if条件成立(Left_Sig <= 3),它“决定”拉高Read_Req。
在同一个时候 .v 检测到在该时间点 Write_Req 和 Read_Req的过去值均为拉高状态,它“决定”读入数据8'd8和读出数据8'd6。所以在T22的未来,Write_Req拉低,Read_Req持续拉高,数据8'd8被写入FIFO,数据8'd6被读出。
在T23的时候 .vt 进入步骤15,亦即B方对FIFO读操作。B方检测到在该时间点Left_Sig 的过去值是2,if条件成立(Left_Sig <= 3),它“决定”拉高Read_Req。
在同一个时候 .v 检测到在该时间点 Read_Req的过去值为拉高状态,它“决定”读出数据8'd7。所以在T22的未来,Read_Req持续拉高,数据8'd7被读出。
在T24的时候 .vt 进入步骤16,B方读操作结束,并且“决定”拉低 Read_Req。在同一个时候 .v 检测到在该时间点 Read_Req 的过去值被拉高,它“决定”读出数据8'd8。所以在T24的未来,Read_Req被拉低,数据8'd8被读出。
呼 ~ 先给笔者松一口气 ... 在仿真结果中 T8~T16 对于使用Left_Sig针对同步FIFO的写入读出操作,发挥到很好的作用,没有发生写入失败的意外。在T18~T23,是一个假想的状态,假设用两方,A方和B方,一方对FIFO写操作,一方对FIFO读操作,它们都是用控制信号,结果没有发生任何意外,真是可喜可贺。
Full_Sig 和 Empty_Sig 既然不适合同步FIFO,反之Left_Sig 却帮助到同步FIFO。但是Left_Sig 的使用方法比较别扭一点,使用者必须清楚FIFO的深度为前提。
事实上实验十七和十八是围绕着“模块之间的沟通与控制信号的关系”故事,不过是利用同步FIFO来借题发挥而已。我们知道仿顺序操作典型的控制信号,有Start_Sig 和 Done_Sig,然而同步FIFO中的 Write_Read,Read_Req对于Start_Sig; Full_Sig,Empty_Sig和 Left_Sig 对于Done_Sig。它们有几分相似。
说实话,同步FIFO的调用更有几分难度,但是只要了解了,掌握了,又是向前踏出一大步 ......
在这一章节当中,我们要测试在实验十八中所建立的FIFO模块。
上图是久违的建模,该组合模块 multiplier_interface.v 是一个乘法器接口。基于实验十八中的同步FIFO将它作为该接口的输入缓冲,它拥有的深度为4,位宽为16。其中FIFO_Write_data的高八位是被乘数A,低八位是乘数B。组合模块中的“控制程序”担任FIFO和乘法器之间的协调控制。它从FIFO读出数据,过滤数据,送往乘法器,然后启动乘法器(U1_Read_Data[15:8] 是被乘数A,U1_Read_Data[7:0]是乘数B)。乘法器是实验七的Modified Booth 乘法器·改。
(在这里涉及许多“低级建模”的基础,如果读者不明白笔者在说什么,请好好的复习《Verilog HDL那些事儿》第五章- 接口建模。)
该FIFO模块和实验十八中的FIFO模块没有什么区别,只是位宽为16位罢了。
multiplier_interface.v 是一个组合模块。第20~29行实例了FIFO模块,38~47行实例化了乘法器。在15, 33 分别声明了寄存器 isRead, isStart(哎 ... modelsim的编译器不给力的关系,寄存器必须在调用之前声明,不然modelsim编译不通过。),isRead用来驱动FIFO的Read_Req(26行),isStart用来启动乘法器(42行)。
第51~71是控制程序。控制程序的开始,步骤0先检测 FIFO 的 Left_Sig ( U1_Left_Sig ),如果FIFO不为空,就从FIFO读出数据(注意:乘法器U2的A,B输入口,是直接由U1_Read_Data驱动,27行,43~44行)。然后步骤i递增。
在66~68行步骤1,控制程序启动乘法,并且等待直到乘法器完成操作,然后返回步骤0(67行)。
在75行,笔者把 FIFO 的 Left_Sig 直接引出来。
上面是 .vt 文件。 从步骤0~4(47~65行)利用控制信号 Left_Sig 分别向FIFO写入数据 12*9,33*10, 40*5,127*127, 37*21。步骤0~4的写法都有一个共同点,if条件先判断FIFO是否不为满,如果是就写入数据,并且递增i进入下一个步骤。否则拉低Write_Req,直到FIFO不未满为止。
步骤5~8(67~68行)是空置4个时钟。
步骤9(70~72行),if先判断FIFO是否不为满,如果是就写入数据 9*8。如果不是,就拉低Write_Req直到FIFO不未满为止。
在这里读者可能会对上述的写法产生许多疑问?但是具体的结果还是要看仿真结果。
上图是仿真结果。在T0的时候 .vt 是步骤0,它检测到在该时间点 Left_Sig的过去值是4,if条件成立(Left_Sig >= 1)它“决定”向FIFO写入 12*9。在同一个时间,控制程序在步骤0,它检测到在该时间点 Left_Sig 的过去值是4, if条件不成立(Left_Sig <= 3),所以它“决定”作罢。在T0的未来 数据12*9被发送在FIFO_Write_Data。
在T1的时候 .vt 是步骤1,它检测到在该时间点 Left_Sig的过去值是4,if条件成立(Left_Sig >= 1)它“决定”向FIFO写入 33*10。在同一个时候,控制程序在步骤0,它检测到在该时间点 Left_Sig 的过去值依然是4,if条件不成立,它“决定”作罢。所以在T1的未来,数据33*10发送在FIFO_Write_Data。
在T2的时候 .vt 是步骤2,它检测到在该时间点 Left_Sig的过去值是3,if条件成立(Left_Sig >= 1)它“决定”向FIFO写入 40*5。在同一个时候,控制程序在步骤0,它检测到在该时间点 Left_Sig 的过去值依然是3,if条件成立,它“决定”从FIFO读取数据。所以在T2的未来,数据40*5发送在FIFO_Write_Data,数据12*9会被读出。
在T3的时候 .vt 是步骤3,它检测到在该时间点 Left_Sig的过去值是2,if条件成立(Left_Sig >= 1)它“决定”向FIFO写入 127*127。在同一个时候,控制程序在步骤1,它“决定”启动乘法器。所以在T3的未来,数据127*127发送在FIFO_Write_Data,数乘法器开始执行。
在T4的时候 .vt 是步骤4,它检测到在该时间点 Left_Sig的过去值是2,if条件成立(Left_Sig >= 1)它“决定”向FIFO写入 37*21。在同一个时候,控制程序依然在步骤1,它正等待乘法器的完成信号。所以在T4的未来,数据37*21发送在FIFO_Write_Data,数乘法器依然执行中。
在T5,T6,T7,T8的时候 .vt 是步骤5,6,7,8, 它什么都不干。在同一个时候,控制程序依然在步骤1,它正等待乘法器的完成信号。
在T9,T10,T11的时候 .vt 是步骤9,它检测到在该时间点 Left_Sig的过去值是0,if条件不成立(Left_Sig >= 1)它什么都不干。在T9,T10的时候,控制程序依然在步骤1,它正等待乘法器的完成信号。
大约在T11的时候 ,控制程序接收到乘法器的完成信号,它决定关闭乘法器,并且返回步骤0。
在T12的时候.vt 是步骤9,它检测到在该时间点 Left_Sig的过去值是0,if条件不成立(Left_Sig >= 1)它什么都不干。同一个时候,控制程序在步骤0,它检测到在该时间点left_Sig的过去值是0,if条件成立(Left_Sig <= 3)它“决定”从FIFO读取数据,并且进入下一个步骤。所以在T12的未来,数据33*10从FIFO读取,并且控制程序进入步骤1。
在T13的时候.vt 是步骤9,它检测到在该时间点 Left_Sig的过去值是1,if条件不成立(Left_Sig >= 1)它“决定”写入9*8。同一个时候,控制程序在步骤1,它“决定”从启动乘法器。所以在T13的未来,数据9*8会发送在 FIFO_Write_Data上,然而乘法器开始启动。
在T14,的时候 .vt 进入步骤10,它的工作已经完成后了,所以它会不停的发呆。在同一个时候,控制程序在步骤1,它等待乘法器的完成信号。
在T15~T19的时候控制程序在步骤1,它等待乘法器的完成信号。
大约在T20的时候,控制程序得到乘法器的反馈信号,所以它“决定”关闭乘法器,并且进入步骤0。
在T21的时候控制程序在步骤0,它检测到在该时间点left_Sig的过去值是0,if条件成立(Left_Sig <= 3)它“决定”从FIFO读取数据,并且进入下一个步骤。所以在T12的未来,数据40*5从FIFO读取,并且控制程序进入步骤1。
大约在T29的时候,控制程序得到乘法器的反馈信号,所以它“决定”关闭乘法器,并且进入步骤0。
在T30的时候控制程序在步骤0,它检测到在该时间点left_Sig的过去值是1,if条件成立(Left_Sig <= 3)它“决定”从FIFO读取数据,并且进入下一个步骤。所以在T12的未来,数据127*127从FIFO读取,并且控制程序进入步骤1。
大约在T38的时候,控制程序得到乘法器的反馈信号,所以它“决定”关闭乘法器,并且进入步骤0。
在T39的时候控制程序在步骤0,它检测到在该时间点left_Sig的过去值是2,if条件成立(Left_Sig <= 3)它“决定”从FIFO读取数据,并且进入下一个步骤。所以在T12的未来,数据37*21从FIFO读取,并且控制程序进入步骤1。
大约在T47的时候,控制程序得到乘法器的反馈信号,所以它“决定”关闭乘法器,并且进入步骤0。
在T48的时候控制程序在步骤0,它检测到在该时间点left_Sig的过去值是3,if条件成立(Left_Sig <= 3)它“决定”从FIFO读取数据,并且进入下一个步骤。所以在T12的未来,数据9*8从FIFO读取,并且控制程序进入步骤1。
大约在T56的时候,控制程序得到乘法器的反馈信号,所以它“决定”关闭乘法器,并且进入步骤0。
大约在T57候控制程序在步骤0,可是到目前为止FIFO已经为空了,控制程序检测到在该时间点的Left_Sig的过去值为4,if条件不成立(Left_Sig <= 3),它什么都不干。
那么我们可以下结论,该乘乘法器接口的工作已经完毕。
在这个实验中,我们建立了乘法器接口来测试实验十八的同步FIFO模块。可是在仿真结果中笔者没有直接引出FIFO的Write_Req 和 Read_Req信号,乘法器的Start_Sig 和Done_Sig 信号,取而代之的是笔者只是引出FIFO的Left_Sig信号。
笔者实在无力将全部信号引出,然后在仿真结果中一个一个慢慢解释,如果这样做笔者估计会精尽人亡。相反的,如果读者能好好的理解4.1~4.3章中的重点,即使没有笔者的解释,读者也能明白。
在这里,我们来说说仿真中的几个重点吧:
同步FIFO模块被调用的时候,最重要的还是使用控制信号 Left_Sig 来进行数据写入和数据读出。有几个比较经典的情况是在 T5~T12的时候,由于FIFO已经为满了 .vt 在步骤9,根据Left_Sig 得知 FIFO目前的状态。.vt等待直到T13的时候在FIFO不未满的状态下,才“决定”写入数据。
还有一点就是在T15和之后,那时候的 .vt 已经完成工作。反之控制程序还在继续工作者,控制程序在步骤0~1之间一直轮替着:读取FIFO的数据;然后启动乘法器;等待乘法器完成;在这期间,没有任何意外发生。向FIFO写入的5个数据,都成功被乘法器操作出来。
在实验十九,笔者利用了同步FIFO解决了,数据被卡在FIFO里边的问题。要完全攻略同步FIFO,笔者做了许多的准备 .....
到目前为止,笔者使用过许多种的建模来完成实验的要求,下面我们稍微回顾一下。
假设实验的要求是:
A/ B = Q 和 R
Q * R = 答案
从实验的题目中,我们知道我们需要一个除法器执行A除以B得到结果Q 和 R,然后再需要一个乘法器完成 Q * R 来得到最终答案。
上面是笔者最爱的”低级建模”中相关的仿顺序操作,它的优点就是容易控制。但是它的缺点是,两次性操作之间有时间间隔。
假设除法器和乘法器的都使用10个时钟来工作的话,那么每一次的调用都会先启动除法器模块,然后等待它完成操作,并且求得Q和R。得到Q和R之后,启动乘法器模块,然后等待它完成操作,并且求得最终答案。
每一次操作最大的时钟消耗:
命令控制模块的沟通时间为2个(Start_Sig 和Done_Sig消耗之间为2个时钟),
除法器模块的沟通时间为2个,等待除法器模块完成的时间为10个,
乘法器模块的沟通时间为2个,等待乘法器模块完成的时间为10个,
一共使用 2 + 2 + 10 + 2 + 10 = 26个时钟。
所以说,两次性操作之间的时间间隔是26个时钟。
尽管如此,仿顺序操作的建模对时钟消耗如此之多,如果实验的速度要求不任性的话,这个缺点可以接受的。
为了解决放顺序操作建模中的缺点(两次性操作之间有时间间隔),笔者可以在命令控制模块的前面加上FIFO作为输入缓冲,这也成为了“低级建模”中相关的接口建模。这样作的好处,每一次调用都可以无需等待内部操作。同样它也有坏处,将模块封装为借口,会使得内部的建模层次和连线关系变成复杂。往往如果没有图形帮助的话,接口建模的工作可是一件苦差事。
假设上述的办法读者不能接受,那么笔者建议可以使用流水操作建模来完成实验的要求。虽说流水操作的建模在连线关系上或者层次上都比上述的建模来得更精简。但是读者不要忘了,流水操作的建模“模块必须有固定的操作步骤”,此外它还有一个致命的缺点就是潜伏时间。这些潜伏时间会根据调用频率的不同,变成不可预测,最终使得模块的调用变成困难。
这样也不行,那样也不行,笔者到底要怎样做什么才好呢?当人逼急的时候就会跳墙,(传说中的终极美食“人跳墙“就是这样由来的)笔者就从接口建模捉一点优点,然后又从流水操作建模捉一点优点,然后整合这些优点再建立出新的建模 .......
上图是两个操作模块的接口建模 ... 笔者先建立简单的除法器接口,然后又建立简单的
乘法器接口。然后将它们串联起来,就会成为下图:
除法器接口的Done_Sig驱动乘法器接口FIFO的Write_Req,Data 驱动FIFO_Write_Data。然而除法器接口右边的Left_Sig由乘法器接口的Left_Sig驱动。
在这里,我们使用了流水操作的“外壳”使得连线关系变得简单,然后在里边使用了简单的接口建模使得控制变成容易,结果是一石二鸟。这样的建模方法已经是“混种”了,它的优缺点是父类的一半一半。
实际上笔者“不承认”这样的建模方法是“另一个新的建模方法”。笔者把它提出来只是要说明“建模技巧之间混种的可能性”而已。
再来我们从层次的角度去分析建模:
上图是(低级建模)接口建模的层次图,接口建模的基本思路就基于仿顺序操作,越高级的建模,它的层次就越高。
注意看中间的控制模块,它凸起来的高度都比其他模块来得多。这也是没办法的事,因为控制模块几乎要联系所有模块,结果使它又凸又肥,这也意味着控制模块的代码量会比其他模块来得多。所以呀,只要控制模块有什么差错,该接口模块就不能正常工作了。
还有一点就是,如果层次越多,这也表示每一个层次之间的“沟通时间”(延迟时间)就越多。(如果把上图换成图形,读者猜猜看它们之间有多少的Start_Sig和Done_Sig控制信号)
上面的左图时流水操作的建模,每一个模块的高度都根据模块的操作步骤,但是说流水操作建模的连线关系很最整洁而,且模块之间的沟通时间也是最段(几乎是1个时钟而已)。但是流水操作有诸多的缺点,如潜伏时间无法预测,建模的难度高等,这使得它黯淡无光。
上面的右图是混种的建模。很显明,该建模拥有父类的优缺点的一半一半,说好不见得好到那里去,说坏却不能挑剔什么。
在这里到底要使用那一种建模来完成实验的要求?如果是笔者的话,笔者还是偏爱低级建模的接口建模,因为它最友善,毕竟用久了,多少都有感情。但是实际的情况还是要根据实验的要求而定。
继续上述的实验要求:
·A / B = Q 和 R , 并且 Q * R = 答案。
然后再追加几个要求:
(一)除法器使用实验八的传统除法器,乘法器使用实验一得传统乘法器。
(二)模块的操作效率上尽可能提升,而且简化模块之间的连线关系。
根据第2项追加要求,读者们可能会立即反应流水操作的建模是首选,但是不能忽略第1项的追加要求,就是除法器和乘法器均为传统的除法器和乘法器。我们知道传统除法器和乘法器都有一个特性,就是根据操作数据的不同就有不同操作步骤和时钟消耗。然而流水操作的建模必须基于“固定的操作步骤,固定的时钟消耗”,显然流水操作的建模时不适合的。
然后再根据第1项的追加要求,低级建模的接口建模肯定是首选,但是考虑了第2项追加要求得话,显得接口建模的执行效率有点低了。所以在这里我们必须使用混种的建模来完成实验的要求,因为无论是第1项还是第2项追加要求,混种的建模都可以胜任。
第18~28行是同步FIFO的实例化,其中也包括同步FIFO的连线关系。在24行 A_Left_Sig的输出口是由U1,同步FIFO的U1_Left_Sig来驱动。第28~33行是由实验八传统除法器实例化而成的U2。该除法器的输入驱动(被除数和除数)是由U1同步FIFO的U1_Read_Data信号([15..8]代表被除数,[7..0]代表除数。)。
第37~64行是控制程序。在步骤0(51~53行)控制程序先检查FIFO是否为不为空,如果使得的话就拉高 isRead,并且从FIFO读取数据,否则的话拉低 isRead。在步骤1(55~57行)控制程序启动除法器,拉低isRead(57行),并且等待除法器反馈完成信号(56行)。
步骤2(59~61行),控制程序判断从外部(右边)的FIFO是否为不为满,如果是的话就拉高 isDone,并且将除法器完成操作的数据写入外部的FIFO(60行)。否则的话拉低 isDone。在步骤3(63~64行)控制程序拉低isDone,并且清理步骤i,以示步骤从新开始。
在72行 Product的输出,是由除法器U2的操作结果 U2_Quotient 和 U2_Reminder 联合驱动(33行)。
第16~23行是U1同步FIFO的实例化。在23行的A_Left_Sig 是由同步FIFO的U1_Left_Sig来驱动。第27~31行是U2乘法器的实例化(实验一得传统乘法器),然而乘法器的输入驱动(被乘数和成熟)则由U1同步FIFO的U1_Read_Data信号([15..8]为被乘数,[7..0]为乘数)。
第35~56行是控制程序。步骤0(47~49行)判断U1的FIFO是否为不为空,如果是的话就拉高isRead,从FIFO读取数据。否则的话就拉低isRead并且等待。在步骤1(51~53行)控制程序启动乘法器和拉低isRead,并且等待乘法器反馈完成信号。当乘法器反馈完成信号,关闭乘法器,并且返回步骤0。
注意:乘法器接口的Product直接由U2乘法器的product驱动(31行)。
exp20_top 组合模块是用来组合除法器接口和乘法器接口。
第3~10行是组合模块的输入输出口,14~16行是仿真输出。24~39行是divider_interface.v的实例化,43~51行是multiply_interface.v的实例化。连线关系基本上和图形一样,笔者就不罗嗦了。但是有一点请注意,在55~57行的仿真输出,笔者将 U1 的的完成信号(55行)引出,它充当驱动 U2 的 Write_Req。56行的 SQ_U1_Product 同样也被笔者引出它充当 U2 的 FIFO_Write_Data。57行的 SQ_U2_A_Left充当U2的Left_Sig,它也一样被笔者引出。
第52~80行是仿真的操作。步骤0~3分别写入4个不同的数据(54~68行),第70~71行停止了5个步骤(时钟)。然后在步骤9又写入另一组数据(73~75行)。在步骤10(77~78行),拉低Write_Req并且永远的徘徊。
在这里笔者不想再一个时钟一个时钟的解释了,这样笔者会升天的 ... 我们先看第一段 .vt 往 .v 写入 5个数据,当 .vt 写完5个数据U1还是没有完成操作(SQ_U1_Product)。在第二段,U1已经求得结果,U1判断U2的FIFO状态(SQ_U2_A_Left_Sig),条件成立,然后就拉高Done_Sig(SQ_U1_Done_Sig),并且将求出的结果写入U2的FIFO。
在第三段,U2已经完成第一个数据的操作(注意Product)。在第四段,U1已经完成第二个数据的操作(SQ_U1_Product),然后它判断U2的FIFO的状态(SQ_U2_A_Left_Sig),发现不为满,拉高Done_Sig(SQ_U1_Done_Sig), 将已经完成的数据U2的FIFO。
在第五段,U2已经完成第二个数据的操作(注意Product),在同一个时间U1完成第三个数据的操作(SQ_U1_Product),然后它判断U2的FIFO的状态(SQ_U2_A_Left_Sig),发现不为满,拉高Done_Sig(SQ_U1_Done_Sig)就将以完成的数据写入U2的FIFO(注意SQ_U1_A_Left_Sig).
在第六段,U1已经完成第四个数据的操作(SQ_U1_Product),然后它判断U2的FIFO的状态(SQ_U2_A_Left_Sig),发现不为满,拉高Done_Sig(SQ_U1_Done_Sig)将已经完成的数据U2的FIFO。在同一个时候U2还没有完成第三个数据的操作。
在第七段,U1已经完成第五个数据的操作(SQ_U1_Product),然后它判断U2的FIFO的状态(SQ_U2_A_Left_Sig),发现不为满,拉高Done_Sig(SQ_U1_Done_Sig)将已经完成的数据U2的FIFO。在同一个时候U2还没有完成第三个数据的操作。
在第八段,U2完成第三个数据的操作(注意Product)。这时候的U1已经完成工作,并且休息中。
在第九段,U2完成第四个数据的操作(注意Product)。在第十段,U2完成第四个数据的操作(注意Product)。
从仿真结果上,我们可以得到以下的结果:
.vt 文件独立于U1,U1独立于U2,为什么这样说呢?在第一段的时候 .vt 已经将5个数据写入U1后,它的工作就结束了,U1和U2还在工作。然后在第八段U1已经完成5个数据的操作,反之U2还在工作。知道第十段U2还完成操作。
在这里同步FIFO作为了输入缓冲的作用。如果接口建模的角度来说,FIFO独立化了每一个模块。
在第一段至第十段,在每一个时钟中每一个模块都在工作,这也使得时钟的利用率大大的提升(符合实验的第二项追加要求。)
在实验二十中,我们看到了混种的建模,实现了该实验的挑剔要求。当然,笔者还是不承认混种建模是一种新的建模技巧,混种建模原本就是为了实现实验的不合理要求(各
种设计要求),基于原有的建模创建而成。所以笔者不承认它是什么新的建模技巧。在实验二十中所使用的混种建模,它拥有流水操作建模的外壳,接口建模的内部。换一句话说,如果笔者再给出不一样的实验要求,混种建模的基本形态会再一次更动和变形。
这一章的中心好似都是围绕着同步FIFO而展开。从了解Start_Sig与Done_Sig的协调性到掌握同步FFIO,笔者都只在讲述一个重点,那就是:“模块沟通”。Verilog HDL语言模块之间的沟通不仅是在“代码”的形式表现出来,而且还藏在步骤和时钟之上。笔者非常看重“模块的沟通”,因为笔者明白到单文件主义(单模块)是满足不了实验的要求,如果采取多模块的办法,那么模块之间的协调操作更是重要了。
稍微回忆一下“低级建模”,在“低级建模”之中,模块之间的协调操作,笔者都是以“步骤”的形式来实现。实际上“步骤”就是时钟(时序)的显性指示,因为考虑到初学着对“时钟”的模糊,所以才没有深入。最好的例子就是 Start_Sig 和 Done_Sig充当模块沟通的控制信号,您只要懂得使用它们(了解使用步骤),用不着去理解它们(不要关心时序上的关系)。
但是当读者逐渐深入 Verilog HDL语言,读者会发现这样是完全不够的。就好比实验十七到实验十八同步FIFO的改进,单单掌握步骤是应付不了。在前面的前面的前面(第一章),笔者说过步骤i(步骤)是时钟显性指示,它告诉我们“第几个时钟(上升沿)发生什么事情”,如果模块去掉步骤i(步骤),在时序上该模块的动作会显得非常模糊。
这一章笔记的最后最让笔者感到兴奋的是,当笔者了解模块之间沟通的简单规则,混种建模的实现就存在可能性(这可发现使笔者兴奋了足足一个晚上)。要实现混种建模的前提条件即是,掌握十足各种建模技巧,强化了解步骤与时钟的关系性。混种建模有什么好处,估计笔者不用说读者都会理解。
假设一个实验要求,某个建模技巧无法充分发挥,那么可以往设计里面添加其他的建模技巧,使得实验的要求达成。用一句话说就是,建模的弹跳性更强。就如发生自实验二十的身上,乘法器和除法器披着流水操作的外皮,里边确实简化的接口建模。
嗯,话说长了,笔者差不多要结束Verilog系列的笔记了。这系列的笔记真的花了笔者很长的时间,消耗了笔者许多的精力,对健康有很大的打击。笔者逐渐深入Verilog HDL语言,就发现 Verilog HDL 的世界越大,越了解Verilog HDL语言的可能性和潜能。但是笔者的力量有限,没有继续深入的力气了,最后笔者可以给出的结论是:
如果站在低级建模的角度来讲,系统建模会是Verilog HDL 语言某个程度的极限。此外模块的沟通不可能仅限与这本笔记所提及的。最后还有混种建模的可能性。
文章评论(0条评论)
登录后参与讨论