我是靠谱客的博主 淡淡冰棍,最近开发中收集的这篇文章主要介绍(8)systemverilog 事件与信箱systemverilog 线程以及线程间的通信关注作者,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

systemverilog 线程以及线程间的通信

文章目录

  • systemverilog 线程以及线程间的通信
    • 一、线程的使用
    • 二、停止线程
    • 三、线程间的通信
      • 1.事件
      • 2.旗语
      • 3.邮箱
  • 关注作者

一、线程的使用

标准的Verilog对语句有两种分组方式——使用begin…end或fork…join。begin…end中的语句以顺序方式执行,而fork…join中的语句则以并发方式执行。后者的不足是必须等fork…join内的所有语句都执行完后才能继续块内后续的处理。
SV中引入了两种新的创建线程的方法,使用fork…join_none和fork…join_any语句。
在这里插入图片描述

  • fork…join在T1、T2、T3都结束后才会执行后续线程;
  • fork…join_any在T1、T2、T3中任何一个结束后都会继续执行后续线程,当然T1和T2还会继续执行。注意join_any块后的那个语句的执行在并发块的第一个语句之后;
  • fork…join_none不用等待T1、T2、T3中任何一个线程结束就可以继续执行后续线程,当然T1、T2、T3最终都要执行结束。注意join_none块后那个语句的执行早于fork…join_none内的任何语句。

二、停止线程

正如你需要在测试平台中创建线程,你也可能需要停止线程。Verilog中的disable语句可以用于停止SV中的线程。

  1. 通过禁止一个标签可以精确地指定需要停止的块;
  2. 使用典型的Verilog语句disable来停止一个署名块中的所有线程。SV中引入disable fork语句使你能够停止从当前线程中衍生出来的所有子线程;

三、线程间的通信

测试平台中的所有线程都需要同步并交换数据。在最基本的层面上,一个线程等待另一个,例如环境对象要等待发生器执行完毕。多个线程可能会同时访问同一资源,例如被测设计中的总线,所以测试程序需要确保有且仅有一个线程被许可访问。在最高的层面上,线程需要彼此交换数据,例如从发生器传递给代理的事务对象。所有这些数据交换和控制的同步被称为线程间的通信。在SV中可使用事件、旗语和信箱来完成。

1.事件

Verilog事件可以实现线程的同步。就像在打电话时一个人等待另一个人的呼叫,在Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着,等待事件的变化。其它的线程可以通过->操作符来触发事件,解除对第一个线程的阻塞。
SV从几个方面对Verilog事件做了增强。事件现在成为了同步对象的句柄,可以传递给子程序。这个特点允许你在对象间共享事件,而不用把事件定义成全局的。最常见的方式是把事件传递到一个对象的构造器中。
在Verilog中,当一个线程在一个事件上发生阻塞的同时,正好另一个线程触发了这个事件,则竞争的可能性便出现了。如果触发线程先于阻塞线程执行,则触发无效。SV中引入了triggered()函数,可用于查询某个事件是否已被触发,包括在当前时刻。线程可以等待这个函数的结果,而不用在@操作符上阻塞。

event e1,e2;
initial begin
	$display("@%0t:1:before trigger",$time);
	->e1;
	wait(e2.triggered());
	$display("@%0t:1:after trigger",$time);
end

initial begin
	$display("@%0t:2:before trigger",$time);
	->e2;
	wait(e1.triggered());
	$display("@%0t:2:after trigger",$time);	
end

运行结果为:

@0:1:before trigger
@t:2:before trigger
@0:1:after trigger
@0:2:after trigger

tip: 如果在循环中使用wait(handshake.triggered()),一定确保在下次等待之前时间可以向前推进。否则你的代码将进入一个零时延循环,原因是wait会在单个事件触发器上反复执行。

2.旗语

使用旗语可以实现对同一资源的访问控制。想象一下你和你爱人共享一辆汽车的情形。显然,每次只能有一个人可以开车。为应对这种情况,你们可以约定谁持有钥匙谁开车。当你用完车以后,你会让出车子以便对方使用。车钥匙就是旗语,它确保了只有一个人可以使用汽车。在操作系统的术语里,这就是大家所熟知的"互斥访问",所以旗语可被视为一个互斥体,用于实现对同一资源的访问控制。

当测试平台中存在一个资源,如一条总线,对应着多个请求方,而实际物理设计中又只允许单一驱动时,便可使用旗语。在SV中,一个线程如果请求“钥匙”而不得到,则会一直阻塞,多个阻塞的线程会以先进先出的方式进行排队。

  1. 旗语有三种基本操作。使用new方法可以创建一个带单个或多个钥匙的旗语,使用get可以获取一个或多个钥匙,而put则可以返回一个或多个钥匙。如果你试图获取一个旗语而希望不被阻塞,可以使用try_get()函数,它返回1表示有足够多的钥匙,而返回0则表示钥匙不够。
...
semaphore sem;
initial begin
	sem=new(1);
	sem.get();
	sem.put();
end
  1. 使用旗语时有两个地方需要小心。
  • 第一,你返回的钥匙可以比你取出来的多。你可能会突然间有两把钥匙而实际上只有一辆汽车。
  • 第二,当你的测试程序需要获取和返回多个钥匙时,务必谨慎。假设你剩下一把钥匙,有一个线程请求两把而被阻塞,这时第二个线程出现,它只请求一把,那么会有什么结果呢?在SV中,第二个请求get(1)会悄悄地排到第一个请求get(2)的前面,先进先出的规则在这里会被忽略掉。

3.邮箱

