声明:本文为黑金动力社区(http://www.heijin.org)原创教程,如需转载请注明出处,谢谢!
黑金动力社区2013年原创教程连载计划:
http://www.cnblogs.com/alinx/p/3362790.html
《FPGA那些事儿—Modelsim仿真技巧》REV4.0 PDF下载地址:
http://www.heijin.org/forum.php?mod=viewthread&tid=22730&page=1&extra=#pid165612
第四章 激励文本就是仿真环境
4.1 验证语言是什么?
4.2 验证语言与时序表现①
4.3 验证语言的时序表现②
4.4激励文本的布局
4.5 仿真模型与反馈输出①
4.6 仿真模型与反馈输出②
总结
第四章 激励文本就是仿真环境
4.1 验证语言是什么?
一般描述语言分为两个部分,亦即“综合”还有“验证”,也是俗称的综合语言还有验证语言。为了方便,笔者时常把综合语言说为“建模语言”,然而在笔者的概念之中,验证语言的价值是可有可无,为什呢?验证语言虽然给人看似“很强”,但是许多关键字用处都不大。此外,验证语言也是初学Verilog 的负担,不管怎么说,笔者就是欢喜不起来 ...
传统流派时常说道:“建模用综合语言,仿真用验证语言”,其实这种说法是非常不负责任的,建模还有仿真之间的差异有如笔者曾说那样,前者是实际环境的建模,后者则是虚拟环境的建模,亦即实际建模还有虚拟建模。实际建模会受限与实际环境,虚拟建模则是相反的情况。由此可见,两者拥有大同的概念,不过环境却是小异。但是,又是什么原因将描述语言一分为二呢?
所谓综合,就是可以描述实际硬件的关键字,所谓不可综合就是无法描述实际硬件的关键字,首先让我们来了解一下 initial 这个关键字:
reg CLOCK, RESET; // 仿真 initial CLOCK = 0; initial RESET = 1; reg [3:0]Reg1 = 4’d0; // 建模
代码4.1.1
initial 的作用有如自己命名般——初始化资源。如代码4.1.1所示,仿真中,笔者先是声明两个寄存器 CLOCK 还有 RESET,然后再使用 initial 赋予它们初值。换之,建模中,笔者先是声明寄存器 Reg1 然后再直接使用赋值操作符 = 给 Reg1赋予初值 4’d0。
好奇的同学可能会问:“初始化还有复位化有什么区别?”,
真是一个好问题 ... 初始化是编译器的活动,或者编译器给予的值,也称为初值;然而复位化则是硬件的实际活动,或者说复位结果给予的值,也称为复位值。
1. module abc_simulation(); 2. ...... 3. reg Reg1; // 声明 Reg1 4. initial Reg1 = 0; // 初始化 Reg1为0 5. ...... 6. always @ ( ... negedge RESET ) 7. if( !RESET ) Reg1 <= 1’b1; // 复位化 Reg 为1 8. ...... 9. endmodule
代码4.1.2
如代码4.1.2所示,在一个名为 abc的仿真环境当中,笔者先是声明 Reg1然后在给予初值1’b0,紧接着给予 Reg1 复位值 1’b1。上述一系列的简单动作告诉我们一个事实,初始化是编译器活动,然而复位化本身是硬件活动,实际上不属于编译器的范畴。然而,仿真环境 abc 却模拟(在线)硬件的复位活动,似的Reg1给予复位值1。结果而言,Reg1从头到底一共赋值两次,其一是编译器给予的初值1’b0,其二是经由仿真模拟复位活动给予的复位值1’b1。
事实上,initial也使用与建模当中,不过如代码4.1.1所示,建模可以使用 = 赋值操作符省略 initial 这个关键字实现初始化。此外,默认下的编译器都会给予所有资源初值0。
再者,让我们瞧瞧 # 延迟操作 ... 验证语言有一个延迟操作符 # ,外表看上去是普通的井号,作用却是延迟仿真时间。
1. `timescale 1 ps/ 1 ps // 声明时钟刻度 2. module abc_simulation(); // 仿真环境 3. ...... 4. reg CLOCK; // 声明寄存器 5. initial begin 6. CLCOK = 0; // 初始化寄存器 7. forever #5 CLOCK= ~CLOCK; // 每隔5隔时钟刻度重复一次CLOCK取反动作 8. end 9. ...... 10. always @ ( posedge CLOCK ) // 每次CLOCK又低变高执行一下 11. Reg1 <= #5 4’b5; // Reg1赋值 4’b5之前延迟 5 个时钟刻度 12. ...... 13. endmodule
代码4.1.3
如代码4.1.3所示,笔者先是在仿真环境abc的第4行声明寄存器CLOCK,然后在第6行给予初值0。第7行的 forever是验证语言,它的作用虽然相似always,以及实时执行某种操作,但是两者却是不同的东西。
1. always @ ( 铭感区域 ) Reg1 <= ~Reg1; 2. always @ ( * ) Reg1 = ~Reg1; 3. always Reg1 <= ~Reg1; 4. forever Reg1<= ~Reg1;
代码4.1.4
如代码4.1.4所示(第1行),always 不仅有铭感区域,而且铭感区域也必须满足条件才能实现操作。虽然我们可以为 always 的铭感区域输入 * (第2行),好让第2行实现第4行的作用,可是事实却是相反。建模中,第2行的代码是用来声明组合逻辑,换做仿真也是如此。第3~4行是验证语法,作用是仿真时间每过一个刻度的就取反一下Reg1。第3~4行综合无法实现,因为综合没有“仿真时间”这种东西。
图4.1.1 forever 产生的理想时钟。
所以 代码4.1.3 的 forever #5 CLOCK = ~CLOCK; 表示,仿真时间没隔5隔刻度,就是取反一下 Reg1的内容;只要这个动作重复2次,我们就会产生一个宽度为10个时钟刻度50%占空比的理想时钟。换句话说,只要这个动作无限重复下去我们就会产生源源不断的理想时钟,结果如图4.1.1所示。
什么是时钟刻度呢?时钟刻度可以理解为最小的仿真时间,如代码4.1.3所示,第1行出现的`timescale 就是用作声明时钟刻度,然而 1ps / 1ps 就是时钟刻度的单位,语法如下所示:
`timescale 实数单位 / 小数单位 // 语法格式 `timescale 1ps / 1ps // 例子1 `timescale 1ns / 500ps // 例子2
例子1,实数单位为 ps级别,小数单位也是 ps级别,亦即一个时钟刻度拥有1ps的实际时间。例子2,实数单位为 ps级别,小数单位也是ps级别,亦即一个时钟刻度拥有 1ns 又 500ps 的实际时间。时钟刻度是仿真时间的最小单位,如果操作forever #5 CLOCK = ~CLOCK 一例子1执行20次,那么:
图4.1.2 时钟刻度为 1ps/1ps,20个时钟刻度,10个时钟。
20 * 5 * 1ps = 100ps,或者10个拥有 10ps的理想时钟,结果如图4.1.2所示。
如果操作forever #5 CLOCK = ~CLOCK 一例子2执行20次,那么:
图4.1.3 时钟刻度为 1ns/500ps,20个时钟刻度,10个时钟。
20 * 5 * 1ns 500ps = 150000ps 或者 150ns,又或者10个拥有 1.5ns的理想时钟,结果如图4.1.3所示。
当理想时钟产生成功,我们就有最基本的环境输入,接着我们就可以使用时钟沿(由低变高)触发always的铭感区域,亦即模拟建模的RTL级设计。如代码4.1.3的第10行所示,always @ ( posedge CLOCK ),然而第11行则表示,每一个CLOCK上升沿,Reg1都赋值 4’d5,期间赋值的前面是延迟操作符——#。
图4.1.4 代码4.1.3 仿真结果。
图4.1.4是代码4.1.3的仿真结果,其中光标C0~C1分别指向T0还有T0的半周期。如图4.1.4所示,先是时间点T0,以及第一个CLOCK的上升沿,always块触发成功,此刻Reg1也决定赋值 4’d5,不过意外的是 ... Reg1的赋值操作遭受 # 延迟操作符的阻碍。#5 表示延迟5个时钟刻度,根据代码4.1.3的刻度单位,5个时钟刻度亦即 5 * 1ps
,也就是5ps物理延迟,恰好是半个时钟周期。
结果如图4.1.4所示,Reg1原本应该在C0输出未来之星4’d5,很遗憾的是 ... 经过 # 阻碍以后,未来值4’d5出现在C1而不是C0。# 延迟操作符之所以认为验证语言,因为它可以实现物理延迟,然而 # 延迟操作符的使用也仅限于仿真环境(虚拟建模)而已。换之,# 延迟操作符在建模环境(实际建模)是绝对无法通过,为何呢?
其一,建模环境没有仿真时间这种概念。
其二,建模环境是用来创建理想模块,结果无法描述 5ps 这样小气的物理延迟。
看完整体代码4.1.3 我们可以这样结论道:
initial,forever,还有 # 都是验证语。initial实现资源初始化;forever每隔一个时钟刻度重复一次操作;# 根据时钟刻度,实现延迟。`timescale 声明时钟刻度,然而实际上 `timescale 不是充分的验证语言,它的身上流着一半预处理操作的血(详细情况请自行去浏览语法书) 。我们虽然也可以在建模环境使用 `timescale,不过会被综合器无视掉,真是遗憾 ...
在此,笔者借助上述几个验证语言的目的是为了告诉读者仿真还有建模之前的微妙关系。正如笔者强调那样,仿真充其量就是在虚拟环境下实现建模(虚拟建模),实际环境有实际时间,结果仿真环境也必须拥有仿真时间。仿真时间的刻度(仿真时间的最小单位)必须使用 `timescale 声明;实际环境有时钟源,然而仿真可以借助 forever 还有 # 关键字产生虚拟时钟。
最后还有一个重点就是 ... 仿真环境是否实现(模拟)物理参数,那是自由行的选择。如代码4.1.3所示, 如果执行 Reg1 <= #5 4’d5 操作,Reg1会经过半个周期的延迟才能输出未来值 4’d5;换之,如果执行 Reg1 <= 4’d5操作,Reg1不用经过延迟就能输出未来值 4’d5。
“仿真使用验证语言”——这句话虽然没有错,因为仿真确实需要使用验证语言来模拟实际的环境,但是仿真不完全等价验证语言 .... 嗯,该怎么说呢?根据笔者的理解,仿真就是在虚拟的环境下实现建模,而且我们必须经由一些手段来模拟实际环境,如实际时间,实际时钟信号,实际复位信号。为此,我们必须借助验证语言的力量,产生虚拟时间(仿真时间),虚拟时钟信号,虚拟复位信号等。
仿真绝对不是传统流派所言那样,建模(综合)和仿真完全被分割开来,成为两种不同的平台。笔者对此无比愤怒,因为这番不负责任的强言强语,已经害死无数的初学者,笔者自然也是其中一人。为此,读者需要确切明白,建模还有仿真本是同根,差别只有概念(环境)而已。以此类推,建模所用的一切方法完全可以照搬到仿真去,这样做不仅可以大大减少学习的劳动,此外我们还可以更加有力掌握建模。
不过非常遗憾的是,权威还有主流不像笔者如此看待仿真,它们始终认为建模还有仿真应该分开。因为如此,描述语言就有综合还有验证之分,举例而言:
always #5 CLOCK = ~CLOCK; forever #5 CLOCK = ~CLOCK;
上面有两组产生虚拟时钟的方法, always 常被认为它是属于综合语言,然而 forever 不管怎么看都是验证语言的东东。always关键字可以产生虚拟时钟,forever关键字也可以产生虚拟时钟,但是参考书不推荐使用 always 产生虚拟时钟 ... 原因也非常荒唐,因为always 有太多综合的味道,不怎么适合仿真。因此,forever就成为验证版本的 always ...
以此类推 ... 实现同样作用的关键字,就有综合版本还有验证版本。所以说,学习验证语言实际上是重复学习综合语言一番 ... 此外,好死不死,传统流派却是倾向“调试风格”仿真手段。笔者也曾经说过,调试是顺序语言的测试手段,为求单向结果。就这样,许多初学者容易产生混乱 ... 它们的脑子好不容易在建模阶段中习惯并行思路,可是
“调试风格”的仿真手段,脑子必须180度切换至顺序模式 ... 结果,那些来不及切换脑子的同学,脑子就会当机,导致瓶颈。
为了证明笔者不是胡说八道,笔者再举个例子:
fork ... join 语法书说是并行块, begin ... end 则是顺序块,不过这种认知也仅使用在于保守的仿真概念而已。为了吐糟权威主义还有传统流派那些有腐旧有局限的仿真概念,笔者简单举例2个不同风格的代码,但是结果都是一样,然后笔者会逐个数落它们。
1. `timescale 1ps/1ps
2. ......
3. reg CLOCK;
4. ......
5. initial CLOCK = 0;
6. forever #5 CLOCK = ~CLOCK;
7. ......
8. begin
9. #00 Reg1 = 4’d0; // 等价 Reg1 = 4’d0;
10. #10 Reg1 = 4’d1;
11. #10 Reg1 = 4’d2;
12. #10 Reg1 = 4’d3;
13. end
代码4.15
代码 4.1.5 声明1ps/1ps的时钟刻度,也使用 forever产生虚拟时钟,亦即一个时钟需要10个时钟刻度,周期为10ps。如代码4.1.5所示,它使用 begin ... end 块为 Reg1 赋值,根据 begin .. end的作用,Reg1先在0个时钟刻度的时候赋值 4’d0;经过10个时钟刻度以后赋值 4’d1;再及经过10个时钟刻度以后赋值 4’d2;再再经过10个时钟刻度以后赋值4’d3。
在此,每10个时钟刻度代表一个时钟,因此 Reg1的赋值操作可以这么认为 ... 第一时钟,Reg1赋值 4’d0;第二个时钟,Reg1 赋值4’d1;第三个时钟,Reg1赋值4’d2;第四个时钟 Reg1赋值 4’d3。
图4.1.5 代码4.1.5产生的时序。
图4.1.5是代码4.1.5产生的时序,仿真时间0ps的时候 Reg1输出 4’b0;仿真时间10ps的时候,Reg1输出4’b1;仿真时间 20ps的时候,Reg1输出 4’b2;仿真时间30ps的时候,Reg1输出 4’b3。在此CLOCK只是一个转为摆设而用的花瓶而已,除了美观以外,有没有它 Reg1还是会照样按照仿真时间输出结果。换句话说,代码4.1.5的CLOCK信号,还有Reg1是保持独立的关系。真是可怜的时钟信号 ...
1. `timescale 1ps/1ps 2. ...... 3. reg CLOCK; 4. ...... 5. initial CLOCK = 0; 6. forever #5 CLOCK = ~CLOCK; 7. ...... 8. fork 9. #00 Reg1 = 4’d0; 10. #10 Reg1 = 4’d1; 11. #20 Reg1 = 4’d2; 12. #30 Reg1 = 4’d3; 13. join
代码4.1.6
代码4.1.6使用 fork ... join 块取代 begin ... end 块。代码4.1.6声明 1ps/1ps的时钟刻度之余,它也在第6行使用 forever 产生虚拟时钟,一个时钟为10个时钟刻度,周期时间也为10ps。代码4.1.6使用 fork ... join 为 Reg1执行赋值,然而fork ... join 相比 begin ... end,它不是顺序作用,而是并行作用。如代码4.1.6所示, Reg1 是并行赋值 4’d0, 4’d1,4’d2,还有 4’d3。
不过,此刻 # 延迟操作符的作用好比条件判断般,当时钟刻度为0的时候 Reg1 就赋值 4’d0;当时钟刻度为10的时候,Reg1就赋值 4’d1;当时钟刻度为20的时候,Reg1就赋值4’d2;当时钟刻度为30的时候,Reg1就赋值 4’d3。笔者再强调一下,fork ... join造就 Reg1 同时执行4种结果的赋值,不过时钟刻度作为开关条件。用建模来表示的话,代码4.1.6会是这种感觉:
if( timescale == 0 ) Reg1 = 4’d0; else if( timescale == 10 ) Reg1 = 4’d1; else if( timescale == 20 ) Reg1 = 4’d2; else if( timescale == 30 ) Reg1 = 4’d3;
图4.1.6 代码4.1.6产生的时序。
图4.1.6是代码4.1.6会产生的时序, 图4.1.6和图4.1.5虽为相同结果, 但是两者的 Reg1输出却是不同的操作行为。图4.1.5会根据仿真时间接续(顺序)输出结果,图4.1.6则将仿真时间作为结果的输出条件。此刻时钟信号也有同样的囧境,因为它只是摆设的花瓶而已,除了美观意外就没有其他作用。时钟信号它真是太可怜了 ...
解释完毕代码 4.1.5还有 代码4.1.6以后,笔者接着要开始数落它们了。笔者之所以看它们不顺眼,其一它们既然冷落如此重要的时钟信号,其二仿真手段太接近“调试风格”了。根据代码 4.1.5还有代码 4.1.6不管它们使用 begin ... end 还是 fork ... join 为 Reg1赋值,由始至终的Reg1赋值操作始终让人觉得好似c语言那样:
reg1 = 0; delayps( 10 ); reg1 = 1; delayps( 10 ); reg1 = 2; delayps( 10 ); reg1 = 3;
根据 RTL级设计的基础,我们知道寄存器如果没有时钟源触发是不会发生操作,可见代码4.1.5还有代码4.1.6已经完全违背这个重点。对此,笔者实在无法接受 ...
1. `timescale 1ps/1ps
2. ......
3. reg CLOCK;
4. ......
5. initial CLOCK = 0;
6. forever #5 CLOCK = ~CLOCK;
7. ......
8. always @ ( posedge CLOCK )
9. case(i)
10. 0: begin Reg1 <= 4’d0; i <= i + 1’b1; end
11. 1: begin Reg1 <= 4’d1; i <= i + 1’b1; end
12. 2:begin Reg1 <= 4’d2; i <= i + 1’b1; end
13. 3:begin Reg1 <= 4’d3; i <= i + 1’b1; end
14. .......
15. endcase
代码4.1.7
代码4.1.7是大伙熟悉的仿顺序操作的用法模板,i指向时钟之余,i又指向步骤,当然这些都不是重点。如代码4.1.7所示,在T0的时候(步骤0),Reg1赋值4d’0;在T1的时候(步骤1),Reg1赋值4’d1;在T2的时候(步骤2),Reg1赋值4’d2;在T3的时候(步骤3),Reg1赋值4’d3。
图4.1.7 代码4.1.7产生的时序。
图4.1.7是代码4.1.7产生的时序结果,虽然代码4.1.7与代码4.1.6还有代码4.1.5都有同样的时序结果,但是代码4.1.7 相交之下,它有决定性的不同 ... 亦即,代码4.1.7不是根据仿真时间为Reg1执行赋值,而是根据时钟信号的上升沿为Reg1赋值。如图4.1.7所示,在T0的时候,Reg1决定赋值4’b0,结果Reg1输出未来值4’b0;在T1的时候,Reg1决定赋值4’b1,结果Reg1输出未来值 4’b1;在T2的时候,Reg1决定赋值4’b2,结果Reg1输出未来值4’b2;在T3的时候,Reg1决定赋值4’b3,结果Reg1输出未来值4’b3。
代码4.1.5~7之间的区别如表4.1.1所示:
表示4.1.1 代码4.1.5~7 之间的区别。
代码 \ 细节 |
CLOCK信号 |
Reg1赋值控制 |
时序表现 |
时钟指向 |
代码4.1.5 |
花瓶 |
仿真时间 |
没有 |
没有 |
代码4.1.6 |
花瓶 |
仿真时间 |
没有 |
没有 |
代码4.1.7 |
不是花瓶 |
CLOCK信号 |
有 |
有 |
如表4.1.1所示,代码4.1.5与代码4.1.6因为使用仿真时间控制Reg1的赋值操作,结果
CLOCK信号沦落为花瓶。问题不仅而已,代码4.1.5还有代码4.1.6虽有时序图,但是时序图却没有时序表现,此外代码页没有指向时钟,这些问题的源头是以为它们忽略CLOCK信号的关系。
相反的,代码4.1.7不在视CLOCK信号为花瓶,而且还充分使用它控制Reg1的赋值操作,结果代码4.1.7所产生的时序图也包含时序表现。然而,更为重要的是 ... 代码4.1.7有用指向工具指向时钟。
代码4.1.5~7经过一轮的搏斗以后,代码4.1.5~6虽然外表上给人简洁的假象,可是暗地里它是破烂不堪的。换之,代码4.1.7给人感觉它比前两者书写较多代码,实际上这是支撑整体的结果。笔者曾经说过结构的重要性,传统流派为了贪图早期的便利,于是无视结构的重要性,最终为后期带来许多麻烦。
但是代码4.1.5~6最为糟糕的地方是“调试风格”的仿真习惯,这是最危险也是最可怕的祸种。这种破坏性十足的危险习惯,会为精神产生无数令人绝望的切糕。因为它已经偏离RTL级设计的基础,而且代码整体也非常松散,好似摇摇欲坠的高塔,代码的数量越是增加,危险性越是提升。但是笔者所担心的问题是思维上的疲劳,初学者在建模阶段要适应并行模式已经是非常吃力了,如果“调试风格”的仿真习惯导致思维来不起切换,又或者切换过度 ... 最终降低学习的效率。
当然,笔者之所以举例代码4.1.5~7是为了揭露权威主义还有传统流派那种破烂不堪的仿真习惯之余。此外,笔者还要强调,传统流派不过是将验证语言作为无意义的重复性学习而已。它们保守既局限的仿真思想,实际上已经扭曲验证语言原本的面膜,还有仿真实际的意义。它们这样作,一切仅仅是为了稳固自己的地位而已,然而却要牺牲千千万万的我们,实在可恶,实在可悲,实在可恨 ....
笔者既然已经插代码4.1.5~6已经那么多刀了,笔者也无妨多插一刀。用仿真时间控制赋值操作仅仅适用几个时钟数量的操作而已。如果操作消耗N十个时钟,然后又有N个资源需要赋值,代码4.1.5~6就会立即自爆 ... 因为代码4.1.5~6另外一个弱点就是没有结构支撑,所以代码4.1.5~6是不适合稍微操作复杂一点,或者信号数量稍微所一点的仿真。
最后让我们来总结一下这个章节的重点:
根据笔者的观念,建模还有仿真之间其实只有“概念”或者“环境”的差别而已,亦即前者为实际建模,放着为虚拟建模。验证语言真正的意义就是模拟实际环境,例如模拟实际时间(仿真时间),产生虚拟时钟信号,或者虚拟复位信号等。仿真不是使用验证语言,而是仿真需要验证语言。但是传统流派却扭曲这个事实。
传统流派还有权威主义为了面子,为了地位 ... 它们死硬将“综合语言”还有“验证语言”分为两个不同平台的东西。因此,常规上,传统流派的仿真方法,实际是重复综合语言的学习而已。话虽如此,这种倾向“调试风格”的仿真习惯,不仅是违背RTL级设计的初衷,而且还180度扭曲仿真真实的意义。
代码4.1.5~7之间的比较就是最好的例子,代码4.1.7不仅可以实现代码4.1.5~6的时序结果,而且代码4.1.7的风格同时也适用于“建模(实际建模)”还有“仿真(虚拟建模)”之间,可谓是一箭双雕。笔者之所以故意举例代码4.1.5~7是为了表示,学习验证语言最为严重的误解意外,笔者还要表达初学者最容易掉入的仿真瓶颈。
传统流派是“调试风格”的仿真习惯,简单说就是一种顺序思维。换之,笔者强调的是仿真应该使用并行思维。笔者这样做是为了“建模(实际建模)”还有“仿真(虚拟建模)”共享同样的思维模式,好让初学者避免因思维切换而引起的脑袋短路问题。脑袋短路时其次的问题,天生节能主义的笔者,当然不愿因浪费精力学习重复性,而且又毫无意义的仿真。
总结完后,好奇的同学可能会问道“笔者,验证语言有很多意义不明的关键字?”,这位同学问得没错,事实却是如此。如果同学翻看官方的语法书,读者一定会看到许多陌生的验证语言,而且数量还非常可观。在这庞大的杨振语言里,其实函数性质的验证语言占据多数,例如常见的 $readmemb(); 还有 $display();。
好奇的读者可能有会问道:“笔者,我们是不是学习全部呢?”,作为初学笔者不建议 .... 事实上,只要掌握其中几个就够用了。正如笔者前面所述那样,只有传统流派才会重复无意义的学习而已,事实上“建模”也好,“仿真”也好,我们只要换个思路即可。其实两者拥有许多共同的地方。好奇的读者又可能会不耐烦的问到:“可是,我家的叫兽却说 ... ”,不要再可是了!详情会在后面继续 .... 烦死了!
4.2 验证语言与时序表现①
某天上午,笔者正在灼热又毒辣的天空下四处游走,支撑不了的笔者被逼躲到树荫下乘凉,忽然耳边有人叫道。
“小哥,天气那么炎热 ... 要不要来杯凉爽的豆浆呢?”,豆浆小贩道。
“嗯嗯,正好口渴 ... 老板来杯特凉特大号的!”,笔者宛如发现绿洲般兴奋道。
“小哥,久等了 ... 这是为小哥特别准备的豆浆!”,豆浆小贩答道。
笔者二话不说,立即将豆浆往口里送去 ... 咕噜咕噜(喝水声),怎么越喝味道越觉得奇怪,豆浆既然没有豆浆香,而且甜度未免太过了吧 ... 这真是在豆浆吗,还是糖水?笔者不禁怀疑道。
“老板,怎么豆浆完全没有豆浆的味道?”,笔者向豆浆小贩理论道。
“小哥,别开玩笑了 ... 小弟的豆浆无论外表怎么看都是普通豆浆呀。”,豆浆小贩答道。
“老板,你才是别开玩笑了,你自己尝尝看,这是豆浆还是糖水?”,笔者强硬道。
忽然,豆浆小贩眯着眼睛,用手将笔者拽到里边,然后用虚无的口调说道。
“小哥,小弟是卖豆浆没错,只是比例有点不同而已 ... ”,豆浆小贩道。
“比例,怎么比例法?”,好奇心忽然驱使笔者反问道。
“5%豆浆,45%糖,50%水 ... “,豆浆小贩低声道。
“纳尼!老板,这种比例已经不是普通豆浆了。”,笔者惊讶道。
“嘻嘻,小哥不说,小弟也不说,有谁又会知道呢?”,豆浆小贩诡异笑道。
笔者一心想要继续理论,可是忽然出现夏天不该出现的阴风,豆浆小贩也随之消失在眼前。此刻,笔者真的吓着了 ... 刚才那是什么?难道是天气太热导致自己看见幻觉吗?不过,嘴巴里那股不是豆浆又似豆浆的味道还残留着, 难道!? 疙瘩忽然爬满全身,笔者一刻也不想久留此地,哪怕外边的环境依然是毒辣的酷热。
笔者踩着熟悉的回家路,眺望熟悉的建筑物,不过却埋头思考方才遇见的情况。话说,这种经历又不是第一次,笔者用不着大惊小怪,然而让笔者苦恼的是它们出现的原因。
“小弟的豆浆无论外表怎么看都是普通豆浆呀”,还有“小哥不说,小弟也不说,有谁又会知道呢?”,不知为何 ... 豆浆小贩所言的两句话却一直漂浮在脑海中。想着想着
,嘴巴随之吐出一句领悟的“原来如此”!
读者千万别误会,上述内容并不是说灵异故事,这个小节我们还在继续谈论验证语言,但是为了营造学习气氛,笔者才借用灵异故事作为比喻。故事说道,如果豆浆没有豆浆,不管外表再怎么像极豆浆,豆浆也不是豆浆。如果不是笔者敏感,又或者豆浆小贩如果没有实话实说,笔者真的不知道那杯豆浆既然不是豆浆!
这个故事告诉我们一个非常重要的信息,如果将豆浆反应在时序的身上,我们可以这样说道:如果时序没有时序表现,不管外表再怎么像极时序,时序也不是时序。不过不同的是,时序没有豆浆小贩告诉我们这个豆浆(时序)不是豆浆(时序),除非我们细心观察。
好奇的同学可能会问道:“时序不是用来看嘛?怎么时序还在乎有没有时序表现?”,这位同学的好奇绝对有理。正如笔者所言,不是豆浆的豆浆,实际上还有豆浆的外表,喝起来也有豆浆的成分,不过却没有豆浆的味道。时序表现可以比拟是豆浆的味道,正宗的豆浆换做理想时序一定会有浓浓的豆香,然而豆香(时序变现)可以是,启动沿,锁存沿,时间点事件,即时事件,寄存器沟通延迟等。
反之,没有豆香的时序,外表看似时序,不过里边却没有所谓的启动沿,锁存沿,或者其他什么的。事实上,传统流派的仿真习惯可以比喻为不是豆浆的豆浆,没有时序表现的时序。不过,为何时序表现对于时序而言那么重要呢?其实这是见仁见智的问题,但是对于笔者来说追求至高无上的原汁原味是人类的天性。话说,除了傻子就没有人会喜欢不是豆浆的豆浆。
正如比例只有5%的豆浆已经不再是豆浆那样,不是时序的时序在某种程度上已经偏离时序的本质了。那种豆浆有喝等于没喝,或者喝了是否又会拉肚子呢?仿真虽说是模拟实际环境执行虚拟建模,不管环境是什么,时序应有的时序表现我们还是应该保留下来,不然时序就会失去意义。
一些无良的奸商会使用豆浆精粉充当豆浆的原料,豆浆精粉是可以实现豆浆味道的化学味素,我们知道化学物品吃多一定会对身体造成伤害,最坏还会致癌。同样的道理,使用太多没有时序表现的时序,最终也会扭曲我们对时序的认识。根据笔者的学习经验,时序可以分为两种,以及“理想时序”,还有“物理时序”。理想时序是时序原有的理想状态,物理时序则是时序发生在物理下的状态。不管时序理想或者物理与否,时序表现之间的差别仅是大同小异的。
话了那么多,笔者还是举例例子比较容易搞明白:
1. module adder( input [3:0]D1,D2, output [3:0]Result);
2. reg [3:0]rData;
3. always @ ( * ) rData = D1 + D2; // 组合逻辑声明,加法器。
4. assign Result = rData;
5. endmodule
代码4.2.1
笔者先是建立一个加法器,内如如代码4.2.1所示。该加法器有两组输入——D1和D2,还有一组输出——Result(第1行);第2行声明一个暂存用的寄存器 rData;第3行声明一个组合逻辑,rData则是寄存 D1与D2的综合;第4行用 rData驱动 Result。这是一个单纯的建模,不过根据理想时序还有建模技巧而言,其一该加法器没有时钟供源,所以它是不择不扣的组合逻辑级设计。其二,第3行的rData用 = 操作符赋值,表示adder输出即时结果,话说引发即时事件。
图4.2.1 代码4.2.1的adder加法器。
如代码4.2.1所示,虽说我们在2行声明,不过这是 Verilog 的描述表现而已,因为加法器却没有使用时钟,所以综合结果用不着使用寄存器,而是使用逻辑门资源就能简单组合而成,结果如图4.2.1所示。再者假设该加法器是理想状态。
1. `timescale 1ps/1ps 2. module adder_simulation(); 3. ...... 4. initial 5. begin 6. #00 D1 = 4’d1; D2 = 4’d1; 7. #10 D1 = 4’d2; D2 = 4’d2; 8. #10 D1 = 4’d3; D2 = 4’d3; 9. #10 D1 = 4’d4; D2 = 4’d4; 10. end
代码4.2.2
代码4.2.2 是传统流派的仿真风格,第1行声明时钟刻度的单位为 1ps/1ps,接着在第6~9分别每隔10ps经过就为D1与D2赋予不同的结果。
图4.2.2 代码4.2.2驱动代码4.2.1加法器的时序。
图4.2.2是代码4.2.2驱动该加法器所产生的时序图,但是图4.2.2并不存在CLOCK信号,换之则有仿真时间。根据代码4.2.2的驱动结果,在0ps的时候D1与D2分别输入值4’d1,由于我们假设加法器是理想状态,所以所有物理延迟都是0,随之Result输出值4’d2也在0ps当中;10ps的时候,D1与D2分别输入4’d2,结果Result输出4’d4;20ps的时候,D1与D2分别输入4’d3,结果Result输出4’d6;30ps的时候,D1与D2分别输入4’d4,结果Result输出4’d8。
代码4.2.1的加法器,还有代码4.2.2仿真的方式是无伤大雅的组合。因为组合逻辑本来就没有时钟的概念,再加上理想状态的组合逻辑,物理延迟都是0时间。结果而言,图4.2.1有没有时序表现也没有关系,因为实际情况也会产生类似的时序图,所以大体看上去,这个豆浆姑且还是豆浆。不过,事实上果真也是如此吗?
1. module adder( input CLOCK,input [3:0]D1,D2, output [3:0]Result);
2. reg [3:0]rData;
3. always @ ( posedge CLOCK ) rData <= D1 + D2; // 加法模块。
4. assign Result = rData;
5. endmodule
代码4.2.3
笔者稍微将代码4.2.1的加法器改动一下,然后在第1行为他添加时钟信号之余,第3行rData的赋值方式也稍微修改一下。
图4.2.3 代码4.2.3的adder模块。
图4.2.3是代码4.2.3生成的adder模块,由于该模块配载了CLOCK信号,所以综合器必须使用寄存器用暂存还有输出驱动,此刻代码4.2.3的第2行 rData会有真正的形体了,不像代码4.2.1那样,rData只是单纯的描述表现而已。图4.2.3还显示,模块里边有一个相似异或的符号是加法器,它的真实身份却是代码4.2.1,不过在此它变成小弟而已。根据代码4.2.3表示,D1与D1会先经过那个像足异或符号的加法器,然后再经由时钟沿江加法结果打入寄存器当中。最后再由rData的输出Q驱动Result输出端。
1. `timescale 1ps/1ps 2. module adder_simulation(); 3. ...... 4. reg CLOCK; 5. intial CLOCK = 0; 6. forever #5 CLOCK = ~CLOCK; 7. ...... 8. initial 9. begin 10. #00 D1 = 4’d1; D2 = 4’d1; 11. #10 D1 = 4’d2; D2 = 4’d2; 12. #10 D1 = 4’d3; D2 = 4’d3; 13. #10 D1 = 4’d4; D2 = 4’d4; 14. end
代码4.2.4
代码4.2.4是根据代码4.2.4修改以后的结果,虽然我们在第6行产生时钟,不过第10~13行还是老样子,我行我素般无视时钟,仅仅借用仿真信号驱动。
图4.2.4 代码4.2.3用代码4.2.4驱动产生的时序图。
图4.2.4是代码4.2.3的加法模块经由代码4.2.4驱动以后所产生的时序图。如图4.2.4所示,D1与D2输出时根据仿真时间,然而Result的输出却根据时钟沿。传统流派比较倾向使用仿真时间,它们认为认为时钟是花瓶,所以时钟对于它们来说非常抽象又陌生。结果,它们一定会认为这幅理想时序图那里肯定有问题,因为它们无法理解,如果时钟沿不处于数据D1与D2的正中央,数据又如何打入寄存器当中?这时候,豆浆不是豆浆的问题发生了。
仿真时间就好比豆浆的水成分,仿真时间使用越多,水成分的占据比例就会越多,豆浆的味道因此也会随之变淡,渐渐豆浆再也不是豆浆,时序也会失去原本“味道”。理解理想时序的朋友会非常清楚,实际上图4.2.4是没有问题,但是时钟的概念却是非常模糊,因为没有使用时钟也没有指向时钟的关系。
再加上D1与D2又是根据仿真时间驱动,因而我们根本无法知晓D1与D2是触发“时间点”事件,还是“即时事件”?
1. `timescale 1ps/1ps 2. module adder_simulation(); 3. ...... 4. reg CLOCK; 5. intial CLOCK = 0; 6. forever #5 CLOCK = ~CLOCK; 7. ...... 8. initial 9. begin 10. #5 D1 = 4’d1; D2 = 4’d1; 11. #15 D1 = 4’d2; D2 = 4’d2; 12. #15 D1 = 4’d3; D2 = 4’d3; 13. #15 D1 = 4’d4; D2 = 4’d4; 14. end
代码4.2.5
修改代码4.2.4成为代码4.2.5是一种非常赔了夫人又折兵的补救手段 ... 如代码4.2.5所示,笔者针对第10~13行的延迟参数做出一番修改,以及D1与D2的输入之前多了5ps延迟。
图4.2.5 代码4.2.5驱动4.2.3所产生的时序。
图4.2.5是代码5.2.5驱动代码4.2.3模块所产生的时序图。如图4.2.5所示,D1与D2的输入相较图4.2.4,稍微延迟5ps(半个时钟周期)。根据常规理解,时钟沿因为处于数据D1与D2的正中央所以,数据才会有效打入寄存器。但是,代码4.2.5这种弄巧成拙的补救手段只能勉强挤出几滴时序表现而已,而且还把美丽的时序弄成丑陋的物理时序,因为信号已经失去对齐性。
Verilog是理想本质,输出的信号理应理想,所以Verilog是没有能力描述类似 D1与D2这种物理延迟。坏心眼的同学可能会反驳道:“ 图4.2.5中,D1与D2不过只是延迟半周期而已嘛,如果使用时钟的下降沿触发不就行了嘛,笨蛋笔者! ”,喷喷 ... 这位同学想用就用吧,乱葬岗那里处很快就会发现你的尸体。原因很简单,如果D1与D2不是延迟半周期的话,你又该怎么办呢?
1. `timescale 1ps/1ps
2. module adder_simulation();
3. ......
4. reg CLOCK;
5. intial CLOCK = 0;
6. forever #5 CLOCK = ~CLOCK;
7. ......
8. reg [3:0]i,D1,D2;
9. always @ ( posedge CLOCK )
10. case( i )
11. 0:
12. begin D1 <= 4’d1; D2 <= 4’d1; i <= i + 1’b1; end
13. 1:
14. begin D1 <= 4’d2; D2 <= 4’d2; i <= i + 1’b1; end
15. 2:
16. begin D1 <= 4’d3; D2 <= 4’d3; i <= i + 1’b1; end
17. 3:
18. begin D1 <= 4’d4; D2 <= 4’d4; i <= i + 1’b1; end
19. ......
20. endcase
代码4.2.6
为此,笔者进一步为代码4.2.5美化成为代码4.2.6。如代码4.2.6所示,不仅使用用法模板好让D1与D2的赋值操作是根据时钟变化而不是根据仿真时间。此外,笔者也使用i指向步骤还有时钟,而且D1也D2也有明确的时序表现,亦即D1与D2都会发生时间点事件。
图4.2.6 代码4.2.6驱动代码4.2.3所产生的理想时序。
图4.2.6是代码4.2.6驱动代码4.2.3的模块所产生的理想时序。如图4.2.6所示,笔者之所以故意画出两行时钟信号时为了强调,D1与D2不仅是由时钟控制,而且代码4.2.3所描述的加法器也是由时钟控制。此外,笔者也故意标示红色箭头表示启动沿,绿色箭头表示锁存沿。D1与D2根据时钟信号的启动沿输出未来值,加法器根据时钟信号的锁存沿读取D1与D2的未来值,然后输出相关的未来值。
读者是否可以感觉得到 ... 代码4.2.6还有图4.2.6除了使用仿真时间产生模式时钟信号以外,所有时序活动都是使用时钟控制,要时钟沿(启动沿和锁存沿)有时钟沿,要时间点事件有时间点事件,要信号对齐信号就对齐 ... 整体充溢满满的时序香,啊~实在美丽极了 ... 这才是笔者梦寐以求,浓浓时序香的时序图,浓浓豆浆香的豆浆,咕噜噜 ... (饮水声),哈 ...(豪爽的吐气声),实在美味极了!再来一杯!咳咳咳 ... 废话说太多了,结果给豆浆呛到了,真是及时报。
这个章节,笔者想传达的信息组要有两个 ... 其一,就是豆浆不是豆浆,时序不是时序的细节问题。时序表现好比豆浆的豆香,时序表现越丰富豆香就越浓,时序更有时序的味道。豆浆浓不浓,时序香不香其实这是非常主观的想法。举例而言,许多著名的演奏家未演奏之前,他们都会把自己关在安静中酝酿音乐情绪。此刻,如果读者不小心插身而过,读者会感觉到浓浓的特殊氛围,那就是音乐香。
再举个例子,艺术家未作画之前都会死死盯着白板,有人说他们是正在将想象具体化,又有人说他们在培养创作的感觉。此刻,如果我们插身而过,我们会感觉独特的艺术气场,艺术家称为艺术香。作为外人,我们是很难理解他们的怪异举止,然而这些怪人却相信,只要“香气”的浓度越高表现就会越好,事实是否如此只有当局者知道 ...
仿真的时候,如何培养或者酝酿仿真情怀,笔者认为那也是仿真的课题之一。因为仿真时基于时序,而且时序表现就是时序的“香气”,笔者始终感觉,如果时序表现越是强烈,笔者的仿真欲就会越高,仿真也会越做越起劲。笔者无法理解,那种感觉究竟是不是错觉,香气越弄,时序越有真实感,这里意指的真实感不是破烂不堪的物理时序,而是时序原有的究级理想面貌。笔者一直有一股强烈的冲动想要抓住它,每当触手可及的时候,它就会立即消失不见,原来仿真已在不知不觉的情况下完成了 ....
读者很可能会认为笔者是怪咖,笔者不否认,因为笔者与他们(演奏家或者艺术家)都有相似的本质,亦即追求美丽的原始冲动。笔者一直以来都强调着,心境还有心情是非常重要的 ... 好心情就有好表现,反之亦然。传统流派造就的时序太丑陋了,看着心情也会掉到谷底 ... 更别谈仿真了。
除了上述非常私人的原因以外,笔者也想透过这篇章节像读者表达 ... 传统流派作仿真为何会基于物理时序?原因正如前面所言,传统流派的仿真习惯非常倾向“调试风格”,后果造就它们过度依旧仿真时间,当时间长了时序就会开始暴走不受控制。为此,它们需要采取补救手段,结果时序就那样变成破烂不堪。
如果有一杯自高无上的香浓豆浆,还有一杯灌水灌过度的豆浆 ... 请问读者会怎样选择呢?想必没有人会喜欢不是豆浆的豆浆,同样也没有人会喜欢不是时序的时序。早期,那是传统流派横行的年代,笔者因为没有选择,因此笔者才会强迫自己喝下不是豆浆的豆浆。坏东西喝多了胃壁也会穿洞,因此笔者告诫自己要好好珍惜生命。
此外,笔者也想告诉读者 ... 类似传统流派这种仿真手段,已经停滞N年了,说得难听一点,那是适合门级仿真的仿真手段。笔者当然不是随意口吐妄言,读者随意翻开基本参考书就能验证这点,然而这种仿真手段却不适合门级以外的仿真。期间会出现,问题能容会臃肿,内容凌乱等问题。笔者曾经遇见有5级嵌套if的激励内容,说实话,笔者的蛋蛋立即就掉在地上然后昏死过去 ...
笔者最后还是再强调一下,味道还有喜好本来就是因人而异,有人认为传统流派是绝对正统;也有人会认为新时代就要新表现;所以不是豆浆的豆浆是否还是豆浆,都是见仁见智的问题。
4.3 验证语言的时序表现②
验证语言拥有无数个关键字,一般都是系统函数占据多。根据官方手册,系统函数开头都有 $ 美金的符号,紧接着会有函数名还有参数选项 ... 常见的系统函数如表4.3.1所示:
表4.3.1
系统函数 |
用途 |
$display(); |
打印信息在信息显示界面 |
看着看着,好奇的同学可能会问道:“笔者,系统函数难道只有1个吗?”。是呀!根据笔者的习惯,常用的系统函数的确只有1个而已。好奇的同学可能又会问:“笔者,别开玩笑了!”,笔者没有开玩笑 ... 官方手册的确记录许多系统函数,不过像夏老师那样努力的人勉强也能举例几个。换之,像笔者这种懒人自然也只有这种程度而已。
$display()函数与C语言的 printf() 函数非常相识,不过$display() 函数稍微细腻一些,如,$display() 函数常用的格式还有换码如表4.3.2所示:
表4.3.2
格式\换码 |
用途 |
%h |
输出十六进制 |
%d |
输出十进制 |
%b |
输出二进制 |
%c |
输出字符 |
%f |
输出浮点数 |
\n |
换行 |
有C语言功底的同学当然对表4.3.2不会感觉陌生,除了 %b以外。Verilog语言相比C语言,前者拥有更强的为操作能力,对此 $display() 函数也凸出这点。$display() 函数的常见用法如代码4.3.1所示:
1. always @ ( posedge CLOCK ) 2. case( i ) 3. 0: 4. begin reg1 <= 4’d10; i <= i + 1’b1; end 5. 1: // 打印二进制格式 6. begin $display( “reg1 = 4’b%b ”, reg1); i <= i + 1’b1; end 7. 2: // 打印十六进制格式 8. begin $display( “reg1 = 4’h%h ”, reg1); i <= i + 1’b1; end 9. 3: // 打印十进制格式 10. begin $display( “reg1 = 4’d%d ”, reg1); i <= i + 1’b1; end 11. endcase
代码4.3.1
根据代码4.3.1所示,笔者现在步骤0为reg1赋值4’d10,然后在步骤1~3相序打印不同的输出格式,打印结果如下所示。
reg1 = 4’b1010 reg1 = 4’ha reg1 = 4’d10
教授使用 $display() 函数不是笔者真正的目的,在此读者需要仔细思考 ... 一些系统函数如 $display()函数等,它们就是怎么样的存在?然而,它们又会拥有怎样的时序表现呢?系统函数实际上是一种犯规,或者称为异存在的存有,形象点好比现实世界的超人或者孙悟空般,能力超凡,给人感觉一点也不现实。话虽如此,既然来到仿真的世界,它们再怎么犯规也要遵守最基本的时序法则。
1. always @ ( posedge CLOCK ) 2. case( i ) 3. 0: 4. begin 5. reg1 <= 4’d10; i <= i + 1’b1; end 6. $display( “reg1 = 4’b%b ”, reg1); // 打印二进制格式 7. $display( “reg1 = 4’h%h ”, reg1); // 打印十六进制格式 8. $display( “reg1 = 4’d%d ”, reg1); // 打印十进制格式 9. end 10. endcase
代码4.3.2
系统函数如 $display() 一般引发即时事件,即时事件也是无视时钟的意思。如代码4.3.2所示,是笔者基于代码4.3.1的修改。根据修改结果,原本分别需要使用3个时钟输出的操作,变成在一个时钟内完成。笔者先是在第5行使用 <= 操作符赋值,然后接续在第6~8行输出不同格式的结果。读者事先猜猜一下,就是会是怎样的打印信息呢?
# reg1 = 4'b0000
# reg1 = 4'h0
# reg1 = 4'd0
启动仿真不一会,打印结果也会跟着出现,如上所示,所有打印结果分别都是0,是不是觉得很奇怪?其实这样打印结果是非常合情合理,正如笔者所言那样,系统函数 $display()就算是超人它也必须遵守时序规则,在这里(时序)它是触发即时事件的家伙。不过根据理想时序的原理,reg1是触发时间点事件,结果此刻reg1的内容是过去值0(0值是初值也是复位值), 所以$display()函数也相序打印值0。
1. always @ ( posedge CLOCK ) 2. case( i ) 3. 0: 4. begin 5. reg1 = 4’d10; i <= i + 1’b1; end 6. $display( “reg1 = 4’b%b ”, reg1); // 打印二进制格式 7. $display( “reg1 = 4’h%h ”, reg1); // 打印十六进制格式 8. $display( “reg1 = 4’d%d ”, reg1); // 打印十进制格式 9. end 10. endcase
代码4.3.3
代码4.3.2相较代码4.3.3内容是大同小异,除了第5行 reg1的赋值操作由 = 改为 <=。此刻再重新执行仿真就会得到以下的打印信息。
# reg1 = 4'b1010
# reg1 = 4'ha
# reg1 = 4'd10
啊拉!?是不是觉得很神呢,此刻由于reg1赋予即时值 4’d10,所以 $display() 函数才得以输出0值以外的结果。代码4.3.2还有代码4.3.3告诉我们一个简单的道理,不管对方是老孙还是玉皇大帝,既然站在时序这块土地上,当然就要乖乖遵守这个世界的法则,不然就要吃不了兜着走。
此外,笔者也想透过代码4.3.2与代码4.3.3告诉读者一个信息, 函数系统的本质其实是顺序语言, 所以系统函数的常见用法也非常倾向“调试风格”,例如常见的参考书都会这样使用它 ...
1. begin 2. #00 reg1 = 4’d10; 3. $display( “reg1 = 4’b%b”, reg1 ); 4. #10 reg1 = 4’d12; 5. $display( “reg1 = 4’b%b”, reg1 ); 6. ...... 7. end
代码4.3.4
如代码4.3.4所示,这是传统流派的爱用方法,亦即使用仿真时间控制整体时序。常规上,我们会这样解读:先在第2行将仿真时间延迟0个刻度,然后为reg1赋值4’d10;在第3行打印reg1;在第4行将仿真时间延迟10个刻度,然后为reg1赋值4’d12;在第5行打印reg1。此刻,代码4.3.4就会变成不择不扣的“调试风格”,因为代码4.3.4解读起来与一般的顺序语言没有两样。
这种半吊子的仿真习惯造很容易流失时序表现,最终造就豆浆不是豆浆,时序不是时序的窘境。系统函数固然好用,但是我们也要选择最适合的用法,函数始终不是仿真的主角,它的作用宛如跑龙套般,偶尔在镜头出现几分钟就行了。换个角度而言,系统函数虽然不是主角,如果喜剧没有路人甲乙丙丁会让人觉得太假了,这就是跑龙套的重要性。同样道理,笔者偶尔也会使用打印函数输出一些信息用于教程,但是更多时候直接观察时序图的信息才是至关重要。
===================================================================
再者一些仿真语言,如for,while等循环功能。for是半阴半阳的奇怪家伙,虽然语法书上解释它是综合语言,但是它的实际行为更接近验证语言。举例而言,在建模的时候 for 是不建议使用的,因为它容易扰乱时钟控制,因此for的作用仅限用于初始化 ram 而已,如下代码4.3.5所示:
1. reg [7:0]i; // 声明 i 2. reg [7:0] ram [127:0]; // 声明 ram 3. 4. for(i = 0; i < 128; i = i + 1) // 使用 for初始化 ram 5. ram[i] = i;
代码4.3.5
现今的集成环境如Quartus II,我们已经可以使用 mif 文件初始化 ram,为此for的作用显得更加没有价值,所以人们将for称为建模的鹌鹑,灰心满满的 for 就这样消失在建模的舞台中。不知何时 for 在仿真光芒四射,,笔者曾在前面说过,传统流派的仿真风格是非常倾向“调试”,for自然而然就成为传统流派的爱将。
for的常见用法如代码4.3.6所示:
1. begin 2. for( i = 0; i < 128; i = i + 1 ) 3. ram[i] = i; 4. for( i = 0; i < 128; i = i + 1 ) 5. $display( “ram[%d] = %d”, i, ram[i] ); 6. for( i = 0; i < 128; i = i + 1 ) 7. reg1 = #10 ram[i]; 8. ...... 9. end
代码4.3.6
如代码4.3.6所示,for的行为会展示顺序语言满满的既视感。对此,笔者不禁会怀疑,这样还是仿真吗?不错,for的副作用就是冲淡豆浆,好让豆浆不在是豆浆,因为代码4.3.6丁点时序香也没有,这种失去时序香的东西,已经不再是笔者梦寐以求的仿真。为此,笔者决定要复仇,誓死驱赶for,抹杀for!
建模之所以无法接受for ... 其一for本质是顺序语言之余,其二建模必须有顺序结构支持for才得以运作。不管怎么样,既然我们已经知晓for的秘密,就算没有for我们也是一样可以实现循环,因此仿顺序操作就诞生了。i是一件有趣的工具,它除了指向这个,指向那个以外,只要充分使用i也能实现各种循环操作。
1. always(posedge CLOCK)
2. case(i)
3.
4. 0,1,2,3,4,5,6,7:
5. begin reg1 <= reg1 + 1’b1; i <= i + 1’b1; end
6.
7. endcase
代码4.3.7
代码4.3.7是非常简单的循环操作,只有将i指向多时钟操作,如代码4.3.7所示,reg1一共递增7次。在此,代码4.3.7与代码4.3.6有明确的区别,代码4.3.6的for是无视时钟实现循环操作,换之代码4.3.7的i是根据时钟实现循环操作。相比之下,代码4.3.7才是香浓的豆浆,然而代码4.3.6仅是不是豆浆的豆浆。
好奇的朋友可能会问:“笔者,i有没有办法实现多次数的循环?”,绝对有!我的朋友 ... 笔者曾经在章节3举例过,笔者也无妨重复解释。
1. always(posedge CLOCK) 2. case(i) 3. 4. 0: 5. if(C1 == 8) begin C1 <= 4’d0; i <= i + 1’b1; end 6. else begin reg1 <= reg1 + 1’b1; C1 <= C1 + 1’b1; end 7. 8. endcase 9. 10. always(posedge CLOCK) 11. case(i) 12. 13. 0,1,2,3,4,5,6,7: 14. begin 15. reg1 <= reg1 + 1’b1; 16. if( C1 == 8 -1 ) begin C1 <= 4’d0; i <= i + 1’b1; end 17. else C1 <= C1 + 1’b1; 18. end 19. 20. endcase
代码4.3.8
如代码4.3.8所示,这是两种不同风格的仿真操作,第1~8行是时钟概念比较松散的循环操作;第10~20行则是针对紧密控时的循环操作。两者虽有明确的差异,但是两者都是根据时钟发生循环操作,有浓浓的时序香。反之 for 却是根据步骤或者仿真时间执行的循环操作,时序香非常稀淡。
如果i可以取代for循环,i同样可以取代while循环,因为两者适用同样的道理。笔者是豆浆的爱好者,一生都在追求浓厚豆香的豆浆,实现循环一定要伴随满满的时序表现,不然这种循环不要也罢。不过,笔者不是恶魔,笔者也不会赶尽杀绝,笔者也为 for留一条生路。
在前面,笔者说过系统函数如 $display() 是时序的异存在,其实 for 也是同样的异存在。在笔者的眼中,for会引起即时事件,举例而言。
1. always @ ( posedge CLOCK ) 2. case( i ) 3. 4. 0: 5. begin // 必须使用 = 赋值操作,配合 for 的即时事件 6. for( x = 0; x < 8; x = x + 1 ) reg1 = reg1 + 1'b1; 7. $display( "reg1 = 4'b%b",reg1 ); 8. i <= i + 1'b1; 9. end 10. 11. endcase
代码4.3.9
代码4.3.9是一种比较独特的仿真风格,此刻for被认为是即时事件的触发者,不管for愿不愿意,for都被时钟控制得死死。如代码4.3.9所示,步骤0实际上只是停留一个时钟而已,然而第6行的for既然执行8次循环操作,以致递增 reg1八次。为了配合for所触发的即时事件,reg1的必须使用 = 赋值操作符。笔者也曾经说过,即时事件是无视时钟的事件,不管第6行的for是循环执行8次,还是9999次,它都是在一个时钟内完成。
图4.3.1 代码4.3.9的仿真结果(exp14_simulation)。
简单完成8次循环操作以后,reg1也取得即时值8,然后再由第7行的$display() 函数输出,最后第8行的i递增以示下一个步骤。图4.3.1是代码4.3.9的仿真结果,相对实现 exp14_simulation。如图4.3.1所示,C0指向时间点T0,然而此刻的x输出即时值8,而且reg1也输出即时值8。这种仿真结果暗示,for循环触发即时事件,受牵连的reg1同样也触发即时事件,两起事件都是发生在同样一个时钟内。换句话说,for循环受限与时钟并且没有暴走。
代码4.3.9是笔者为 for留下的仁慈,即使它脱离传统流派,它也能为美味的豆浆出一份力 ... 此刻for可以骄傲说道:“俺是触发即时事件,俺也有时序表现! ”,这种共赢局面才是我们期望的结果。作为补充笔者还是要强调一下,for对时序来说,它是异世界的存在,虽然仿真允许它们为时序出一份力。换做建模(实际建模),不管 for再怎么努力,它始终无法得到认同,就算凑巧综合成功,我们也必须付出相应的代价。举例而言:
1. case( i ) 2. 3. 0,1,2,3,4,5,6,7: 4. begin reg1 <= reg1 + 1’b1; i <= i + 1’b1; 5. 6. endcase 7. 8. case( j ) 9. 10. 0: 11. begin for( x = 0; x < 8; x = x + 1’b1 ) reg2 = reg2 + 1’b1; i <= i + 1’b1; end 12. 13. endcase
代码4.3.10
代码4.3.10 有两个过程,亦即i(第1~6行)与j(第8~13行)。过程i利用8个时钟递增 reg1 八次 ... 换之,过程j则是利用 for 递增 reg2 八次。reg1 与 reg2 虽有相同的操作目的,但是却有不同的操作过程,前者是利用时钟引发时间点事件,后者则是无视时钟引发即时事件。过程i无疑会综合成功,换之过程j虽然也会综合成功,不过两者是完全不同的综合结果。
图4.3.2 过程i建模示意图。
在建模的角度上,过程i的建模十一图如图4.3.2所示,实际上它只有一个累加资源,然后利用时钟不断反馈输出,不断重复相同操作,直至满意结果为止。
图4.3.3 过程j建模示意图。
相比之下,过程j使用超过1个以上的累加资源,建模示意图如图4.3.3所示。因为for触发即时事件,而且for也重复递增结果八次,所以在人眼无法触及的情况下,8个称为即时层的累加资源建立而成。输入结果每经过一个累加即时层就执行一次累加操作,直至8个即时层游走完毕。为了明确区分过程i与过程j的区别,笔者建立简单的比较表格(表格4.3.2)。
表格4.3.2 过程i与过程j的区别。
过程 \ 比较参数 |
时钟消耗 |
资源消耗 |
操作模式 |
过程i |
8 |
少 |
循环操作 |
过程j |
1 |
很多 |
即时操作 |
如表4.3.2所示,过程i消耗时钟多却消耗资源少;反之,过程j消耗时钟少却消耗资源多。反实际上,过程i与过程j之间的比较已经是“优化与平衡”的范畴,有时候建模必须衡量某种平衡点,如果FPGA设备的逻辑资源稀少,我们必须考虑善用时钟作为优化的手段;换之,如果操作速度作为优先,那么善用资源是一种比较适宜的优化手段。
笔者曾经说过,建模还有仿真之间的区别就在乎“物理环境”还有“虚拟环境”而已。属于虚拟环境的仿真,FPGA设备拥有无限的逻辑资源,所以for要循环多少次也没有问题,反之亦然。不管怎么样,我们还是把话题却换回来 ...
1. `timescale 1ps/1ps 2. reg CLOCK; 3. initial CLOCK = 0; 4. forever #5 CLOCK = ~CLOCK; 5. 6. begin // 传统流派仿真手段 7. #00 for(x = 0;x < 8; x = x + 1)reg1 = reg1 + 1’b1; 8. #10 for ( x = 0;x < 8; x = x + 1 ) reg2 = reg2 + 1’b1; 9. #10 for ( x = 0;x < 8; x = x + 1 ) reg3 = reg3 + 1’b1; 10. end 11. 12. always @ ( posedge CLOCK ) // 笔者惯用仿真手段 13. case( i ) 14. 15. 0: 16. begin for(x = 0;x < 8; x = x + 1)reg1 = reg1 + 1’b1; i <= i + 1’b1; end 17. 1: 18. begin for(x = 0;x < 8; x = x + 1)reg2 = reg2 + 1’b1; i <= i + 1’b1; end 19. 2: 20. begin for(x = 0;x < 8; x = x + 1)reg3 = reg3 + 1’b1; i <= i + 1’b1; end 21. 22. endcase
代码4.3.11
我们足够仿真的真实面貌,其实for的使用可以多姿多彩,如代码4.3.11所示。第6~10行是传统流派的仿真手段,亦即使用仿真时间控制for循环执行递增操作。仿真时间为0的时候为 reg1递增8次;仿真时间为10的时候为 reg2递增8次;仿真时间为20的时候为reg3递增8次。
换之,第12~22是笔者惯用的仿真手段,假设每隔10个仿真时间恰好是1个时钟 ... 如代码4.3.11所示,第10~15行有3个步骤,每一个步骤停留一个时钟,而且每一个步骤也有使用for执行递增操作。步骤0,for为reg1递增8次,然后i递增以示下一个步骤;步骤1,for为reg2递增8次,然后i递增以示下一个步骤;步骤2,for为reg3递增8次,然后i递增以示下一个步骤。
图4.3.4 代码4.3.11,第6~10行产生的时序。
图4.3.4是使用传统流派产生的时序,reg1在0ps输出值8,reg2在10ps输出值8,reg3在20ps输出值8。由于代码4.3.4第6~10是根据仿真时间输出结果,所以图4.3.4有没有CLOCK信号也无关紧要。换句话说,这是一个没有时钟概念,也没有任何时序表现的时序图,所以它不是笔者爱喝的豆浆。
图4.3.5 代码4.3.11,第12~22行产生的时序。
换之,图4.3.5是经过笔者的仿真手段所产生的时序。如图4.3.5所示,reg1在T0输出即时值8,reg2在T1输出即时值8,reg3在T2输出即时值8。在此,for不仅按照时钟工作,for也有时序表现,所以这是一杯满满豆香的豆浆。当然,重点不仅而已,亮眼的同学一定会发现,如果实际的FPGA设备有足够的逻辑资源,其实这种仿真手段也适用建模。笔者是一名贯彻信念活下去的男人,同样手段适用不同环境就是节能本质最美的绝华。
===================================================================
最后总结道,笔者在这个小节引用两个常用的验证语言,亦即 $display()函数,还有for关键字来表示,验证语言的时序表现。传统流派是一种倾向调试的仿真手法,所以往往会埋没验证语言的时序表现。换之,笔者强烈建议使用时钟而不是仿真时间作为仿真和控制的手段,因为这样做有许多好处。其一,验证语言可以展示时序表现之余,偶尔同样的手段也使用两种环境。
Verilog有一些比较暧昧的关键字,for就是好例子,由于它们实在太难控制了,往往会在建模(实际环境)造成巨大的破坏(使用for会消耗大量逻辑资源)。因此 for却被认为是验证语言的一伙,因为虚拟的仿真环境拥有无限的资源任由它消耗。此外,还有一些完全属于验证语言的关键字,如 fork ... join,forever等,实际上它们是综合语言相对应的兄弟姐妹,例如:forever 与 always,begin ... end 与 fork ... join,至于使不使用它们是见仁见智的问题。
验证语言主要是系统函数占据多,因此许多系统函数有如 $display()函数一样触发即时事件。不过系统函数它们完全属于仿真的,所以综合(建模)的时候是绝对不允许系统函数出现。虽然系统函数看似很多很强大,其实系统函数存在一定风险,正如笔者所言,由于系统函数的本质太接近顺序语言了,如果用法不妥当会很容易抹杀时序的时序表现 ... 系统函数的关键字符是美金$。
验证语言除了系统函数占据多以外,其实预处理也有可观的数量,常见的预处理有 `timescale,`define等,预处理是综合器(编译器)的工作,常常放在建模或者仿真的最开头。大多数的预处理都适用于建模与仿真,举例而言 `timescale 写在仿真开头是声明时钟刻度。换之,如果`timescale 写在建模开头,它就会被综合器(编译器)无视掉,预处理的关键字符是上标点 `。
不管怎么说,这个章节只有抛砖引玉的作为而已,更多更详细的验证语言,读者必须自行需找其它参考。验证语言的关键字,系统函数,还有预处理,由于它们比较偏向调试风格,又或者本身就是活生生的顺序语言 ... 不过,笔者为了养成统一的思维模式(并行思维),所以不怎么喜欢它们。虽然“欢喜”还有“必要”是不同的话题,但是过多涉及它们无疑是给仿真带来极大的学习负担,因此笔者建议最小程度使用它们就好。
又一次,笔者在炎热的天空下四处跑动,已经极限的笔者正在使命寻找城市的绿洲,走着走着笔者来到一处树荫下,歇气还不及几口,旁边就有人叫道,原来是一位卖豆浆的小贩正在向笔者哈拉。
“小哥,今天的天气还真是热到过分”,豆浆小贩说道。
“是呀 ... 我快要成为暑糕了”,笔者回答道。
“小哥,要不要来一杯清凉又美味的豆浆呢?”,豆浆小贩示意道。
“哎呀,真是雪中送炭呀老板!给我特凉特大杯哪一种!”,笔者兴奋道。
“好的,这是为小哥特地准备的豆浆。”,豆浆小贩笑道。
笔者眼也不眨一下就顺势接过豆浆小贩手中的杯子,然后立即往口里送去 ...
“嗯唔!?老板这是什么豆浆,实在太美味了 ... ”,笔者惊讶道。
此刻,诡异的事情发生了,树荫下只有笔者一人而已,不知什么时候那个豆浆小贩已经凭空消失 ... 一股时曾相似的既视感忽然穿过脑髓,笔者不禁打了一个冷颤。
“见鬼了,又是哪位豆浆小贩 ... ”,笔者粗言道。
不过,可怖的情绪却渐渐被口里那股浓浓的豆香滋润并且消去,笔者带着满意的心情继续赶路 ...
4.4激励文本的布局
激励是什么?根据笔者的理解,激励这词其实是 “刺激”与“反应”的复合体,刺激表示输入,反应表示输出。所谓激励文本,就是所有“刺激与反应”的集中营 ... 简单点说,激励文本就是实现虚拟活动,也就是仿真环境。事实上,仿真环境是笔者的主观观念,笔者认为建模(实际建模)还有仿真(虚拟建模)本是同根,只有概念(环境)不同而已。
常规观念,亦即传统流派,它们认为建模(实际建模)还有仿真(虚拟建模)是不同平台的东西,所以两者之间可以使用不同的思维,模式还有手段。传统流派认识测试文本是一个测试作用的个体,而不是一座测试作用的环境,然而这个个体是偏向“调试”(顺序)的风格还有模式。
图4.4.1 传统流派,用户,模块还有激励的关系图。
激励文本由于被传统流派视为个体,所以“用户(设计者)”“模块(仿真对象)”,“激励(激励文本)”,成为一种由上之下,倒金字塔的阶层关系图。如图4.4.1所示,设计者占据最高,而且也是分量最大,可见它有所多么重要的地位,反之激励文本不仅占据最低而且分量也是最小,可想而是它的地位是多么卑贱。
形象点说,用户可以是主人A,激励文本可以是奴隶C,然而模块(仿真对象)可以是爱犬之类的宠物B。主人A除非有命令告知奴隶C,否则主人A与奴隶B平时是不相往来,说得难听一天就是主人A是宠物B的主人,宠物B是奴隶C的主人,所以主人A与奴隶C之间是没有任何纽带。
假设,主人A用千万黄金从极东买了一只宠物恐龙B回来,有一天主人A突然心血来潮想知道恐龙B的战斗表现,于是主人A会用奴隶C去测试恐龙B。无情的主人A满脑子只在乎恐龙B的战斗表现而已,至于奴隶C的死活,主人A一律没有兴趣。换句话说,传统流派认识测试文本的是一位不值钱的个体,或者是一只实验性的小白鼠。
市场上,小白鼠是廉价的科学消耗品,价值和用完即丢的抹巾一样。所以说,传统流派重视激励文本的程度是非常之低,结果它们不会花费而外的资源还有精力去维护激励文本。因为如此,激励文本内容相比模块内容(仿真对象)会是更加不堪入目的乱,乱到让人抓狂又尖叫。
图4.4.2 笔者观念中的仿真环境。
根据笔者的认识,激励文本不仅不是一个廉价的个体,而是一座非常贵重的仿真环境,然而我们就是创建这个环境的神。此外,仿真还有3个必须重视的结构性,其一是仿真对象的结构性,其二是仿真环境的结构性,其三是仿真过程的结构性,三者之间的结构性简单点说:
(一)仿真对象的结构性也是笔者时常挂在嘴边“模块的结构”,如低级建模,用
法模板等。
(二)仿真过程是指基本输入,基本输出还有环境输入等内容。仿真过程所谓的结
构性是指仿真过程的用法模板。
(三)仿真环境的结构性就是激励文本的的布局,也是各种仿真过程在激励文本的
的位置。
其(一)没有什么好说,这是学习Verilog的基础,笔者已经在建模篇讲得很清楚。其(二)是其(一)的缩水,懂得其(一)自然也会晓得其(二)。其(三)是仿真的关键,笔者认为激励文本一个仿真环境,然而仿真过程还有仿真对象都是个体,仿真环境所谓的结构性是指,个体之间如何布局才能使得整体产生最大“表达能力”还有“清晰能力”。
有些同学可能会黯然笑道: ”激励文本想怎样写就怎样写嘛,干嘛还要分那么多 ... 真是爱找麻烦的笔者呀。“,这位同学有所不知了,为什么人的屁股不是长在脑袋?如果屁股长在脑袋,大小便既不是要倒立不可?人是如此,激励文本也是如此,结构的重要性不管在什么方面上都有同样的道理。
其实,很久以前笔者就一直在冷嘲热讽传统流派的粗野,传统流派不会在乎结构性这种小细节。失去结构不仅会为前期建模带来困扰,同样,后期仿真也会带来诸多不便,由此可见结构性是多么重要。所以说,笔者实在不知道那些崇拜权威还有传统流派的人们是如何大小便的 ...
图4.4.3 仿真环境的布局(结构性)。
图4.4.3曾经出现过许多章节,然而这张图却表示笔者认为仿真环境最有效的布局方式。如图4.4.3所示,激励文本亦即仿真环境可以分为5个个体(部分),亦即环境输入,仿真对象(实例化),虚拟输入,虚拟输出还有其他。环境输入可以是最简单的时钟信号还有复位信号的模拟输出;虚拟输入有基本输入,还有反馈输入;虚拟输出可以是基本输出,还有反馈输出。至于其他则是一些补充作用。
接下来让笔者用简单的实验来讲明一切,打开 exp15 ...
selfadder_funcmod.v
1. module selfadder_funcmod 2. ( 3. input CLOCK, RESET, 4. input Start_Sig, 5. output Done_Sig, 6. input [7:0]WrData, 7. output [15:0]RdData 8. ); 9. /*************************/ 10. 11. reg [3:0]i; 12. reg [15:0]Temp; 13. reg isDone; 14. 15. always @ ( posedge CLOCK or negedge RESET ) 16. if( !RESET ) 17. begin 18. i <= 4'd0; 19. Temp <= 16'd0; // Sum of pratocal product 20. isDone <= 1'b0; 21. end 22. else if( Start_Sig ) 23. case( i ) 24. 25. 0: 26. begin Temp <= 16'd0; i <= i + 1'b1; end 27. 28. 1,2,3,4,5,6,7,8: 29. begin Temp <= Temp + WrData; i <= i + 1'b1; end 30. 31. 9: 32. begin isDone <= 1'b1; i <= i + 1'b1; end 33. 34. 10: 35. begin isDone <= 1'b0; i <= 4'd0; end 36. 37. endcase 38. 39. /*************************/ 40. 41. assign Done_Sig = isDone; 42. assign RdData = Temp; 43. 44. /*************************/ 45. 46. endmodule
selfadder_funcmod 是一个没有意义的功能模块,它的作用就是递增 WrData 八次而已。第3~7行是出入端的声明,第4~5行的 Start_sig 与 Done_Sig 表示它是一个仿顺序性质的模块;第11~13行是相关寄存器的声明,i用于指向时钟还有步骤(当然也包含过程),Temp用作暂存空间,isDone用来反馈完成。第18~20行是相关的复位操作;第22~37是核心功能。步骤0是用来初始化相关的寄存器;步骤1~8是八次的递增操作;步骤9~10是用来反馈完成信号。第41~42行是相关的输出驱动。
图4.4.4 selfadder_funcmod产生的理想时序。
如图4.4.4所示,这是 selfadder_funcmod 产生的理想时序图,此图也表示该模块的一次性操作过程。C0指向T0,此刻如果笔者拉高 Start_Sig 又为 WrData输入 8,根据理想时序的理论,数据之间传输至少需要一个时钟,因此在下一个时钟的上升沿该模块就立即运行,时间大约是20ps的时候。该模块在步骤0(20ps)初始化 Temp,所以输出没有变化;步骤1(30ps)执行第一次递增,结果输出未来值8;步骤2(40ps)执行第二次递增,结果输出未来值16;其它以此类推直到步骤8(100ps)的时候,八次递增已经完成,结果输出未来值64;
步骤9~10(110ps~120ps)是完成信号的产生,结果Done_Sig拉高一个时钟,也是C1~C2指向的地方。因此该模块整体经过时间是10ps~120ps(步骤0~步骤10),一共消耗110ps时间,再假设1个时钟为10ps的话,那么一次性的递增操作需要消耗11个时钟。如何解读图4.4.4,理想时序的时间点事件是关键,不相信的读者可以一个时钟一个时钟计算看看是不是i是否正确指向时钟呢?
接着让我们来看看这家伙的仿真环境(激励文本)到底是如何编写的!?
selfadder_funcmod.vt
1. `timescale 1 ps/ 1 ps 2. module selfadder_funcmod_simulation(); 3. 4. reg CLOCK; 5. reg RESET; 6. reg Start_Sig; 7. reg [7:0]WrData; 8. wire Done_Sig; 9. wire [15:0]RdData; 10.
第1行是时钟刻度声明最小单位为1ps/1ps;第4~9行是相关的寄存器还有连线声明,它们分别对应仿真对象 selfadder_funcmod 的出入端,由于仿真环境没有实际的输入引脚还有输出引脚,所以用reg作为输入引脚的配置,wire作为输出引脚的配置。最后稍微注意一下地2行的命名方式,笔者习惯为仿真环境的后缀名取为 simulation,前缀名这是仿真对象的名字。
11.
12. /***********************************/
13.
14. initial
15. begin
16. RESET = 0; #10; RESET = 1;
17. CLOCK = 1; forever #5 CLOCK = ~CLOCK;
18. end
第14~18行是环境输入的声明,也是模拟时钟信号还有复位信号。第14行的initial 表示编译的时候,第16行的 RESET 为初值0,然后第17行的CLOCK为初值1;接着,第16行的 RESET 会延迟拉低10个时钟刻度之后又拉高,第17行的CLOCK则会每隔5个时钟刻度取反。第14~17行有些读者可能看不明白,内容都是我们之前所学过的东西,不过只是换个形式而已,结果如下:
intiial begin RESET = 0; CLOCK = 1; end begin #10 RESET = 1; end begin forever #5 CLOCK = ~CLOCK;
20. /***********************************/
21.
22. selfadder_funcmod U1
23. (
24. .CLOCK(CLOCK),
25. .RESET(RESET),
26. .Start_Sig(Start_Sig),
27. .Done_Sig(Done_Sig),
28. .WrData(WrData),
29. .RdData(RdData)
30. );
31.
第22~30行是仿真对象的实例化,内容非常直接,读者自己看着办吧。
32. /***********************************/ 33. 34. reg [3:0]i; 35. 36. always @ ( posedge CLOCK or negedge RESET ) 37. if( !RESET ) 38. begin 39. i <= 4'd0; 40. Start_Sig <= 1'b0; 41. WrData <= 8'd0; 42. end 43. else 44. case( i ) 45. 46. 0: 47. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 48. else begin WrData <= 8'd8; Start_Sig <= 1'b1; end 49. 50. 1: 51. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 52. else begin WrData <= 8'd9; Start_Sig <= 1'b1; end 53. 54. 2: 55. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 56. else begin WrData <= 8'd10; Start_Sig <= 1'b1; end 57. 58. 3: 59. begin i <= i; end 60. 61. endcase 62. 63. /***********************************/ 64. 65. endmodule
第36~61行是虚拟输入的激励内容,其中也包括基本输入还有反馈输入。首先第39~41行是相关的复位操作,第44~59行是虚拟输入的过程(第36~61实际上与仿真对象的核心功能拥有相似的结构性,其实这是善用用法模板的结果)。步骤0是为仿真对象的WrData输入8值,步骤1则是输入9值,步骤2则是输入10值,步骤3则是结束操作(停留)。
为进入解释仿真结果之前,再让笔者好好说明一下仿真环境的结构性。激励文本第12~18行是环境输入的部分;第20~30行是仿真对象实例化的部分;第34~61行则是虚拟输入的部分。在此,好奇的同学会问:“虚拟输出在哪里呢?”
图4.4.5 selfadder_funcmod仿真环境的布局。
如图4.4.5所示,根据该仿真环境的布局方式,CLOCK信号与RESET信号属于环境输入,Start_Sig信号与WrData信号属于虚拟输入,其中Start_Sig兼为基本输入还有反馈输入,具体内容往后继续。虚拟输出的Done_Sig还有RdData都是基本输出,而且两者都由仿真对象 selfadder_module 产生以后直接投射在波形图当中。
所以说,我们没有必要在激励文本中多此一举,创建多余的 Done_Sig 与 RdData虚拟输出。此外,读者必须注意一下 Start_Sig 与 Done_Sig实际上是一种问答信号,因此 Start_Sig需要根据 Done_Sig 的反馈状态再一次决定拉高又或者拉低 Start_Sig,所以 Start_Sig除了第一次的基本输入之余,还要根据Done_Sig产生另一个虚拟输入。
46. 0: 47. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 48. else begin WrData <= 8'd8; Start_Sig <= 1'b1; end
接着让我们稍微切换步骤0 ... 一开始的时候,由于第47行的if条件为成立(仿真对象还没有一次性的递增操作),结果第48行的代码优先执行。此刻,基本输入WrData与Start_Sig就开始产生,Start_Sig拉高以示仿真对象使能开始工作,WrData则是递增操作所需的输入。
图4.4.6 selfadder_funcmod_simulation 的仿真结果(exp15)。
图4.4.6是仿真结果,其中光标C0分别指向递增模块三次性的递增操作。如图4.4.6所示,笔者为了清晰波形图,稍微分类一下信号。CLOCK与RESET是环境输入;Start_Sig与WrData是虚拟输入;Done_Sig与RdData是虚拟输出;至于i除了指向仿真对象的内部过程以外还有指向虚拟输入的激励过程。
一开始的时候(C0指向的地方),步骤0为WrData输入8并且拉高Start_Sig,然后仿真对象就会在下一个时钟沿开始工作。仿真对象用了11个时钟完成递增操作并且反馈完成信号以示一次性的操作结束。步骤0则会根据Done_Sig的结果拉低Start_Sig(反馈输入)不使能仿真是对象,然后i递增以示下一个虚拟输入的产生。虚拟输入产生3次,因此Temp会有3种基本输出,亦即64,72还有80。
===================================================================
在这里,有些同学可能会抓头喊道,自己无法完全解读图4.4.6的仿真结果 ... 这是当然的,此刻笔者只是简单演示一下仿真环境的布局方式而已,我们还没有深入联系仿真结果,激励内容,还有仿真对象等,每个时钟的实际活动 ... 简单点说,我们还没有准备好将时序活动(仿真结果)与代码(仿真对象与激励文本)之间的联系并且做出解析。所以读者不能够完全解读图4.4.6一点也不奇怪,关于这一点,笔者会在往后讲解。
不过,任性的同学可能会闹脾气道:“不管嘛!我就是要虚拟输出嘛!”,好啦好啦,别闹脾气就是了,笔者服输了,打开 exp16 ...
64. reg [3:0]j; 65. reg FlagA; 66. 67. always @ ( posedge CLOCK or negedge RESET ) 68. if( !RESET ) 69. begin 70. j <= 4'd0; 71. FlagA <= 1'b0; 72. end 73. else 74. case( j ) 75. 76. 0: 77. FlagA <= ~FlagA; 78. 79. endcase
exp16相比exp16的激励文本——selfadder_funcmod_simulation(仿真环境),笔者在虚拟输入的下面添加虚拟输入。其中,笔者在第64~65行声明寄存器j用于控制(指向)虚拟输出的产生过程,然而FlagA是没有意义的基本输出。如第76~78行所示,FlagA只会毫无意义的翻来翻去而已 ...
图4.4.7 exp16的仿真结果。
图4.4.7是添加虚拟输出以后的仿真结果,图4.4.7相比图4.4.6多了一个 FlagA信号。如图4.4.7所示,FlagA在整个时序过程只会翻来覆去而已 ... 多么无聊的家伙。
图4.4.8 exp16仿真环境(激励文本)的布局。
图4.4.8相比图4.4.5,在仿真对象的下面多了一个基本输出FlagA,不过不同的是FlagA信号不是仿真对象产生的输出,而是激励文本产生的输出。
最后笔者可以这样结论道:
到目前为止,我们可能还无法看道,仿真环境经过结构性的布局之,后到底凸显那种优越?不过不管怎么样,有结构性的激励文本域没有结构性的激励文本一定存在明显的差距,因为没有结构的激励文本比起有结构的激励文本一定会非常破烂不堪(乱)。此外,这个章节笔者也经由 selfadder_funcmod 表示,仿真对象的结构性(模块结构),仿真过程的结构性。
仿真对象的结构性就是 selfadder_funcmod 模块本身的内容,那是基础中的基础。笔者在 selfadder_funcmod 采用仿顺序操作的用法模板,此而i指向时钟之余又指向步骤(当然也包括过程)。此外,仿真过程的结构性则是 ... 笔者用i指向虚拟输入的产生过程,用j指向虚拟输出的产生过程。事实上,仿真过程的结构形式非常相似模块的结构,或者说是模块结构的简化版,因为两者都是采用同样的用法模板。
4.5 仿真模型与反馈输出①
图4.5.1 仿真环境的布局。
未进入本章之前,我们再来简单复习一下仿真环境的布局方式。如图4.5.1所示,激励文本也是仿真环境,位于顶层的当然莫属环境输入,环境输入模拟最基本也是最重要的时钟信号还有复位信号。位于环境输入的下面是仿真对象,仿真对象可以经由模块实例化而成,仿真对象也可以直接在仿真环境中建立。
接着是虚拟输入,虽然环境输入还有虚拟输入都是“输入”,不过不如环境输入有份量。
虚拟输入有两个基本分类,亦即基本输入,还有反馈输入。虚拟输入的后面是虚拟输出,虚拟输出也有两个基本分类,亦即基本输出与反馈输出。基本输出一般都是仿真对象产生的反应,很少人为添加在激励文本当中,例如实验exp15的 RdData信号与 Done_sig 信号。不过,我们也可以自行添加在激励文本当中,例如实验 exp16的 FlagA就是如此。
除此之外,虚拟输出还有一种令人厌烦的反馈输出,也是这个章节要探讨的东西。其实,笔者曾经在其它章节稍微讨论过它,不过目的是用来解释i为什么要指向过过程,而没有过多深入。那么笔者再一次正式提问“什么又是反馈输出呢?”
图4.5.2 仿真模型①。
如图4.5.2所示,这是仿真模型①,也是最基本的仿真模型,亦即激励内容产生虚拟输入刺激仿真模块,仿真模块接受刺激以后就会产生相关的反应,然后直接将结果放映在wave界面上,在这种情况下,虚拟输入只有基本输入,虚拟输出也只有基本输出而已。
应用仿真模型①的仿真对象一般都是左耳进右耳出的单纯功能,常见的例子有门级逻辑仿真。
仿真模型①是参考书曝光率最强,也是最粗糙的仿真模型,一般上我们无法在它的身上发现一些如:结构性或者时序表现等细节。仿真模型①比较依赖仿真时间,而且仿真风格也很“调试化”。仿真模型①虽然是最基础也是最常用的仿真模型,不过它的能力却非常有限,单纯的它实际上是无法满足千奇百怪的仿真要求,为了弥补不足,其它仿真模型也相续面世。
图4.5.3 仿真模型②。
图4.5.3是仿真模型②的示意图,仿真模型②相较仿真模型①,它多了更多细节。如图4.5.3所示:
(一)激励内容产生基本输入刺激仿真对象,仿真对象产生反应以后便将基本输出
投射在wave界面上。
(二)激励内容产生基本基本输入刺激仿真对象,仿真对象产生反应以后将基本输
出反馈给激励内容,然后激励内容再产生反馈输入进一步刺激仿真对象。
图4.5.3 等价的仿真模型②。
图4.5.3是等价的仿真模型②,如图4.5.3所示:
(三)除了仿真对象产生基本输出以外,激励内容也产生基本输出并且投射在wave
界面。
(四)激励内容之间也可以相互刺激。
仿真模型②基于仿真模型①之后扩展更多细节,仿真模型②使用的频密性不仅比仿真模型①还高,而且仿真模型②也能兼容仿真模型①,仿真模型②的应用例子有算法模块,问答式模块等功能稍微多样化的模块。然而,悲观而言,仿真模型②拥有更多数量的信号,而且方向性也非一处,此外过程(激励内容)也很多。
因此,我们必须采取有结构性的环境布局(仿真环境的结构性),有结构性的激励内容(仿真过程的结构性),有时序表现的理想时序。一般上,仿真模型②只会使用仿真时间模拟环境输入,绝对不会使用仿真时间控制激励内容 ... 反之,仿真模型②会非常乐意使用时钟控制一切信号。换句话说,门级以外的仿真对象,绝对有理由应有仿真模型②,传统的仿真手段绝对会用到手残。
图4.5.4 FPGA驱动硬件。
仿真对象很多时候不仅仅是为了观察输出而已,仿真对象还必须与实际硬件发生互动。如图4.5.4所示,假设笔者为FPGA创建模块A用作驱动硬件B,其中FPGA是主机,硬件B是从机,而且FPGA又是两方访问,换句话说FPGA除了向硬件B发生写入动作意外,FPGA也会为硬件B执行读出操作。
俗语有云,无穴不来风,事出必有因,由于仿真模型①与仿真模型②无法实现上述内容,因此仿真模型③面世了。虽说仿真模型③的诞生是为了仿真实际硬件,实际上仿真模型③也兼容仿真模型②与仿真模型①。
图4.55 仿真模型③。
图4.5.5是仿真模型③的示意图,相较其它仿真模型,仿真模型多了一个虚拟硬件。仿真模型①与②的仿真对象要么就是讲基本输出投射在wave界面上,要么就是将基本输出反馈给激励内容。仿真模型③,仿真对象会产生基本输出刺激虚拟硬件,虚拟硬件接受刺激以后除了直接将反应(基本输出)投射在wave界面之余,虚拟硬件也会将反应反馈给仿真对象,此刻这种输出方式称为“反馈输出”。
图4.5.6 仿真模型③,虚拟硬件等价仿真对象。
如果根据等价关系去分析图4.5.5,虚拟按硬件谓是另一个仿真对象,结果如图4.5.6所示。不过,一般没有人会特意为虚拟硬件建模,并且在仿真环境当中实例化成为另一个仿真对象 ... 好奇的同学可能会怀疑,笔者是否又在偷懒?别误会,如果虚拟硬件是功能复杂的IC的话,假设是串行储存器AT24CXX 好了,试问同学是否有信息建模而成呢?答案当然是否定的,这种事情世上也只有傻子去干而已。
说来非常惭愧,笔者就是那个傻子 ... 要建模一块虚拟硬件,说实话那种程度的活儿,会耗死笔者的小命,幸好笔者迷途知返,不然笔者早就变成一具尸体。根据节能的角度而言,我们不会为了测试实际硬件的部分功能,结果特意从轮子开始创建整个虚拟硬件,就算能力允许,本质也不允许笔者这样做,因为这种做法太没有效率了 ... 为此,我们要测试那个功能就模拟那个功能即可。
图4.5.7 仿真模型③,激励内容等价仿真对象(虚拟硬件)。
图4.5.6是另外一种等价的仿真模型③,它没有虚拟硬件,虚拟硬件也不是仿真对象,取而代之它是一段激励内容。在此,我们会用“信号”来描述虚拟硬件的部分功能,然而描述过程就写在激励内容当中,至于那个“信号”又是什么信号呢?呵呵,想知道吗?笔者就大发慈悲告诉你们吧!
从第一章至这个章节以来,笔者一直在强调“早期有很好建模,后期就有好仿真” ... 在建模的阶段,笔者建议使用用法模范统一建模风格之余,笔者也建议使用i指向模块的内部过程(指向步骤)。虽然这些做法在大体上是为了稳固的结构,增强内容的清晰度,还有提高模块的表达能力 ... 然而这些所作所为却为仿真产生意想不到的正面效果,这种偶然究竟是一场意外的幸运,还是一连串“神的恶作剧”?
正如笔者曾在第三章节3.5所言那样,既然i有能力指向模块的内部过程,同样也表示i有能力标示模块的运行状态。然而,这些运行状态对仿真而言可是非常贵重的“描述材料”,我们可以用这些信息来描述虚拟硬件的部分功能。理论上的确如此,不过在此之前我们必须做足一切事先的准备,亦即规划激励文本的布局,创建可以最大程度支持仿真模型③,并且兼容仿真模型②还有仿真模型①的仿真环境。
不管怎么样,笔者还是用实验来说话吧,打开exp17:
图4.5.8 哈罗功能模块的建模图。
如图4.5.8所示,假设有一个名为哈罗的功能模块,笔者用它驱动硬件C。哈罗功能模块的左方有 Start_Sig信号与 Done_Sig信号充当开关,右方的 WrData信号与RdData信号主要是访问硬件C。
hello_funcmod.v
1. module hello_funcmod 2. ( 3. input CLOCK, RESET, 4. input Start_Sig, 5. output Done_Sig, 6. output[7:0]WrData, 7. input [7:0]RdData, 8. output [3:0]SQ_i 9. ); 10. /*************************/ 11. 12. reg [3:0]i; 13. reg [7:0]rData; 14. reg isDone; 15. 16. always @ ( posedge CLOCK or negedge RESET ) 17. if( !RESET ) 18. begin 19. i <= 4'd0; 20. rData <= 8'd0; 21. isDone <= 1'b0; 22. end 23. else if( Start_Sig ) 24. case( i ) 25. 26. 0: 27. begin rData <= 8'hAA; i <= i + 1'b1; end 28. 29. 1: 30. if( RdData == 8'hBB ) begin i <= i + 1'b1; end 31. 32. 2: 33. begin rData <= 8'hCC; i <= i + 1'b1; end 34. 35. 3: 36. if( RdData == 8'hDD ) begin i <= i + 1'b1; end 37. 38. 4: 39. begin rData <= 8'hEE; i <= i + 1'b1; end 40. 41. 5: 42. if( RdData == 8'hFF )begin i <= i + 1'b1; end 43. 44. 6: 45. begin isDone <= 1'b1; i <= i + 1'b1; end 46. 47. 7: 48. begin isDone <= 1'b0; i <= 4'd0; end 49. 50. endcase 51. 52. /*************************/ 53. 54. assign Done_Sig = isDone; 55. assign WrData = rData; 56. 57. /*************************/ 58. 59. assign SQ_i = i; 60. 61. /*************************/ 62. 63. endmodule
地3~8行是出入端的声明,其中SQ_i信号时是将内部过牵引出去;第12~13行是相关寄存器的声明,第19~21行则是复位操作;第23~50行是该模块的核心功能,步骤0输出数据 8’hAA;步骤1等待数据8’hBB;步骤2发送数据 8’hCC;步骤3等待数据 8’hDD;步骤4发送数据 8’hEE;步骤5等待数据 8’hFF;步骤6~7则是反馈完成信号。看着看着,是不是觉得该模块的功能很简单呢?
还未仿真之前,先让我们好好假设一下硬件C的基本功能:
(一)接收数据 8’hAA,反馈数据 8’hBB;
(二)接收数据 8’hCC,反馈数据 8’hDD;
(三)接收数据 8’hEE,反馈数据 8’hFF;
然后我们再给予上述所有信息创建仿真模型③。
图4.5.9 exp17的仿真模型③。
图4.5.9就是 exp17的仿真模型③,Start_Sig信号由激励内容产生,Done_Sig信号还有WrData信号则由仿真对象产生,至于RdData信号是反馈输出,它由硬件C(虚拟硬件)
产生。笔者曾在前面说过,不管硬件C的功能再怎么简单,我们都不会特意从轮子开始创建整个虚拟硬件,相反我们与采取替代又等价的手段。
图4.5.10 exp17等价的仿真模型③。
如图4.5.10所示,我们采用另外一种等价的仿真模型③,其中虚拟硬件则有一段激励内容取代,此外仿真对象也将内部过程牵引出来。当我们了解这些信息以后,我们就可以开始创建仿真环境了。
hello_funcmod_simulation.vt
1. timescale 1 ps/ 1 ps 2. module hello_funcmod_simulation(); 3. 4. reg CLOCK; 5. reg RESET; 6. reg Start_Sig; 7. reg [7:0]RdData; 8. wire [7:0]WrData; 9. wire Done_Sig; 10. wire [3:0]SQ_i; 11. 12. /***********************************/ 13. 14. initial 15. begin 16. RESET = 0; #10; RESET = 1; 17. CLOCK = 1; forever #5 CLOCK = ~CLOCK; 18. end 19. 20. /***********************************/ 21. 22. hello_funcmod U1 23. ( 24. .CLOCK(CLOCK), 25. .RESET(RESET), 26. .Start_Sig(Start_Sig), 27. .Done_Sig(Done_Sig), 28. .WrData(WrData), 29. .RdData(RdData), 30. .SQ_i( SQ_i ) 31. ); 32. 33. /***********************************/ 34. 35. reg [3:0]i; 36. 37. always @ ( posedge CLOCK or negedge RESET ) 38. if( !RESET ) 39. begin 40. i <= 4'd0; 41. Start_Sig <= 1'b0; 42. end 43. else 44. case( i ) 45. 46. 0: 47. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 48. else begin Start_Sig <= 1'b1; end 49. 50. 1: 51. begin i <= i; end 52. 53. endcase 54. 55. /***********************************/ 56. 57. always @ ( posedge CLOCK or negedge RESET ) 58. if( !RESET ) 59. begin 60. RdData <= 8'd0; 61. end 62. else 63. case( WrData ) 64. 65. 8'hAA: RdData = 8'hBB; 66. 8'hCC: RdData = 8'hDD; 67. 8'hEE: RdData = 8'hFF; 68. 69. endcase 70. 71. /***********************************/ 72. 73. endmodule
如代码 hello_funcmod_simulation 所示,第1行是时钟刻度(最小仿真时间)的声明,结果为1ps/1ps;第2行则是声明仿真环境——hello_funcmod_simulation;第4~10行是相关的寄存器与连线声明,由于仿真环境当中没有实际的链接,因此reg充当输入链接,wire充当输出链接。第14~18行是环境输入,RESET信号拉低10ps,CLOCK信号的周期则是10ps。
第22~31行是仿真对象——哈罗功能模块的实例化; 第37~53行是虚拟输入的激励内容;第57~69行则是虚拟输出的激励内容。首先让我们来瞧瞧虚拟输入的产生过程,根据图4.5.10所示,仿真对象的左边只有 Start_Sig信号与Done_Sig信号而已,然而第37~53行的激励内容则是负责这些信号的基本输入还有反馈输入。
我们知道好罗功能模块有仿顺序建模的外形,因此虚拟输入程仅是在步骤0(第48行),单纯地拉高 Start_Sig信号(基本输入),接着又根据 Done_Sig信号的反馈结果(第47行)拉低 Start_Sig信号(反馈输入)。步骤0完成后,i递增以示下一个步骤,然后虚拟输入就结束过程(第47行)。
第57~69行是虚拟输出的激励内容 ... 在此,笔者先举例反馈输出最简单的方法。第60行是寄存器RdData的复位操作,根据图4.5.9所示,RdData是虚拟引脚给仿真对象的输入链接,可是仿真环境又没有实际的链接,结果用reg替代。第63~69行是虚拟输出的激励内容,笔者采用最简单的 case ... endcase 用作反馈输出。根据硬件C的基本功能:
(一)如果WrData的输入数据是 8’hAA,RdData就反馈输出数据8’hBB;
(二)如果WrData的输入数据是 8’hCC,RdData就反馈输出数据8’hDD;
(三)如果WrData的输入数据是 8’hEE,RdData就反馈输出数据8’hFF;
结果第63~69行完全对应上述要求。
图4.5.11 exp17的仿真结果。
图4.5.11 是 hello_funcmod_simulation 的仿真结果,其中就有光标C0~C4分别指向各种过程,为了清晰wave界面笔者也稍微根据环境布局分类信号,结果如图4.5.11所示。
C0指向整个激励过程的开始,首先虚拟输入会拉高Start_Sig信号,然后就停留在原步等待仿真对象反馈完成信号(注意最下面的指向信号i)。
C1指向仿真对象开始操作的时间点,此刻仿真对象会经由 WrData信号输出数据 8’hAA。在下一个时钟,虚拟输出就会根据WrData的输出结果,再经由RdData信号反馈数据8’hBB;又在下一个时钟,仿真对象接受反馈数据以后将i递增以示下一个步骤(注意仿真对象的指向信号SQ_i)。
C2指向仿真对象经由 WrData信号输出第二个数据8’hCC,然而虚拟输出也再下一个时钟接收然后产生经由RdData反馈数据8’hDD。又在下一个时钟,仿真对象接收反馈数据8’hDD以后,将i递增以示下一个步骤。
C3指向仿真对象正在发送第三个数据的时候,此刻仿真对象会经由 WrData信号输出数据8’hEE,虚拟输出则再下一个时钟接收数据8’hEE并且产生反馈数据 8’hFF。过后,仿真对象将在下一个时钟接收反馈数据 8’hFF,然后将i递增以示下一个步骤。C4则是指向仿真对象产生完成信号产生的时候,Done_Sig信号是用来通知虚拟输入一次性的操作过程已经圆满结束。
因此,虚拟输入在下一个时钟接收到 Done_Sig信号以后,立即拉低 Start_Sig信号(反馈输入)表示结束使能仿真对象,并且将i递增递增以示下一个步骤,此刻,指向信号i的未来值成为1。
65. 8'hAA: RdData = 8'hBB; 66. 8'hCC: RdData = 8'hDD; 67. 8'hEE: RdData = 8'hFF;
hello_funcmod_simulation的仿真结果大致上就是这种感觉,有些同学可能会非常好奇为什么代码的第65~67行是用 = 赋值操作符,而不是 <= 负值操作符。其实,这是笔者的小习惯,硬件在理想的状态下当然是有多快就多快将输出反馈出去 ... 结果而言, = 产生的即时值相比 <= 产生的未来值,即时值比较快。
这个小节,笔者除了解释3种仿真模型以外,笔者也简单举例一下仿真模型③的使用方法,此外笔者也顺便演示一下反馈输出的产生过程。不过很遗憾的是,指向信号SQ_i在此只是用来表示仿真对象的内部过程而已,并没有实际参与虚拟输出的活动。事实上,硬件C在exp17的功能要求还是非常简单的程度,所以指向信号SQ_i用不着派上用场。往后笔者会继续提高硬件C功能要求,以致虐待读者直到需要它 ... 嘻嘻嘻。
4.6 仿真模型与反馈输出②
笔者在上一个章节除了演示仿真模型③的用法以外,笔者也简单举例反馈输出的产生过程。这个章节,我们会继续未完的故事,并且好好认识一下,环境布局(仿真环境的结构性),指向信号,还有仿真模型之间是如何合作无间,以致满足虚拟硬件不断提高的功能要求。
图4.6.1 exp17的仿真模型③。
图4.6.2 exp17等价的仿真模型③。
前情提要,我们需要创建一座仿真环境用作测试哈罗功能模块与硬件C之间的沟通,结果如图4.6.1所示。不过,笔者不建议创建完成的虚拟硬件,为此笔者选择等价的替代方法,如图4.6.2所示,笔者仅在激励内容模拟硬件C的部分功能而已。此外,硬件C的第一次功能要求如下:
(一)接收数据 8’hAA,反馈数据 8’hBB;
(二)接收数据 8’hCC,反馈数据 8’hDD;
(三)接收数据 8’hEE,反馈数据 8’hFF;
由于第一次的功能要求过于简单,所以指向信号SQ_i没有出场的机会。结果当天晚上,SQ_i跑来向笔者哭诉说它是多么期待当天的演出。无奈之下,笔者必须不断提高硬件C的功能要求,以致满足指向信号 SQ_i的心愿为止。硬件C第二次的功能要求如下:
(一)第一次接收数据 8’hAA, 反馈数据 8’hBB;
(二)第二次接收数据 8’hAA,反馈数据 8’hDD;
(三)第三次接收数据 8’hAA,反馈数据 8’hFF;
因此如此,我们需要重新修改一下 exp17的内容,打开exp18:
hello_funcmod.v
1. module hello_funcmod 2. ( 3. input CLOCK, RESET, 4. input Start_Sig, 5. output Done_Sig, 6. output[7:0]WrData, 7. input [7:0]RdData, 8. output [3:0]SQ_i 9. ); 10. /*************************/ 11. 12. reg [3:0]i; 13. reg [7:0]rData; 14. reg isDone; 15. 16. always @ ( posedge CLOCK or negedge RESET ) 17. if( !RESET ) 18. begin 19. i <= 4'd0; 20. rData <= 8'd0; 21. isDone <= 1'b0; 22. end 23. else if( Start_Sig ) 24. case( i ) 25. 26. 0: 27. begin rData <= 8'hAA; i <= i + 1'b1; end 28. 29. 1: 30. if( RdData == 8'hBB ) begin i <= i + 1'b1; end 31. 32. 2: 33. begin rData <= 8'hAA; i <= i + 1'b1; end 34. 35. 3: 36. if( RdData == 8'hDD ) begin i <= i + 1'b1; end 37. 38. 4: 39. begin rData <= 8'hAA; i <= i + 1'b1; end 40. 41. 5: 42. if( RdData == 8'hFF )begin i <= i + 1'b1; end 43. 44. 6: 45. begin isDone <= 1'b1; i <= i + 1'b1; end 46. 47. 7: 48. begin isDone <= 1'b0; i <= 4'd0; end 49. 50. endcase 51. 52. /*************************/ 53. 54. assign Done_Sig = isDone; 55. assign WrData = rData; 56. 57. /*************************/ 58. 59. assign SQ_i = i; 60. 61. /*************************/ 62. 63. endmodule
上面代码是根据硬件C第二次功能要求修改的结果,如代码 hello_funcmod 所示,步骤0,步骤2与步骤4,WrData的输出结果全为 8’hAA。
hello_funcmod_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module hello_funcmod_simulation(); 3. 4. reg CLOCK; 5. reg RESET; 6. reg Start_Sig; 7. reg [7:0]RdData; 8. wire [7:0]WrData; 9. wire Done_Sig; 10. wire [3:0]SQ_i; 11. 12. /***********************************/ 13. 14. initial 15. begin 16. RESET = 0; #10; RESET = 1; 17. CLOCK = 1; forever #5 CLOCK = ~CLOCK; 18. end 19. 20. /***********************************/ 21. 22. hello_funcmod U1 23. ( 24. .CLOCK(CLOCK), 25. .RESET(RESET), 26. .Start_Sig(Start_Sig), 27. .Done_Sig(Done_Sig), 28. .WrData(WrData), 29. .RdData(RdData), 30. .SQ_i( SQ_i ) 31. ); 32. 33. /***********************************/ 34. 35. reg [3:0]i; 36. 37. always @ ( posedge CLOCK or negedge RESET ) 38. if( !RESET ) 39. begin 40. i <= 4'd0; 41. Start_Sig <= 1'b0; 42. end 43. else 44. case( i ) 45. 46. 0: 47. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 48. else begin Start_Sig <= 1'b1; end 49. 50. 1: 51. begin i <= i; end 52. 53. endcase 54. 55. /***********************************/ 56. 57. reg [3:0]j; 58. 59. always @ ( posedge CLOCK or negedge RESET ) 60. if( !RESET ) 61. begin 62. RdData <= 8'd0; 63. j <= 4'd0; 64. end 65. else 66. case( j ) 67. 68. 0: 69. if( WrData == 8'hAA ) begin RdData = 8'hBB; j <= j + 1'b1; end 70. 71. 1: 72. if( WrData == 8'hAA ) begin RdData = 8'hDD; j <= j + 1'b1; end 73. 74. 2: 75. if( WrData == 8'hAA ) begin RdData = 8'hFF; j <= j + 1'b1; end 76. 77. 3: 78. j <= j; 79. 80. endcase 81. 82. /***********************************/ 83. 84. endmodule
同样,根据硬件C的第二次功能要求,激励文本的虚拟输出也作出相关的修改。首先让我们好好思考一下,硬件C的第二次功能要求都是根据接收结果8’hAA作出反应,然而比较麻烦的是,数据8’hAA有分为第一次,第二次还有第三次。为此,相似 epx17当中的 case ... endcase 用法实在不妥,因此我们必须采取其它手段。
如代码第68~80行所示,笔者应用仿顺序操作的用法模板,然后再根据步骤将反馈数据RdData按次序作出3次输出。先是步骤0检测 WrData信号是否 8’hAA?是就反馈数据 8’hBB,然后将j递增以示下一个步骤,同样行为也发生在步骤1与步骤2的身上
。此外,笔者也事先作好保险措施,将RdData 改为即时值(注意赋值操作符)。万事虽然已经具备,不过第68~80行的虚拟输出是否可以发挥预期的效果,说实在笔者真心不知道 ...
图4.6.3 exp18的仿真结果。
图4.6.3是 hello_funcmod_simulation 的仿真结果。啊!我的天呀 ... 读者看见什么嘛?如图4.6.3所示,如果Done_Sig信号没有拉高就表示激励过程失败了,亦即图4.6.3不是预期所要的仿真结果,究竟问题是发生在哪里呢?让我们一起瞧瞧吧。
C0指向整体激励的开始,此刻虚拟输入拉高Start_Sig以示仿真对象开始工作。C1指向仿真对象开始工作的时候,仿真对象现在步骤0经由WrData信号输出结果8’hAA,然后将i递增以示下一个步骤。在下一个时钟(C2指向的地方),虚拟输出接收结果并且经由RdData反馈数据 8’hBB,然后将j递增以示下一个步骤。
C3指向的地方,恰好是仿真对象停留在步骤1的时候,此刻仿真对象读取 RdData的过去值为8’hBB,if条件成立,然后将i递增以示下一个步骤。那么问题发生了!由于
WrData的输出结果始终是 8’hAA,再同样的时刻(C3指向的地方)虚拟输出步骤2的if条件也成立了,因为它读到 WrData的过去值是 8’hAA,结果它经由 RdData信号反馈数据 8’hDD,并且将j递增以示下一个步骤。
C4指向的地方是仿真对象停在步骤2的时候,此刻仿真对象还在准备经由 WrData信号更新结果而已,由于WrData之前还有之后的结果都一样,所以波形图上的 WrData没有发生任何变化,不过实际上仿真对象确实已经将它更新。完后,仿真对象将i递增以示下一个步骤。糟了糟了,问题像雪球越滚越大了。
在同一个时候(C4指向的地方),虚拟输出判断 WrData的过去值为 8’hAA,因此反馈数据 8’hFF,并且将j递增以示下一个步骤。C5指向的地方是也是仿真对象停留在步骤3的时候,此刻仿真对象还在傻傻等待接收反馈数据 8’hDD,但它不知道反馈数据 8’hDD老早已经跑去火星,亦即它已经错过数据 8’hDD。呜呜呜 ... 就这样,仿真对象永远都傻傻般停留在步骤3等待反馈数据 8’hDD的到来 ...
好奇的同学可能会这样问道:“是不是仿真对象还有虚拟输出之间出现不协调的时序沟通?结果数据接收错乱了?”的确如此 ... 好奇的同学可能又会反问道:“这起意外究竟是谁的错?仿真对象,还是虚拟输出呢?”类似问题我们不能随便妄下定论,实际上是谁都没错,是谁都有责任 ...
这种情况就像发生车祸一般,当务之急不是相互指着是非而是优先送急伤者才是。虚拟输出相比仿真对象,虚拟输出更像是受伤的一方,我们会优先修改虚拟输出,再者才考虑仿真对象。在此,仿真对象内部的指向信号——SQ_i它就派上用场了,前几回由于SQ_i没有出演机会,它整天都是以泪洗脸,此刻就是它大发光彩的时候,好让累积许久的能量一次性爆发出来。因此,请打开 exp19:
哈罗功能模块再 exp19当中没有发生任何修改,所以笔者就不重复了。
hello_funcmod_simulation.vt
1. `timescale 1 ps/ 1 ps 2. module hello_funcmod_simulation(); 3. 4. reg CLOCK; 5. reg RESET; 6. reg Start_Sig; 7. reg [7:0]RdData; 8. wire [7:0]WrData; 9. wire Done_Sig; 10. wire [3:0]SQ_i; 11. 12. /***********************************/ 13. 14. initial 15. begin 16. RESET = 0; #10; RESET = 1; 17. CLOCK = 1; forever #5 CLOCK = ~CLOCK; 18. end 19. 20. /***********************************/ 21. 22. hello_funcmod U1 23. ( 24. .CLOCK(CLOCK), 25. .RESET(RESET), 26. .Start_Sig(Start_Sig), 27. .Done_Sig(Done_Sig), 28. .WrData(WrData), 29. .RdData(RdData), 30. .SQ_i( SQ_i ) 31. ); 32. 33. /***********************************/ 34. 35. reg [3:0]i; 36. 37. always @ ( posedge CLOCK or negedge RESET ) 38. if( !RESET ) 39. begin 40. i <= 4'd0; 41. Start_Sig <= 1'b0; 42. end 43. else 44. case( i ) 45. 46. 0: 47. if( Done_Sig ) begin Start_Sig <= 1'b0; i <= i + 1'b1; end 48. else begin Start_Sig <= 1'b1; end 49. 50. 1: 51. begin i <= i; end 52. 53. endcase 54. 55. /***********************************/ 56. 57. always @ ( posedge CLOCK or negedge RESET ) 58. if( !RESET ) 59. begin 60. RdData <= 8'd0; 61. end 62. else 63. case( SQ_i ) 64. 65. 0: RdData = 8'hBB; 66. 2: RdData = 8'hDD; 67. 4: RdData = 8'hFF; 68. 69. endcase 70. 71. /***********************************/ 72. 73. endmodule
exp19的激励文本,笔者稍微修改了一下第57~69行的虚拟输出。笔者抛弃步骤j,取而代之就是使用指向仿真对象内部过程的信号SQ_i。exp19的虚拟输出与exp17的虚拟输出有点相似,不过 case ... endcase 之间的判断信号是 SQ_i。我们知道仿真对象在步骤0的时候发送第一次数据8’hAA,结果第65行是针对步骤0的反馈操作,RdData赋值为8’hBB;仿真对象在步骤2发送第二次数据8’hAA,所以虚拟输出再第66行为RdData赋值 8’hDD;仿真对象在步骤4发送第三次数据 8’hAA,因此虚拟输出再67行为RdData赋值8’hFF。
完后再让我们仿真一次看看:
图4.6.4 exp19的仿真结果。
图4.6.4是exp19的仿真结果 ... 如图4.6.4所示,Done_Sig信号可以完美输这就表示仿真结果已经符合预期的期待。C0指向的地方是整个激励过程的开始,此刻虚拟输入开始拉高Start_Sig以示使能仿真对象。在同一时刻,虚拟输出也产生反应,然后立即输出反馈数据 8’hBB。
C1指向的地方是仿真对象开始操作的时候,此刻步骤对象在步骤经由WrData信号输出8’hAA。由于仿真对象还是停留在步骤0(注意SQ_i的过去值),因此虚拟输出没有产生任何变化。C2指向仿真对象停留在步骤1的时候,仿真对象检测 RdData的过去值,结果满足if条件,然后将i递增以示下一个步骤。
C3指向的地方是仿真对象停留在步骤2的时候,此刻仿真对象决定输出第二次输出8’hAA,完后将i递增以示下一个步骤。再同一个时刻,虚拟输出检测SQ_i的过去值是2,结果产生反应输出反馈数据 8’hDD。
C4指向的地方却是仿真对象停留在步骤3的时候,此刻它检测RdData的过去值为 8’hDD,因此if条件成立,i递增以示下一个步骤。之余虚拟输出则没有什么活动发生。
C5指向的地方是仿真对象停留在步骤4的时候, 此刻他决定为信号WrData输出 8’hAA,然后再将i递增以示下一个步骤。再同一个时刻,虚拟输出检测 SQ_i的过去值为4,它产生反应并且输出反馈数据 8’hFF。
C6指向的地方是仿真对象停留在步骤4的时候,此刻它检测到 RdData的过去值为 8’hFF,事后将i递增以示下一个步骤。换之,此刻的虚拟输出正闲着没事干。至于C7与C8指向的地方则是仿真对象的步骤6~7,并且也是完成信号产生的步骤(注意SQ_i的过去值)。虚拟输入再C8指向的时刻检测 Done_Sig的过去值是1,因此它决定拉低Start_Sig(反馈输入)不使能方针对象,然后再将i递增以示下一个步骤。
虽然虚拟输出再C8之后的时钟里还在根据SQ_i结果产生反应,不过这是不管紧要的小,因为我们已经确定在C0~C8之间,仿真对象可以正确的完成一次性的操作,这样就已经足够了。在此,读者是否渐渐发觉指向信号SQ_i是如何巧妙描述硬件C的部分功能呢?总结exp17~19的仿真结果,我们可以结论道 ...
虽然虚拟输出可以根据仿真对象输出的 WrData产生相关的反馈输出,不过这是一种非常被动的方式,而且方法也有诸多局限,exp18就是最好的证明,因为exp18无法实现硬件C的第二次功能要求。为此,仿真对象内部的指向信号——SQ_i就会凸显自己的重要性,SQ_i除了指向过程以外,SQ_i也表示仿真对象当前的操作状态,结果我们可以根据SQ_i指向的内部状态实装虚拟输出,让它成为描述虚拟硬件的原材料。
除了SQ_i以外,仿真对象的使能信号 Start_Sig也能成为描述虚拟硬件的原材料,就让笔者稍微扩充一下 exp19的虚拟输出:
57. always @ ( posedge CLOCK or negedge RESET ) 58. if( !RESET ) 59. begin 60. RdData <= 8'd0; 61. end 62. else if( Start_Sig ) 63. case( SQ_i ) 64. 65. 0: RdData = 8'hBB; 66. 2: RdData = 8'hDD; 67. 4: RdData = 8'hFF; 68. 69. endcase
大致的感觉如上述代码所示,笔者在第62行用Start_Sig信号为虚拟输出添加一行if条件,就这样 ... 虚拟硬件也有使能的功能了。
虽然章节4.5与4.6我们都是在谈论仿真模型还有反馈输出的故事而已,不过眼睛犀利的朋友,隐约可以察觉这一切的一切必须是仿真环境拥有结构性作为前提,此外仿真对象还有仿真过程的结构性也很重要,不然的话我们就不能轻易应用仿真模型还有实现反馈输出了。
总结
不知不觉之间笔者已经将第四章搞定了,这个章节算是仿真的第二核心吧。第四章的一开始我们就在讨论验证语言的定义,如果没有深入一切,而且一切又是任由传统流派道听途说,结果我们就会觉得验证语言似乎很难很强大。事实上,验证语言只是占据仿真的小部分而已,而且常用的验证语言不过也是小猫两三只而已。我们真是被传统流派吓倒了。
根据笔者的认识,建模(实际建模)还有仿真(虚拟建模),本是同根,差异就有概念(环境)而已。实际上,两者可以共享同样的手段还有思维,建模虽然拥有实际的输入还有输出,不过建模会受限于实际环境。反之,仿真虽然没有物理的局限性,但是仿真必须模拟实际环境中存在的东西,如时钟信号还有复位信号就是最好的例子。
为此,我们必须使用验证语言模拟时钟信号还有复位信号,从这点上 ... 我们就可以看出验证语言的作用不过是仿真的补助性行为而已,绝对没有传统流派所说般那样严重。不过真正让人头疼的问题不是验证语言的使用方法,而是如何展示验证语言的时序表现。传统流派的仿真手段非常倾向调试风格之余,它们也非常依赖仿真时间 ... 实际上,这种仿真手段还是停留在门级模块的测试而已。
这种仿真手段不仅有自身局限性,也不怎么适合功能操作复杂的仿真对象。同样,这种仿真手段其实也是抹杀时序表现最大的凶手,因而产生豆浆不是豆浆,时序不是时序的问题。有人可能会问,时序有没有表现真的那么重要吗?时序失去如果时序表现,时序终究再也不是时序,而是一副没有意义的白纸而已,因为“精华”早已不复存在,这种感觉好似没有味道的豆浆一样。
系统函数,还有预处理占据验证语言的大多数,然而真正对仿真有用的关键字用5只手指也能表示出来。一般上笔者不怎么推荐过度学习验证语言,因为许多验证语言不仅没有实际的用处,而且又难学,此外还有验证语言是在重复综合语言的功能而已。常规上,验证语言的本质是非常接近顺序语言,为此过多使用它们很难让我们适应仿真应有的并行模式。
笔者虽然讨厌验证语言,不过我们还是需要最小程度利用它们。学习验证语言的关键就是理解它们的时序表现,换句话说就是如何使用时钟控制它们而且不然它们暴走。验证语言有没有时序表现,其实这是非常主观的问题,笔者认为有是因为笔者有追求美味豆浆的原始冲动,至于他人就见仁见智了。
此外,我们还有谈论激励文本的布局,亦即仿真环境的结构性。布局其实是一种自然艺术,好的环境布局就会产生正面发的效果,反之亦然。理解笔者的读者自然不会觉得奇怪,因为笔者是一位重视结构至死的男人,笔者认为前期有好建模,后期就有好仿真,此外笔者也一样重视仿真对象的结构性还有仿真过程的结构性。为什么笔者会如此强调结构的重要性呢?
仿真环境的结构性也好,仿真过程的结构性也好,这一切一切都是为了支撑仿真模型。
根据笔者的理解,仿真模型有3种,其一是最基础也是门级模块仿真应用最多的模型。仿真模型②相较仿真模型①拥有更大的功能扩张,期间结构性的支撑也会略显重要。最后一个仿真模型也是仿真模型③。它的诞生是为了仿真虚拟硬件,而且它也能兼容前面两个仿真模型。仿真模型③是非常讲究结构性的仿真模型,如果仿真环境,仿真对象,还有仿真过程,其中一放缺少结构都有可能失撑仿真模型③。