SystemVerilog(6):线程通信

1、线程

  • 线程即独立运行的程序。
  • 线程需要被触发,可以结束或者不结束。
  • 在 module 中的 initial 和 always,都可以看做独立的线程,它们会在仿真 0 时刻开始,而选择结束或者不结束。
  • 硬件模型中由于都是 always 语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源,因为它们并不会结束。
  • 软件测试平台中的验证环境都需要由 initial 语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此软件测试端的资源占用是动态的。
  • 软件环境中的 initial 块对语句有两种分组方式,使用 begin...end fork...join
  • begin...end 中的语句以顺序方式执行,而 fork...join 中的语句则以并发方式执行。
  • 与 fork...join 类似的并行方式语句还包括 fork...join_any fork...join_none

一些概念的澄清:

  • 线程的执行轨迹是呈树状结构的,即任何的线程都应该有父线程。
  • 父线程可以开辟若干个子线程,父线程可以暂停或者终止子线程。
  • 当子线程终止时,父线程可以继续执行。
  • 当父线程终止时,其所开辟的所有子线程都应当会终止。

2、线程的控制

2.1 三种fork

  • fork_join:最长时间的程序走完,才会结束 fork 块;
  • fork_join_any:最短时间的程序走完,就会结束 fork 块,但 fork 语句里未走完的程序依旧会执行(前提是Initial未结束);
  • fork_join_none:一运行就会结束 fork 块,但 fork 语句里未走完的程序依旧会执行(前提是initial未结束);

2.2 等待所有fork

在sv中,当程序中的 initial 块全部执行完毕,仿真器就退出了。如果我们希望等待 fork 块中的所有线程执行完毕再退出结束 initial 块,我们可以使用 wait fork 语句来等待所有子线程结束。

task run_threads ;
    fork
        check_trans (tr1); //线程1
        check_trans (tr2); //线程2
        check_trans (tr3); //线程3
    join_none
    ...
    //等待所有fork中的线程结束再退出task
    wait fork;
endtask

2.2 停止单个fork

在使用了 fork ...join any 或者 fork...join_none 以后,我们可以使用 disable 来指定需要停止的线程。

parameter TIME_OUT = 1000;

task check_trans( Transaction tr) ;
    fork 
        begin
            //等待回应,或者达到某个最大时延
            fork: timeout block
                begin
                    wait(bus.cb.addr == tr.addr) ;
                    $display("@%0t: Addr match %d",$time, tr.addr);
                end
                #TIME_OUT $display("@%0t: Error: timeout",$time);
            join_any
            disable timeout_block ;
        end
    join_none
endtask

disable fork可以停止从当前线程中衍生出来的所有子线程。

initial begin
    check_trans (tro) ; //线程0
    //创建一个线程来限制disable fork的作用范围
    fork //线程1
        begin
            check_trans(tr1); //线程2
            fork //线程3
                check_trans(tr2); //线程4
            join
            //停止线程1-4,单独保留线程0
            #(TIME_OUT/2) disable fork;
        end
    join
end

2.3 停止被多次调用的任务

如果你给某一个任务或者线程指明标号,那么当这个线程被调用多次以后,如果通过disable去禁止这个线程标号,所有衍生的同名线程都将被禁止。

task wait_for_time__out(int id) ;
    if (id == 0)
    fork
        begin
            #2;
            $display ("@%0t: disable wait_for_time_out", $time);
            disable wait_for_time_out;
        end
    join_none
    fork: just_a_little
        begin
            $display ("@0t: 8m: 80d entering thread", $time, id);
            #TIME_OUT;
            $display("@%0t:%m: %0d done", $time, id);
        end
    join_none
endtask

initial begin
    wait_for_time_out(0); //Spawn thread 0
    wait_for_time_out(1); //Spawn thread 1
    wait_for_time_out(2); //Spawn thread 2
    #(TIME_OUT*2)
    $display("@%0t: All done", $time);
end
  • 任务 wait_for_time_out 被调用了三次,从而衍生了三个线程。
  • 线程0在#2延时之后禁止了该任务,而由于三个线程均是"同名”线程,因此这些线程都被禁止了,最终也都没有完成。

3、线程间的通信

  • 测试平台中的所有线程都需要同步并交换数据。一个线程需要等待另一个。
  • 多个线程可能同时访问同一个资源。线程之间可能需要交换数据。
  • 所有这些数据交换和同步称之为线程间的通信(IPC,Interprocess Communication) 。

