SV——线程及线程间的通信(一)

​ 实际硬件中,计算是并发进行的,在Verilog中通过initial、always、连续赋值来模拟,在测试平台中为了模拟、检验Verilog中的这些语句块,tb使用许多并发的线程。

1. 线程的定义和使用

1.1 定义线程

initial 、always、assign都是进程,初次之外还有:

  1. fork join

    其内的语句并发,fork-join块执行完才执行后面的语句。

  2. fork join_none

    其内 的语句并发,并且不会阻塞块之后的语句,块内语句与块之后的语句是并发关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
initial begin
语句1
#10
fork
语句2
语句3
join_none
语句4
语句5
end
// 执行顺序
// #0 语句1
// #10 语句2,语句3,语句4并发执行
// #10 语句4执行完之后才执行语句5。4执行完之后,即使2,3没执行完,也会接着执行5,因为fork块内语句与之后的语句是并行的,不会阻塞之后的语句
  1. fork join_any

    与fork Join_none类似,只是,先要执行一条块内的进程。

1
2
3
4
5
6
7
8
9
10
11
initial begin
fork
#20 $display("@ %0t [line1] seq 20",$time);
#10 $display("@ %0t [line2] seq 10",$time);
join_any
#15 $display("@ %0t [line3] after seq 15",$time);
end
//结果
@ 10 [line2] seq 10
@ 20 [line1] seq 20
@ 25 [line3] after seq 15

分析:先要执行fork内的语句,line1与line2的并行,先执行完line2,此时仿真时间是10ns。从现在开始也可以执行fork块之后的语句,也就是line3,line3还需要15ns才输出,而此时line1已经仿真的10ns,再需要10ns就可以输出了,所以先输出line1,然后是line3

1.2 动态线程

在类中创建线程,用上面的三个fork块。

每个fork块都可以看成启动了一个线程。

如果循环启动线程,需要用automatic关键字,来自动创建变量,这样为每个线程单独分配内存。

1
2
3
4
5
6
7
initial begin
for(int i=0;i<3;i++)
fork
automatic int k=i;
$display(k);
join_none
end

1.3 等待所有子线程结束

在SV,所有的initial都执行完就结束仿真了,有的线程执行时间长,可能还没执行完,仿真就结束了。

1
2
3
4
5
6
7
initial begin
....
run_fork(); //调用run_fork()之后,继续执行a<=b,最后一条语句结束,仿真结束,此时
// run_fork()可能还没执行完。
a <= b;
....
end

wait fork来等待线程都执行完。

1
2
3
4
5
6
7
8
9
task run();
fork:fork_all
run_fork0(); //线程0 ;任务内有fork
run_fork1();//任务内有fork
run_fork2();//任务内有fork
...
join_none
wait fork; // 等待fork_all中的所有线程都执行完
endtask

1.4 停止线程 disable

  1. 停止单个线程

    1
    2
    3
    4
    5
    6
    7
    8
    initial begin
    fork:timeout_fork //fork有标识符
    run_fork0();//任务内有fork
    run_fork1();//任务内有fork
    join_none
    #100;
    disable timeout_fork; // disable+标识符
    end
  2. 停止多个线程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    initial begin
    run_fork2();
    fork // timeout_fork
    begin
    run_fork0();//任务内有fork
    run_fork1();//任务内有fork
    #100 disable fork;// 将disable_fork进程中所有的子进程都停止,不需要标识符。
    end
    join
    end

    timeout_fork块用来限制要disable的fork的范围,上方代码中的disable对run_fork2()没影响。

  3. 禁止多次被调用的任务

    如果在任务中启动了进程,当禁用这个任务的时候,会停止所有由该任务启动的进程;在其他地方也调用了该任务,那么其他的那些进程也会被disable。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    task time_out(input int i);
    if(i==0)
    #2 disable time_out; // 如果i等于0,那么2ns之后停止任务
    ....
    endtaks
    initial begin
    time_out(0);
    time_out(1);
    time_out(2);
    end

    在2ns时,停止time_out(0)任务,此时也会导致另外两个任务也disable,使它们不能执行完。

    所有disable任务要慎重。

2. 线程间通信

