西洲渡

一只还没迈入IC领域的小菜鸡

SV系统验证(8)——线程同步、通信和虚方法

线程控制

并行线程

  • Verilog中与顺序线程begin…end相对的是并行线程fork…join
  • SV引入了两种新新的创建线程的方法,fork..join_none和fork…join_any。

    fork…join需要所有并行的线程都结束以后才会继续执行。
    fork…join_any则会等到任何一个线程结束以后就继续执行。
    fork…join_none则不会等待其子线程而继续执行。

image.png

需要注意的是,fork..…join any和fork…join_none执行后,其一些未完成的子程序仍将在后台运行。
如果要等待这些子程序全部完成,或者停止这些子程序可以使用wait fork或者disable fork

  • wait fork 是等待当前语境所有的的fork语句块全部结束(并非放行)之后就可以执行。
  • disable fork 是关闭当前语境所有未执行完的fork语句块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
initial begin:ini procl 
$display("@%t fork-join_any entered",$time);
fork
exec(1,10); //任务一需要10ns
exec(2,60); //任务二需要60ns
join_any //此fork块需要10ns放行,60ns结束
$display("@%t fork-join_any exited",$time);
$display("@%t fork-join_none entered",$time);
fork
exec(3,30); //任务三需要30ns
exec(4,40); //任务四需要40ns
join_none //此fork块需要0ns放行,40ns结束
$display("@%t fork-join_none exited",$time);

wait fork; //由于第一个fork最长60ns全部执行完,第二个fork在第一个fork执行10ns后才开始,执行完的时刻为50ns时,第一个fork执行完的时刻为60ns时
$display("@%t ini_procl exited",$time); //因此两个fork全部执行完的时间为60ns

//disable fork; //一旦运行到此,所有的为执行完的fork都强制结束。
// $display("@%t ini_procl exited",$time);
end

时序控制

SV可以通过延迟控制或者事件等待来对过程块完成时序控制。

  • 延迟控制即通过#来完成。
  • 事件(event)控制即通过@来完成。
  • wait语句也可以与事件或者表达式结合来完成。
    1
    2
    3
    4
    5
    6
    7
    #10 rega=regb;      //延时控制

    @r rega=regb; //时间控制
    @(posedge clock) rega=regb;

    real AOR[]; //wait表达式控制
    initial wait(AOR. size()>0)....;

进程间同步和通信

概述

  • 测试平台中的所有线程都需要同步并交换数据。
  • 一个线程等待另外一个,例如验证环境需要等待所有激励结束、比较结束才可以结束仿真。比如监测器需要将监测到的数据发送至比较器,比较器又需要不同的缓存获取数据进行比较。

事件(event)

  • 可以通过event来声明一个命名event变量,并且去触发它。
  • 这个命名event可以用来控制进程的执行。
  • 可以通过”->”来触发事件。
  • 其它等待该事件的进程可以通过**@**操作符(边沿触发)或者 wait()(电平触发)来检查event触发状态来完成。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    event e1,e2,e3; 
    task automatic wait_event(event e,string name);
    $display("@%t start waiting event %s",$time,name);
    @e;
    $display("@%t finish waiting event 8s",$time,name);
    endtask
    initial begin
    fork
    wait_event(el,"el"); //开始时 等待事件1
    wait_event(e2,"e2"); //开始时 等待事件2
    wait_event(e3,"e3"); //开始时 等待事件3
    join
    end
    initial begin
    fork
    begin #10ns->e1 end; //开始10ns时刻 触发事件1
    begin #20ns->e2 end; //开始20ns时刻 触发事件2
    begin #30ns->e3 end; //开始30ns时刻 触发事件3
    join
    end

    //如果将event换成bit,每次触发的时候反向,那么传入参数需要变成(ref bit e)实时监控信号变化

    event类型传递的类似于句柄,自带实时监测功能。
    @和wait一般没有区别,但同一时刻既触发又等待是时候,两者有区别。

