线程和线程间的通信

介绍

Verilog的寄存器传输级上通过initial和always块语句,实例化,连续赋值语句来模拟并发的活动。为了模拟和检验这些语句块,测试平台使用许多并发执行的线程。如果按照软件的思维理解硬件仿真,仿真各个模块是独立运行的线程(thread),模块(线程)在仿真一开始便并行执行,除了每个线程会按照内部产生事件来触发过程语句块之外,同时还依靠相邻模块间的信号变化来完成模块之间线程的同步。这个过程需要借助线程之间的通信(IPC)来完成。如Verilog事件,事件控制,wait语句,SV信箱和旗语。

什么是线程?

(1)线程是独立运行的程序,需要被触发,可以结束或者不结束

(2)module的initial和always都可以看作独立的线程,会从仿真0时刻开始,可以选择结束或者不结束。硬件模型中都是always语句块,可以看成多个独立运行的线程,而这些线程会一直占用仿真资源,因为他们并不会结束。而软件测试平台验证环境都是用initial语句去创建,在仿真过程中验证环境的对象可以创建和销毁,因此软件测试端资源的占用是动态的。

SV除了fork...join语句之外,还引入了两种新的创建线程的方法——fork...join_none和fork...join_any语句,下例是在类中创建线程的例子:

class Gen_drive;
    task run(int n);
        Packet P;
        fork
            repeat(n) begin
                p=new();
                assert(p.randomize());
                transmit(p);
            end
        join_none
    endtask
    task transmit(input Packet p);
    ...
    endtask
endclass

Gen_drive gen;
initial begin
    gen=new();
    gen.run(10);
end

注意,事物处理器不是在new()函数里启动的,构造函数只是对数值进行初始化,并不启动任何线程,把构造函数和真正的进行事务处理的代码分开,允许你在开始执行事务处理代码之前修改任何变量。

线程中的变量

当你使用循环来创建线程时,如果在进入下一轮循环前没有保存变量值,会碰到常见而难以发现的错误。如果函数调用的是静态存储,那么每个线程都会共享相同的变量,这会导致后面的调用会覆盖前面调用的值。所以在并发线程中务必使用自动变量来保存数值。

不良的代码

program no_auto;
    initial begin
        for(int j=0;j<3;j++)
            fork
                $write(j);    //得到的都是最后的索引值
            join_none
        #0 $display("\n");
    end
endprogram

#0阻塞了当前线程,在上例时延使得当前线程必须等到所有fork...join_none语句中产生的线程执行完之后才得以运行。最后打印出来的是“3 3 3”,正确的代码如下所示:

initial begin
    for(int j=0;j<3;j++)
        fork
            automatic int k=j;
            $write(k);
        join_none
    #0 $display;
end

在每轮循环之中,k的一个拷贝被创建并设置为j的当前值,然后fork...join_none被调度,包括k的拷贝。在循环完成后,#0阻塞了当前线程,因此三个线程一起运行,打印出各自的拷贝值k。需要注意的是,如果代码是在使用自动存储的程序或者模块里,那么声明可以不使用关键词automatic。

等待所有的线程

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

停止线程

正如你需要在测试平台中创建线程,你可能也需要停止线程。Verilog中disable语句可以用于停止SV中的线程。

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

如果正确的总线地址来得足够早,则wait结构先完成,fork...join_any得以执行,之后disable结束剩余的线程,但是如果在TIME_OUT时延完成前总线地址都没有得到正确的值,那么错误的警告信息就会被打印出来,join_any被执行,而后的disable将结束wait线程。

使用disable可以用来停止一个署名块的所有线程,当然,你想停止多个线程,你可以使用disable fork语句来停止当前线程中衍生出来的所有线程。

initial begin
    check_trans(tr0);    //线程0
    fork     //线程1
        begin
            check_trans(tr1);    //线程2
            fork    //线程3
                check_trans(tr2)    //线程4
            join
            #(TIME_OUT/2) disable fork;
        end
    join
end

在一个时延之后,disable fork停止了线程1及其所有子线程2-4。线程0在带有disable的fork...join之外,所以不受影响。

当你从某个块内部禁止线程的时候一定要小心,你停止的可能比预期的更多。比如你在某个任务之中禁止了该任务,这也会停止所有由该任务启动的线程。如果该任务已经被多个线程调用,禁止其中的一个将导致它们全部被禁止。

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: %m:%0d entering thread",$time,id);
            #TIME_OUT;
            $display("@ % 0t: %m:%0d done",$time,id);
        end
    join_none
endtask

initial begin
    wait_for_time_out(0);
    wait_for_time_out(1);
    wait_for_time_out(2);
end

如图,任务wait_for_time_out被调用了三次,从而衍生了三个线程。线程0在#2延时之后禁止了该任务。只要运行了这段代码,就可以看到三个线程都启动了,但是因为线程0的disable语句,这些线程都没有完成。

线程间的通信