​ 测试平台中所有的线程需要传递数据,可能多个线程同时要访问同一个数据,测试平台的代码是使得同一时间只有一个线程能访问。

​ 这些数据交换和控制的同步叫做线程间的通信(IPC)。

3. 事件 event

SV中对Verilog中的event做了扩展:

1. event可以作为参数传递给方法。

2. 引入了triggered函数

Verilog中由@,->操作符来阻塞和触发事件。如果一个线程在阻塞事件的同时,另一个线程同时触发了事件,那么可能发生竞争,如果触发先于阻塞,那么错过触发。

SV中引入了triggered函数,它可以查询事件是否被触发,包括当前时间片触发(time slot)。触发了返回1.这样可以用wait来等待这个函数的结果,而不必用@来阻塞。

@e1是边沿敏感的阻塞语句;wait(e1.triggered())是电平敏感的。

1
2
3
4
5
6
7
8
9
10
11
12
event e1,e2;
initial begin:i1
@e1; // 先执行i1块,发现阻塞
$display(....);
->e2; //执行完代码后触发e2,开始执行i2
end
initial begin:i2
#1; // 1ns后触发e1,并且阻塞在e2
->e1;
@e2;
$display(...);
end
1
2
3
4
5
6
7
8
9
10
11
12
event e1,e2;
initial begin
wait(e1.triggered());
$display(....);
->e2;
end
initial begin
#1;
->e1;
wait(e2.triggered());
$display(...);
end

3.1 在循环中使用事件

@e1是边沿敏感的阻塞语句;wait(e1.triggered())是电平敏感的。

在循环中使用事件,如果循环是0延时的,那么会有点问题:

  1. 电平敏感的阻塞

    1
    2
    3
    4
    5
    6
    initial begin
    forever begin
    wait(e1.triggered());
    $display(...);
    end
    end

    wait会持续地触发,仿真时间不会向前推进。因为wait触发,执行了一个循环之后,还在当前时间片,e1.triggered()还是返回1,wait继续触发。

    改进:在循环中加入延迟。

  2. 边沿敏感的阻塞

    1
    2
    3
    4
    5
    6
    initial begin
    forever begin
    @e1;
    $display(...);
    end
    end

    边沿的触发,即使0延迟,只触发一次。

3.2 事件作为参数

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
class Generator;
event e;
function new(event e1) //传入事件
this.e = e1;
endfunction
task run()
...
->e; //触发
endtask
endclass
class Driver;
event e;
function new(event e2);//传入事件
this.e=e2;
endfunction
task run();
@e; //等待触发
// wait(e.triggered());
...
endtask
endclass
program test;
Generator gen;
Driver drv;
event e;
initial begin
gen=new(e);
drv=new(e);
fork
gen.run();
drv.run();
join
end
endpragram

3.3 等待多个事件

如果有多个发生器,那么需要等待所有的发生器的线程都执行完。

方法一、用wait fork

1
2
3
4
5
6
7
8
9
10
11
12
event done[N];// N是发生器数目
initial begin
foreach (gen[i])begin
gen[i]=new(done[i]);
gen[i].run();
end
foreach(gen[i]) fork
automatic int k=i;
wait(done[k].triggered());
join_none
wait fork; //等待所有的fork执行完
end

方法二、用计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
event done[N];// N是发生器数目
int cnt;
initial begin
foreach (gen[i])begin
gen[i]=new(done[i]);
gen[i].run();
end
foreach(gen[i]) fork
automatic int k=i;
begin //begin块
wait(done[k].triggered());
cnt++; //触发一个,计数加一。
end
join_none
wait(cnt==N); //等待计数到N。说明所有的fork执行完毕,所有的事件都触发
end

方法三、摆脱事件,只用静态变量来计数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Generator ;
static int cnt=0;
task run();
cnt++; // 调用run计数加一
fork
begin
....
cnt--; //代码执行完毕,cnt减一。
end
join_none
endtask
endclass
initial begin
foreach (gen[i]) gen[i]=new();
foreach (gen[i]) gen[i].run();
wait(Generator::cnt == 0); //gen启动时都+1,结束时都-1,最终结果0.
end