旗语(semaphore)

  • 旗语从概念上讲,是一个容器。
  • 在创建旗语的时候,会为其分配固定的钥匙数量。使用旗语的进程必须先获得其钥匙,才可以继续执行。
  • 旗语的钥匙数量可以有多个,等待旗语钥匙的进程也可同时有多个。
  • 旗语通常用于互斥,对共享资源的访问控制,以及基本的同步
    1
    2
    3
    4
    5
    6
    7
    //创建旗语,并为其分配钥匙的方式如下:
    semaphore sm;
    sm=new();
    new(N=0); //创建一个具有固定钥匙数量的旗语,并生成N(默认为0)把钥匙
    get(N=1); //从旗语那里获取一个(默认)或多个钥匙(阻塞型),钥匙可用返回1继续执行,钥匙不足,进程阻塞,排队拿取钥匙,等待队列先进先出(FIFO)
    put(N=1); //将一个或多个钥匙返回到旗语中
    try_get(N=1);//尝试获取一个或多个钥匙而不会阻塞(非阻塞型),钥匙可用,返回1继续执行,要是不足,返回0,但不阻塞。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    semaphore mem_acc_key; 
    int unsigned mem [int unsigned];
    task automatic write(int unsigned addr, int unsigned data); //写函数
    mem_acc_key.get(); //获取钥匙
    #1ns; //写1ns
    mem[addr]=data; //写入数据
    mem_acc_key.put(); //还钥匙
    endtask
    task automatic read(int unsigned addr, output int unsigned data); //读函数
    mem_acc_key.get(); //获取钥匙
    #1ns;
    if(mem.exists(addr)) //读取数据
    data=mem[addr];
    else data='x;
    mem_acc_key.put(); //还钥匙
    endtask
    initial begin
    int unsigned data=100;
    mem_acc_key=new(1); //生成钥匙
    forever begin
    fork
    begin
    #10ns;
    write('h10, data+100);
    $display("@%t write with data %0d", $time, data);
    end
    begin
    #10ns;
    read('h10, data);
    $display("@%t read with data %0d", $time, data);
    end
    join
    end
    end

信箱(mailbox)

  • 信箱mailbox可以使得进程之间的信息得以交换,数据可以由一个进程写入信箱,再由另外一个进程获得。信箱在创建时可以限制其容量,或者不限制
  • 当信箱容量写满时,后续再写入的动作会被挂起,直到信箱自据从中读取,使得信箱有空间以后才可以继续写入。不限制容量的信箱则不会挂起写入信箱的动作。
    1
    2
    3
    4
    5
    6
    7
    8
    //创建信箱的方式如下:
    new(N=0); //创建信箱,默认不限定大小(即N=0),传如正参数表示限定信箱容量
    put(); //将信息按照FIFO顺序写入信箱,如果信箱满了,则put任务挂起,直到有新空间
    try_put(); //试着按照FIFO写入信箱,但不会阻塞,写入成功返回1,写入失败返回0
    get(); //获取信息同时取出数据,若信箱为空,get任务挂起,直到有信息可以读取,读取的信息会同时从信箱移除
    peek(); //获取信息但不会取出数据,更像是一种拷贝,信箱中的信息不会被移除
    try_get()/try_peek(); //试着取出数据但不会阻塞,若读取成功返回1.读取失败返回0
    num(); //获取信箱信息的数目,通常结合读写信函数,防止被阻塞。

参数化信箱

  • 默认的信箱,在没有指定类型的情况下,可以储存任何类型的数据,但为了避免运行错误和类型不匹配,建议在声明时指定储存类型。
  • 这种参数化信箱的方式可以在编译的时候检查类型不匹配的情况。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    module tb; 
    mailbox #(int) mb; //限定信箱内容类型
    initial begin
    int data;
    mb=new(8); //限定信箱容量
    forever begin
    case($urandom()%2)
    0: begin
    if(mb.num()<8)
    begin
    data=$urandom_range(0,10);
    mb.put(data);
    $display("mb put data %0d", data);
    end
    end
    1: begin
    if(mb.num()>0)
    begin
    mb.try_get(data);
    $display("mb get data %0d", data);
    end
    end
    endcase
    end
    end endmodule

    typedef mailbox #(string) s_mb; //限定信箱内容为字符串
    s_mb mb = new(8);

虚方法

概述

  • 类的成员方法(变量不行)可加以修饰词virtual(虚方法)。
  • 虚方法是一种基本的多态(polymorphic)结构。
  • 一个虚方法可以覆盖基类的同名方法。
  • 在父类和子类中声明的虚方法,其方法名、参数名、参数方向等都应该保持一致
  • 在调用虚方法时,它将调用句柄指向对象的方法,而不受句柄类型的影响

应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class BasePacket; 
int A=1;
int B=2;
function void printA;
$display("BasePacket::A is %d",A);
endfunction: printA
virtual function void printB; //父类必须声明虚方法,声明后此函数优先级下降
$display("BasePacket:: Bis %d",B);
endfunction: printB
endclass: BasePacket

class My_Packet extends BasePacket;
int A=3;
int B=4;
function void printA;
$display("My_Packet::A is %d",A);
endfunction: printA
virtual function void printB; //子类可以不声明虚方法
$display("My_Packet::B is %d",B);
endfunction: printB
endclass: My_Packet

BasePacket P1=new;
My_Packet P2=new;

initial begin
P1.printA; //displays ' BasePacket::A is 1.
P1.printB; //displays ' BasePacket::B is 2'
P2.printA; //displays ' My_Packet::A is 3.
P2.printB; //displays ' My_Packet::B is 4.
//在子类句柄赋值给父类句柄的时候,虚方法开始起作用
P1=P2; //Pl has a handle to a My packet object
Pl.printA; //displays ' BasePacket::A is 1,
Pl.printB; //diaplaya ' Hy Packet::B is 4 latest derived method
//由于经过句柄赋值,终归是子类对象,虚方法会寻找子类的函数
end

类型转换

静态转换

  • 静态转换操作符不对转换值进行检查。
  • 转换时指定目标类型,并在要转换的表达式前加上单引号’
  • Verilog对整数和实数类型,或者不同位宽的向量之间进行隐式转换
    1
    2
    3
    4
    int i; 
    real r;
    i=int'(10.0-0.1);//cast is optional
    r=real'(42);//cast is optional

动态转换

  • 将子类的句柄赋值给父类的句柄可以(子->父),在我们将父类的句柄赋值给子类的句柄时编译将会报错。
  • $cast()系统函数可以将父类句柄转换为子类句柄,只要该柄指向的是一个子类的对象。(在父类需要访问子类变量的时候用到,父类句柄访问子类方法可以使用虚方法)