Verilog中,一个线程总是总是要等待一个带@操作符事件。这个操作符是边沿敏感,所以它总是阻塞着,等待事件的变化。其他的线程可以通过->操作符来触发事件,结束对第一个事件的阻塞。但是,握手信号事件可能在同一时间被触发,由于delta cycle的时间差使两个初始块可能无法等到e1或者e2。更安全的方式是使用event的方法的triggered()。

但是在使用wait(handshake.triggered())的时候,要注意循环语句。一定要确保在下次等待之前时间可以向前推进,否则你的代码将进入一个零时序循环,原因是wait会在单个事件触发器上反复执行。例如:

forever begin
    wait (handshake.triggered());
    $display("Receive next event");
end

这是一个零时许循环。要么需要把时延放到一个事件处理循环之中,或者采用边沿敏感的时序语句@。最后值得注意的是:事件可以像参数一样传递给子程序。比如:function new(event done);事件可以放在传递参数列表之中。

semaphore旗语可以实现对同一资源的访问控制,当测试平台中存在一个资源,比如一条总线,对应着多个请求方,而实际物理设计中又只允许单一驱动时,便可以使用旗语。旗语有三种基本操作:new方法可以创建一个带单个或者多个钥匙的旗语,get可以获取一个或者多个钥匙,put可以返回一个或者多个钥匙。如果你试图获取一个旗语而希望不被阻塞,可以使用try_get()函数。

之所以要使用semaphore,是因为对于线程之间共箱资源的使用方式,应该遵循互斥访问原则。控制共享资源的原因在于,如果不对访问作控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象称之为“线程不安全”。

program automatic test(bus_ifc.TB bus);
    semaphore sem;    //创建一个旗语
    initial begin
        sem=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<=t.addr
    ...
    sem.put(1);
    endtask
endprogram

如果要在两个线程之间传递信息呢?如果仅仅使用发生器线程去调用驱动器中的任务,发生器需要知道到达驱动器任务的层次化路径,这会降低代码的可重用性。此外,这种代码风格还会迫使发生器与驱动器在同一速率运行,在一个发生器需要控制多个驱动器的情况下会引发同步问题。

解决办法是SV的信箱。从硬件角度去看,对信箱的最简单的理解是将它看成是具有源端和收端的FIFO。当源端线程试图向一个容量固体且饱和的信箱里放入数值时,会发生阻塞直到信箱里的数据被移走。同样地,如果收端线程试图从一个空信箱里移走数据,它也会被阻塞直到有数据放入。mailbox是一种对象,需要用new()来例化。例化的时候可以有一个可选的参数来限定其存储的最大数量。size是0或者没有指定,则信箱是无限大的,可以容纳任意多的条目。put()可以数据放入mailbox,get()可以从信箱移除数据。peek()可以获取对信箱里数据的拷贝而不移除它(白嫖hhh)。

值得注意的是,一个典型的漏洞是在循环外面构造一个对象,然后使用循环对对象进行随机化并将它们放入信箱。因为实际上只有一个对象,它被一次一次随机化。所以你最后得到的是一个含有多个句柄的信箱,所有句柄都指向同一个对象。从信箱里获取的代码实际上只能见到最后一组的随机值。如下所示:

task generator_bad(int n,mailbox mbx);
    Transaction t;
    t=new();
    repeat (n) begin
        assert(t.randomize());
        $display("GEN: Sending addr=%h",t.addr);
        mbx.put(t);
    end
endtask

所以要确保每个循环中都含有构造对象,对象随机化并放入信箱三个完整的步骤。

class Generator;
    Transaction tr;
    mailbox mbx;

    function new(mailbox mbx);
        this.mbx=mbx;
    endfunction
    task run(int count);
        repeat(count) begin
            tr=new();
            assert(tr.randomize);
            mbx.put(tr);
        end
    endtask
endclass

class Driver;
    Transaction tr;
    mailbox mbx;

    function new(mailbox mbx);
        this.mbx=mbx;
    endfunction
    task run(int count);
        repeat(count) begin
            mbx.get(tr);
            @(posedge bus.cb.ack);
            bus.cb.kind<=tr.kind;
            ...
        end
    endtask
endclass

program automatic mailbox_example(bus_if.TB bus,...);
    `include "transaction.sv"
    `include "generator.sv"
    `include "driver.sv"

    mailbox mbx;
    Generator gen;
    Driver drv;
    int count;

    initial begin
        count=$urandom_range(50);
        mbx=new();
        gen=new(mbx);
        drv=new(mbx);
        fork
            gen.run(count);
            drv.run(count);
        join
    end
endprogram

信箱在缺省的情况下类似于容量不限的FIFO,消费方取走物品之前生产方可以向信箱里放任意数量的物品。但是你希望消费方处理完物品之前生产方阻塞住,以便两个线程步调一致。因此,你也可以使用定容信箱,它在两个线程之间扮演着一个缓冲器的角色。

总结

你的设计可以用很多并发运行的独立块来建模,所以测试平台也要产生很多激励流来监测并发线程的反应。SV中的fork...join和fork...join_any和fork...join_none用于动态创建线程的结构。线程之间可以用事件,旗语,信箱以及@事件控制和wait语句来实现通信和同步,最后使用disable可以中止线程。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值