3.1 事件event

  • Verilog中,一个线程总是要等待一个带@操作符的事件。这个操作符是边沿敏感的,所以它总是阻塞着、等待事件的变化。
  • 其它线程可以通过 -> 操作符来触发事件,结束对第一个线程的阻塞。
  • 这就像在打电话时,一个人等待另一个人的呼叫。
  • 唯一不需要 new( ) 的方法。

3.1.1 事件的边沿阻塞

event e1,e2;

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

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

打印结果如下所示:

@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
  • 第一个初始化块启动,触发e1事件,然后阻塞在e2上。
  • 第二个初始化块启动,触发e2事件,然后阻塞在e1上。
  • e1 和 e2 在同一个时刻被触发,但由于 delta cycle 的时间差使得两个初始化块可能无法等到 e1 或者 e2。
  • 所以,更安全的方式可以使用event的方法 triggered ( )

3.1.2 等待事件的触发

  • 可以使用电平敏感的 wait (e1.triggered ( ) ) 来替代边沿敏感的阻塞语句 @e1
  • 如果事件在当前时刻已经被触发,则不会引起阻塞。否则,会一直等到事件被触发为止。
  • 这个方法比起 @ 而言,更有能力保证,只要 event 被触发过,就可以防止引起阻塞。
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
@0: 2: before trigger
@0: 1: after trigger
@0: 2: after trigger

3.2 旗语semaphore

  • semaphore 可以实现对同一资源的访问控制。
  • 对于初学者而言,无论线程之间在共享什么资源,都应该使用 semaphore 等资源访问控制的手段,以此避免可能出现的问题。
  • semaphore 有三种基本操作。
    • new( ) 方法可以创建一个带单个或者多个钥匙的semaphore;
    • 使用 get( ) 可以获取一个或者多个钥匙;
    • put( ) 可以返回一个或者多个钥匙。
  • 如果你试图获取一个 semaphore 而希望不被阻塞,可以使用 try get( ) 函数。
    • 返回 1 表示有足够多的钥匙;
    • 返回 0 则表示钥匙不够。
program automatic test(bus_ifc. TB bus) ;
    sermaphore semn; //创建一-个semaphore

    initial begin
        sen = new(1); //分配一个钥匙
        fork
            sequencer(); //产生两个总线事务线程
            sequencer();
        join
    end
    
    task sequencer;
        repeat($urandom%10) //随机等待0-9个周期
        @bus.cb;
        sendTrans();        //执行总线事务
    endtask

    task sendTrans;
        sem.get(1); //获取总线钥匙
        @bus.cb;    //把信号驱动到总线上
        bus.cb.addr <= t.addr;
        ...
        sem.put(1); //处理完成时把钥匙返回
    endtask
endprogram

3.3 信箱mailbox

  • 线程之间如果传递信息,可以使用 mailbox
    • mailbox 和队列 queue 有相近之处。
  • mailbox 是一种对象,因此也需要使用 new( ) 来例化。
    • 例化时有一个可选的参数 size 来限定其存储的最大数量。
    • 如果 size 是 0 或者没有指定,则信箱是无限大的,可以容纳任意多的条目。
  • 使用 put( ) 可以把数据放入 mailbox,使用 get( ) 可以从信箱移除数据。
    • 如果信箱为满,则 put( ) 会阻塞;
    • 如果信箱为空,则 get( )会阻塞。
  • 使用 peek( ) 可以获取对信箱里数据的拷贝而不移除它。
  • 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞方法,即哪些是立即返回的,而哪些可能需要等待时间的。
program automatic bounded;
    mailbox mbx ;

    initial begin
        mbx = new(1); //容器为1
        fork
            //Producer线程
            for(int i=1; i<4; i++) begin
                $display("Producer: before put (80d)", i);
                mbx.put(i) ;
                $display("Producer : after put (80d)", i);
            end
            //consumer线程
            repeat(4) begin
                int j;
                #1ns mbx.get(j);
                $display("consumer : after get (80d)",j);
            end
        join
    end
endprogram

打印结果如下所示:

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

关于mailbox的其它特性也需要加以了解:

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

3.4 三种方法的比较

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

 

 

参考资料:

[1] 路科验证V2教程

[2] 绿皮书:《SystemVerilog验证 测试平台编写指南》第2版

posted @ 2022-07-17 15:20  咸鱼IC  阅读(715)  评论(0编辑  收藏  举报