声明:本文为黑金动力社区(http://www.heijin.org)原创教程,如需转载请注明出处,谢谢!
黑金动力社区2013年原创教程连载计划:
http://www.cnblogs.com/alinx/p/3362790.html
《FPGA那些事儿—Modelsim仿真技巧》REV3.0 PDF下载地址:
http://www.heijin.org/forum.php?mod=viewthread&tid=22492&page=1&extra=#pid163257
第三章 理想就是美丽
3.1 理想时序
3.2 时间点事件还有即时事件的时序表现
3.3 指向时钟的i
3.4 指向过程的i
3.5 激励的好帮手是i
3.6 协调的时序
总结
第三章 理想就是美丽
3.1 理想时序
笔者虽然已经写过无数关于理想时序的用法,然而笔者却未曾思考它的本质?理想时序究竟是什么,想必读者非常好奇吧!?首先,让我们先来理解一下理想的概念,理想不等价完美,举例而言: 假设旅游A的一人费用是1000元,世界上也只有傻子准备1000元正而已,换做常人的准备金额通常都是费用的好几倍。
如果读者准备5000元,那么这笔5000元就是理想的准备金额,至于完美准备金额当然是越多越好。结果而言,理想时序并不是完美时序,完美时序好比无穷无上限的准备金额一样,根本不可能存在在这个世界上,完美顶多只是幻想中的一滴浪漫,或者盲目的一丝自负而已。
图3.1.1 一对寄存器与物理延迟。 | 图3.1.2 物理时序。 |
笔者曾近还是传统流派的门徒之时,一位臭脸的师兄恰好负新弟子的指导工作,师兄非常不厌烦般在黑板上绘出两幅意义不明的涂鸦,如图3.1.1与3.1.2所示,然后命令我们自己想办法用 Verilog 表达出来,一个时辰以后提交答案。就这样,一群师弟们便吵吵嚷嚷起来。
抱怨的抱怨,瞎搞的瞎搞,总之每个人泥菩萨过江就是了。沉思将喧闹的环境隔开,笔者注释黑板嘀咕道,图3.1.1当中的 tPath,Tclk2还有Tco都是物理延迟,然而图3.1.2就是物理延迟造成的物理时序,物理时序有一个特征,那就是信号失去对齐性。图3.1.1姑且还能使用Verilog 表达出来,结果如代码3.1.1所示:
1. reg Reg1,Reg2; 2. 3. always @ ( posedge CLK ) 4. begin 5. Reg1 <= Sig; 6. Reg2 <= Reg1; 7. end
代码3.1.1
但是Verilog就没有办法表达图3.1.2,因为Verilog不能描述 tPath, Tclk2还有 Tco等物理延迟。想着想着,于是笔者便结出“不可能”三个字的结论,不可能的事情就是不可能完成。节能的本质绝对不允许无用功的行为,与其浪费气力笔者还不如睡觉好。不知不觉之间周围的吵杂声渐渐逝去,原来是师兄回来了,各个师弟便紧张兮兮坐回原位,师兄来回扫视一样,眼见人人拿着代码,唯有笔者两手空空,一阵迅风忽然划过耳边,脸颊来不仅疼痛,笔者整个人便落在地上 ... 死了,这次笔者的第一次死亡。
图3.1.3 一对理想的寄存器。 |
图3.1.4 理想时序。 |
从大神殿复活回来以后,笔者便一直被关在地牢里面壁,打从第一次笔者充分感觉到传统流派的无理暴力,笔者没有折服暴力的理由。于是,笔者将记忆当中的图3.1.1还有图3.1.2按自己的感觉重绘出来,结果如图3.1.3还有图3.1.4所示。图3.1.3是的一对寄存器的理想模型,理想的东西当然产生的理想时序,结果就是3.1.4。
图3.1.5 硬模型。
好奇的同学可能会问: ”那些物理延迟消失在哪里了?“,物理延迟并不是消失,而是打从一开始它们根本不存在。根据笔者的想象,一般软模块成功编译以后会产生模型,接着模型就可以下载到开发板当中,如图3.1.5所示。笔者之所以模型为硬模型,是因为综合器给予模型相对应的物理信息,不过这些相对应的物理信息只是表面估计,实际上却与下载以后的物理信息持有一定差别。
图3.1.6 软模型。
经过综合以后的模型,也有选择不下载到开发板的权利,而是经过Modelsim编译成为仿真对象,最终下载到虚拟环境,此刻模型称为软模型。软模型自身虽然也有携带综合器给予的物理信息,但是使不使用就是另外一回事。事实上,软模块也可以省略综合这个步骤,直接交由Modelsim编译成为仿真对象直接仿真,过程如图3.1.6所示。结果,笔者可以这样总结道:
图3.1.5与图3.1.6的共同点就是起源与软模块,然而软模块却是未综合未编译的原始种子,它是理想,它没有污垢。换之,综合以后的软模块可以选择自己的演化方向,成为硬模型还是软模型。硬模型没有权利拒绝物理信息,相反软模型可以接受或者拒物理信息。如果软模型不接受物理信息,这意味着它继承软模块的理想本质。因此笔者可以断言,物理信息是后生加上去的枷锁,好让时序失去原先无垢的本质。
物理信息好比沉重的包袱,压在背上增加负担,这种感觉完全表达 Verilog 描述物理时序的痛苦。Verilog本属理想,产生的时序理所当然也是理想,可是为什么传统流派却不这么认为呢?鬼才知道那般家伙的脑子在思考什么,不然也不会突然一脚把笔者踢死。就这样,反叛的念头才在心底深处逐渐萌芽。
笔者选择理想时序不仅因为是理想时序可以节能,因为理想时序是仿真的真理。领悟理想时序让笔者失眠三个夜晚,期间笔者一直在思考理想时序究竟拥有怎样的特性?
l 其一理想时序必须拥有理想时钟源。
l 其二理想时序不会失去对齐性。
l 其三理想时序拥有2个触发事件(时序表现)。
图3.1.7 时钟路劲延迟。
理想的时钟源不存在时钟路径的延迟,如图3.1.7所示是存在时钟路径延迟的物理模型还有物理时序。时钟源所产生的时钟源,经过不同物理延迟的Tclk1与Tclk2以后,Reg1与Reg2的时钟源被失去对齐性。Tclk2与Tclk1之间的差别也称为时钟差——Clock Skew。
图3.1.8 时钟源抖动。
另外一种产生时钟差的缘由不是时钟路径延迟而是时钟源的输出抖动。如图3.1.8所示,假设CLK1与CLK2的路径并不存在延迟,但是时钟源的抖动会导致CLK1与CLK2失去对齐性。所谓的时钟抖动就是时钟源的输出延迟并不一致,有时大(Max),有时小(Min),一般是时钟源劣质或者老化引起。
图3.1.9时钟周期不一致。
另一种较为严重的问题是时钟周期不一致,如图3.1.9所示,虽然时钟源没有输出抖动,而且时钟路径也不存在任何物理延迟,可是时钟源却非常不稳定,结果导致时钟周期有时大有时小。这种现象一般有两个凶手,其一就是时钟源劣质或者老化;其二就是寄存器的时钟供源端出现问题。
图3.1.10 区域性的固定时钟周期。
虽然建模允许一个以上的时钟源出现,不过不同的时钟源只允许出现在各种的区域而已。如图3.1.10所示,图中有两个区域,而且各个区域都拥自己固时钟源,而且还是固定性周期。除非有特别的机制,不然不同时钟源的区域,信号不能随意往来。
为什么理想的时钟源如此重要?因为时钟源在仿真当中视为“环境输入”,环境在生态学是指生物赖以生存的需求,如空气,土地,食物,水源等。如果环境状态不佳,生命也不会好到那里去。所以说,时钟源是仿真首屈一指的需求,不仅仿真对象需要它,而且激励内容同样也需要它。
建模一般分为无时钟源的组合逻辑建模,另一种则是有时钟源的时序逻辑建模,也是俗称的RTL级建模。笔者曾经所过,仿真既是虚拟建模,而且也是虚拟的时序逻辑建模,时钟源除了提供模块活动的心跳以外,时钟源也领导所有模块的同步性。既然时钟源如此重要,时钟源怎么可以不理想呢?
图3.1.11 物理时序与理想时序。
理想时序的特性除了理想时钟源以外,理想时序的信号不会失去对齐性,亦即不存在物理延迟。如图3.1.11所示,Verilog本质所属理想,所以描述不了左边的物理时序,换之右边则是Verilog可以描述的理想时序。所谓对齐性,既是信号不仅不会跳来跳去之余,信号还会依准时钟沿划出一块一块。
宏观上,信号对齐就是上述这么一回事;微观上,信号对齐除了拥有更高的表达能力以外,时序细节也更加清晰。然而最为重要是信号对齐以后,两种时序触发事件,亦即“时间点事件”还有 ”即时事件“才能有效划分特征。那么,什么又是时序触发事件呢?
<= // 非阻塞赋值
= // 阻塞赋值
Verilog 有2种赋值操作符,亦即 <= 非阻塞赋值操作符,还有 = 阻塞赋值操作符,然而,关于两种赋值操作符的差别,传统流派总是一笑而过或者干脆装傻不知,所以笔者才讨厌它们。
<= 非阻塞赋值操作符,一般用于时序逻辑建模(设计),如:
always @ ( posedge CLK )
begin Reg1 <= Sig_In; Reg2 <= Reg1; end
= 阻塞赋值操作符,一般用于组合逻辑建模(设计),如:
always @ ( * ) // 输出选择器
if( Start_Sig_A ) rLED = LED_U1;
else if( Start_Sig_B ) rLED = LED_U2;
else rLED = x;
又或者用于连线或者输出驱动,如:
wire LineA;
wire LineB;
assign LineB = LineA; // LineA连线 LineB,亦即LineA驱动LineB连线
assign LED = rLED; // 寄存器 rLED,驱动LED输出端。
以上纯属两种赋值操作符的建模表达方式而已,然而它们真正的价值还有差别只有在于理想时序之间才能凸显出来。
图3.1.12 时间点事件 T-1~T0。
首先,让我们来了解一下什么是时间点事件。图3.1.12的左图有一对寄存器,期间Reg1从读取信号到输出信号的瞬间,时序何种变化是我们必须理解的地方。图3.1.12的右图是Reg1的时序活动,时序上边标示的 T-1,T0,还有T1是时钟的经过,然而T负值除了表示初始化还有复位化以外,T负值还表示准备时刻。
在T-1的时候,SigIn送入一块值为逻辑1的数据,然后该时间点结束。在T0的时候,Reg1被CLK1的上升沿触发,该触发沿也称为锁存沿,然后Reg1会锁存该时间点的过去值——逻辑1,接着决定发送逻辑1。T1时间点结束以后,Reg1便输出未来值——逻辑1。图3.1.12对应的时序行为描述如下:
input SigIn; // 输入端声明
reg Reg1; // 寄存器声明
always @ ( posedge CLK1 )
begin Reg1 <= SigIn; ...... // 时序行为描述
图3.1.13 时间点事件 T0~T1。
当Reg1读入逻辑1的SigIn以后,SigIn便会游走Reg1与Reg2之间,然后再经由Reg2Q输出,过程如图3.1.13的左图所示。图3.1.13的右图则是Reg1与Reg2的时序活动,在T0的最后(图3.1.12的结果)Reg1输出逻辑1的未来值,然而时间的巨轮现在已经转向T1。在T1的时候,CLK2的锁存沿造就Reg2读取Reg1Q的过去值(亦即T0的未来值),然后Reg2在T1的结束之际决定输出逻辑1的未来值。
图3.1.14 时间点事件形象化。
从旁观看,整体时序具体一点程好似Reg1将一块SigIn数据搬入然后又搬给Reg2,Reg2接收之后又将输出搬出,过程如图3.1.14所示。然而,此刻Reg1和Reg2想干什么或者该怎么干都是发生在时间点那个瞬间。图3.1.13对应的时序行为描述如下:
input SigIn; // 输入端声明
reg Reg1; // 寄存器声明
always @ ( posedge CLK1 )
begin Reg1 <= SigIn; Reg2 <= Reg1; end // 时序行为描述
至于Reg2在时间点T1之后想将数据版给谁,这点笔者就不知道了,毕竟Reg1与Reg2的行为描述就是那么丁点而已,寂寞的朋友可以自行添加Reg3或者Reg4看看。总结来说,时间点事件,分为:“时间点”,亦即此刻谁谁想干什么,操作标志为 <=;“过去值”,亦即此刻送来的东西;“未来值”,亦即此刻送去的东西。就这样,一刻又一刻的时间点连接起来,成为理想的时序图。
图3.1.15 物理延迟tData。
除了时间点事件以外,理想时序还有即时事件,顾名思义即时就是立即,马上,英文叫做 immediately。不过,为什么即时称为即时?而且,即时的由来又是什么?如图3.1.15的左图所示,读者尝试将组合逻辑想象成为数据加工工厂,然而在现实世界当中,无论是怎么优秀的加工技术也需要一定的加工时间,因此数据加工时间称为tData。
假设有一块意义不明的数据送入组合逻辑加工,经过tData的时间以后,数据贴上创可贴表示加工完毕。图3.1.15的右图是整体过程的时序图,意义不明的数据被送入组合逻辑以后,会经过tData的延迟,然后再经由组合逻辑输出。
图3.1.16 即时效果。
理想时序不是现实世界,而是梦境中的美丽世界,那里不存在任何物理延迟也不存在1ns或者3.2ns等丑陋的数值。如图3.1.16所示,意义不明的数据送入不久就立即加工完毕并且输出,期间加工即时完成,如此模块笔者称为即时模块。
图3.1.17 瘦骨如柴的数据。
假设数据是一支瘦骨如柴的东西,而不是一块肥嘟嘟的东西,那么即时效果更佳显眼,过程如图3.1.17所示。注意图3.1.117右图,此刻时间经过在也没有意义,因为数据加工好比变魔术般瞬间完成。这个道理也告诉我们,即时结果在物理时序当中是无视时钟,无视时间的犯规存在,读者是不是觉得很神奇呢?因此称为即时事件。
即时事件一般的时序行为描述如下:
rData = Temp + 1; //组合逻辑的加法器
rData = Temp * 2; //组合逻辑的乘法器
有些同学可能会非常担心的问:“即时事件如此无视时钟还有无视物理延迟,真的没有问题吗?”,笔者也没有法子呀,理想时序的即时事件本来就是这么一回事,鬼叫笔者只是探索者而不是创作者,笔者看见什么就写什么而已。笔者稍微强调一下,即时事件无视时间也无视时钟,所以有没有时钟源驱动即时模块还是照样跑照样跳。
举例而言,常见的选择器芯片,都有表3.1.1所示的大致功能:
表3.1.1 常见的选择器功能。
输出 / 输入 |
D1 |
D2 |
Q1 |
0 |
0 |
Q2 |
0 |
1 |
Q3 |
1 |
0 |
Q4 |
1 |
1 |
表3.1.1当中表示有两个输入——D1与D2,根据两者取值不同输出通道也会经由不同,然而表3.1.1也告诉我们一个事实,既是输出会根据输入不同而即时生效,这是即时事件也是选择器最理想的功能状态,所以没有必要考虑数据加工的物理时间。(芯片手册当然会标注实际的时间参数,但是如果没有必要的话,用户一般会无视)
即时事件与时间点事件不同,前者无视时钟后者必须时钟支援,假设即时事件不小心掉入时间点事件当中,它们又会产生怎么样的花火呢?有趣,实在有趣。
3.2 时间点事件还有即时事件的时序表现
首先让我们瞧瞧两种相关语法,亦即 begin ... end 与 frok ... join。
参考书曾经说过begin ... end 是顺序块,然而 frok ... join是并行块,然而类似说法只是语法方面的规定和说明而已。先不管它们是顺序块或者并行块,根据笔者的理解 begin ... end 是综合语言的括号功能,类似c语言的{};反之,frok ... join则属于验证语言,不过笔者不太喜欢验证语言,所以暂时无视它。
事实上,操作是否并行执行还是顺序执行并不是取决语法本身的规定或者说明,而是取自语言的本质。Verilog打从一出生就是并行性质,不管我们要不要。换句话说,不用我们去管,并行操作就是Verilog的默认操作 ... 很遗憾的是Verilog 天生并不支持顺序操作,所以我们才需要建立仿顺序结构,好让Verilog支持顺序操作。
相反的道理,C语言一出生就是顺序(步骤)性质,也就说顺序操作是默认操作,并行操作确实C语言一辈子的痛,为此才会出现任务调度这种机制,好让C语言模仿并行操作。因此,Verilog 语言的“begin ... end” 等价C语言的“{}”,这样的理解绝对有理。
exp01_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp01_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. Reg2 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 <= 4'd2; Reg2 <= 4'd3; i <= i + 1'b1; end 29. 30. 1: 31. i <= i; 32. 33. endcase 34. 35. /********************/ 36. endmodule
exp01_simulation.vt是笔者建立的激励文本,话是这么说 ... 不过里边却不包含仿真对象,所以可以称为虚拟建模或者单纯的仿真环境而已。exp01_simulation.vt 保存在 Experiment02 的目录下,读者再根据自己的喜好用notepad软件代开或者其他什么都行。
我们暂时不管激励文本的建立过程,在此读者只要明白笔者在第4~12行产生时钟信号还有复位信号,它们皆是环境输入。
接着,笔者在第14行建立寄存器i,用于指向步骤或者时;第15行是寄存器Reg1与Reg2的声明,用于暂存结果;第17~23行是相关的复位操作;第25~33行是仿顺序操作用法模板。如果读者无法上述几行代码的语中意思也是理所当然的事情,不打紧,详细内容会在后面的章节补充。
步骤0(第27~28行),Reg1 赋值 4'd2而Reg2赋值 4'd3,根据时间点事件的解释,这个时间点 Reg1会输出 4'd2的未来值,而Reg2会输出 4'd3的未来值。接下来,我们就要启动 Modelsim来验证这个事实。
exp01_simulation.vt 并不是依赖集成环境建立而成的 .vt文件,所以必须经过手动编译才能启动仿真,不过贴心的笔者已经事先完善了,读者只要独立启动 Modelsim,然后再调出 wave界面即可。(wave界面只要沿着View菜单选择 wave选项即可)。
① 沿着File菜单选择Open。 |
②选择 .wlf 文件并且打开。 |
③ 沿着FIle菜单选择 Load。 |
④ 选择 .do 文件并且打开。 |
图3.2.1 打开预先建立的wave界面。
当我们打开wave界面,wave界面则是什么东西也没有,朋友别慌 ... 这是正常的现象。如图3.2.1所示,①沿着File菜单选择 Open;②选择 .wlf 文件并且打开,不过这时候wave界面还是空空如也,因为方才读者只是读入笔者预先保存的wave界面配置文件
而已;③在此沿着File菜单选择 Load;④选择 .do 文件并且才开,此刻读者已经加载成功笔者预先设置好的wave显示状态。
图3.2.2exp01_simulation.vt 的仿真结果。
图3.2.2是 exp01_simulation 的仿真结果,其中光标C1指向的地方既是步骤0或者说时间点T0。仿真结果完全符合预期的猜测,因为在T0的未来,Reg1和Reg2输出未来值4'b0010 亦即 4'd2, 还有 4'b0011,亦即4'd3。
exp02_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp02_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. Reg2 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 <= 4'd2; Reg1 <= 4'd3; i <= i + 1'b1; end 29. 30. 1: 31. i <= i; 32. 33. endcase 34. 35. /********************/ 36. endmodule
假设顽皮的笔者故意更改一下 exp01_simulation.vt 文件的28行的 Reg2 <= 4'd3 成为 Reg1 <= 4'd3,然后再仿真看看会发生什么一回事。想必许多同学一定会认为这是语法错误,然后编译无法通过是否?答案是否定的,第28行的代码不仅无法没有错误,而且编译也成功通过 ... 感觉是不是很蹊跷呢?
图3.2.3 exp02_simulation.vt 的仿真结果。
重复图3.2.1的动作,打开笔者预先保存好的文件,显示以后的wave界面如图3.2.3所示。光标C1同样指向步骤0,也是时间点T0 ... 此刻,Reg2因为没有赋值操作,所以没有任何未来值。反之,此刻的Reg1并不是输出 4'd2的未来值,而是 4'd3的未来值,为什么会这样呢?
1. begin
2. Reg1 <= 4'd2;
3. Reg1 <= 4'd3;
4. ...
有人会认为这是纯粹的语法Bug,不过笔者却认为这是编译次序的灰色特性。我们知道不管是什么语言的编译器,都是一行一行按着次序编译的代码。根据笔者的理解,编译器会先编译第2行 Reg1 <= 4'd2,接着再编译第3行的 Reg1 <= 4'd3,随之第3行的编译结果会覆盖第2行之前的编译结果,因此 Reg1 最终输出未来值4'd3 而不是 4'd2。
exp03_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp03_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. Reg2 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 = 4'd2; Reg2 <= 4'd3; i <= i + 1'b1; end 29. 30. 1: 31. begin Reg1 = 4'd4; Reg2 <= 4'd6; i <= i + 1'b1; end 32. 33. 2: 34. i <= i; 35. 36. endcase 37. 38. /********************/ 39. endmodule
笔者之前曾经说过,即时事件是无视时钟的操作,如果即时事件不小心被时钟源波及会是什么样的结果呢?步骤0(第27~28行)Reg1 用 = 操作符赋值 4'd2,Reg2 则用 <= 操作符赋值 4'd3;步骤1(第30~31行)Reg1用 = 4'd4,Reg2则用 <= 赋值4'd6。
图3.2.4 exp03_simulation 仿真结果。
重复图3.2.1的方法,打开原先保存好的wave界面显示状态。图3.2.4是 exp03_simulation.vt 的仿真结果,图中光标C1指向时钟点T0,然而光标C2指向时间点C2。根据代码第27~31行所示,Reg1与Reg2没有直接的关系,T0的时候Reg1用 =操作符赋值4'd2,而Reg2用 <= 操作符赋值 4'd3,所以Reg1输出即时值 4''d2,Reg2则输出未来值4'd3。在T1的时候,Reg1用 = 操作符赋值4'd4,而Reg2用 <= 操作符赋值4'd6,结果Reg1输出即时值 4'd4,Reg2则输出未来值4d6。
好奇的同学可能会问:“怎么两个赋值操作符的结果都是一样?”的确,表面上却是这么一回事,不过细微来讲的话,就算即时结果再怎么无视时钟,但是Reg1的输入源被时间点(时间沿)掌控着,所以即时结果不得不屈服。
1. `timescale 1 ps/ 1 ps 2. module exp04_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. Reg2 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 = 4'd2; Reg2 <= Reg1; i <= i + 1'b1; end 29. 30. 1: 31. i <= i; 32. 33. endcase 34. 35. /********************/ 36. endmodule
为了体现即时事件的能力,exp04_experiment.vt 的步骤0(第27~28行)笔者将Reg1与Reg2建立关系,Reg1 先用 = 操作符赋值 4'd2, 然后 Reg2则用 <= 赋予 Reg1的过去值。终究Reg2是否会读取 Reg1的过去值还是读取Reg1的即时值,答案在仿真结果当中。
图3.2.5 exp04_simulation 仿真结果。
重复图3.2.1的方法,打开exp04_simulation原先保存好的wave界面显示状态。如图3.2.5所示,光标C1指向时间点T0,此刻Reg1与Reg2建立关系,根据时间点事件的作用,Reg2应该读取此刻 Reg1的过去值,亦即0才是,然后输出未来值0。这个事实告诉我们,Reg2没有读取Reg1的过去值0,而是读取Reg1的未来值2,结果Reg2才会输出未来值2。
图3.2.6 时钟沿,时钟块,数据块。
整合技巧有这样一个想法,为了实现紧密控时,时钟还有数据必须拥有“具体单位”。一般参考书常说N个时钟时钟是指时钟沿,然而这种概念是非常抽象和空虚的。为此,时钟块,还有数据块的概念就诞生,所谓时钟块是指两个时钟沿的长度,举例第一个时钟块,以时钟沿1算起,长度介于时钟沿1~2之间;第二个时钟块,以时钟沿2算起,长度介于时钟沿2~3之间,其它以此类推,过程如图3.2.6所示。
所谓数据块是指时钟沿划分的数据长度,如图3.2.6所示,数据A有一块数据在于时钟沿1~2之间,长度是1个时钟(时钟块);数据B也有一块时钟位于时钟沿2~4之间,长度则是2个时钟(时钟块) 。(注意:块也可以看成周期)
上述内容告诉我们一个事实,数据的长度是由时钟来决定。笔者曾经说过即时事件无视时钟,就算即时结果没有数据长度也没有关系,或者说数据长度应该越短越好,然而理想状态当然是0个时间。
图3.2.7 即时层的概念。
即时结果不是没有显示在理想时序图当中,而是即时结果是在太窄,窄到肉眼看不见。即时结果往往发生在时钟的细缝当中,如果使用未来科技将它放大几万倍,假设放大对象是时钟沿2,结果我们可以看到位于时钟沿2~2之间有一个称为即时层的存在,而且即使结果就在哪里产生,结果如图3.2.7所示。
即时层也有其它命名,如:时钟无效空间或者时间停止空间等非常科幻的取名。根据笔者的妄想, 即时层可以发生无穷无尽的即时事件,也可以添加没有上限的小即时层 ... 笔者承认自己是歪歪看多了所以才会脱离现实。
如图3.2.5所示,光标C1指向的时钟沿当中,Reg1在即时层当中被赋予即时值2,即使结果无视时钟,所以即时生效,然后Reg2又被赋予Reg1的即时值2,最后Reg2输出未来值2。
exp05_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp05_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. Reg2 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg2 <= Reg1; Reg1 = 4'd2; i <= i + 1'b1; end 29. 30. 1: 31. i <= i; 32. 33. endcase 34. 35. /********************/ 36. endmodule
exp05_simulation.vt 与 exp04_simulation.vt 相比,语法方面虽然没有改变,但是在第28行当中 Reg1 = 4'd2 与 Reg2 <= Reg1 的执行次序被笔者修改过。exp04_simulation.vt 是先执行 Reg1 = 4'd2 然后再执行 Reg2 <= Reg1; 换之, exp05_simulation.vt 则是先执行 Reg2 <= Reg1,接着再执行 Reg1 = 4'd2。
图3.2.8 exp05_simulation 仿真结果。
重复图3.2.1的方法,打开exp05_simulation原先保存好的wave界面显示状态.如图3.2.8所示,笔者只是更改执行次序而已,然而仿真结果却不一样。光标C1指向时间点T0,此刻Reg2 读取 Reg1 的过去值0,而 Reg1则赋予即时值2,结果Reg2输出未来值0,而Reg1输出即时值2.
同学可能会觉得奇怪,当当更换执行次序而已,输出结果却不一样,为何呢?其实这是编译器的灰色特性,编译器在编译的时候会数顺序执行,Reg2 <= Reg1先被编译,然而该被操作视为时间点事件。在T0的时候,此刻Reg1的过去值是0,所以Reg2读取到0值。换之,Reg1 = 4'd2 只是重复 exp03_simulation的现象而已。
exp06_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp06_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2,Reg3; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. Reg2 <= 4'd0; 23. Reg3 <= 4'd0; 24. end 25. else 26. case( i ) 27. 28. 0: 29. begin Reg1 = 4'd2; Reg2 = Reg1 + 4'd2; Reg3 <= Reg2; i <= i + 1'b1; end 30. 31. 1: 32. i <= i; 33. 34. endcase 35. 36. /********************/ 37. endmodule
exp06_simulation.vt 是在 exp04_simulation.vt 的基础上再添加一行即时操作,如代码行29所示,笔者新添另一个寄存器 Reg3,然而 Reg1 赋予即时值4'd2,Reg被赋予的即时值是由 Reg1的即时结果再加上2,Reg3则赋予Reg2的即时值。
图3.2.9 exp06_simulation的仿真结果。
重复图3.2.1的方法,打开exp06_simulation原先保存好的wave界面显示状态。如图3.2.9所示,光标C1指向时间点T0,此刻Reg1被赋予即时值2,而Reg2同样也是赋予即时值,但是赋值对象是Reg1的即时结果再加上2,至于Reg3则赋予Reg2的即时结果。结果再T0的未来,Reg1输出即时值2,Reg2输出即时值4,Reg3则是输出即时值4。
为什么会发生这样的事情呢?其实这是,编译器灰色特性惹的祸,根据编译器的编译次序:
(一)Reg1 = 4'd2;视为即时事件。
(二)Reg2 = Reg1 + 4'd2;视为即时事件,期间Reg1的即时结果则是上一层的即
时值2.
(三)Reg3 <= Reg2;虽然视为时间点事件,但是赋值源则是 Reg2的即时结果。
以上现象告诉我们一个事实,亦即“即时层可以插入无限即时事件”。因为在时间点T0之上已经发生两起即时事件,即 Reg1 = 4'd2 和 Reg2 = Reg1 + 4'd2,而且 Reg2 = Reg1 + 4'd2 当中的 Reg1也是即时生效的即时值。最后Reg3再赋予 Reg2的即时结果3,而不是Reg2的过去值。
exp07_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp07_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #10 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1,Reg2,Reg3; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd9; 22. Reg2 <= 4'd0; 23. Reg3 <= 4'd0; 24. end 25. else 26. case( i ) 27. 28. 0: 29. begin Reg2 <= Reg1; Reg1 = 4'd2; Reg3 <= Reg1; i <= i + 1'b1; end 30. 31. 1: 32. i <= i; 33. 34. endcase 35. 36. /********************/ 37. endmodule
最后让我们再看一段较为匪夷所思的现象,exp07_simulation的第21行 Reg1的复位值为9,然而根据第29行,先是执行Reg2 <= Reg1,然后Reg1赋予即时结果2,最后再执行 Reg3 <= Reg1。读者是不是开始觉得有点头晕了?不打紧,我们只要根据编译次序还有时间点事件与即时事件之间的关系作出推断,一切结果都是符合逻辑的。
图3.2.10 exp07_simulation 的仿真结果。
重复图3.2.1的方法,打开exp07_simulation原先保存好的wave界面显示状态。如图3.2.10所示,光标C1指向时间点T0,此刻 Reg1的过去值(复位值)是9,而且Reg2先是读取Reg1的过去值9,然后输出未来值9。不一会儿,此刻即时事件突然发生了,Reg1被赋予即时值2,结果Reg1输出即时值2;此外,此刻Reg3再也不是赋予 Reg1的过去值9而是即时值2,因此Reg3也输出即时值2。
为了理清思路,我们可以理解这一切都是编译器惹的祸:
(一)Reg2 <= Reg1;视为时间点事件,Reg2赋予Reg1的过去值。
(二)Reg1 = 4'd2;视为即时事件,Reg1赋予即时值。
(三)Reg3 <= Reg1;虽然也是时间点事件,但是赋予结果则是Reg1的即时值而
不是过去值。
看完这么多有关时间点事件还有即时事件在理想时序上产生的现象,读者很可能会疑惑道:“知道这些秘密我们终究又能得到什么好处?”当然好处可多得是。我们知道HDL拥有并行的本质,亦即单个时钟可以执行无数个时间点事件。不过根据笔者的妄想,即时事件会产生即时层,然而即时层可以插入无数次即时事件。这个事实告诉我们,时间点事件和即时事件虽然都是并行操作,但是前者需要时钟后者则不需要时钟。
举例而言,假设笔者为了求出D的结果,然而D求得的过程必须经过以下4个步骤:
A = 1;
B = A + 1;
C = B + 1;
D = C + 1;
如果笔者用时间点事件求得D,笔者至少需要消耗4个时钟,而过程发生如下:
T0执行 A <= 1;
T1 执行 B <= A + 1;
T2 执行 C <= B + 1;
T3 执行 D <= C + 1;
换之,如果笔者使用即时事件求得D,笔者仅需要消耗一个时钟而已,过程发生如下:
T0 执行 A = 1; B = A + 1; C = B + 1; D <= C + 1;
这个事实告诉我们,原本求得D必须消耗4个时钟,不过即时事件却将D的求得压缩至1个时钟而已。因此我们可以断定,即时事件拥有“偷时钟”的作用。读者千万别小看偷时钟的作用,偷时钟可以减少时钟消耗至于,例如一些时序非常紧张的设计也有可能多一个时钟也不行,因此即时事件就会派上用场。
当然,即时事件的好处也不仅局限于偷时钟而已,即时事件可以实现真正意义上的精密控时,此外即时事件在仿真当中也有很大的用处。然而,使用即时事件的真正目的是建立即时模块。读者目前很可能暂时无法理解笔者所说的这番话,但是读者必须记住,理想时序除了时间点事件存在以外,也有同等身价的即时事件存在。
区分它们不仅仅只是 <= 阻塞操作符与 = 非阻塞操作符之间的识别而已,我们还要配合编译器的灰区使用才能实现它们的意义。为了帮助读者建立大概的概念,读者可以这样理解:先执行即时事件,再执行时间点事件,HDL描述如下:
begin
Reg2 = Reg1; // 即时事件
Reg3 <= Reg2; // 时间点事件
end
所以,千万不要搞错即时事件还有时间点事件的执行顺序噢!
===================================================================
刮着刮着,整间地牢已经刻满笔者的想法,笔者不禁感叹道,理想时序既然存在如此宝贵,如此重要的信息,为什么传统流派难视若无睹?是真瞎还是假傻,鬼才知道。不管怎么样,笔者不禁怀疑,传统流派所说的一切是否属实或者纯粹坑人?笔者想做点什么改善一下现状,可是传统流派人多势众笔者只是寡人一名,寡不敌众,而且有谁又会相信笔者的话语呢?
3.3 指向时钟的i
西元二零一三年的今天,我们到处都可以看到 i叉叉,如著名的iPhone,黑金的iBoard等等,i叉叉真是无奇不有,于是乎笔者也顺应流行,创建i——仿顺序操作。什么是仿顺序操作?仿顺序操作是低级建模当中非常重要的一环,本质上Verilog是没有结构去支持顺序操作,因此仿顺序操作才会诞生。放顺数操作最基本的认识就是使用Verilog创建可以支持顺序的操作结构。
其中i就是仿顺序操作的标志,为什么是i而不是j或者k呢?不知道,只是直觉性这么选择而已。i有许多功能,其中一项功能就是指向步骤,如代码3.1.1所示:
1. always @ ( posedge CLOCK ... ) 2. .... 3. case( i ) 4. 5. 0: 6. begin reg1 <= reg2; i <= i + 1'b1; end 7. 8. 1: 9. begin ... i <= i + 1'b1; end 10. 11. ... 12. 13. 9: 14. begin ... i <= 4'd0; end 15. 16. endcase
代码3.3.1
代码3.3.1很简单,always底下声明 case ... endcase,然后括号中就是i。默认起始步骤是0,当步骤完成以后 i就会递增以示下一个步骤,其他如此类推,直到所有步骤操作完毕就返回步骤0。换之仿真,i除了指向步骤以外还有指向时钟的功能,如代码3.3.2所示:
1. always @ ( posedge CLOCK ... ) 2. .... 3. case( i ) 4. 5. 0: 6. begin Reg1 <= 4'b1; i <= i + 1'b1; end 7. 8. 1: 9. begin Reg2 <= 4'b2; i <= i + 1'b1; end 10. 11. 2: 12. begin Reg3 <= 4'b3; i <= i + 1'b1; end 13. ...
代码3.3.2
代码3.3.2的操作非常简单,从步骤的角度上我们可以这样解读,即Reg1在步骤0赋值4'b1,Reg2在步骤1赋值4'b2,Reg3在步骤赋值4'b3。换之理想时序则可以这样解读,即Reg1在时间点T0决定赋值4'b1,Reg2在时间点T1决定赋值4'b2,Reg3在时间点3决定赋值4'b3。
图3.3.1 代码3.3.2对应的理想时序图。
代码3.3.2根据理理想时序的解读结果,最终会产生图3.3.1所对应的理想时序图。如图3.3.1所示,在T0的时候,由于Reg1决定赋值4'b1,结果Reg1输出未来值 4'b1;在T1的时候,Reg2决定赋值4'b2,结果输出未来值4'b2;在T2的时候,Reg3决定赋值4'b3,结果输出4'b3的未来值。
代码3.3.2只要换个方式解读就会产生不同的结果,读者是不是觉得很神奇呢?接着,再让我们看一段代码3.3.3:
1. always @ ( posedge CLOCK ... ) 2. ...... 3. case( i ) 4. 5. 0: 6. begin Reg1 <= 4'b2; i <= i + 1'b1; end; 7. 8. 1: 9. begin Reg2 <= Reg1; i <= i + 1'b1; end; 10. ......
代码3.3.3
代码3.3.3常规说道,即步骤0 Reg1赋值4'b2,步骤1 Reg2赋值 Reg1的结果。如果换做理想时序解读代码3.3.3,亦即在时间点T0的时候,Reg1决定赋值4'b2,结果Reg1输出未来值4'b2;在时间点T1的时候,Reg2决定读取Reg1的过去值,结果Reg2赋值4'b2,然后Reg2输出4'b2的未来值。
图3.3.2 代码3.3.3对应的理想时序图。
图3.3.2是代码3.3.3用理想时序去解读所对应的时序图。如图所示,在T0的时候,Reg1输出4'b2的未来值;在T1的时候,Reg2决定读取Reg1的过去值,结果Reg2读取4'b2并且输出未来值4'b2。 我们知道理想时序并不存在1ns~7ns等物理延迟,再加上代码3.32或者3.3.3一个步骤只有一个操作而已,结果步骤与步骤之间敲好是一个时钟块,两个时间沿。
图3.3.3 i指向步骤又指向时钟。
如图3.3.3所示,左边当中的步骤0与步骤1只停留一个时钟,因此我们只要将脑袋向时钟转90°角,我们就会看见效果。笔者需要强调一下,右图是脑海想象的理想时序图,而不是仿真结果。“为什么会那么神奇?”,读者可能会这样问及 ... 这就是理想时序融合i以后所有的效果,模块会提升一倍的表达能力,这也是主动设计最基本的基础——清晰的代码。
C语言一般都是代码越精简,代码就会越清晰,因为人类的左脑是线性处理,减少代码就好比缩短操作线程一般。换之Verilog,曾经何时笔者也是如此认为,不过笔者却为自己的天真付出惨痛的带价。事实上,Verilog并不是代码越精简就会越清晰,即使我们将整体代码精简到1到2成,代码不仅不会清晰度,反之还有可能弄巧成拙,为什么呢?那是因为Verilog是并行语言。
二十一世纪的今天,科技高度发展人类的文明也随之飞速成长,然而悲剧多过喜剧 ... 智能科技没有节制发展造就人们贪图便利,大步倾向左脑的使用,什么都要快!什么都要傻瓜!什么都要轻松!真是不知所谓的效率标准,尤其搞电子的人们,问题更是严重。
许多人之所以那么容易适应顺序语言,那是因为环境影响再加上后天性的用脑习惯。
顺序语言有线性的本质,这种感觉宛如游走在单向通道里,通道的距离好比操作线程的劳动程度,亦即代码越精简通道就越短,通道越短操作线程就越快结束。换句话所说,距离越短,整体印象更加容易放入左脑。此外,我们只要遵循通道的流向,亦即代码行段(线性地址),如果出现障碍物妨碍前进,我们就要设法移除障碍即可。
相反的,Verilog恰恰好是相反的东西,Verilog有并行的本质,这种感觉宛如游走多项通道的迷宫,又或者说 Verilog 是由无数单向通道组成的大迷宫。我们知道在迷宫直走就会迷路,左脑最大程度只能处理相接的单向通道而已,但是这样作对走出迷宫只有很小的帮助而已,在此我们就要指望右脑了。
图3.3.4 笔者要回家——单向道。
假设小小的笔者要回家,如图3.3.4所示,叉叉是妨碍物,小格子是建筑物,左图是一段单向到路程,没有什么好解释,笔者只要一直向前冲笔者就能回家。换之右图是多向道的回家路,小时候笔者必须借助特定的建筑物才能平安回家,如图3.3.4的右图所示,首先经过红色建筑物以后右转;经过绿色建筑物以后左转;经过黄色建筑物以后左转;经过蓝色建筑物以后右转,笔者就能回家。
在此,充满颜色的建筑物使笔者认识回家路途更加清晰,当然其中还有左转右转等小动作,但是大体上有颜色的建筑物就是回家路的关键。并行性质好比图3.3.4右图的回家路,线性行为有可能回永远回不了家,因为这种4 * 4 布局会产生 4! ( 4*3*2*1 ) 24个可能性的回家路。当然我们可以使用排除法寻找回家路,如果是布局是更加复杂或者有更多可能性的话,使用排除法无疑是自杀的行为因为人的经历是有限的,更何况是小时候的笔者?
笔者需要稍作一下补充,记忆有色建筑物,笔者回家绝非100%成功,然而有色建筑物确实提升清晰度将迷路降低(或者说缩变数/可能性),好让回家更加有效和准确。假设读者是一位顺序(线性),惯用左脑的小孩,读者就会尝试每一条回家路直到成功而已,如果运气好读者就能早点回家,反之运气稍差的话就会成为报纸的头条新闻(失踪)。
继续话题,如果i有能力指向步骤,而且又能指向时钟的话,代码的清晰度无疑会大大增加,即使不用实现仿真,我们只要稍微想象一下,时序图就会浮现在我们的脑海中。
不过,前者所诉都是单个步骤停留一个时钟作为前提,如果单个步骤停留超过1个时钟的话,i是否又能指向时钟呢?答案是肯定的,在此我们有几种选择:
1. always @ (posedge CLOCK) 2. ... 3. case( i ) 4. 5. 0,1,2,3: 6. begin Reg1 <= 4'b2; i <= i + 1'b1; end 7. 8. .... 9. 10. endcase
代码3.3.4
代码3.3.4表示,Reg1 赋值 4'b2等操作足够执行用足4个步骤,如果使用理想时序来解读的话,就是时间点T0~T3 Reg1决定赋值 4'b2。
图3.3.5 代码3.3.4对应的理想时序图时序图。
图3.3.5是对应代码3.3.4的理想时序图。如图所示,时间点T0 Reg1输出未来值4'b2;时间点T1 Reg1输出未来值 4'b2;时间点T2 Reg1输出未来值4'b2;时间点 T3 Reg1输出未来值4'b2。这是一种直接又简单的多时钟指向方法,即时步骤停留多少时钟,i的数值也跟着增加多少,如代码3.3.4所示,同样 Reg1 <= 4'b2操作共占据步骤1,2,3,4。这种方法虽然好用,但是仅适合小数量的多时钟指向而已,如果我们遇见大数量的多时钟指向,假设100个时钟,同样操作既不是要占据步骤1致100,如代码3.3.5所示,如果当中有误,我们既不是一牵动全山吗?为此我们需要另一个多时钟指向技巧。
1. case( i ) 2. 3. 0 ..... 100: 4. begin Reg1 <= 4'b2; i <= i + 1'b1; end 5. 6. endcase
代码3.3.5
1. always @ (posedge CLOCK) 2. ... 3. case( i ) 4. 5. 0,1: 6. begin 7. Reg1 <= 4'b2; 8. 9. if( C1 == 4 -1 ) begin C1 <= 4'd0; i <= i + 1'b1; end 10. else C1 <= C1 + 1'b1; 11. end 12. 13. endcase
代码3.3.6
代码3.3.6是整合技巧的一项功能,其中 if( C1 == 4-1 )控制步骤时钟逗留的数量,而 Reg1 <= 4'b2既是该步骤的操作,如果用理想时序解读的话,我们则可以这样表达:在T0的时候,操作Reg1 <= 4'b2逗留4个时钟;在T1的时候,操作Reg1 <= 4'b2又逗留4个时钟。
图3.3.6 代码3.3.6对应的时序图。
如图3.3.6多事是对应代码3.3.6的理想时序图,时间点T0决定Reg1在步骤0执行4个时钟的 Reg1 <= 4'b2赋值操作,于是T0接续4个时钟都会输出未来值 4'b2;时间点T1决定 Reg1在步骤1执行4个时钟的 Reg1 <= 4'b2 赋值操作,于是T1接续4个时钟都输出未来值4'b2。
笔者曾在扫盲文说过,仿真不推荐执行对象拥有多时钟,亦即“超烦模块”。既然不仿真,为什么还要创建针对多时钟指向的整合技巧呢?整合技巧本来就是为了避免过度依赖仿真才会被创建,即仅凭代码表达就能脑补理想时序图。归根究底,不管是哪一种方法,目的也是为使i指向时钟,好让时钟有个标志可以记录和追踪,最终提升代码的表达能力。
说了那么多,不管i再怎么厉害,既然i身在时序它就要遵守时序的表现规则,然而i究竟如何在理想时序上指向时钟,这是非常有学习的价值。
exp09_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp08_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #5 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [3:0]Reg1; 16. 17. always @ ( posedge CLK or negedge RSTn ) 18. if( !RSTn ) 19. begin 20. i <= 3'd0; 21. Reg1 <= 4'd0; 22. end 23. else 24. case( i ) 25. 26. 0: 27. begin Reg1 <= 4'd1; i <= i + 1'b1; end 28. 29. 1: 30. begin Reg1 <= 4'd2; i <= i + 1'b1; end 31. 32. 2: 33. begin Reg1 <= 4'd3; i <= i + 1'b1; end 34. 35. 3: 36. begin Reg1 <= 4'd4; i <= i + 1'b1; end 37. 38. 4: 39. i <= i; 40. 41. endcase 42. 43. /********************/ 44. endmodule
exp08_simulation 是一段非常简单的代码,我们先不管部分验证语言,Reg1先在步骤0在执行赋值 4'b1;步骤1赋值4'b2;步骤2赋值4'b3;步骤就3赋值4'b4;步骤4停止操作。如果用理想时序解读,即Reg1在时间点T0 赋值4'b1;时间点T1赋值4'b2;时间点T2赋值4'b3;时间点T3赋值4'b4;
图3.3.7 exp09_simulation 仿真结果。
重复图3.2.1的方法,打开事先准备好的wave状态记录。如图3.3.7所示,i一般建议放置在信号的最下方,此外光标C0~C1分别指向时间点(时钟沿)T0~T3,完后我们就可以开始分析图中的时序活动。首先是时间点T0,此刻i的过去值(复位值)是0,所以i当前指向时间点T0,时间点结束i会自动递增。同样时刻,Reg1决定赋值 4'b1,所以Reg1输出4'b1的未来值。
当时间点来到T1的时候,此刻i的过去值是1,因此i当前指向时间点T1,时间点结束会使i递增。同一时刻,Reg1决定赋值4'b2,所以Reg1输出未来值4'b2;而时间点T2的时候,此刻i的过去值是2,因此i指向时间点T2,时间点结束会使i递增。同一时间点,Reg1决定赋值4'b3,所以Reg1输出未来值4'b3;最后时间点T3,i的过去值是3,所以i指向是时间点T3。时间点T4,,Reg1决定赋值 4'b4,因此Reg1输出未来值4'b4。
读着读着,读者是否觉得有点眼花缭乱呢?起初笔者也是如此,实际上Modelsim并没有提供任何指向时钟的工具,当然我们可以手动添加光标,如图3.3.7所示那样。笔者手动添加光标C0~C3以示指向T0~T3,但是Wave界面以外光标就不能使用了,关于这点多少让人觉得遗憾。因此,笔者才要利用 Verilog的代码本身实现时钟指向功能,为此笔者可曾思考一段时间,不知巧合还是必然,只要结合仿顺序操作i还有理想时序,i自然而然会指向时钟。
话虽如此,i实际上并不是指向时钟,而是i此刻的过去值正好吻合时钟数,因此我们才会错认i确实指向时钟。结局不管怎么样,死马当活马医,节能主义教导笔者要充分使用资源。最后笔者还是强调一下,i是否指向时钟,实际上是i此刻的过去值正确吻合时钟经过的个数,此外操作也必须是使用一个时钟执行,虽然也有其他技巧指向多时钟 ... 但是,一般都是一个i指向一个操作,指向一个时钟。
3.4 指向过程的i
图3.4.1 PC的概念。
i除了指向步骤还有时钟以外,i还能指向过程 ... 不过“指向过程”这句话又是什么意思呢?我们事先追忆一下大一所学过的“计算机结构”,传统处理器结构都有一个PC——Program Counter,亦即计数器。PC的功能是指向处理器下一位要执行的指令地址,
然而指令又经常储存在内存当中,因此PC有时候也成为指向内存地址,概念如图3.4.1所示。处理器每执行一位指令,PC就会自行递增,除非我们使用特殊指令妨碍PC或者更动PC,否则PC永远的工作就是指向还有递增。
机器语言就是顺应这个传统的PC处理器器件,因此机器语言才有顺序或者线性的本质。结果而言,C语言也好,汇编语言也好,C++还是Jave等高级语言也好,本质上都一样,既是顺序语言。对于顺序语言而言,代码之间最粗是行数差,步骤差,最细就是指令的地址差,不管它们有什么“差”,顺序语言永远都会伴随一个指向工具,实时指向下一个要处理的代码,步骤,还是指令。然而,时钟频率既是处理器的执行频率,也是PC的更新频率,简单说就是差与差之间的切换间隔。
不管顺序语言再怎么庞大,操作再怎么复杂,它始终都是单向通道,指向工具就是引导主角走出通道的利器。换句话说,只要前面出现障碍,指向工具就会停止更新地址,指向工具亦即指向错误行数,步骤,还是指令地址。笔者究竟是为了什么才会长篇大论述说指向工具?在此,笔者想表明,顺序语言之说以拥有那么强的排错能力,八九不离十都是指向工具在作怪,换做并行语言呢,结果又是如何?
Verilog语言是描述语言的家族之一,天生就有并行的性质,同时它也是一只天然呆。Verilog 的脑中除有时钟以外,它不曾知道“步骤”是什么?“过程”是什么?但它更加不知道自己有能力描述“步骤”,还有“过程”。不过,步骤还有过程对于Verilog来说没有时钟来得重要,因为失去时钟的它顶多只能描述组合逻辑而已。
好奇的读者可能会问“笔者为什么要强调过程的概念呢?”,回答这个问题之前,我们必须知道“一个过程是由N个步骤组合而成”,然而“ 一个步骤则是由N个时钟组合而成”,结果而言:
过程 = N步骤,如果过程等价N步骤
步骤 = N时钟,如果步骤等价N时钟
过程 = N时钟,那么过程既等价N时钟
因此我们可以这样表达:
时钟 => 步骤 => 过程
上述表达式则表示“时钟产生步骤,步骤产生过程”,这个表达式指明了时钟,步骤还有过程之间的层次关系,亦即没有时钟,步骤不能产生,没有步骤过程不能产生。同时也表示时钟比起后面两者更加重要。
笔者使用i指向时钟是为了强化“时钟”的印象;笔者使用i指向步骤是为了加强“步骤”的印象;笔者使用i指向过程是为了强化“过程”的印象。这些行为归根究底就是增加代码的清晰度之余,还有就是增加Verilog的排错能力。就这样,Verilog既有能力指向过程当中的错误。
但是并行语言相较顺序语言之间,指向工具还存在根本性的性质差别。笔者曾在前面说过,顺序语言不仅没有时钟概念,而且也是单向通道。换之,并行语言不仅重视时钟,而且也是多向通道。顺序语言的指向工具是系统自行创建,然而并行语言的指向工具必须手动创建。此外,顺序语言的指向工具非常智能,相比之下并行语言的指向工具就略显逊色了。
图3.4.2 单向过程与多项过程。
图3.4.2表示单向过程与多项过程的示意图。左图是单向过程,聪明的指向工具正在指着当前执行步骤;右图则是多向过程,举例中有3个过程,每一个过程都有独自的笨蛋指向工具i。此外,各个过程也有独自的步骤还有时钟供源,3个过程形成全体过程。多向过程相较单向过程,要确保的东西太多,简直要喊救命。
多向过程还有一个比较蛋疼的问题,相较单向过程一个步骤卡死,全体过程就会崩溃,但是多向过程既是出现一个过程奔溃,也不会发生全体奔溃,即使输出结果亦也不是预想所要。看到这里,读者的蛋蛋是否在颤抖呢?心想仿真不仅不单纯而且还如此猥琐?没错这就是仿真不为人知的一面,此刻只是恐怖切糕略显一角而已,往后仿真还有更多黑暗等待我们绝望。如果读者不小心被吓着,笔者衷心谢罪 ... 仿真就是因为猥琐,所以我们才要事事增加模块的表达能力,增加代码的清晰度,增加时序的控制能力。
这个章节的目的就是要讨论如何让i实现指向过程?笔者爱用的低级建模,有一种称为用法模板的书写习惯,如下所示:
1. always @ ( posedge CLOCK ... )
2. case( i )
3.
4. 0:
5. begin ... i <= i + 1'b1; end
6.
7. 1:
8. ...
9.
10. endcase
always 下创建一个 case ... endcase,然后case条件是 i。不管建模大事小事,还是有事无事,笔者都非常建议使用上述用法模板,这样作除了可以稳定风格之余,我们还可以细化仿真,助长后期建模,总之好处就是多得数不完。i指向过程的原理非常简单,如果过程出现问题,那么i就会停留在该处有问题的步骤当中。总之还是使用实验来说话比较直白。
exp09_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp09_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #5 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [2:0]j; 16. reg [3:0]Reg1; 17. 18. always @ ( posedge CLK or negedge RSTn ) 19. if( !RSTn ) 20. begin 21. i <= 3'd0; 22. Reg1 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 <= 4'd1; i <= i + 1'b1; end 29. 30. 1: 31. begin Reg1 <= 4'd2; i <= i + 1'b1; end 32. 33. 2: 34. if( j == 3) i <= i + 1'b1; 35. 36. 3: 37. begin Reg1 <= 4'd3; i <= i + 1'b1; end 38. 39. endcase 40. 41. /********************/ 42. 43. reg [3:0]Reg2; 44. 45. always @ ( posedge CLK or negedge RSTn ) 46. if( !RSTn ) 47. begin 48. j <= 3'd0; 49. Reg2 <= 4'd0; 50. end 51. else 52. case( j ) 53. 54. 0: 55. if( i == 2 ) j <= j + 1'b1; 56. 57. 1: 58. begin Reg2 <= 4'd1; j <= j + 1'b1; end 59. 60. 2: 61. j <= j; 62. 63. 3: 64. begin Reg2 <= 4'd2; j <= j + 1'b1; end 65. 66. endcase 67. 68. /********************/ 69. 70. endmodule
exp09_simulation 有两个 always 块,亦即拥有两组仿顺序操作。第18~39行是由i指向步骤,Reg1执行操作;第45~66行则由j指向步骤,Reg2执行操作。为了方便理解,读者可以暂时将两者看成两组过程。由i指导的过程 (简称i过程), 先是步骤0 把Reg1赋值4'b1,然后步骤1赋值4'b2;至于步骤3必须先满足 j 等于 2的条件才能继续执行操作。
换之,由j指导的过程(简称j过程),先是步骤0判断 i是否等于 2,如果是就继续执行操作;步骤1则是Reg2赋值4'b1;步骤2是人为卡死。按照常规解读,i过程先行一步,直到步入步骤2,j过程才会开始执行,此刻i过程必须等待过程执行一定程度才能继续执行。至于j过程,开始执行不久就会卡死在步骤2当中,因此全体过程也随之奔溃起来。
相反的,换做时序方法解读,i过程与j过程是并行发生,而且也没有所谓过程奔溃的概念,顶多就是操作停留在某个步骤而已。不管增氧,我们还是先看exp09_simulation仿真结果再下定论。
图3.4.3 exp09_simulation 仿真结果。
重复图3.2.1的方法,打开事先保存好的wave界面显示状态。如图3.4.3所示,左边笔者分类两组过程,其中指向工具i与j皆是放在组内的最尾端。此外还有四位光标C0~C3,它们分别标志时间点T0~T3。时间点T0的时候,此刻Reg1在步骤0决定赋值4'd1,然后输出未来值4'd1,i也因此递增。同一个时刻,j过程在步骤0判断i的过去值是否4'b2,结果不是,所以没有动作发生。
时间点T1的时候,此刻Reg1在步骤1决定赋值4'd2,然后输出未来值4'd2,i也因此递增。同样时刻,j过程在步骤0判断i的过去值是否4'd2,结果不是,所以什么动作也没有发生。时间点T2的时候,此刻i过程步入步骤2判断j的过去值是否为4'd3,结果不是,所以没有动作发生。同一时刻,j过程在步骤0判断i的过去值是否为4'd2,结果是,Reg2决定赋值 4'd1,j也因此递增。
时间点T3的时候,此刻i过程在步骤2判断j的过去值是否为4'd3,结果不是,所以没有动作发生。同样时刻,j过程因为笔者的坏心眼,所以卡在步骤1,结果没有动作发生。由于j过程卡在步骤1,结果i过程也跟着卡在步骤2,在接续的时间点之内,i过程和j过程任然没有动作发生。
常规上整体过程已经可以称为崩溃,但是Verilog不是顺序语言,而且时序也没有崩溃的概念,时序最多结果不是预期所要而已。那么重点来了 ... 读者尝试想象一下,如果此刻没有i指向过程i,没有j指向过程j,读者究竟会沦落当何等的窘境呢?因为没有标志指向问题所在,所以读者必须逐个时钟分析所有过程,试问自己会不会吓死不偿命呢?嗯姆姆(品尝声),这就是切糕的味道,美味吗?
换个情况,如果存在指向工具可以指向有问题的位置(步骤停留的地方),我们就可以发挥单向寻道的能力。j过程停留在步骤2,因此步骤2就是j过程的问题所在。换之,i过程停留在步骤2,因此步骤2就是i过程的问题所在。多向过程与单向过程指的向工具的排错过程大同小异,单向过程是指向工具来回游走一个过程,而多向过程则是指向工具来回游走多个过程。
图3.4.4 单步调试工具。
事实上,Modelsim默认下也有指向工具,不过称为“单步调试”,如图3.4.4所示。不过非常遗憾的是,“单步”这词不仅显示它是直走撞墙的傻瓜,而且“调试”这词更加暴露它是偏向顺序操作的家伙。笔者曾在前面说过,调试不等价仿真,因为仿真是并行发生,追求多向过程,然而调试是顺序发生,追求单向结果。结果而言,单步调试工具
根本无用武之地,笔者不是说它不好,只是它不适合仿真而已。
究竟是否笔者不了解它,还是笔者的仿真风格与它相性不好呢?答案是见仁见智的,因为笔者的仿真风格基于理想时序之余,而且时序还和代码之间拥有非常强的联系。单步调试同一此刻只能指向一个步骤(代码行)而已,但是最为人痛心的是它没有能力指向时钟。时钟是仿真的一切,因为没有时钟无法产生步骤,没有步骤不能产生过程。
不过非常讽刺的是,传统流派把它当成宝贝来看护,就算未曾认真使用过它。想必传统流派打从一开始就错当仿真不是追求多向过程,而是像极顺序语言那样追求单向结果,其实这是一座大坑,数以万计的初学者早就已经入坑身亡了,笔者的第二次死亡就是掉入这个大坑当中。不管时间流逝多久,那种无尽黑暗逐渐吞噬全身的恶心体验,笔者时时刻刻都无法释怀 ...
3.5 激励的好帮手是i
图3.5.1 激励文件。
i除了指向时钟,步骤,还有过程以外,i还是激励的好帮手。虽然我们还未曾认真学习过激励,不过作为扫盲文的经验,激励基本上可以分为5个部分,最上端是环境输入,亦即虚拟时钟信号与复位信号;第二段是仿真对象的声明;第三段是,虚拟输入也是基本输入还有反馈输入;第四段是虚拟输出,亦即基本输出还有反馈输出;第五段则是其他,结果如图3.5.1所示。
我们知道i可以指向过程,换句话说,i也是仿真对象的活动标志,简单而言就是仿真对象不管偷懒还是装病都会一一反映在i当中。这句话未完全理解之前,先让我们来了解一下,所谓“反馈”究竟是激励何等定义?
基本输入还有基本输出的定义非常单纯,基本输入是给与仿真对象的刺激,然而基本输出就是仿真对象接受刺激以后所产生的反应。所谓反馈输入又名第二次输入,它不是基本输入,因为它需要根据基本输出才能做出相对应的刺激,常见反馈信号如 Done_Sig就是其中之一。至于什么是反馈输出呢?
图3.5.2 模式一:单纯的功能输出。
功能仿真一般有两种模式,其一就是观察仿真对象的功能输出,亦即只有基本输出,如图3.5.2所示。
图3.5.3 模式二:假想对象
其二,我们不仅需要观察仿真对象的基本功能,我们还要使用基本输出刺激假想对象,然而假想对象所产生的反应就是反馈输出,结果如图3.5.3所示。所谓假想对象实际是存在脑海当中的虚拟硬件,它是妄想所以并不存在与仿真对象或者激励内容当中,但是测试仿真对象非使用它不可。
图3.5.3 反馈输出示意图。
假设有个实际环境,我们想要使用FPGA驱动某个储存IC,此刻FPGA是主机而IC从机,其中连接双方是双向IO的Data信号,如图3.5.3的左图所示。FPGA驱动IC的的驱动程式就是我们的仿真对象,然而IC在仿真环境中却不存在,如图3.5.4的右图所示
。实际环境中,如果FPGA为IC写数据的,相对之下仿真环境的仿真对象会产生基本输出,基本输出可以是一串8'hA0, 8'hE3等数据信号,然后以时序方式呈现在wave界面当中。
换之,如果实际环境中的IC给FPGA读数据 ... 相较之下,仿真环境并不存在任何实际IC,而且虚拟IC也是想象的产物,此刻我们必须模拟虚拟硬件所读出的数据,这种人为的输出也称为反馈输出。实际硬件(IC)接受命令以后,知道命令的意义,然后产生相关的输出。反之,虚拟硬件宛如痴呆一般,它不仅没有命令的概,它更没有自律的能力,没叫它动它就不动,可是叫它下午3时吃饭,才却会准时在下午3时吃饭 .... 实在是搞不懂的家伙。
虚拟IC虽然痴呆但却晓得吃饭,但是重点是必须告诉它“准确时刻”。那么问题来了,
何为“准确时刻”?是夕阳西落还是满月悬挂?不是,这些都不是!准确时刻是指“虚拟硬件什么时候该产生什么输出”,然而准确时刻必须根据仿真对象的内部过程来决定。
Yes!我们终于找到重点了!前期,我们用i指向过程不仅仅是加强仿真的排错能力,实际上我们为了此刻做好准备,话说i强不强呀!?
不过,这里也有一个遗憾的消息,亦即我们必须手动将i从仿真对象当中牵引出来。这是非常麻烦的活儿,假设一个仿真对象里边包含N个功能,指向信号自然而然也有N个,结果我们必须将全部都牵引出来,感觉如代码3.5.1所示。
1. module abc_funcmod 2. ( 3. input CLOCK, RESET, 4. ... 5. output [3:0]SQ_i, SQ_j, SQ_k ... // 出入端声明指向信号 6. ); 7. 8. reg [3:0]i,j,k; // 建立指向工具 9. ... 10. assign SQ_i = i; // 牵引指向信号 11. assign SQ_j = j; 12. assign SQ_k =k; 13. 14. endmodule
代码3.5.1
代码3.5.1显示一个模块abc_funcmod,里边至少有3指向信号,亦即i,j与k。我们先在出入端声明 SQ_i,SQ_j和SQ_k,它们分别对应i,j,k的输出,至于SQ是Simulation Output的缩写,意思是指仿真输出。
图3.5.4 手动添加仿真信号。
虽然自动编译还有手动编译不用故意引出信号,我们只需几个简单步骤就可以将它们添加成为仿真信号。如图3.5.4所示,位于sim界面右键选择仿真对象,这样就可以将所有信号添加成为仿真信号。不过,这种临时抱佛脚的手段,仿真信号仅有观察作为,只能用来显示过程是否卡死,绝对不能引用在激励文本当中。
所以说,我们必须执行如同代码3.5.1的劳动,如此一来i,j,k这些指向信号才能用作激励。那么,究竟如何使用指向信号呢?方法很简单,如代码3.5.2所示。
1. module abc_funcmod_simulation(); 2. ...... 3. wire[3:0]SQ_i,SQ_j,SQ_k; // 指向信号 4. ....... 5. abc_funcmod U1 // 仿真对象声明 6. ( 7. .CLOCK( CLOCK ), 8. .RESET(RESET), 9. ...... 10. .SQ_i( SQ_i ), // 指向信号i引出 11. .SQ_j( SQ_j ), // 指向信号j引出 12. .SQ_k( SQ_k ) // 指向信号k引出 13. ); 14. ...... 15. always @ ( posedge CLOCK ) 16. ...... 17. if( SQ_i == 3 && SQ_j == 4 && SQ_k == 5 ) ... // 使用指向信号 18. ...... 19. endmodule
代码3.5.2
先为仿真对象牵引指向信号以后,然后在激励文本当中实例化仿真对象 abc_funcmod,最后又在激励文本当中使用这些指向信号,结果如代码3.5.2所示。忧心的同学可能会担心,如果仿真对象的过程数量过多,那么指向信号亦也不是很多?如此一来建模是不是很麻烦?而且实例化以后,激励内容会不会不美观呢?同学就别担心了,因为只要好好遵守低级建模的准则,基本上指向信号最多只是2~3个而已。
不知不觉中话题又忽然扯远了 ... 如何使用指向信号实际是件麻烦事,内容涉及太多基础知识了,所以笔者就不多做实例了,详细内容往后我们再谈也不迟。在此,读者只要好好记住,如果i有能力指向过程,这也表明i有能力“标志过程”,例如i等于0的时候仿真对象A正在初始化;i等于1的时候仿真对象症在执行加法等。
我们就是使用这些“过程标志”来“描述不存在的虚拟硬件”。实际上,我们不可能为了仿真一段小功就费神去创建一个虚拟硬件,因为这是一件非常不明智的举动,就算再现实际硬件的10%功能,也不是说创建就能创建的程度。笔者曾经尝试创建某IC的虚拟硬件,不过后来却放弃了 ... 虽然10%的功能仿真起来可以正常运作,但是激励容量已经达到臃肿的程度,如此一来激励内容的清晰度就会大打折扣。
此外,节能本性也不允许笔者去干一些“大力完小事”的劳动,因此笔者才会动歪脑经,借用指向信号来描述虚拟硬件,至于功能再现几%完全是根据需要所定。如此一来,这样才符合笔者“小力完大事”的节能主义,好让自己拥有更多足够的精力用来解读时序图。
3.6 协调的时序。
我们知道i有能力指向时钟,步骤,过程,甚至还能描述“虚拟硬件”用作产生反馈输出。我么之所以要求i指向这个又指向那个,其实背后就是为了实现“同步”这个重要任务。一般情况下,同步是指两组对象使用相同频率发生动作,例如一起哭一起笑,换做时序的话,就是数个模块使同时钟频率的意思。不过在此,同步除了上述意义以外,笔者认为“同步”还有另一个更具有意义的意思,那就是“协调”。
图3.6.1 同步与协调。
举例而言,假设有两组四位的LED灯,左边一组,右边又一组,结果如图3.6.1所示。
两组LED分别使用4个时间发生动作,红色表明亮,灰色表明灭。T0的时候,左边的LED群一起亮,然而右边的LED群是最左边的一颗LED点亮而已;T1的时候,左边的LED群一起灭,换之右边的LED群从左边数起第二颗LED点亮而已;T2的时候,左边的LED群又一起亮,右边的LED群则是左数第三颗LED点亮而已;T3的时候,左边的LED群又一起灭,反而右边的LED群是左数第四颗LED点亮而已。
换句话说,左边的LED群呈现闪耀功能也是同步操作,而右边的LED群展示流水功能也是协调操作。好奇的朋友可能会纳闷道:“协调就协调嘛!协调那里相似同步呢?”
,这位同学真是提出一个好问题,接着让我们仔细分析一些图3.6.1的两组LED群。首先左边的LED群只有一个动作,即时亮灭而已,换之右边的LED群除了亮灭动作以外,还有流水动作。
朋友尝试想象一下,如果没有节拍流水动作是否会实现?答案当然是不可能,那么再请朋友想象一下如果动作不协调,流水效果还是流水效果吗?答案也是肯定的。在此,我们得到一个重要的信息,亦即同步在某种意义上有如“固定间隔的节拍”,然而协调就是建立在这些“固定间隔节拍”之上的一系列动作(操作)。
在自然界当中“同步”和“协调”是息息相关的好兄弟,就拿人体来举例。心跳供源全身可变性的固定节拍,别名又指动物时钟。紧接着,全身上下的细胞军团便依赖生物时钟发出一系列的协调操作,如喉咙——食道:
图3.6.2 食道的协调操作。
图3.6.2是食道运送食物的示意图,首先我们非常清楚死人的是不会吞食物,因为死人没有心跳,因此组成食道的细胞军团并也没有时钟供给,结果失去节拍 ... (血液停止循环,细胞得不到资源滋润也是其中一个原因)所以图3.6.2所示是活人吞咽食物的正常过程。如图3.6.2所示,食物完全送到胃部至少需要4个时间,除了地心引力拉扯食物向下以外,其实更为重要是食道壁——收缩和膨胀的协调操作,食物最终才能成功抵达胃部。
图3.6.3 流水效果比拟食道的协调操作。
假设食道的长度恰好是4颗LED灯的数量,其中LED点亮代表食道壁膨胀状态,灭灯表示食道壁收缩状态。如图3.6.3所示,LED群协调般一亮一灭,结果食道送入胃部完全吻合LED灯的流水效果。笔者承认自己YY想太多了,细胞军团的协调操作实际上比流水效果还要复杂一万倍以上,笔者这样做只是为了让读者有个感知的认识而已,所以情有可原。归根究底,人体的协调操作和仿真究竟有又何关系呢?
道家说过,太空是大宇宙,人体是小宇宙,或者说人体是宇宙的缩影,所以道家相信天星变化会直接或者间接影响人类。如此类推,仿真也可以是人体的缩影,所以仿真存在协调操作一点也不奇怪,可是为何协调对仿真来说是如此重要呢?如果仿真有时钟用量不超过10个,或者单一对象还是单一操作,其实有没有协调都没有关系。反之,如果仿真有时钟用量超过10个,多个对象或者多组操作的话,结果就会凸显协调的重要性。
协调简单而言就是,在准确的时候,发生准确的动作,然后产生预想所要的结果,然而协调必须是“之上两个对象在一定的时间用量内”才能产生的现象。其中如何断定“准确的时候”还有“准确的动作”都是仿真应该追求的最终目的。所以说,仿真绝对不像传统流派所言那样,随便哈拉几下波形图就可以收工。那么协调又如何具体表现在仿真当中呢?
我们必须事先满足两个首要条件:其一,理解理想时序的时序表现;其二,明确指向时钟,步骤,过程等。读者不要怀疑,前面所有章节都是为此而铺垫 ...
exp10_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp10_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #5 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [2:0]j; 16. reg [3:0]Reg1; 17. 18. always @ ( posedge CLK or negedge RSTn ) 19. if( !RSTn ) 20. begin 21. i <= 4'd0; 22. Reg1 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 <= 4'd1; i <= i + 1'b1; end 29. 30. 1: 31. i <= i; 32. 33. endcase 34. 35. /********************/ 36. 37. reg [3:0]Reg2; 38. 39. always @ ( posedge CLK or negedge RSTn ) 40. if( !RSTn ) 41. begin 42. j <= 3'd0; 43. Reg2 <= 4'd0; 44. end 45. else 46. case( j ) 47. 48. 0: 49. begin j <= j + 1'b1; end 50. 51. 1: 52. begin Reg2 <= Reg1; j <= j + 1'b1; end 53. 54. 2: 55. j <= j; 56. 57. endcase 58. 59. /********************/ 60. 61. endmodule
第25~33行是i过程,第39~57行是j过程。首先是i过程的步骤0执行 Reg1 赋值 4'b1操作,然后停留在步骤1。j过程的则是现在步骤0偷懒,然后才在步骤1读取Reg1的值,最后停留在步骤2。exp10_simulation 虽然是习以为常的代码,但是当中却包含有趣的信息,为什么j过程必须先放空一个步骤才然后在步骤1读取Reg1的值,而不是一开始在步骤0读取Reg1的值。
那些不清楚的朋友当然会如此认为,反之那些已经理想时序表现的朋友非常清楚,j过程是为了在时钟点T1,读取Reg1此刻的过去值4'b1(T0的未来值) .. 是不是如此,就让我们一同瞧瞧仿真结果。
图3.6.4 exp10_simulation 仿真结果。
重复图3.2.1的方法打开事先保存好的显示状态。如图3.6.4所示,这里有两组信号亦即i过程与j过程,然后又有光标C0~C1分别指向时钟点T0~T1。在T0的时候,i过程的Reg1决定在步骤0赋值4'b1,所以Reg1输出4'b1的未来值。同一时刻,j过程却在步骤0发呆,所以什么事情也没发生。
在T1的时候,i过程已经结束操作,所以什么事情也没发生。同一时刻,j过程的Reg2决定读取Reg1的过去值4'b1,结果Reg2输出4'1的未来值。至于时间点T2,i过程和j过程已经没有动作发生,往后时间点也是如此。
果真,我们的猜测是正确的 ... j过程之所以故意在步骤0放空,原因是为了等待Reg1在T0更新值为4'b1。然后,j过程在时间点T1亦即步骤1,再读取Reg1的过去值,因此Reg2成功输出 4'b1的未来值。这种外表看似非常孩子气的仿真结果,可是里边却隐藏大量值得让人思考的价值信息。
Q1:为什么j过程的Reg2会在步骤1成功读取Reg1的过去值?
Q2:其二,为什么j过程又会知道步骤1才能最快读取Reg1在T0的更新值4'b1?
Q3:其三,这种慢一拍的数据传输又是什么现象?
首先读者必须理解,exp10_simulation 改如代码3.6.1所示,我们也可以得到一样的结果。
1. case( i ) 2. 3. 0: 4. begin Reg1 <= 4'b1; i <= i + 1'b1; end 5. 6. 1: 7. begin Reg2 <= Reg1; i <= i + 1'b1; end 8. 9. 2: 10. i <= i; 11. 12. endcase
代码3.6.1
但是笔者也曾经说过,相识Verilog这种描述语言是并行性质,习惯并行操作才能真正发挥它的能力。代码3.6.1与 exp10_simulation 相较,前者好比顺序操作,因为只有一个过程,然而后者就是如假包换并行操作,因为两组操作同时发生。这是一种非常奇怪的情形,exp10_simulation 只要换做代码 3.6.1的形式,不知为何代码更容易理解,反之将代码 3.6.1拆分为 exp10_simulation 般两组过程,头就开始发疼。笔者认为倾向左脑使用的后遗症,因为左脑是线性运作,遇上多线运作左脑会忽然短路几下也不成。
当操作或者过程有两组以上的时候,协调就会发挥比金银贵的重要性。在此,可以这样回答以上3个问题:
A1:这是因为(理想时序)时间点事件的关系。
A2:这时因为我们有用i指向步骤还有时钟。
A2:数据传输之间存在一个延迟的硬道理。
答案结果中,A1与A2完全符合协调的事先条件,因此我们可以断定 exp10_simulation已经却却事实在协调操作,不然Reg2是不会成功在T1的时候读取Reg1在T0发送的未来值4'b1。
exp11_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module exp11_simulation(); 3. 4. /*********************/ // environment signal 5. reg CLK, RSTn; 6. 7. initial 8. begin 9. RSTn = 0; #10; RSTn = 1; 10. CLK = 0; forever #5 CLK = ~CLK; 11. end 12. /********************/ 13. 14. reg [2:0]i; 15. reg [2:0]j; 16. reg [3:0]Reg1; 17. 18. always @ ( posedge CLK or negedge RSTn ) 19. if( !RSTn ) 20. begin 21. i <= 4'd0; 22. Reg1 <= 4'd0; 23. end 24. else 25. case( i ) 26. 27. 0: 28. begin Reg1 = 4'd1; i <= i + 1'b1; end 29. 30. 1: 31. i <= i; 32. 33. endcase 34. 35. /********************/ 36. 37. reg [3:0]Reg2; 38. 39. always @ ( posedge CLK or negedge RSTn ) 40. if( !RSTn ) 41. begin 42. j <= 3'd0; 43. Reg2 <= 4'd0; 44. end 45. else 46. case( j ) 47. 48. 0: 49. begin Reg2 <= Reg1; j <= j + 1'b1; end 50. 51. 1: 52. j <= j; 53. 54. endcase 55. 56. /********************/ 57. 58. endmodule
exp11_simulation 与 exp10_simulation 相比,更改的地方不多。i过程的步骤0,Reg1赋值 4'b1,不过是使用 = 赋值操作符。j过程则是没有放空步骤0,而是Reg2在步骤步骤0读取Reg1的过去值。在此,我们知道i过程在步骤0当中,Reg1引起即时事件,j过程的Reg2则是引起时间点事件。我们知道即时事件无视时钟,不过不同的是 Reg1与Reg2是两个过程的居民。此刻Reg2能否成功读取,i过程当中Reg1的即时值呢?
图3.6.5 exp11_simulation 仿真结果。
重复图3.2.1的方法,打开事先保存好的显示状态。如图3.6.5所示,仿真结果有两组过程,亦即i过程与j过程,此外还有光标C0表示时间点T0。在T0的时候,i过程的Reg1在步骤0赋予即时值4'b1。在同一个时刻,j过程的Reg2在步骤0读取得到Reg1的即时值而不是过去值4'b1,结果输出未来值4'b1。
13. case( i ) 14. 15. 0: 16. begin 17. Reg1 = 4'b1; 18. Reg2 <= Reg1 19. i <= i + 1'b1; 20. end 21. 22. 1: 23. i <=i; 24. 25. endcase
代码3.6.2
代码3.6.2的效果虽然等价 exp11_simulation的仿真结果,但是读者必须注意,既是结果相同,两者还是完全不同的东西。代码3.6.2的Reg1与Reg2是在同一个步骤发生即时事件,然而 exp11_simulation的Reg1与Reg2则间隔两组过程发生即时事件。不管怎么说,两者的区别是非常清楚的,代码3.6.2不用考虑协调的问题,然而exp11_simulation则需要 ...
如果读者这样问:
Q1:Reg1的值4'b1为什么会即可生效?
Q2:Reg2怎么又知道,在步骤0就可以读到 Reg1的更新结果4'b1?
Q3:数据传输之间没有时钟延迟?
A1:因为Reg1使用 = 赋值操作符,结果引发即时事件。
A2:这是因为i不仅指向步骤,而且还有即时事件在作怪。
A3:即时事件没有时钟概念,尽管读取对象不同过程。
其中,A1还有A2完全符合协调的前提条件 ... 在此有一个重点我们必须认真思考一下,如果 Reg1赋予不是即时值!?如果i没有指向时钟或者步骤的话,实际上协调操作是不会发生的。
协调只是一个概念,协调也可以是一种代名词,如果要说它存在,它却没有形体,协调就是这种暧昧的存在。仅有一组过程也不会发现协调的痕迹,当两组过程正在有效互动的时候,协调就会路出尾巴。举个而言,当读者寂寞一人在跳舞的时候,协调是不存在的。相反的,如果读者参与10人以上的千手观音舞蹈,协调的重要性就会凸显出来,协调发挥到极致的千手舞动,宛如华丽的流水般,一起一落都是如此优美,好似马陆虫的百只美腿一起移动。
“顺序语言有没有协调的概念?”,好奇的朋友可能会这样问 ... 顺序语言仅有一组过程,所以不存在协调的概念,不过这种情形也只有限定在“任务调度机制”出现之前。“任务调度”说得难听一点就是顺序语言模仿并行操作,也可以称为仿并行操作,其中Thread(线程)或者Task(任务)用作表示独立的一组过程,只要两组任务或者线程同时活动,协调自然而然就会出现。
不过,顺序语言的比起描述语言,前者没有后者般的细腻而且也不优美。不管顺序语言再怎么模仿并行操作,顺序语言时钟存在致命的步骤差,或者指令差(亦即步骤或者指令之间的延迟),这种感觉还比马陆虫的美腿移动起来一卡一卡般怪异。高频时代的今天,虽然Ghz级别的频率时钟可以缩短(平滑)步骤差或者指令差的间隔时间,以致卡位现象不那么明显,但是指令差或者步骤差还是存在的。
协调真的那么重要吗?很重要,非常重要,重要到不得了,尤其是仿真这一环!仿真的作用除了可视化时序以外(波形图),还要准确指明“哪一个信号在什么时候发生什么”,而且还要准确联系“这个信号之所以会那样,一定是什么代码在作怪”。除此之外,仿真为建模提供一个虚拟的模拟环境,换句话说仿真是再现功能活动的实际环境。实际环境也是现实空间,我们之所以可以同时运动10支手指,那是因为现实空间有“并行”这一机制。
当我们使用10支手指巧妙地透过键盘敲出 apple 五个英文字,其中手指敲击键盘这一系列动作称为“过程”,输出apple五个英文字称为“结果”(目的)。 apple五个英文字之所以可以成功输出,其实人们的手指还有键盘在不知不觉间,已经同时发生协调了。反过来说,仿真无疑是为了测试模块是否有效输出结果,输出结果之前必须经过一系列过程,为了得到正确的结果,过程必须正确无误发生才行。
假设时序表现不了解,过程没有指向,步骤没有指向,甚至时钟没有指向,这种情况“协调”又何处谈起呢?因为一切尽是模糊不堪 ... 协调对于仿真之所以那么重要,因为时序是非常铭感的东西,各种信号一起协调运作才会得到我们预想所要的仿真结果,多一个时钟或者少一个信号也有可能照成仿真结果出现偏差。
如果时序要求允许容差的话,小程度的偏差的仿真结果不会影响整体大局。虽然这个章节笔者只有两个举例而已,不过不容置疑的是,两小例子已经表明协调的重要性。假设读者试想写入8个数据到ram当中,协调性良好的情况之下,发送方只要只要8个时钟或者8个步骤即可,然而 ram 需要9个时钟或者9个步骤即可,如代码3.6.3所示:
1. reg [3:0]rData,rAddr; 2. 3. always @ ( posedge Clock ) // 发送方,i过程 4. ...... 5. case(i) 6. 7. 0,1,2,3,4,5,6,7: 8. begin rAddr <= i; rData <= i; i <= i + 1'b1; end 9. 10. endcase 11. 12. reg [3:0] ram [3:0] 13. 14. always @ ( posedge Clock ) // 接收方,j过程(ram) 15. ...... 16. case( j ) 17. 18. 0: 19. j <= j + 1'b1; 20. 21. 1,2,3,4,5,6,7,8: 22. beging ram[ rAddr ] <= rData; j <= j + 1'b1; end 23. 24. endcase
代码3.6.3
代码3.6.3有两组过程,其中i过程是发送方,然而j过程ram则是接收方。发送方只有8个时钟,或者8个步骤向 ram发送8个地址数据rAddr,还有8个信息数据rData。换之,接收方使用9个时钟,或者9个步骤接收来之发送方的8个地址数据,还有8个信息数据,其中步骤0是用来等待沟通延迟,因为模块与模块之间沟通至少需要一个时钟;步骤1~8则是接收8组数据的操作。
图3.6.6 i过程发送8组数据,j过程接收8组数据的理想时序图。
理想的时序过程如图3.6.6所示,其中红色箭头称为启动沿,别名也是发送沿,主要是输出此刻(时间点)的未来值,绿色箭头称为锁存沿,别名也是读取沿,主要是用来读如此刻(时间点)的未来值。如图3.6.6所示,i过程在T0~T7分别发送8组数据,然后j过程则在T1~T8接收8组数据。在此我们可以断定,协调已经发生了,因为i过程是如此有效发送8组数据,然而j过程又是如此准确接收8组数据。
代码3.6.3或者说ram传输时序是比经典的例子,常常会有同学问道“为什么数据慢一拍”,等奇萌的问题,然而笔者却会回答道“这是因为时序不协调”。上述所诉,算是比较初级的问题,随着建模程度增加,信号的数量也会跟着增加,随之代码的清晰度越显重要,因为清晰的代码,时钟指向,步骤指向,过程等指向表示却是如此直接了当。再加上理想时序在背后作祟,信号之间的协调操作就会简单起来。
传统流派没有不仅没有理想时序的概念,也没有任何指向工具,所以传统流派自负的物理时序往往都是不协调的,光是看着笔者就想死。不协调的物理时序已近无数次伤害笔者那玻璃般脆弱的心灵 ... 痛苦,怨念,还有复仇等负面感情最终引发奇迹般的歪道思想。又一次,师兄召集所有初学师弟聚在一堂,然后吩咐所有人在一个时辰以内完成黑板所示的时序要求。
那是笔者人生第一次遵守歪道,写出自己专属的时序表达式 ... 瞬间笔者成为纵目注视的对象,师兄看着意义不明的描述代码,然后问道:“这是什么东西?”,笔者昂然的回答道:“ 这是协调的理想时序 ...“ 。此外,协调也可以看是必然性,因为协调还有必然性共同指向“什么东西在什么时候发生什么”。至于笔者为什么会怎样说,往后自然会知晓。
总结:
不知不觉间,这个小节也写完了 ... 这篇小节勾起笔者无数的回忆还有感情波动,笔者既是越写越激动,内容虽然属实不过也有几分夸张的成分。小节3是学习仿真最为重要的内容,因为笔者所用的仿真技巧都是基于这些。其中,最为核心当然是理想时序,理想时序是非常对齐又整洁的时序,相较物理时序它太美丽了。另一方面,理想时序也是强调语言的理想本质还有功能的理想状态。
虽然笔者选择理想时序的缘由是犯懒的关系,不过理想时序却为笔者开启仿真的另一道大门。理想时序有两种时序表现,亦即时间点事件还有即时事件 ... 前者,有分此刻时间点,此刻过去值,还有此刻未来值;后则则是无视时钟的犯规存在。不过而言,整体时序一般都是时间点事件发生而成,即时事件虽然只是部分的小插曲而已,单它(即时事件)偶尔偷一下时钟就心满意足了。
此外,理想时序还非常强调清晰度,亦即代码的清晰度,还有时序的清晰度,为了达到这个目的 ... 我们必须手动使用i指向时钟,步骤,还有过程。指向时钟虽然简单不过却是非常重要的基础,许多初玩仿真的朋友认识时钟,是非常模糊而且又陌生。它们不能有效控制时钟,最大的原因是时钟不清晰,时钟也没有标志,一句盖过就是没有指向时钟的工具。
i有能力指向时钟以后,随之i又可以指向步骤,还有指向过程。指向步骤也是仿顺序操的核心基础,其次也称为用法模板,传统的状态机太死板又不好控制,所以笔者使用步骤取代状态机,虽然本篇没有详细解释,不过指向步骤是非常容易明白的概念。如果一个步骤停留一个时钟,就称为单步骤(时钟)指向;如果一个步骤停留多个时钟,就称为多步骤(时钟)指向。
时钟产生步骤,随之步骤又会产生过程。简单而言,过程是一系列的步骤,Verilog相较顺序语言,指向过程是多向道,亦即无数过程同时运行。为什么我们要指向过程?原因有两个,其一就是增加仿真的排错能力,其二就是为了表述不该存在的虚拟硬件。虚拟硬件是脑海的产物,仿真期间我们不会刻意创建它,而是选择最简单的方式,借用过程的指向信号描述它们。虚拟硬件的标志是激励内容的反馈输出。
我们之所以用i指向这个,又指向那个,完全是为了协调在铺垫。协调的简单意义除了”准且的时候“或者”准确的动作“以外,还有”准确的结果“,因此代码和时序不仅不能不清晰,而且信号也不能失去指向标志。协调是建立在同步之上,同步是建立在并行之上,亦即协调至少需要两组过程同时活动才能显露痕迹。协调虽然是抽闲的概念,但它确实存在,仿真需要它,仿真依赖它,如果时序不协调,仿真结果也会出现偏差。