第7章线程和进程间通信
在实际硬件中,时序逻辑在时钟边缘被激活,而组合逻辑则在任何输入变化时不断变化。所有这些并行活动都在Verilog RTL中使用initial和always块,以及偶尔的gate和连续赋值语句进行模拟。为了刺激和检查这些块,您的测试平台使用许多执行线程,所有线程都并行运行。testbench环境中的大多数块都使用事务处理程序建模,并在它们自己的线程中运行。
SystemVerilog调度程序是选择接下来运行哪个线程的交通警察。您可以使用本章中的技术来控制线程,从而控制您的测试平台。
这些线程中的每一个都与它的邻居通信。在图7.1中,发电机将刺激传递给agent。environment类需要知道生成器何时完成,然后告诉其余的testbench线程终止。这是通过进程间通信(IPC)构造完成的,比如标准的Verilog事件、事件控制和等待语句,以及SystemVerilog邮箱和信号量
SystemVerilog LRM可以互换使用“线程”和“进程”。术语“进程”通常与Unix进程联系在一起,每个进程都包含一个在自己的内存空间中运行的程序。线程是轻量级进程,可以共享公共代码和内存,并且比典型进程消耗的资源少得多。这本书使用了“线程”这个术语。然而,“进程间通信”是一个如此常见的术语,以至于在本书中使用了它。
图7.1测试台环境块
7.1. 处理线程
虽然所有的线程结构都可以在模块和程序块中使用,但是testbench属于程序块。因此,代码总是从时间为0时开始执行的初始块开始。不能在程序中放入always块。然而,您可以通过在初始块中使用永久循环轻松地解决这个问题。
经典的Verilog有两种对语句进行分组的方法——使用begin…end或fork…join。begin…end语句是顺序运行的,而fork…join语句是并行执行的。后者的限制非常有限,因为fork…join中的所有语句必须在块的其余部分继续之前完成。因此,Verilog testbench很少使用这个特性。
SystemVerilog引入了两种新的创建线程的方法——fork…join_none和fork…join_any语句,如图7.2所示。
图7.2分叉…连接块
testbench使用事件、@事件控制、等待和禁用状态等现有结构,以及信号量和邮箱等新语言元素来通信、同步和控制这些线程。
- 使用fork…join and begin…end
样例7.1有一个fork…join并行块和一个封闭的begin…end顺序块,并显示了两者的区别。
示例7.1 begin…end和fork…join的交互
父
子线程
图7.3叉…连接块
在下面的输出中,fork…join中的代码是并行执行的,因此延迟时间较短的语句会在延迟时间较长的语句之前执行。正如示例7.2中所示,fork…join在最后一条语句(以#50开头)之后完成。
示例7.2 begin…end和fork…join的输出
- 使用fork生成线程…join_none
一个fork…join_none块会调度该块中的每一条语句,但会在父线程中继续执行。样例7.3与样例7.1相同,不同之处是连接被转换为join_none。
样例7.3 Fork…join_none代码
这个框图类似于图7.3。注意,样例7.4中join_none块后面的语句在fork…join_none内的任何语句之前执行。
示例7.4 Fork…join_none输出
- 使用fork…join_any来同步线程
join_any块调度该块中的每一条语句。然后,当第一个语句完成时,在父线程中继续执行。所有其他保留线程继续。样例7.5与前面的示例相同,只是连接被转换为join_any。
示例7.5 Fork…join_any代码
注意,在示例7.6中,语句$display(" after join_any ")在并行块中的第一条语句之后完成。
示例7.6 fork…join_any的输出
7.1.1 在类中创建线程
你可以使用fork…join_none来启动一个线程,比如随机事务处理生成器的代码。示例7.7显示了一个带有运行任务的生成器/驱动程序类,该任务创建了N个包。完整的测试工作台为驱动程序、监视器、检查器等提供了类,所有这些类都带有需要并行运行的事务处理。
示例7.7带有运行任务的生成器/驱动程序类
对于示例7.7有几点需要注意。首先,在new()函数中没有启动trans- actor。构造函数应该只是初始化值,而不是启动任何线程。将构造函数与执行实际工作的代码分离,允许您在开始执行对象中的代码之前更改任何变量。
这允许你注入错误、修改默认值和改变对象的行为。接下来,run任务在fork…join_none块中启动一个线程。线程是事务处理程序的一部分,应该在那里生成,而不是在父类中。
7.1.2 动态线程
Verilog的线程是非常可预测的。你可以阅读源代码并计算初始化,always和fork…join块,以知道一个模块中有多少线程。另一方面,SystemVerilog允许动态创建线程,并且不需要等待线程完成。
在样例7.8中,testbench生成随机事务并将它们发送到DUT, DUT将它们存储一段预定的时间,然后返回它们。testbench必须等待事务完成,但不希望停止生成器。
样例7.8动态线程创建
当调用check_trans任务时,它会生成一个线程来监视总线以获取匹配的事务数据。在正常的模拟过程中,许多线程并发运行。在这个简单的示例中,线程只打印一条消息,但是您可以添加更复杂的控件。
7.1.3 线程中的自动变量
当有一个生成线程的循环,而在下一次迭代之前没有保存变量值时,就会出现一个常见但微妙的错误。样例7.8只在带有自动存储器的程序或模块中工作。如果check_trans使用静态存储,那么每个线程都将共享相同的变量tr,以便以后调用
将覆盖以前设置的值。同样地,如果示例在repeat循环中有fork…join_none,它将尝试使用tr来匹配传入的事务,但它的值将在下一次循环中改变。总是使用自动变量在并发线程中保存值。
示例7.9在for循环中有一个fork…join_none。SystemVerilog在fork…join_none中调度线程,但它们直到原始代码块之后才会执行,这是因为#0延迟。因此,示例7.9打印“3 3 3”,这是循环结束时索引变量j的值。
示例7.9 Bad fork…join_none within a loop
示例7.10在循环中执行bad fork…join_none
#0延迟阻塞当前线程,并重新安排它在当前时隙中稍后启动。在示例7.10中,延迟使得当前线程在fork…join_none语句中产生的线程之后运行。这种延迟对于阻塞线程很有用,但您应该小心,因为过度使用会导致竞争条件和意外的结果。
你应该在fork…join语句中使用自动变量来保存一个变量的副本,如示例7.11所示。
示例7.11 fork中的自动变量…join_none
join_none块被分成两部分,声明和过程代码。带有初始化的自动变量声明运行在for循环中的线程中。在每个循环中,创建一个副本k和j的当前值。然后叉的身体…join_none(写)美元计划,包括k的副本。循环完成后,## 7.0阻塞当前线程,所以三个线程运行,印刷复制的价值的k。当线程完成,和其他没有离开在当前时间段,SystemVerilog进展到下一个语句和美元显示执行。
样例7.12跟踪样例7.11中的代码和变量。自动变量k的三个副本称为k0、k1和k2。
执行自动变量代码的步骤示例7.12
编写样例7.11的另一种方法是在fork…join_none之外声明自动变量。示例7.13工作在一个带有自动存储功能的程序中。
示例7.13 fork中的自动变量…join_none
7.1.4 等待所有产生的线程
在SystemVerilog中,当程序中的所有初始块都完成后,模拟程序就会退出。样例7.14展示了如何生成许多仍在运行的线程。使用wait fork语句等待所有子线程。
使用wait fork来等待子线程
7.1.5 跨线程共享变量
在类的例程中,可以使用局部变量、类变量或程序中定义的变量。如果忘记声明变量,SystemVerilog会查找更高的作用域,直到找到匹配的为止。如果代码的两部分是这样的话,这可能会导致微妙的错误
无意中共享同一个变量,可能是因为您忘记在最内层作用域中声明它。
例如,如果您喜欢使用索引变量i,请注意,testbench的两个不同线程不会同时在For循环中使用这个变量。或者您可能忘记在类中声明一个局部变量,如Buggy,如下所示。如果你的程序块声明了一个全局i,那么这个类只使用全局i而不是你想要的局部i。除非程序的两个部分试图同时修改共享变量,否则您甚至可能不会注意到这一点。
使用共享程序变量的错误
解决方案是在包含变量所有使用的最小作用域中声明所有变量。在样例7.15中,在for循环中声明索引变量,而不是在程序或类级别声明。更好的是,尽可能使用foreach语句。
7.2. 禁用线程
正如您需要在testbench中创建线程一样,您也需要停止它们。Verilog disable语句适用于SystemVerilog线程。下面的部分将展示如何异步禁用线程。这可能会导致意想不到的行为,因此您应该注意在线程中途停止时的副作用。相反,您可能希望将算法设计为在稳定点检查中断,然后优雅地放弃其资源。
7.2.1 禁用单个线程
下面是check_trans任务,这一次使用fork…join_any加上disable来创建一个超时的手表。在本例中,您正在禁用一个标记块,以精确地指定要停止的内容。
最外层的fork…join_none与样本7.8相同。这个版本在fork…join_any中实现了两个线程的超时,这样简单的wait语句就会与延迟的$display并行执行。如果正确的总线数据以足够快的速度返回,则等待构造完成,执行join_any,然后禁用将终止剩余的线程。但是,如果总线数据在timeout延迟完成之前没有得到正确的值,则会打印错误消息,执行join_any,而disable则通过wait终止线程。
示例7.16禁用线程
要注意,因为您可能无意中停止了太多带有disable标签的线程。如果有多个驱动程序或监视器对象同时运行,则该语句会停止执行该标记块的每个进程。如果你的代码只有一个实例,禁用标签是一个安全的方法来停止线程。
7.2.2 禁用多个线程
示例7.16使用了经典的Verilog disable语句来停止命名块中的线程。SystemVerilog引入了disable fork语句,这样您就可以停止从当前线程派生的所有子线程。
要注意,因为您可能会无意中停止使用disable fork的太多线程,比如从周围的任务调用创建的线程。您应该始终使用fork…join将目标代码包围起来,以限制禁用fork语句的范围。
接下来的几个示例使用示例7.16中的check_trans任务。您可以将此任务视为执行#TIME_OUT。样例7.17在fork…join中有一个额外的begin…end块,使语句顺序执行。
示例7.17限制禁用fork的范围
图7.4显示了衍生线程的示意图。
初始化begin check_trans(tr0) fork
…
加入结束
图7.4叉…连接方框图
线程1 check_trans(tr1) fork
…
加入## 7.TIME_OUT / 2
禁用叉
代码调用启动线程0的check_trans。接下来一个fork…join创建线程1。在这个线程中,一个由check_trans任务生成,另一个由最内层的fork…join生成,fork…join通过调用任务生成线程4。延迟之后,一个禁用的fork停止,所有的子线程2-4。线程0在fork…join块之外,所以它不受影响。
样例7.18是样例7.17的更健壮版本,其中disable带有一个标签,该标签显式地指定要停止的线程。
示例7.18使用disable label停止线程
7.2.3 禁用多次调用的任务
当你在一个块里面禁用一个块的时候要小心——你可能会停止的比你预期的要多。正如预期的那样,如果您在任务内部禁用一个任务,它就像一个return语句,但它也会杀死由该任务启动的所有线程。此外,一个禁用标签将使用该代码终止所有线程,而不仅仅是当前线程。
在示例7.19中,wait_for_time_out任务被调用了三次,产生了三个线程。然后,线程0也会禁用#2ns之后的任务。当您运行这段代码时,您将看到三个线程开始运行,但是没有一个线程结束,因为线程0中的禁用将停止所有三个线程,而不仅仅是一个线程。如果这个任务在一个被实例化多次的驱动程序类中,一个disable标签可以停止所有的块。
样例7.19使用禁用标签停止任务
7.3. 进程间通信
测试台上的所有这些线程都需要同步和交换数据。在最基本的级别上,一个线程等待另一个线程,例如环境对象等待生成器完成。多个线程可能尝试访问单个资源,比如DUT中的总线,因此testbench需要确保一个且只有一个线程被授予访问权。在最高级别上,线程需要交换数据,比如从生成器传递到代理的事务对象。所有这些数据交换和控制同步称为进程间通信(IPC),它在SystemVerilog中通过事件、信号量和邮箱实现。这些将在本章的其余部分进行描述。
IPC通常有三个部分:创建信息的生产者、接受信息的消费者和携带信息的通道。生产者和消费者在单独的线程中。
7.4. 事件
Verilog事件同步线程。它类似于电话,一个人等待另一个人的电话。在Verilog中,线程等待带有@操作符的事件。这个操作符是边缘敏感的,所以它总是阻塞,等待事件改变。另一个线程用->操作符触发事件,解除第一个线程的阻塞。
System Verilog在几个方面增强了Verilog事件。事件现在是可以传递给例程的同步对象的句柄。这个特性允许您跨对象共享事件,而不必将事件设置为全局的。最常见的方法是将事件传递到对象的构造函数中。
在Verilog中总是存在竞争条件的可能性,即一个线程阻塞一个事件,同时另一个线程触发该事件。如果触发线程exe-在阻塞线程之前终止,则会错过触发器。SystemVerilog引入了触发状态,允许您检查事件是否被触发,包括在当前时间段内触发。线程可以等待这个函数,而不是用@操作符阻塞。
7.4.1 事件边缘的阻塞
当运行样例7.20时,一个初始块启动,触发它的事件,然后阻塞另一个事件,如样例7.21的输出所示。第二个块开始,触发它的事件(唤醒第一个),然后阻塞第一个事件。然而,第二个线程因为错过了第一个事件而被锁定,因为它是一个零宽度的脉冲。
示例7.20在Verilog中阻塞事件
示例7.21阻塞事件的输出
7.4.2 等待事件触发器
而不是边缘敏感块@e1,使用级别敏感的等待(e1。三角-基尔)。如果在此时间步骤中触发了事件,则不会阻塞。否则,它将等待直到事件被触发。
示例7.22等待事件
当您运行示例7.22时,一个初始块启动,触发它的事件,然后阻塞另一个事件。第二个块启动,触发它的事件(唤醒第一个事件),然后阻塞第一个事件,产生示例7.23中的输出。
示例7.23等待事件的输出
其中一些示例具有竞争条件,可能在每个模拟器上执行的结果都不完全相同。例如,示例7.23中的输出假设当第二个块触发e2时,执行跳转回第一个块。第二个块触发e2、等待e1并在控制返回到第一个块之前显示消息也是合法的。
7.4.3 循环中使用事件
可以用一个事件同步两个线程,但要谨慎使用。
如果你在循环中使用wait (handshake.triggered),请确保在再次等待之前提前时间。否则,当等待在单个事件触发器上一遍又一遍地继续时,代码将进入零延迟循环。示例7.24错误地使用了a
级别敏感的阻塞语句,用于通知事务已就绪。
示例7.24等待事件导致零延迟循环
正如您学习了总是在always块中放置延迟一样,您也需要在事务处理循环中放置延迟。样本中的边缘敏感延迟语句
7.25每个事件触发器只持续一次。
示例7.25等待事件的边缘
如果需要在一个时间段内发送多个通知,则应该避免事件,并查看其他内置队列的IPC方法,如信号量和邮箱,本章后面将讨论这些方法。
7.4.4 通过事件
如上所述,SystemVerilog中的事件可以作为参数传递给例程。在示例7.26中,事务处理程序使用事件来在事件完成时发出信号。
样例7.26向构造函数传递事件
7.4.5 等待多个事件
在示例7.26中,有一个触发单个事件的单个生成器。如果您的testbench环境类必须等待多个子进程(比如N个生成器)完成,该怎么办?最简单的方法是使用wait fork,它等待所有子进程结束。问题是,这还需要等待所有事务处理程序、驱动程序和由环境生成的任何其他线程。你需要更有选择性。您仍然希望使用事件在父线程和子线程之间进行同步。
您可以在父线程中使用for循环来等待每个事件,但只有当线程0在线程1之前结束,线程1在线程2之前结束,等等,这才会起作用。如果线程按顺序完成,那么您可能正在等待触发多个周期的事件。
解决方案是创建一个新线程,然后在那里为每个生成器的每个事件上的每个块生成子线程,如示例7.27所示。现在你可以做一个等待叉,因为你更有选择性了。
示例7.27使用wait fork等待多个线程
解决这个问题的另一种方法是跟踪已经触发的事件的数量,如示例7.28所示。
示例7.28通过计数触发器等待多个线程
这个稍微没那么复杂。为什么不删除所有事件,只是等待正在运行的生成器的数量?这个计数可以是一个静态变量
在Generator类中。注意,大多数线程操作代码已经被单个wait构造所取代。示例7.29中的最后一个块使用类作用域解析操作符::等待计数。您可以使用任何句柄,如gen[0],但那将不那么直接。
示例7.29使用线程计数等待多个线程
7.5 信号量
信号量允许你控制对资源的访问。想象一下,你和你的配偶共用一辆车。很明显,一次只能有一个人驾驶。你可以通过同意谁有钥匙谁就能驾驶来处理这种情况。当你用完车后,你把车让给别人用。关键是确保只有一个人能进入汽车的信号量。在操作
系统术语中,这被称为“互斥访问”,因此信号量被称为“互斥”,并用于控制对资源的访问。
信号量可以在测试台上使用,当你有一个资源,比如总线,可能有多个来自测试台上的请求者,但是作为物理设计的一部分,只能有一个驱动程序。在SystemVerilog中,当一个键不可用时,请求一个键的线程总是会阻塞。多个阻塞线程按FIFO顺序排队。
7.5.1 信号量操作
信号量有三种基本操作。您可以使用新方法创建一个具有一个或多个键的信号量,使用阻塞任务get()获取一个或多个键,并使用put()返回一个或多个键。如果您想尝试获取一个信号量,而不是块,请使用try_get()函数。如果键是可用的,try_get()获取键并返回1。如果没有足够的键,它只返回0。样本
7.30展示了如何使用信号量控制对资源的访问。
样例7.30信号量控制对硬件资源的访问
7.5.2 具有多个键的信号量
对于信号量,有两点需要注意。首先,你可以放回比你拿走的更多的钥匙。突然,你可能有两把钥匙,但只有一辆车!其次,如果您的testbench需要获取和放置多个键,请小心。也许您还剩一个键,而线程请求了两个键,导致它阻塞。现在第二个线程请求一个信号量-应该发生什么?在SystemVerilog中,第二个请求get(1)比之前的get(2)提前,绕过了FIFO顺序。如果您混合不同大小的请求,您总是可以编写自己的类。
这样你就能清楚地知道谁优先。
7.6. 邮箱
如何在两个线程之间传递信息?也许您的生成器需要创建许多事务并将它们传递给驱动程序。您可能想让生成器线程调用驱动程序中的一个任务。如果您这样做,生成器需要知道驱动任务的层次路径,使您的代码更少的可重用性。此外,这种样式强制生成器以与驱动程序相同的速度运行,如果一个生成器需要控制多个驱动程序,这可能会导致同步问题。
可以将生成器和驱动程序视为事务处理程序,它们是通过通道进行通信的自治对象。每个对象从上游对象获得一个事务操作(或像生成器那样创建事务操作),进行一些处理,然后将其传递给下游对象。通道必须允许它的驱动程序和接收器异步操作
实现。您可能很想只使用共享数组或队列,但安全地创建读、写和阻塞的线程可能比较困难。
解决方案是SystemVerilog邮箱。从硬件的角度来看,考虑邮箱最简单的方法是,它只是一个FIFO,具有源和接收器。源将数据放入邮箱,接收从邮箱获取值。邮箱可以有最大大小,也可以无限制。当源线程试图将值放入已满的大小邮箱时,该线程会阻塞,直到该值被删除。同样,如果接收线程试图从为空的邮箱中删除一个值,那么该线程将阻塞,直到将一个值放入邮箱。
图7.5显示了连接生成器和驱动程序的邮箱。
邮箱
图7.5连接两个办理的邮箱
邮箱是一个对象,因此必须通过调用new函数来实例化。它接受一个可选的size参数,以限制邮箱中的条目数量。如果大小为0或未指定,则邮箱是无界的,可以容纳无限数量的条目。
您可以通过put()任务将数据放入邮箱,然后通过阻塞的get()任务将数据删除。如果邮箱已满,则put()将阻塞,如果邮箱为空,则get()将阻塞。如果想查看邮箱是否已满,请使用try_put()。和try_get()查看是否为空。peek()任务获取邮箱中的数据副本,但不删除它。
数据是单个值,例如整数,或任意大小的逻辑或句柄。邮箱从不包含对象,只包含对对象的引用。默认情况下,邮箱没有类型,因此可以将任何数据混合放入其中。不要这样做!通过使用示例中所示的参数化邮箱,强制每个邮箱使用一种数据类型
7.31在编译时捕获类型不匹配。
示例7.31邮箱声明
示例7.32中所示的一个典型邮箱错误是一个循环,该循环将对象运行到一个邮箱中,但对象只在循环之外构造一次。因为只有一个对象,所以它被反复随机化。
示例7.32坏生成器只创建一个对象
图7.6显示了指向单个对象的所有句柄。邮箱只包含句柄,而不包含对象,因此最终会得到一个包含多个句柄的邮箱,这些句柄都指向单个对象。从邮箱获取句柄的代码只看到最后一组随机值。
图7.6对一个对象使用多个手柄的邮箱
示例7.33所示的解决方案是确保您的循环具有构造对象、随机化对象和将其放入邮箱的所有三个步骤。这个bug非常常见,在5.14.3节中也提到过。
Good generator创建了许多对象
结果如图7.7所示,每个手柄都指向一个唯一的对象。这种类型的生成器称为蓝图模式,在8.2节中进行了描述。
图7.7一个对多个对象具有多个把手的邮箱
示例7.34显示了等待来自生成器的事务的驱动程序。
示例7.34好的驱动程序从邮箱接收事务
如果您不希望代码在访问邮箱时阻塞,可以使用try_ get()和try_peek()函数。如果成功,则返回一个非零值;否则,它们返回0。它们比num()函数更可靠,因为条目的数量在测量时和下次访问邮箱时之间可能会发生变化。
7.6.1 测试台上的邮箱
示例7.35展示了一个带有生成器和驱动程序的程序,它们使用邮箱交换事务。
示例7.35使用邮箱交换对象:Generator类
7.6.2 有界的邮箱
默认情况下,邮箱类似于无限制的FIFO——生产者可以在使用者取出对象之前将任意数量的对象放入邮箱。但是,您可能希望这两个线程同步操作,这样生产者就会阻塞,直到消费者处理完对象。
您可以在构造邮箱时指定其最大大小。默认邮箱大小为0,这将创建一个无界邮箱。任何大于0的大小都将创建一个有界邮箱。如果试图放置超过此限制的对象,则put()将阻塞,直到从邮箱中获得对象,从而创建一个空位。
示例7.36绑定邮箱
样例7.36创建可能的最小邮箱,该邮箱可以容纳一条消息。生产者线程尝试将三条消息(整数)放入邮箱,而消费者线程缓慢地每1ns获取一条消息。正如示例7.37所示,第一个put()成功,然后生产者尝试阻塞的put(2)。消费者醒来,从邮箱中获得消息1,所以现在生产者可以完成消息2的放置。
示例7.37有界邮箱的输出
有界邮箱充当两个进程之间的缓冲区。您可以看到生产者如何在消费者读取当前值之前生成下一个值。
7.6.3 与邮箱通信的非同步线程
在许多情况下,由邮箱连接的两个线程应该同步运行,这样生产者就不会领先于使用者。这种方法的好处是,您的整个刺激逻辑单元生成链现在都是同步运行的。只有当最后一个低级别事务完成传输时,最高级别的生成器才能完成。现在,你的测试平台可以准确地判断出所有刺激在什么时候发生了变化
被发送。在另一个示例中,如果您的生成器领先于驱动程序,并且您正在收集生成器的功能覆盖率,那么您可能会记录某些事务已被测试,即使测试过早地停止了。因此,即使邮箱允许您将两者解耦,您可能仍然希望保持它们同步。
如果希望两个线程同步运行,除了邮箱之外,还需要握手。在示例7.38中,生产者和消费者现在是交换的类
使用邮箱的整数,两个对象之间没有显式的同步。结果,如示例7.39所示,生产者甚至在消费者开始之前就运行到完成。
示例7.38不同步的生产者-消费者
上面的示例将邮箱保存在一个全局变量中,以使代码更紧凑。在实际代码中,应该通过构造函数将邮箱传递到类中,并在类级变量中保存对邮箱的引用。
样例7.38没有同步,因此生产者在消费者获得第一个整数之前将所有三个整数放入邮箱。这是因为线程会一直运行,直到出现阻塞语句,而生产者没有阻塞语句。消费者线程在第一次调用mbx.get时阻塞。
示例7.39没有同步输出的生产者-消费者
这个例子有一个竞争条件,因此在某些模拟器上,消费者可以更早地激活。结果仍然是相同的价值是由生产者决定的,而不是由消费者多快看到它们。
7.6.4 使用绑定邮箱和窥视的同步线程
在同步的测试台中,生产者和消费者以同步的步骤操作。这样,您就可以通过等待任何线程来判断输入刺激何时完成。如果线程的操作是不同步的,则需要添加额外的代码来检测最后一个事务何时应用到DUT。
为了同步两个线程,生产者创建一个事务并将其放入邮箱中,然后阻塞,直到消费者完成该事务。这是通过让使用者仅在最终完成事务处理时(而不是在第一次检测到事务时)从邮箱中删除事务来实现的。
样例7.40展示了同步两个线程的第一次尝试,这次使用一个绑定邮箱。使用者使用内置邮箱方法peek()查看邮箱中的数据,而不删除数据。当消费者完成数据处理后,它使用get()删除数据。这就释放了生产者来产生新的价值。如果消费者循环以get()而不是peek()开始,事务将立即从邮箱中删除,这样生产者就可以在消费者完成事务之前醒来。示例7.41给出了这段代码的输出。
与绑定邮箱同步的生产者-消费者示例7.40
示例7.41带有有限邮箱的生产者-消费者输出
您可以看到生产者和消费者步调一致,但是生产者仍然在消费者前面一个事务。这是因为当您尝试执行第二个事务的put时,size=1的有界邮箱只会阻塞
2此行为与VMM通道不同。如果将通道的完整级别设置为1,则put()的第一个调用将事务放置在通道中,但直到事务被删除后才返回。
7.6.5 使用邮箱和事件的同步线程
您可能希望这两个线程使用握手,这样生产者就不会走在消费者前面。消费者已经阻塞,等待使用邮箱的生产者。生产者需要阻塞,等待消费者完成事务。为此,可以向生产者添加一个阻塞语句,比如一个事件、一个信号量或第二个邮箱。样例7.42在生产者将数据放入邮箱后使用事件阻塞生产者。使用者在使用数据后触发事件。
如果在循环中使用wait (handshake.triggered),请确保在再次等待之前提前时间,如前面7.4.3节所示。这种等待在一个给定时间段内只阻塞一次,因此您需要进入另一个时间段。示例7.42使用边缘敏感的阻塞语句@handshake来代替,以确保
生产者在发送事务后停止。边缘敏感语句在一个时隙中工作多次,但如果触发器和块发生在同一个时隙中,则可能出现排序问题。
样例7.42生产者-消费者与一个事件同步
现在生产者在消费者触发事件之前不会前进,如示例7.43所示。
示例7.43带有事件的生产者-消费者输出
您可以看到,生产者和消费者成功地同步运行,因为生产者在旧值被读取之前不会产生新值。
7.6.6 使用两个邮箱的同步线程
另一种同步两个线程的方法是使用第二个邮箱将完成消息发送回生产者,如示例7.44所示。
与邮箱同步的生产者-消费者示例7.44
rtn邮箱中的返回消息只是原始整数的负版本。您可以使用任何值,但是为了进行调试,可以根据原始值来检查这个值。
示例7.45使用邮箱的生产者-消费者输出
从示例7.45中可以看到,生产者和消费者成功地同步运行。
7.6.7 其他同步技术
您还可以通过阻塞变量或信号量来完成握手。事件是最简单的结构,然后在变量上阻塞。信号量相当于使用第二个邮箱,但不交换信息。SystemVer- ilog的绑定邮箱不能像这些其他技术那样工作,因为当生产者放入第一个事务时,没有办法阻塞生产者。样例7.41表明生产者总是先于消费者一个事务。
7.7. 使用线程和IPC构建测试平台
早在1.10节中,您就了解了分层testbench。图7.8显示了不同部分之间的关系。现在您已经知道了如何使用线程和IPC,现在可以使用事务来构建一个基本的测试平台。
图7.8带环境的分层试验台
7.7.1 基本办理人
示例7.46是位于生成器和驱动程序之间的代理类。
示例7.46基本处理程序
7.7.2 配置类
configuration类允许您为每个模拟随机化系统的配置。示例7.47只有一个变量和一个基本约束。
示例7.47配置类
7.7.3 环境类
Environment类(如图7.8中的虚线所示)包含生成器、代理、驱动程序、监视器、检查器、记分板和配置对象,以及它们之间的邮箱。示例7.48显示了一个基本的环境类。
示例7.48环境类
第8章展示了如何构建这些类的更多细节。
7.7.4 测试程序
样例7.49显示了主测试,它在一个程序块中。正如第4.3.4节所讨论的,您还可以在模块中放置测试,但会略微增加出现竞争条件的几率。
样本7.49基本测试程序
7.8. 结论
您的设计被建模为许多独立的块并行运行,因此您的testbench还必须生成多个刺激流并使用并行线程检查响应。它们被组织到一个分层的测试平台中,由顶级环境编排。除了标准的fork…join之外,SystemVerilog引入了强大的构造,如fork…join_none和fork…join_any,用于动态创建新线程。这些线程使用事件、信号量、邮箱以及经典的@事件控制和wait语句进行通信和同步。最后,使用disable命令终止线程。
这些线程和相关的控制构造补充了OOP的动态特性。当对象被创建和销毁时,它们可以在独立的线程中运行,允许您构建一个强大而灵活的testbench环境。
7.9. 练习
- 对于下面的代码,确定每个状态的执行顺序和时间——如果使用了join或join_none或join_any。提示:fork和join/join_none/join_any之间的执行顺序和时间是相同的,只是连接后的语句的顺序和执行时间不同。
- 对于下面的代码,使用和不使用等待叉的输出是什么
是否插入到指定的位置?
- 下面的代码将显示什么?假设事件和任务触发器在声明为automatic的程序中声明。
创建一个名为wait10的任务,该任务将等待10个ns,然后检查是否有一个信号量键可用。当键可用时,退出循环并打印时间。
下面调用练习4中的任务的代码将显示什么?
- 下面的代码将显示什么?
查看265页的图7.8“分层的testbench with environment”,并创建Monitor类。您可以做以下假设。
Monitor类拥有类OutputTrans的知识,该类的成员变量为out1和out2。
DUT和监视器通过一个名为my_bus的接口连接,信号为out1和out2。
接口my_bus有一个时钟块cb。
在每个活动时钟边缘上,Monitor类将对DUT输出(out1和out2)进行采样,将它们分配给OutputTrans类型的对象,并将该对象放在邮箱中。