从硬件角度出发,对信箱的最简单的理解是把它看成一个具有源端和收端的FIFO。源端把数据放进信箱,收端则从信箱中获取数据。信箱可以有数量上的限制,也可以没有。当源端线程试图向一个容量固定并且以及饱和的信箱里放入数值时,会发生阻塞直到信箱里的数据被移走。同样地,如果收端线程试图从一个空信箱里移走数据,它也会被阻塞直到有数据放入信箱里。

信箱是一种对象,必须调用new函数来进行实例化。例化时有一个可选的参数size,用以限制信箱中的条目。如果size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。

使用put任务可以把数据放入信箱,而使用get任务则可以移除数据。如果信箱为满,则put会阻塞;而如果信箱为空,则get会阻塞。peek任务可以获取对信箱里数据的拷贝而不移除它。

缺省情况下,信箱类似于容量不限的FIFO——在消费方取走物品之前生产方可以向信箱里放入任意数量的物品。但是,你可能希望在消费方处理完物品之前让生产方阻塞住,以便使两个线程步调一致。

在构造信箱时可以指定一个最大容量。缺省容量是0,表示信箱容量不限。任何大于0的容量便可创建一个“定容信箱”。如果你试图往信箱里放入多于设定容量的物品,则put会阻塞,直到你从邮箱里搬走物品腾出空间。

  • 在异步线程间使用信箱通信
program automatic unsynchronized;
	mailbox mbx;
	class Producer;
		task run();
			for(int i=1;i<4;i++)begin
				$display("Producer:before put(%0d)",i);
				mbx.put(i);
			end
		endtask
	endclass

	class Consumer;
		task run();
			int i;
			repeat(3)begin
				mbx.get(i);
				$display("Consumer:after get(%0d)",i);
			end
		endtask
	endclass

	Producer p;
	Consumer c;
	
	initial begin
		mbx=new();
		p=new();
		c=new();
		fork
			p.run();
			c.run();
		join
	end
endprogram

上述例子中没有同步信号,导致在消费方还没有取数的时候生产方就已经把三个整数都放进信箱里了。这是因为线程在没有碰到阻塞语句之前会一直运行,而生产方恰好没有碰到阻塞语句。消费方在第一次调用mbx.get的时候就被阻塞了。

在一个同步的测试平台中,生产方和消费方在操作上的步调是一致的。这样,通过等待线程便可知道输入激励什么时候完成。

program automatic unsynchronized;
	mailbox mbx;
	class Producer;
		task run();
			for(int i=1;i<4;i++)begin
				$display("Producer:before put(%0d)",i);
				mbx.put(i);
			end
		endtask
	endclass
	
	class Consumer;
		task run();
			int i;
			repeat(3)begin
				mbx.peek(i);
				$display("Consumer:after get(%0d)",i);
				mbx.get(i);
			end
		endtask
	endclass
	
	Producer p;
	Consumer c;
	
	initial begin
		mbx=new(1);
		p=new();
		c=new();
		fork
			p.run();
			c.run();
		join
	end
endprogram

上例使用了定容信箱实现两个线程同步。消费方使用一个内建的信箱方法peek()来探视信箱里的数据而不将其移出。当消费方处理完数据后,便使用get()移出数据。这就使得生产方可以生成一个新的数据。如果消费方使用get()替代peek()来启动循环,那么事务会被立刻移出信箱,这样生产方可能会在消费方完成事务的处理之前生成新的数据。

Producer:before put(1)
Producer:before put(2)
Consumer:after get(1)
Consumer:after get(2)
Producer:before put(3)
Consumer:after get(3)

可以看到,生产方和消费方步调是一致的,但是生产方仍然比消费方提前一个事务的时间。这是因为,容量为1的定容信箱只有在你试图对第二个事务进行put操作时才会发生阻塞。

你可能希望让两个线程使用握手信号,以使生产方不要超前于消费方。既然消费方以阻塞的方式等待生产方使用信箱,那么生产方也可以以阻塞的方式来等待消费方完成对信箱条目的处理。这可以通过在生产方增加阻塞语句如事件、旗语或第二个信箱来实现。

  • 使用两个信箱来实现线程的同步
program automatic unsynchronized;
	mailbox mbx,rtn;
	class Producer;
		task run();
			int k;
			for(int i=1;i<4;i++)begin
				$display("Producer:before put(%0d)",i);
				mbx.put(i);
				rtn.get(k);
				$display("Producer:after get(%0d)",k);
			end
		endtask
	endclass
	
	class Consumer;
		task run();
			int i;
			repeat(3)begin
				$display("Consumer:before get");
				mbx.get(i);
				$display("Consumer:after get(%0d)",i);
				rtn.put(-i);
			end
		endtask
	endclass
	
	Producer p;
	Consumer c;
	
	initial begin
		mbx=new();
		rtn=new();
		p=new();
		c=new();
		fork
			p.run();
			c.run();
		join
	end
endprogram

关注作者

  • 自述
    作者是一位中科大数字设计专业的研究生,水平有限,如有错误,请大家指正,想要与大家一同进步。
  • 经历
    曾获得国家奖学金,“高教社杯”数学建模国家二等奖等
  • 陆续更新:
    1.与UVM验证相关的system verilog后续内容;
    2.与verilog数字设计相关的一些基础模块设计,例如FIFO,UART,I2C等的书写。
    3.保研与竞赛经历等
  • 微信公众号
    欢迎大家关注公众号“数字IC小白的日常修炼”,期待与大家一同仗剑遨游数字IC世界。

最后

以上就是淡淡冰棍为你收集整理的(8)systemverilog 事件与信箱systemverilog 线程以及线程间的通信关注作者的全部内容,希望文章能够帮你解决(8)systemverilog 事件与信箱systemverilog 线程以及线程间的通信关注作者所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(82)

评论列表共有 0 条评论

立即
投稿
返回
顶部