本文主要给大家介绍如何在仿真中使用fork…join。首先,它的使用格式如下所示:
always fork ... join /*或者*/ initial fork ... join
可以看到在仿真中fork…join使用方法和begin…end一样。不同的是,begin…end是将语句块中的语句按给定次序顺序执行,而fork…join是将语句块中的语句并行执行。同时要注意,begin…end可以用在综合中,但是fork…join在综合中不支持使用。下面将就fork…join的原理和使用进行介绍。
1. 过程语句initial和always
在介绍fork…join语句之前,需要先复习Verilog中的两种结构化过程语句(structed procedure statement):always和initial。这两个语句是行为建模中最基本的两个语句,所有其他的行为语句只能出现在这些结构化的过程语句中。always和initial都是在0时刻执行。always和initial都是独立执行的,并且不可以相互或自身嵌套。
一般在always和initial之后可以接一条行为陈述语句,如果需要接多个行为陈述语句,必须将其分组,通常是通过使用begin…end进行分组。begin…end有些类似于C语言中的 “ { } ”。本文给大家介绍一种另外的分组方法:通过fork…join分组。
2. fork…join原理
由关键字fork…join指定的并行执行的过程语句有以下的特点:
- fork…join中所有的并行块语句都是同时执行
- 语句执行顺序由分配给每个语句的延迟或事件控制(event control)来控制
- 如果指定了延迟或事件控制,则它与进入块的时间有关
注意顺序块(begin…end)和并行块(fork…join)之间的根本区别。 并行块中的所有语句都在进入块时一齐开始执行。 因此,语句在并行块中的写入顺序并不重要。
第2点中事件控制举例如下:
@(posedge clock) q = d; //q = d 在信号clock执行时执行 always @( reset, clock, d) q = d; //q= d在reset,clock或是d为高电平时执行
3. fork…join和begin…end相互转换及混合使用
-
begin…end语句
begin…end语句块中的语句按顺序方式执行。每条语句中的延迟值与其前面的语句执行的模拟时间相关。一旦顺序语句执行结束,跟随顺序语句的下一条语句继续执行,如例1所示。
例1:
begin #2 pulse = 1; #5 pulse = 0; #3 pulse = 1; #4 pulse = 0; #2 pulse = 1; #5 pulse = 0; end
如果顺序语句块在时刻0开始执行,那么2个时间单位过去后,pulse = 1,再过5个时间单位,pulse = 0 … 其对应波形如图1所示:
图1
如果顺序语句块在时刻5开始执行,那么波形会变为如图2所示:
图2
-
fork…join语句
fork…join语句块中的各语句并行执行。fork…join语句块中各条语句指定的延迟值都与fork…join语句块开始执行的时间相关。使用fork…join语句实现例1中同样的波形,如例2所示。
例2:
fork #2 pulse = 1; #7 pulse = 0; #10 pulse = 1; #14 pulse = 0; #16 pulse = 1; #21 pulse = 0; join
如果顺序语句块在时刻0开始执行,那么2个时间单位过去后,pulse = 1,再过5个时间单位,pulse = 0 … 其对应波形如图3所示,与图1一致:
图3
如果顺序语句块在时刻5开始执行,那么波形会变为如图4所示,与图2一致:
图4
-
混用begin…end和fork…join语句
注意fork…join和begin…end可以相互或自身嵌套,在fork…join语句块内是并行执行的规则,在begin…end语句块内是顺序执行的规则,如例3所示。
例3:
always begin:Block_A #4 a = 5; //S1 fork:Blcok_B //S2 #6 b = 7; //P1 begin:Block_C //P2 d = c; //SS1 #5 e = d; //SS2 end #2 f = 3; //P3 #4 g = 2; //P4 #8 h = 4; //P5 join #8 m = 1; //S3 #2 n = 3; //S4 #10 $stop; //S5 end
注意例3中每一行语句都被注释了名字,begin…end中的顺序语句被命名为Sequential:S1-S5,SS1-SS2。fork…join中的并行语句被命名为Parallel: P1-P5。
在观察其结果之前,先分析一下这段代码的结构:Block_A是由begin…end引导,所以S1-S5都是顺序执行,即先执行S1,再执行S2…最后执行S5。Block_B是由fork…join引导,所以P1-P5会在进入Block_B后一齐执行。Block_C由begin…end引导,SS1,SS2也是顺序执行的,所以SS1执行完后,才会执行SS2。
例3中语句执行的时间线如图5所示:
图5
因为例3是在always过程下的语句,所以进入always块后,时刻是0。S1最先执行,所以在第4个时间单位a = 5。顺序执行的S2是fork…join语句,所以之后的P1-P5都是同时执行,按照延时的不同,
- P1: b = 7在第10(4 + 6)个时间单位执行;
- P2: SS1和SS2是顺序执行的,所以,SS1先执行:d = c在第4(4 + 0)个时间单位执行,SS2:e = d在第9(4 + 5)个时间单位执行;
- P3: f = 3 在第6(4 + 2)和时间单位执行;
- P4: g= 2 在第8(4 + 4)和时间单位执行;
- P5: h= 4 在第12(4 + 8)和时间单位执行。
在S2执行完后(按最后执行的第12个时间单位开始计算),S3:m = 1在第20(12 + 8)个时间单位执行,S4:n = 3在第22(20 + 2)个时间单位执行,S5:$stop在第32(22 + 10)个时间单位执行。
4. fork…join使用时会产生竞争(race condition)
fork…join语句块提供了并行执行语句的机制。 但是,如果影响同一变量的两个语句并行执行,可能会出现隐式竞争, 如例4所示。
例4:
reg x, y; reg x_or_y, x_and_y; initial fork x = 1'b0; //P1 y = 1'b1; //P2 x_or_y = x | y; //P3 x_and_y = x & y;//P4 join
由于fork…join中的语句在initial过程下,所以进入initial块后,时刻是0。fork…join语句中P1-P4都是并行执行的。在仿真时,fork…join语句块中的所有语句都会立即执行。 然而,实际上,运行模拟的 CPU 一次只能执行一条语句。如果P1和P2先执行,P3和P4后执行,那么x_or_y = 1’b1, x_and_y = 1’b0;如果P3和P4先执行,P1和P2后执行,那么x_or_y 和 x_and_y 的值都是1’bx。因此,x_or_y 和 x_and_y 的结果是不确定的,并且取决于模拟器的实现。不同的模拟器可能以不同的顺序执行并行语句。 因此,竞争是源于现在模拟器的限制,而不是 fork…join 块的限制。
5. 总结
借用线程的理念,关键字 fork 可以被视为将单个线程拆分为独立并行的线程。 关键字 join 可以看作是将独立并行的线程连接回单个线程, 在fork…join语句块之间的语句就都是并行执行的。使用fork…join的时候可与begin…end相互对照。在仿真的设计中,可以有多种方法实现激励,描述方式也各不相同,但是最终都可以达到同一个目的。
6. 参考文档
- Verilog HDL: A Guide to Digital Design and Synthesis
- Verilog HDL入门(夏宇闻)