欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

程序员文章站 2022-05-07 20:12:26
...

前言

 

这篇文章主要讨论的是数字芯片验证领域,或者说仿真器仿真行为,这一范畴内的时序竞争与冒险。从关联性来讲,内容贴近这一篇博客:

 

https://blog.csdn.net/moon9999/article/details/102983963

 

不过因为最近又对这一内容有了更加深刻的领悟与认识,也意识到之前自己的理解是有一定误区的,所以希望借此记录,与大家分享。

 

本文的很大一部分内容来源自IEEE system verilog标准第四章“Scheduling semantics”,其余来自个人实验与其他相关资料。

 

真实时间与仿真时间

 

一般在功能仿真中,会涉及到两个时间观念:真实时间(或称之为CPU时间)与仿真时间。仿真时间很好理解,例如RTL电路中我们平时总说的第100个时钟周期、第100ns等时间概念均为仿真时间。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

CPU时间则是仿真器真实花费的时间,例如我们仿真了50ms仿真时间,如果电路规模很大的话,可能会花费几小时甚至几天的CPU时间来完成。因此我们可以认为CPU时间就是真实世界的时间。

 

仿真过程中的时间是由一个个的time-slot构成的,由阻塞性的事件(event)与线程(thread/process)推进仿真时间前进。

 

事件与进程

 

system verilog的代码行为是由一个个离散事件组成,运行sv时也就是在执行一个个的事件与线程。值得注意的是,仿真器中线程与事件的执行是串行方式,而真实的RTL电路代码的执行方式是并行执行,仿真器需要通过调度串行事件来模拟芯片真实的并行行为。因此,为了模拟贴近真实电路行为,明确仿真环境行为,避免竞争冒险与采样不确定等危机,SV标准划分了明确的事件调度与代码执行区间。

 

关于进程,通常我们写下的每一句执行性的verilog代码和sv代码,在仿真器看来都是一个进程(更多时候将有时序或时间推进的行为成为进程/线程,因此function一般不被称作进程)。典型的进程包括:

Initia, always, always_comb, always_latch, always_ff, assign, task, 其他赋值语句等。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

事件在标准中包括两种,update event和evaluation event,具体解释如下:

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

简单来讲就是任何一个数值/信号(sv中将数据划分为net型与variable储值型,这个另外讨论吧)的变化都是一个update event,对该变化敏感的若干进程感知该变化后的执行过程是一个evaluation event。因此可以认为进程也是事件的一个子集。

 

需要注意一点,对同一update event敏感的若干进程,在执行时必然时串行执行,但是串行执行顺序标准中没有做规定,不同仿真器可以做出自己的安排。

 

当然我们没有必要咬文嚼字,明确我们写下的执行性代码被编译器转化为一个个事件与进程,在不同时间点(time-slot)和不同触发条件下执行即可。

 

仿真过程

 

仿真时间由无数的time-slot构成的,每个time-slot中由划分了若干个区域,每个区域执行标准中规定的相应进程。一次完整的仿真过程也就是把全部time-slot执行完成的过程。这里贴以下标准中的伪码,做一下简单的分析,当然之后这段伪码我们还会再见。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

一次完整的仿真器执行的功能仿真经历了什么呢?

  1. 仿真时间T归零,仿真开始;
  2. 初始化所有的储值单元和电路单元;
  3. 调度并执行所有初始化阶段的事件在ts 0时刻;
  4. 检查是否所有的time-slot都已经完成,如果还有若干time-slot需要执行,那么推进时间到第一个待执行的time-slot,并且设定仿真时间T为这个time-slot的时刻;举例,如果一个仿真环境只有一句打印,那么仿真会直接在T=0时刻结束,因为0时刻时候就已经没有time-slot待执行了。如果环境中有一句#10ns $display(),那么在0时刻后仿真器会发现还有一个time-slot待执行,因此将仿真时间推进到10ns执行这句打印,之后结束仿真;
  5. 执行T时刻的time-slot中的所有event(事件和进程),直到该time-slot内的全部事件执行结束,返回第4条。

直到将所有非空/待执行的time-slot执行结束,仿真器才会结束仿真过程。

 

Time-slot

于是乎,事情的重点又来到了time-slot。

 

Time-slot对于硬件RTL电路而言,就是一个时刻/时间点,但是对于软件仿真器而言,这一个时刻内发生了很多调度与执行进程。通过调度和组织执行进程的执行,将软件行为尽可能的模拟贴近真实硬件行为。

 

那么我们先将time-slot理解为仿真过程中每一个有事情要做的时间点即可,之后还是根据SV标准进行深入分析。

 

SV标准中将一个time-slot划分为了17个区域,分别是:

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

