所有的数据交换和控制同步被称为线程间的通信(IPC)。在SystemVerilog中可以使用事件、旗语和信箱来完成。
7.3 事件
在Verilog中,@操作符是边沿敏感的,它总是阻塞着,等待着事件的变化。其他的线程可以通过->操作符来触发事件。
SystemVerilog引入了triggered()函数,可用于查询某个事件是否已经被触发,包括在当前时刻。线程可以等待这个函数的结果,而不用在@操作符上阻塞。
7.3.1 在事件的边沿阻塞
结果:(有竞争)
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
7.3.2 等待事件的触发
使用电平敏感的wait(e1.triggered())来替代边沿敏感的阻塞语句@e1.
结果:(有竞争)
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
@0: 2: after trigger
7.3.3 在循环中使用事件
使用事件来实现两个线程的同步,但是务必要小心避免零延时循环。
- wait(handshake.triggered());使得循环在单个事件触发器上反复执行。
- 采用等待事件的边沿可以避免零延时。
7.3.4 传递事件
SystemVerilog允许你在对象间共享事件,事件成为同步对象的句柄,可以传递给子程序。
7.3.5 等待多个事件
测试环境必须等待多个子线程的完成,利用wait fork看上去应该是最容易的解决方法。但是这样的话,你需要等待其衍生出来的许多子线程。这并不是我们所期望的。下面介绍几种方法来实现等待多个事件。
方法一:创建一个新的线程并从中衍生出来子线程,然后保证每个线程阻塞在每一个发生器的一个事件上。
方法二:记录已经触发的事件的数目。
方法三:摆脱所有事件而仅对运行着的发生器进行计数。选用静态变量来存储计数值。
7.4 旗语
可以将旗语看成是一把钥匙,谁拥有钥匙谁就对资源具有使用的权利。旗语可以被视为一个互斥体,用于实现对同一资源的访问控制。
- 一个线程如果请求钥匙而得不到的话,就会一直阻塞。多个阻塞的线程会以先进先出(FIFO)的方式进行排队。
- 如果只有一把钥匙,第一个线程请求两把,第二个线程请求一把,这时先进先出的原则就会被忽略掉,第二个线程会排在第一个线程前面。
- 你返回的钥匙可以比你取出来的多。
- semaphore sem;创建一个旗语
- sem = new(1);分配一把钥匙
- sem.get(1);获取一把钥匙
- sem.put(1);返回一把钥匙
- try_get();获得钥匙而不被阻塞,返回1表示有足够多的钥匙,返回0则表示钥匙不够。
7.5 信箱
如何在两个线程之间传递信息呢?直观的想,发生器创建的事务并传递给驱动器的情况,如果直接让发生器线程去调用驱动器中的任务就可以了。但是这样带来两个问题,第一,发生器需要到达驱动器任务的层次化路径,这会降低代码的可重用行,第二,这样会使得一个发生器需要控制多个驱动器,从而引发同步的问题。
SystemVerilog使用了信箱。从硬件的角度来讲,可以把信箱看成一个FIFO。
- 信箱是一种对象,必须使用new函数来进行实例化。特别是在循环创建随机变量时,需要每一次都对信箱创建一个对象。
- put任务可以将数据放入信箱,get任务可以取出来信箱里的数据。peek任务可以获取对信箱里数据的拷贝而不移除它。try_get()和try_peek()能够防止阻塞情况的发生,成功时返回一个非零值,否则返回零。
- 一个信箱里只放一种类型的数据。
- 缺省容量是0,表示信箱不限容。任何大于零的容量便可以创建一个定容信箱。
7.5.1 测试平台的信箱
一个发生器、一个驱动器使用信箱和顶层程序实现数据交换的过程。
7.5.2 异步线程间使用信箱通信
如果你想要生产方和消费方两个线程一致,那就需要额外的握手信号。
下面介绍几种实现线程同步的方法:
方法一:使用定容信箱和peek来实现线程同步。
- mbx.get(i),移除以后,mbx.put阻塞解除,就会立马产生一个新数据,这样生产方可能会在消费方完成事务处理之前就生成新数据。而改用peek以后,等消费方处理完数据后,再而使用get移出,从而生产方产生一个新的数据。
方法二:使用信箱和事件来实现线程同步。
方法三:使用两个信箱来实现线程的同步。