SV组件实现篇之十:组件间的通信(下)

 

SV组件实现篇之十:组件间的通信(下)...



数据通信的需求

对于一辆车子来讲,如果要实时显示这辆车子的状态,会需要多个仪表,而显示的参数也包括了车速、油量、发动机的转速和温度等,而这些涉及到了各个传感器到汽车控制中枢的通信。如果我们继续上面这辆BYD来模拟这些不同传感器(线程)到车子中央显示的通信的话,就可以利用SV的mailbox(信箱)来满足线程之间的数据通信。





输出结果:



对于mailbox的用法,与FIFO的使用很相似,如果我们将上面的mailbox用队列来替代的话,则可以修改为:





对上面这个通过队列改造,来完成进程间通信的例子,我们可以点出maibox与queue在使用时的差别

  • maibox必须通过new()例化,而队列只需要声明。
  • mailbox可以将不同的数据类型同时存储,然而这么做实际是不建议的;对于队列来讲,它内部存储的元素类型必须一致。
  • maibox的存取方法put()和get()是阻塞方法(blocking method),即使用它们时,方法不一定在同一时刻返回,而对于队列的存取方式来看,push_back和pop_front方法是非阻塞的,会在第一时间返回。因此,在使用queue取数时,需要额外填写wait(queue.size() > 0)才可以在其后对非空的queue做取数的操作。此外,也应该注意,如果要调用阻塞方法,那么只可以在task中调用,因为阻塞方法是耗时的;而调用非阻塞方法,例如queue的push_back()和pop_front()则既可以在task又可以在function中调用。
  • mailbox只能够用作FIFO,而queue除了按照FIFO使用,还有其它应用的方式例如LIFO(Last In First Out)。
  • 对于mailbox变量的操作,在传递参数时,实际是传递的mailbox的指针,因此形式参数在传递时默认的拷贝行为是允许的;而上面的例子在task display中关于queue的形式参数声明的端口方向时ref类型,因为如果采用默认的input方向,那么传递过程中发生的只是数组的拷贝,而在task display中对queue的操作,并不会影响外部调用display的数组本身。因此,在传递数组时,读者需要考虑到要对数组做的是引用,还是拷贝,进而考虑端口声明的方向。


此外,关于mailbox的其它特性,读者在这里也需要加以了解:

  • mailbox在例化时,通过new(N)的方式可以使其变为定长(fixed length)容器。这样在容载到长度N以后,无法再对其写入。如果用new()的方式,则表示信箱容量不限大小。
  • 除了put/get/peek这样的阻塞方法,用户也可以考虑使用try_put/try_get/try_peek等非阻塞方法
  • 如果要显式地限定mailbox中元素的类型,可以通过mailbox #(type = dynamic_type)的方式来声明。例如上面的三个mailbox存储的是int,则可以在声明时进一步限定其类型为mailbox #(int)。


进程同步的需求

除了上面的三种常见需求,有时候,进程之间也需要进行同步(同步)。这里我们来看看,如果要对这辆车熄火(stall)的话,得先看看是否车挂挡在p(park),进而再将车钥匙拔出。这里面,我们可以将stall和park两个线程的同步看做,先由stall发起同步请求,再来等待park线程完成之后的返回,最后再由stall流程进行其余的程序,并且最终结束熄火的整个过程。

有意思的是,我们不妨用上面SV自带的三种进程通信的方式event、semaphore和mailbox来实现进程间的同步问题。

event同步



semaphore同步



mailbox同步



上面三种用来做线程A到线程B再返回线程A的同步方式,输出的结果均为:



从上面这三段代码示例中可以看出,用来做线程同步方式的选择也有多种。而如果要在同步(事件)的同时,也完成一些数据传输,那么更合适的则是mailbox,因为其本生可以用来存储一些数据,而event和semaphore的则更偏向于小信息量的同步,即不包含更多的数据信息。

进程通信要素的比较和应用

在介绍完上面的SV线程通信的三要素之后,我们有必要就这三个要素的特性做出比较,和列出它们一般运用的场景,以供读者参考。

  • event:最小信息量的触发,即单一的通知功能。可以用来做事件的触发,也可以多个event组合起来用来做线程之间的同步。
  • semaphore:共享资源的安全卫士。如果多线程间要对某一公共资源做访问,即可以使用这个要素。
  • mailbox:精小的SV原生FIFO。在线程之间做数据通信或者内部数据缓存时可以考虑用此元素。


monitor到checker的通信方式

最后,我们来看看实际,在另外一位verifier尤在验证MCDF模块arbiter时,是如何进行monitor到checker的通信的。

首先来看看他对arbiter验证环境的框图:



从验证框架中的连接来看,从monitor到checker的通信是为了将monitor观察到的数据输送给checker。我们在上一节《监测器的采样》中已经给出了另外一种解决方式即将interface作为monitor与checker通信的数据中间站。monitor将观察到的数据先写入到interface中的缓存,而checker则同样需要通过虚接口,从目标interface中的缓存中得到监测数据。这种方式必须依赖于interface,而实际上,checker对于interface的依赖程度远远没有monitor那么大,这么做的代价是,checker必须也绑定在interface上,否则没有数据可以获取。

上面验证框架中的例子,是monitor直接将数据写入到checker中。通我们这节一开始就提到的,不同对象之间的通信也同于线程之间的通信。在之前举的BYD汽车的例子,都是一个对象中各个线程的通信;而我们接下来要实际应用的是,不同的对象之间(各个monitor到唯一的checker)的数据通信。在这个例子当中,我们选取了mailbox作为checker内的数据缓存,用来存储从4个monitor送过来的数据:







从上面这个例子中可以看到,在对象chk中例化了4个mailbox,而这些mailbox在arb_checker::new()中例化以后,被用作了存储从monitor送来得监测数据包。注意,这些mailbox在声明时已经做了限定mailbox #(arb_trans),只能用来存储arb_trans的句柄(而不是对象!)。

对于arb_ini_mon和arb_rsp_mon而言,它们中定义的方法put_trans也会将每次监测到的数据包交给checker,但是在传送之前要检查成员mb是否为空,即是否已经与外部arb_checker的mailbox相连接。只有待连接完成之后,才可以进行数据传输,否则数据传输必定是失败的,因为mb这一mailbox的指针依旧是“悬空”的。

接下来,进入顶层环境arb_tb中的例化和连接部分。在对各个对象进行了例化之后,需要单独进入了连接阶段,即arb_tb::connection过程,这一过程中需要将接口连接至各个对象内,同时要要将chk对象内各个mailbox连接至与其平行的monitor内的mb。通过这一个过程,就为接下来从各个monitor到checker的数据通信铺好了道路。

最后,我们需要让各个组件运转起来,在调用各个组件的方法run()之前,需要先等待一点时间,这是为了保证arb_tb::connection阶段已经完成,因为只有连接阶段完成了,才可以没有后顾之忧地让各个组件转动起来,保持组件之间的通信状态。当然,在这里,除了使用#1ps固定时间之后,我们也可以通过本节中介绍的event,由arb_tb::connection来触发事件,进而arb_tb::run才可以在等待到这个事件之后,进行下一步的工作

至此,我们就将线程间的通信以及实际应用中组件之间的通信为大家介绍完了。一旦我们建立了关于验证组件通过接口同DUT的驱动,组件通过接口的采样,以及这一节中组件之间如何通信的认知,那么整个验证结构的连接我们就有了基本的概念。到了下一节,我们将进入比较器的具体实现方式,来看看日常的比较器在实现中常用的手段和建议

谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。


    关注 路科验证


微信扫一扫关注公众号

0 个评论

要回复文章请先登录注册