如果将这些regions展开在一个time-slot,就构成了下面这个图:

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

如果想深入研究这幅图的话,可以参考SV标准,在这里我们还是看简化图比较好,不是太影响理解。下面时画的简化time-slot图:

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

通过简化图我们可以发现,time-slot其实是一个具有对称美感的非常简单的结构,我们再复习一下time-slot中若干regions的功能;

 

Preponed域

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

一个time-slot的入口,也是当前time-slot的采样点,在进入当前的time-slot时立刻采样信号目前的实际值。

 

关于采样值与实际值多说一句,什么是采样值呢?就是在仿真的特定时刻(一般而言就是preponed时刻,当然是一般而言),将信号的数值进行保存以便之后使用。因此我们在环境中使用的信号都是采样值,值得注意的是,采样值在仿真中的大部分时刻与实际值是有差异的,但是不用担心只要写法规范这个差异不会造成问题。

 

以图示表示采样值与电路实际值关系如下图,可以看出在环境在20ns处对实际信号进行了采样,那么在20ns~45ns过程中环境中使用该信号时,使用的值都是A。环境中time-slot的采样点就是preponed域。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

Active域

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

在active域内以任意顺序执行当前active状态的event,简单来讲我们常见的module进程比如assign赋值等都在active域内执行。

 

阻塞赋值直接在本区域完成,非阻塞赋值在本区域开始并被推入NBA域(no blocking region)域等待赋值完成,带有#0时延的线程被推入in-active域(小于最小仿真粒度的#延时四舍五入为#0后也应该推入in-active域,如1ns/1ps时,#0.4ps会被四舍五入为#0);

 

进程内部的操作时顺序完成的,例如begin-end块内的代码;进程间的顺序没有规定,可以以随意顺序执行,例如若干assign赋值,fork-join块中的并行线程;

 

需要注意的是不仅仅RTL代码可以放置在module,验证环境也可以放置在module中,如果这样做的话,验证环境的线程执行时间与RTL代码一致,因此有些工具书上说RTL代码在active/inactive/NBA区域执行,验证环境在re-active/re-inactive/re-NBA域中执行是不准确的;

 

In-active域

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

所有的#0delay线程会被推进到in-active域,等待active域(本轮)中的线程执行完成再执行in-active域中的线程。

 

NBA域

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

非阻塞赋值<=在NBA域中生效,因此如果在NBA域之后采样信号,采样得到的是赋值后的新值。

 

Observed域

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

active/inactive/NBA的线程全部执行完毕后进入observed域,observed域隔离开了module时间片和program时间片,避免了验证环境与RTL线程产生竞争和冒险;

断言在observed域执行匹配和判断,注意匹配所使用的是preponed采样的信号“旧值”;

下面呈镜像对称的re-域是program中的执行域,不再重复。那么接下来我们看下一个time-slot是如何调度这些时间regions的。

 

Ts调度执行

 

先把刚刚看的那幅图搬出来,我们看到在仿真器返现有time-slot待执行时候,就会跳转到这一时间点,并且执行execute_time_slot(T)来执行这一ts内的所有域和域内的进程。


【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

execute_time_slot(T)的伪码如下图,我们来学习一下。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

  1. 执行preponed中进程和pre-active中进程,进程主要是采样和DPI等操作,注意一个ts中这两个域的线程只会执行这一次;
  2. 检查active~pre-postponed regions中是否有非空/待执行的,如果有则开始执行,注意这里的进入执行是while语句,也为这regions不是线性执行的,而是反复检查是否regions全部执行结束,只要有未空regions就会跳回继续执行;
  3. 可以发现第一层while(4行处)的下面是两段while循环,泾渭分明的将module regions(active~post-observed)和program regions(re-active~post-re-NBA)分开,也就是说program regions中的进程一定是在module regions中进程执行的彻彻底底全部执行清之后,才会开始执行;
  4. 看第二层module regions的while循环(5行处),先执行active域中的进程,然后检查active~post-observed regions中是否有进程待执行,如果有就把这些进程放回到active region执行。
  5. 因此,module regions具体执行效果就是:a. 执行active域所有线程,遇到#0delay先挂起放到in-active去,遇到<=非阻塞赋值先把右面的值记下来,进程挂起到NBA域去;b. active内进程处理干净了,看下in-active内是否有挂起来的进程,如果有就把这些进行取出来到active,再进行一下active执行这些进程,如此反复直到active~in-active内都没有进程待执行了;c. 直到active和in-active中的进程全部执行干净,NBA域内非阻塞赋值的进程会生效,如果某一进程对NBA域内的update event敏感,则会在此时被提至active内;d. active~pre-observed域内的进程反复几轮执行完成后,module regions的进程执行结束,进入observed域,之后进入program regions,至此第二层循环(5行处)完成;但是注意这并不意味该time-slot不会再次回到module regions执行,仔细观察伪码和示意图可知,如果module中有进程对program regions event敏感,进程再次被提至active域等待执行;
  6. module regions之后time-slot推进到re-active~post-re-NBA regions循环执行进程,执行完成后检查所有regions中是否还有进程待执行,如果有则回到while循环的开始重复此过程;

 

