实际硬件中,计算是并发进行的,在Verilog中通过initial、always、连续赋值来模拟,在测试平台中为了模拟、检验Verilog中的这些语句块,tb使用许多并发的线程。
1. 线程的定义和使用
1.1 定义线程
initial 、always、assign都是进程,初次之外还有:
-
fork join
其内的语句并发,fork-join块执行完才执行后面的语句。
-
fork join_none
其内 的语句并发,并且不会阻塞块之后的语句,块内语句与块之后的语句是并发关系。
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块内语句与之后的语句是并行的,不会阻塞之后的语句
-
fork join_any
与fork Join_none类似,只是,先要执行完一条块内的进程,才会继续执行fork..join_any后的语句块。
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关键字,来自动创建变量,这样为每个线程单独分配内存。
initial begin
for(int i=0;i<3;i++)
fork
automatic int k=i;
$display(k);
join_none
end
1.3 等待所有子线程结束
在SV,所有的initial都执行完就结束仿真了,有的线程执行时间长,可能还没执行完,仿真就结束了。
initial begin
....
run_fork(); //调用run_fork()之后,继续执行a<=b,最后一条语句结束,仿真结束,此时
// run_fork()可能还没执行完。
a <= b;
....
end
wait fork来等待线程都执行完。
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
-
停止单个线程
initial begin fork:timeout_fork //fork有标识符 run_fork0();//任务内有fork run_fork1();//任务内有fork join_none #100; disable timeout_fork; // disable+标识符 end
-
停止多个线程
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()没影响。
-
禁止多次被调用的任务
如果在任务中启动了进程,当禁用这个任务的时候,会停止所有由该任务启动的进程;在其他地方也调用了该任务,那么其他的那些进程也会被disable。
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())是电平敏感的。
event e1,e2;
initial begin:i1
@e1; // 先执行i1块,发现阻塞
$display(....);
->e2; //执行完代码后触发e2,开始执行i2
end
initial begin:i2
#1; // 1ns后触发e1,并且阻塞在e2
->e1;
@e2;
$display(...);
end
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延时的,那么会有点问题:
-
电平敏感的阻塞
initial begin forever begin wait(e1.triggered()); $display(...); end end
wait会持续地触发,仿真时间不会向前推进。因为wait触发,执行了一个循环之后,还在当前时间片,e1.triggered()还是返回1,wait继续触发。
改进:在循环中加入延迟。
-
边沿敏感的阻塞
initial begin forever begin @e1; $display(...); end end
边沿的触发,即使0延迟,只触发一次。
3.2 事件作为参数
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
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
方法二、用计数器
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
方法三、摆脱事件,只用静态变量来计数
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