高级且易于使用的同步和通信机制对于控制用于建模复杂系统或高反应性测试台的动态过程之间发生的各种交互至关重要。
基本的同步机制是命名的事件类型,以及事件触发器和事件控制结构(即->和@)。这种类型的控件仅限于静态对象。它足以在硬件级别和简单系统级别进行同步,但不能满足高度动态、反应性测试台的需求。
SystemVerilog还提供了一套强大且易于使用的同步和通信机制,这些机制可以动态创建和回收。该集合包括一个信号量内置类和一个邮箱内置类,前者可用于同步和互斥共享资源,后者可用作进程之间的通信通道。
信号量和邮箱是内置类型;尽管如此,它们还是类,可以用作派生其他更高级别类的基类。这些内置类位于内置的std包中(请参阅26.7);因此,它们可以由任何其他范围内的用户代码重新定义。
从概念上讲,信号量是一个bucket。当分配信号量时,会创建一个包含固定数量键的bucket。
使用信号量的进程必须首先从bucket中获取密钥,然后才能继续执行。如果一个特定的进程需要一个密钥,则只能同时进行该进程的固定次数。所有其他钥匙都必须等待,直到有足够数量的钥匙返回到bucket。信号量通常用于互斥、对共享资源的访问控制和基本同步。
创建信号量的示例如下:
semaphore smTx;
Semaphore是一个内置类,提供以下方法:
--使用指定数量的键创建信号量:new()
--从bucket:get()中获取一个或多个密钥
--将一个或多个密钥返回到 bucket: put( )
--尝试在不阻塞的情况下获取一个或多个密钥:try_get()
信号量使用new()方法创建。new()函数的原型如下所示:
function new (int keyCount =0)
keyCount指定最初分配给信号量bucket的键数。当向信号量中放入的键多于移除的键时,bucket中的键数可能会增加到keyCount以上。keyCount的默认值为0。new()函数返回信号量句柄。
信号量put()方法用于返回信号量的键。put()的原型如下:
function void put(int keyCount = 1);
keyCount指定返回到信号量的键数。默认值为1。当semaphore.put()函数被调用时,指定数量的键被返回到信号量。如果进程已挂起等待密钥,则如果返回了足够的密钥,则应执行该进程。
信号量get()方法用于从信号量中获取指定数量的键。get()的原型如下:
task get(int keyCount = 1);
keyCount指定从信号量中获取所需的键数。默认值为1。如果指定数量的键可用,则方法返回并继续执行。如果指定数量的密钥不可用,则该过程将停止,直到密钥可用为止。信号量等待队列是先进先出(FIFO)。这并不能保证进程到达队列的顺序,只是信号量将保留它们的到达顺序。
信号量try_get()方法用于从信号量中获取指定数量的密钥,但没有阻塞。try_get()的原型如下:
function int try_get(int keyCount = 1);
keyCount指定从信号量中获取所需的键数。默认值为1。如果指定数量的键可用,则该方法返回一个正整数并继续执行。如果指定数量的键不可用,则该方法返回0。
邮箱是一种允许在进程之间交换消息的通信机制。数据可以由一个进程发送到邮箱,也可以由另一个进程检索。
从概念上讲,邮箱的行为类似于真实的邮箱。当一封信被投递并放入邮箱时,一个人可以取回这封信(以及其中存储的任何数据)。但是,如果在检查邮箱时信件尚未送达,则人员必须选择是等待信件还是在随后前往邮箱时取回信件。类似地,SystemVerilog的邮箱提供了以可控方式传输和检索数据的过程。邮箱被创建为具有有界或无界队列大小。当限定邮箱包含限定数量的邮件时,该邮箱将变满。尝试将邮件放入已满邮箱的进程应暂停,直到邮箱队列中有足够的空间可用。无边界邮箱在发送操作中从不挂起线程。
创建邮箱的示例如下:
mailbox mbxRcv;
Mailbox是一个内置类,提供以下方法:
--创建邮箱:new()
--将邮件放入邮箱:put()
--尝试在不阻塞的情况下将邮件放入邮箱:try_put()
--从邮箱检索邮件:get()或peek()
--尝试在不阻塞的情况下从邮箱检索邮件:try_get()或try_peek()
--检索邮箱中的邮件数:num()
邮箱是使用new()方法创建的。邮箱new()的原型如下:
function new(int bound = 0);
new()函数返回邮箱句柄。如果绑定参数为0,则邮箱是无边界的(默认值),并且put()操作永远不会阻塞。如果bound为非零,则表示邮箱队列的大小。绑定应为正。负边界是非法的,可能会导致不确定的行为,但实现可能会发出警告。
邮箱中的邮件数可以通过num()方法获得。num()的原型如下:
function int num();
num()方法返回邮箱中当前的邮件数。返回的值应该小心使用,因为它只有在邮箱上执行下一个get()或put()之前才有效。这些邮箱操作可以来自不同于执行num()方法的进程。因此,返回值的有效性取决于其他方法开始和结束的时间。
put()方法将消息放入邮箱中。put()的原型如下:
task put( singular message);
消息是任何单数表达式,包括对象句柄。put()方法以严格的FIFO顺序将消息存储在邮箱中。如果邮箱是用有界队列创建的,则应暂停该过程,直到队列中有足够的空间为止。
try_put()方法尝试将邮件放入邮箱。try_put()的原型如下:
function int try_put( singular message);
消息是任何单数表达式,包括对象句柄。try_put()方法以严格的FIFO顺序将消息存储在邮箱中。此方法仅对有界邮箱有意义。如果邮箱未满,则指定的邮件将被放置在邮箱中,函数将返回一个正整数。如果邮箱已满,则该方法返回0。
get()方法从邮箱中检索消息。get()的原型如下:
task get( ref singular message );
消息可以是任何单数表达式,并且应该是有效的left-hand 表达式。get()方法从邮箱中检索一条消息,即从邮箱队列中删除一条消息。如果邮箱为空,则当前进程将阻塞,直到邮件被放入邮箱。如果消息变量的类型与邮箱中的消息类型不等效,则会出现运行时错误生成。
非参数邮箱是无类型的(见15.4.9),也就是说,单个邮箱可以发送和接收不同类型的数据。
因此,除了发送的数据(即消息队列)外,邮箱实现还应维护put()放置的消息数据类型。这是启用运行时类型检查所必需的。
邮箱等待队列为FIFO。这并不保证进程到达队列的顺序,只是邮箱应保留其到达顺序。
try_get()方法尝试在不阻塞的情况下从邮箱检索邮件。try_get()的原型如下:
function int try_get( ref singular message );
消息可以是任何单数表达式,并且应该是有效的 left-hand 表达式。try_get()方法尝试从邮箱中检索一条消息。如果邮箱为空,则该方法返回0。如果消息变量的类型与邮箱中消息的类型不等效,则该方法返回一个负整数。如果消息可用,并且消息类型与消息变量的类型等效,则检索该消息,并且该方法返回一个正整数。
peek()方法从邮箱中复制消息,而不从队列中删除该消息。peek()的原型如下:
task peek( ref singular message );
消息可以是任何单数表达式,并且应该是有效的left-hand表达式。peek()方法从邮箱中复制一条消息,而不从邮箱队列中删除该消息。如果邮箱为空,则当前进程将阻塞,直到邮件被放入邮箱。如果消息变量的类型与邮箱中的消息类型不等效,则会生成运行时错误。调用peek()方法也可能导致一条消息取消阻塞多个进程。只要邮件保留在邮箱队列中,在peek()或get()操作中被阻止的任何进程都将被取消阻塞。
try_peek()方法尝试在不阻塞的情况下从邮箱复制邮件。try_peek()的原型如下:
function int try_peek( ref singular message );
消息可以是任何单数表达式,并且应该是有效的left-hand表达式。try_peek()方法尝试从邮箱中复制一条消息,而不从邮箱队列中删除该消息。如果邮箱为空,则该方法返回0。如果消息变量的类型与邮箱中消息的类型不等效,则该方法返回一个负整数。如果消息可用,并且其类型与消息变量的类型等效,则复制该消息,并且该方法返回一个正整数。
默认邮箱是无类型的,即单个邮箱可以发送和接收任何类型的数据。这是一种非常强大的机制,不幸的是,由于消息和用于检索消息的变量类型之间的类型不匹配(类型不等效),也可能导致运行时错误。通常,邮箱用于传输特定的消息类型,在这种情况下,在编译时检测类型不匹配非常有用。参数化邮箱使用与参数化类(请参见8.25)、模块和接口相同的参数机制:
mailbox #(type = dynamic_type)
其中dynamic_type表示启用运行时类型检查的特殊类型(默认值)。通过指定类型来声明特定类型的参数化邮箱:
typedef mailbox #(string ) s_mbox;
s_mbox sm=new;
string s;
sm.put("hello");
s.get(s); //s ->"hello"
参数化邮箱提供了与动态邮箱相同的所有标准方法:num(), new(), get(), peek(), put(), try_get(), try_peek(), try_put()。
通用(动态)邮箱和参数化邮箱之间的唯一区别是,对于参数化邮箱,编译器会验证对put、try_put、peek、try_peek、get和try_get方法的调用是否使用与邮箱类型等效的参数类型,以便编译器捕获所有类型不匹配的情况,而不是在运行时。
声明为event数据类型的标识符称为命名事件。可以显式触发命名事件。它可以在事件表达式中使用,以与9.4.2中描述的事件控制相同的方式控制程序语句的执行。命名事件也可以用作从另一个命名事件分配的句柄。命名事件为基础同步对象提供句柄。当进程等待触发事件时,该进程将被放入同步对象中维护的队列中。进程可以等待通过@运算符或使用wait()构造来触发命名事件
检查它们的触发状态。
通过激活具有语法15-1中给出的语法的事件触发语句,使事件发生。
event_trigger ::= // from A.6.5
-> hierarchical_event_identifier ;
| ->> [ delay_or_event_control ] hierarchical_event_identifier ;
语法15-1--事件触发器语法
通过更改事件控制表达式中命名事件数组的索引,不会使事件发生。通过->运算符触发的命名事件会取消阻塞当前正在等待该事件的所有进程。当被触发时,命名事件的行为就像一次触发,即触发状态本身是不可观察的,只有其效果。这类似于边沿可以触发触发器的方式,但无法确定边沿的状态,即(posedge clock)是否非法。
非阻塞事件是使用->>运算符触发的。->>运算符的作用是,该语句在不阻塞的情况下执行,并且在延迟控制到期或事件控制发生的时间内创建一个非阻塞的分配更新事件。此更新事件的作用应是在模拟周期的非阻塞分配区域触发参考事件。
等待事件被触发的基本机制是通过事件控制操作符@。
@ hierarchical_event_identifier;
@操作符会阻止调用进程,直到触发给定事件为止。对于解锁等待事件的进程的触发器,等待进程应在触发进程执行触发器运算符->之前执行@语句。如果触发器首先执行,那么等待过程将保持阻塞状态。
SystemVerilog可以将事件触发器本身(即时)与命名事件的触发状态(在整个时间步长内持续存在(即,直到模拟时间提前)区分开来。命名事件的触发内置方法允许用户检查此状态。
triggered()方法的原型如下:
function bit triggered();
如果给定事件在当前时间步长中被触发,则触发的方法评估为true(1'b1),否则评估为false(1'b0)。如果命名事件为null,则触发的方法返回false。触发器方法在等待构造的上下文中使用时最有用:
wait ( hierarchical_event_identifier.triggered )
使用此机制,无论等待是在触发操作之前执行还是在与触发操作相同的模拟时间执行,事件触发器都应解锁等待过程。因此,触发器方法有助于消除当触发器和等待同时发生时发生的常见竞争条件。阻塞等待事件的进程可能会取消阻塞,也可能不会取消阻塞,这取决于等待和触发进程的执行顺序。但是,无论等待和触发操作的执行顺序如何,等待触发状态的进程总是取消阻止。
event done,blast;//声明两个新的事件
event done_too=done;
task trigger (event ev);
->ev;
endtask
fork
@ done_too;
#1 trigger(done);
join
fork
->blast;
wait(blast.triggered);
join
示例中的第一个fork显示了两个事件标识符done和done_too如何引用同一个同步对象,以及如何将事件传递给触发该事件的通用任务。
在该示例中,一个进程通过done_too等待事件,而实际的触发是通过作为参数传递的触发器任务完成的。
在第二个fork中,一个进程可以在另一个进程(如果fork-join中的进程按源代码顺序执行)有机会执行之前触发事件blast,并等待事件发生。
尽管如此,第二个进程解除阻塞,fork终止。这是因为进程等待事件的触发状态,该状态在时间步长的持续时间内保持在其触发状态。
只有当表达式中的操作数(如触发方法的事件前缀)发生更改时,才会重新评估事件表达式或等待条件。这意味着在当前时间步长结束时,触发方法的返回值从1’b1更改为1’b0不会影响等待触发方法的事件控制或等待语句。
wait_order构造将挂起调用进程,直到所有指定的事件都按给定的顺序(从左到右)触发,或者任何未触发的事件都被无序触发,从而导致操作失败。
wait_order结构的语法如下语法15-2所示:
wait_statement ::= // from A.6.5
...
| wait_order ( hierarchical_identifier { , hierarchical_identifier } ) action_block
action_block ::=
statement _or_null
| [ statement ] else statement
Syntax 15-2—Wait_order event sequencing syntax (excerpt from Annex A)
wait_order为了成功,在序列的任何一点上,应按照规定的顺序触发后续事件,这些事件在该点上都应未触发,或者序列已经失败。之前的事件不限于只发生一次。换句话说,一旦事件按照规定的顺序发生,就可以再次触发,而不会导致构造失败。只有列表中的第一个事件可以等待持久触发的事件。构造失败时所采取的操作取决于是否指定了可选的action_block-else语句(fail语句)。如果指定了它,那么给定的语句将在构造失败时执行。如果未指定fail语句,则失败会生成运行时错误。
例如:
wait_order( a, b, c);
暂停当前进程,直到事件a、b和c按a–>b–>c的顺序触发。如果事件触发顺序不正确,则会生成运行时错误。
例如:
wait_order( a, b, c ) else $display( "Error: events out of order" );
在本例中,fail语句指定,在构造失败时,将显示用户消息,但是没有产生错误。
例如:
bit success;
wait_order( a, b, c ) success = 1; else success = 0;
在本例中,完成状态存储在变量success中,不会生成错误。
事件是一种独特的数据类型,具有几个重要属性。命名事件可以相互赋值。将一个事件赋值给另一个事件时,源事件和目标事件共享源事件的同步队列。从这个意义上说,事件是完全的变量,而不仅仅是标签。
当一个事件变量被赋值给另一个时,这两个变量就合并了。因此,对任一事件变量执行->都会影响等待任一事件变量的进程。
例如:
event a, b, c;
a = b;
-> c;
-> a; // also triggers b
-> b; // also triggers a
a = c;
b = a;
-> a; // also triggers b and c
-> b; // also triggers a and c
-> c; // also triggers a and b
合并事件时,赋值仅影响后续事件控制或等待操作的执行。如果一个进程被阻止等待event1,而另一个事件被分配给event1,则当前正在等待的进程将永远不会被取消阻止。例如:
fork
T1: forever @ E2;
T2: forever @ E1;
T3: begin
E2 = E1;
forever -> E2;
end
join
此示例fork出三个并发进程。每个过程同时开始。因此,在进程T1和T2被阻塞的同时,进程T3将事件E1分配给E2。结果,过程T1将永远不会解除阻塞,因为事件E2现在是E1。为了解锁线程T1和T2,E2和E1的合并必须在fork之前进行。
当事件变量被赋予特殊的null值时,该事件变量与基础同步队列之间的关联将被破坏。当没有事件变量与基础同步队列相关联时,队列本身的资源将可供重用。触发无效事件不应产生任何影响。等待null事件的结果是未定义的,并且实现可以发出运行时警告。
例如:
event E1 = null;
@ E1; // undefined: might block forever or not at all
wait( E1.triggered ); // undefined
-> E1; // no effect
事件变量可以与其他事件变量或特殊值null进行比较。
只有以下运算符才允许用于比较事件变量:
--与另一个事件或与null相等(==)
--与另一个事件或与null的不等式(!=)
--与另一个事件或具有null的大小写相等(==)(语义与==相同)
--大小写不等式(!==)与另一个事件或具有null(语义与!=相同)
--测试布尔值,如果事件为null,则该值应为0,否则为1
例如:
event E1, E2;
if ( E1 ) // same as if ( E1 != null )
E1 = E2;
if ( E1 == E2 )
$display( "E1 and E2 are the same event" );