可以看下如下几段代码:

always @(posedge clk) begin
    if(rst_n == 1'b0)begin
	data_out_vld <= 1'b0;
    end
    else begin
	data_out_vld <= data_out_vld_ff1;//进程1
    end
end
	
assign data_out_vld_back = data_out_vld;//进程2
//进程2对进程1敏感,会在NBA域之后被置于active域内待执行
assign data_out_vld_back = data_out_vld;//进程1 //module region

while(1)begin
    @clk;
    data_out_vld = 1;//进程2 //program region
end
//进程1会在进程2在active域内执行时候被置于active内,等待执行

 

两个典型场景

 

非阻塞赋值

 

aaa@qq.com(posedge clk)begin
    sig_ff <= sig;
end

 

@(posedge clk)在时钟上升沿时发现有进程待执行,因此执行该time-slot regions。采样发生在ts的preponed region,因此采样到sig上一个ts postponed region的值也就是就值A,之后再active执行进程sig_ff <= sig,非阻塞进程在NBA域生效。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

电平赋值

 

assign c = b + 1;

aaa@qq.com(b,c)begin
    a = b + c;
end

 

信号b的跳变是update event,执行后所有对其敏感的进程会放置在相应的region,因此c=b+1和a=b+c进程都置于active region。两个进程随机顺序执行,因此从编译器角度看两种处理都是合乎规范的:c = b + 1先生效,a = b + c后生效,如下图1;a = b + c先生效,c = b + 1后生效,c值update再次将a = b + c至于active使其再生效;

 

两种行为都是符合标准的,看编译器选择,当然这里一般而言对仿真没有影响,真实电路行为也是明确的。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

interface的采样

 

interface中有一个非常非常重要的东西,就是clocking block。clocking block里面我们通常会设置input skew和output skew。如设置default input #1ps,那么mon clocking block中的采样值是时钟上升沿前1ps的采样值,这样就避免了可能的取值不定。如设置default output #1ps,那么drv clocking block会在时钟上升沿后1ps去驱动interface.data(即RTL信号),避免了可能存在的驱动冒险。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

当然这两个值不设置的话,默认input skew应该是#1step,即取值在当前time-slot之前的一个step(即上一个时间片的postponed域,由于前一个ts的postponed域采样值与当前ts preponed域值一样,可认为默认情况下interface clock block采样发生在该time-slot preponed域),默认output skew记不清楚是不是#1step了。

 

【system verilog】time-slot,仿真的竞争与冒险,对齐与采样

 

关于clocking block,我们主要明确两点:

 

  1. 无论在任何位置@clocking block(例如@bus.mon),该时钟对齐发生的时刻都是observed域,因此@bus.mon之后,module所有信号的值都已经update完成,可以看到“新值”。
  2. 无论在任何位置使用bus.mon.sig,使用的都是preponed(或更早,例如default input 1ps)的值,即跳变前的“旧值”。

 

验证环境中对齐和采样

 

验证环境搭建于program中

 

  1. 无论@rtl.clk或是@interface.mon(clocking block)使用rtl.sig(或interface.sig),会使用到的是新值,原因是program位于program regions,此时rtl代码已在module regions完成更新;
  2. 无论@rtl.clk或是@interface.mon(clocking block)使用interface.mon.sig,会使用到的是旧值,原因是interface.mon.sig在preponed region或更之前完成的更新;

 

验证环境搭建于module中

 

  1. @rtl.clk使用rtl.sig(或interface.sig),会导致冒险不一定采样到新值或旧值,原因是rtl.sig的更新可能位于active region,环境中使用rtl.sig也位于active region,两个进程不确定哪一进程先执行;
  2. @rtl.clk使用interface.mon.sig,会使用到的是旧值,原因是interface.mon.sig在preponed region或更之前完成的更新;
  3. @interface.mon(clocking block),使用rtl.sig(或interface.sig),使用到的是旧值,因为@interface.mon对齐行为发生在observered region,rtl信号已在之前的module regions完成更新;
  4. @interface.mon(clocking block),使用interface.mon.sig,原因是interface.mon.sig在preponed region或更之前完成的更新;
相关标签: SV