SystemVerilog_线程


SV线程

语句块

用来将多个语句组织在一起,使得他们在语法上如同一个语句。

  • 顺序块:语句置于关键字begin和end之间,块中的语句以顺序方式执行。
  • 并行块:关键字fork和join/join_any/join_none之间的是并行块语句,块中的语句并行执行。

命名块

给每个块定义的标识名称,将块名加在begin或fork后面;

  • 可以定义块内局部变量
  • 允许定义的块被其他语句调用,如disable语句;

程序和模块

  • 对于硬件的过程块,他们之间的通信可以理解为不同逻辑、时序块之间的通信或者同步,是通过信号的变化来完成
  • 从硬件实现角度看,verilog 通过always,initial过程语句块和信号数据连接实现进程间通信
  • 可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)、等待待定事件(时钟周期)或者时间延时来完成
  • 从软件思维理解硬件仿真,仿真中的各个模块首先是独立运行的线程
  • 模块在仿真一开始便并行执行,除了每个线程会按照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间的信号变化来完成模块之间的线程同步

线程

  • 线程是可以独立运行的程序
  • 线程需要被触发,可以结束或者不结束
  • 在module中的 initial 和 always ,都可以看成独立的线程,他们在仿真 0 时刻开始,而选择结束或者不结束
  • 硬件模型中由于都是always 语句块,所以可以看成是多个独立运行的线程,而这些线程会一直占用仿真资源
  • 软件测试平台中的验证环境都需要有initial 语句块去创建,而在仿真过程中,验证环境中的对象可以创建和销毁,因此,软件测试端的资源占用是动态的
  • 线程的执行轨迹呈树状结构,任何线程都有父线程
    • 当子线程终止时,父线程可以继续执行 
    • 当父线程终止时,其所开辟的所有子线程都应当会终止 
  • 软件环境中的initial  块对语句有两种方式:
    • begin ...  end:内部语句以顺序方式执行
    • fork ... join:内部语句以并发方式执行

线程的控制

创建线程

fork...join/join_any/join_none:   能够从它的每一个并行语句中产生并发进程。

  • join: 父进程阻塞到这个分支产生的所有进程结束;
  • join_any:  父进程阻塞到这个分支产生的任意一个进程结束;
  • join_none:    父进程会与其生成的所有子进程并发执行。在父进程执行一条阻塞语句或者结束之前,所生成的子进程不会启动执行。

 wait fork

  • 在SV中,当程序中的initial 块全部执行完毕,仿真器就退出了。
  • 如果需要等待fork 中的所有线程执行完毕再退出结束initial 块,可以使用 wait fork 语句来等待所有子线程结束
initial begin: fork_join_none
  $display("@%0t, fork_join_none_thread entered", $time);
  fork: fork_join_none_thread
    thread(0, 10);   // 线程1
    thread(1, 20);   // 线程2
    thread(2, 30);   // 线程3
  join_none
  $display("@%0t, fork_join_none_thread exited", $time);
  wait fork;     // 等待所有线程退出
  $display("@%0t, fork_join_none_thread's all sub-threads finished", $time);
end

disable

  • 在使用fork ... join  或者fork ... join_none 以后,可以使用disable 来制定需要停止的线程
  • disable fork 可以停止从当前线程中衍生出来的所有子线程
  • 如果给某一个任务或者线程指明标号,那么当这个线程被调用多次以后,如果通过disable 去禁止这个线程标号,所有衍生的同名线程都将被禁止
initial begin: fork_join_any
  $display("@%0t, fork_join_any_thread entered", $time);
  fork: fork_join_any_thread
    thread(0, 10);
    thread(1, 20);
    thread(2, 30);
  join_any 
  $display("@%0t, fork_join_any_thread exited", $time);
  disable fork_join_any_thread;
  $display("@%0t, disabled fork_join_any_thread", $time);
end

线程间的通信:IPC

  •  测试平台中的所有贤臣都需要同步并交换数据
  • 一个线程等待另一个线程
  • 多个线程可能同时访问同一个资源
  • 线程之间可能需要交换数据

event

  • Verilog 中,一个线程总是要等待一个带 @ 操作符的事件。这个操作符是边沿敏感的,总是阻塞着、等待事件的变化
  • 其他线程可以通过  ->  操作法来触发事件,结束对第一个线程的阻塞
  • event 是边沿触发
  • 可以使用电平敏感的 wait(e1.triggered()) , 来代替边沿敏感的阻塞语句 @e1;只要event 被触发过,就可以防止引起阻塞
  • 明确什么时候使用wait:wait 用于等待一次,如果需要多次等待,则需要在等待到一次后,就清除一次,如uvm中的wait;@ 可以等待多次
event e1, e2;
initial begin
    ->e2;
    @e1;    //  触发e1,在event 边沿触发
end
initial begin
    ->e1;   //  等待e1
    @e2;
end
// e1, e2在同一时刻被触发,由于delta cycle的时间差,可能使得两个初始化无法等到e1 或者 e2

// 推荐使用
wait (e1.triggered());  

 线程间的通知

class car;
    bit start = 0;
    task launch();
        start = 1;
        $display(car is launched);
    endtask
    task move();
        wait(start == 1);
        $display("car is moving");
    endtask
    task driver();
        fork
            this.launch();
            this.move();
        join
    endtask
endclass

module road;
    initial begin
        automatic car byd = new();
        byd.driver();
    end
endmodule

envet 模式

class car;
    event     e_start;
    task launch();
        -> e_start;
        $display(car is launched);
    endtask
    task move();
        wait(e_start.triggered);
        $display("car is moving");
    endtask
    task driver();
        fork
            this.launch();
            this.move();
        join
    endtask
endclass

 semaphore

  •  可以实现对同一资源的访问控制,互斥访问
  • semaphore 的三种基本操作:
    • new()  可以创建一个带单个或者多个钥匙的semaphore
    • get()    可以获取一个或者多个钥匙
    • put()    可以返回一个或者多个钥匙
  • try_get()  : 非阻塞式获取钥匙,函数返回 1 表示有足够多的钥匙,返回  0  表示钥匙不够

 mailbox信箱

  • 线程间如果传递消息,可以使用mailbox
  • mailbox和队列有相近之处
  • mailbox 是一种对象,需要使用 new() 例化;例化时有一个可选的参数 size 来限定其存储的最大数量
    • 如果size 是0 或者没有指定 new(),则信箱是无限大的,可以容纳任意多的条目
    • new(N), 设置容量为 N
  • put():可以把数据放入mailbox;如果信箱已满,则 put 会阻塞;try_put() 非阻塞方法
  • get():可以从信箱移除数据;如果信箱为空,则 get 会阻塞;try_get()  非阻塞方法
  • peek():可以获取对信箱里数据的拷贝而不移除它;try_peek 非阻塞方法
  • 线程之间的同步方法需要注意,哪些是阻塞方法,哪些是非阻塞方法
  • 如果要显式的限定mailbox 中的元素类型,可以通过mailbox #(type=T)  的方式来声明,例如 mailbox #(int)
mailbox 队列
必须通过new()例化 只需要声明队列
可以同时存储不同的数据类型 内部存储的数据类型必须一致

put 和 get 是阻塞方法

调用阻塞方法只能在task中,因为阻塞是耗时的

push_back 和pop_front是非阻塞的

在使用queue 时,需要使用wait(queue.size()>0)才可以在其后对非空队列做取数据操作

mailbox只能用作FIFO   queue 既可以用作FIFO, 也可以用作LIFO
mailbox变量的操作,在传递形式参数是,实际传递并拷贝的是mailbox指针

传递队列的形式参数默认是input方向,传递的是数组的拷贝;

考虑使用ref方向,考虑对队列是引用还是拷贝

 总结

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

SoC