赞
踩
module(模块)作为SV从verilog继承过来的概念,自然地保持了它的特点,除了作为RTL模型的外壳包装和实现硬件行为,在更高层的集成层面,模块之间也需要通信和同步。
对于硬件的过程块,他们之间的通信可理解为不同逻辑/时序块之间的通信或同步,是通过信号的变化来完成的。
从硬件实现角度来看,Verilog通过Always、initial过程语句块和信号数据连接实现进程间通信。
我们可以将不同的module作为独立的程序块,他们之间的同步通过信号的变化(event触发)、等待特定事件(时钟周期)或时间(固定延时)来完成。
如果按照软件的思维理解硬件仿真,仿真中各个模块首先是独立运行的线程(thread)。
模块(线程)在仿真一开始便并行执行,除了每个线程会依照自身内部产生的事件来触发过程语句块之外,也同时依靠相邻模块间信号变化来完成模块之间的线程同步。
linux中的线程也是通过fork来创建的
initial begin
$display("@%0t: start fork...join example", $time);
#10 $display("@%0t: sequential after #10", $time);
fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);
begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join
$display("@%0t: after join", $time);
#80 $display("@%0t: finish after 80", $time);
end
@0: start fork...join example
@10: sequential after #10
@10: parallel start
@20: parallel after #10
@40: sequential after #30
@50: sequential after #10
@60: parallel after #50
@60: after join
@140: finish after 80
总结:可见fork…join内是并行执行的,initial块内程序作为父线程,并创建了四个子线程。
initial begin
$display("@%0t: start fork...join_any example", $time);
#10 $display("@%0t: sequential after #10", $time);
fork
$display("@%0t: parallel start", $time);
#50 $display("@%0t: parallel after #50", $time);
#10 $display("@%0t: parallel after #10", $time);
begin
#30 $display("@%0t: sequential after #30", $time);
#10 $display("@%0t: sequential after #10", $time);
end
join_any
$display("@%0t: after join_any", $time);
#80 $display("@%0t: finish after 80", $time);
end
@0: start fork...join_any example
@10: sequential after #10
@10: parallel start
@10: after join_any
@20: parallel after #10
@40: sequential after #30
@50: sequential after #10
@60: parallel after #50
@90: finish after 80
总结:子线程的创建与fork…join是一致的,不同的是,fork…join要等所有子线程执行完毕才会继续执行父线程的程序,而fork…join_any中只要有一个子线程(最短的)执行完毕,父线程的程序就会被执行的。
在SV中,当程序中的initial块全部执行完毕,仿真器就退出了,也就是如果以上测试代码中没有最后的…finish after…语句,对于join_any和join_none其子线程没有全部执行完毕仿真就结束了。
如果需要等待所有fork块中的子线程全部执行完毕在退出结束initial块,可以使用wait fork语句来等待所有子线程结束。
task run_thread;
...
fork
check_trans(tr1); //thread1
check_trans(tr2); //thread2
check_trans(tr3); //thread3
join_none
...
// 等待所有fork中的线程结束
wait fork;
endtask
在使用了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(tr0); //线程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
其实最准确的关闭线程的方法是,给定线程的标号,关闭指定的线程。
event el, e2;
initial begin
$display("@t0t: 1: before trigger", $time);
-> el;
@e2;
$display("@%0t: 1: after trigger", $time);
end
initial begin
$display("@t0t: 2: before trigger", $time);
-> e2;
@e1;
$display("@t0t: 2: after trigger", $time);
end
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
总结:
进一步:
event el, e2;
initial begin
$display("@t0t: 1: before trigger", $time);
-> el;
wait (e2.triggered());
$display("@%0t: 1: after trigger", $time);
end
initial begin
$display("@t0t: 2: before trigger", $time);
-> e2;
wait (e1.triggered());
$display("@t0t: 2: after trigger", $time);
end
@0: 1: before trigger
@0: 2: before trigger
@0: 1: after trigger
@0: 2: after trigger
总结:
不同的线程之间,有时会有互相告知的需求。比如,我们要开一辆车,在踩油门要行驶之前,首先得看一下汽车有没有发动,那么这辆车可能是这样设计的:
module road;
initial begin
automatic car byd = new();
byd.drive();
end
endmodule
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 drive();
fork
this.launch();
this.move() ;
join
endtask
endclass
//输出结果:
// car is launched
// car is moving
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 drive();
fork
this.launch();
this.move() ;
join
endtask
endclass
当汽车要加速的时候,添加速度显示功能:
module road;
initial begin
automatic car byd = new();
byd.drive();
byd.speedup();
byd.speedup();
byd.speedup();
end
endmodule
class car;
event e_start;
event e_speedup;
int speed = 0;
......
task speedup();
#10ns;
-> e_speedup;
endtask
task display();
forever begin
@e_speedup;
speed++;
$display("speed is %0d", speed);
end
endtask
task launch();
start=1;
$display(”car is launched”);
endtask
task move();
wait(start==1);
$display(”car is moving”);
endtask
task drive();
fork
this.launch();
this.move() ;
this.display() ;
join_none
endtask
endclass
//输出结果:
// car is launched
// car is moving
// speed is 1
// speed is 2
// speed is 3
这里有两点值得分析:
因为使用 triggered的话,再第一次触发以后,下次会认为已经触发过而不再继续等待触发,这和设计功能是违背的,这里需要对@和triggerd按功能区分使用(UVM中提供一种event使用方式,当第一次事件触发以后,会清除此次触发,这样就不存在triggerd的问题了)。
因为父线程中的speedup需要和三个子线程并行执行。
从这个汽车加速的例子来看,如果你需要一直踩着油门不放的话,这个加速的event必定会被不断触发,而当线程A要给线程B传递超过一次事件时,使用公共变量就不再是一个好的选择了。
通过event的触发,可以多次通知另一个线程,注意此时应该使用@。
program automatic test(bus_ifc.TB bus);
semaphore sem; //创建一个semaphore
initial begin
sem=new(1) ; //分配一个钥匙
fork
sequencer() ; //产生两个总线事务线程
sequencer() ;
join
end
task sequencer;
repeat($urandom%10) //随机等待0-9个周期
@bus.cb;
send Trans() ; //执行总线事务
endtask
task sendTrans;
sem.get(1) ; //获取总线钥匙
@bus.cb; //把信号驱动到总线上
bus.cb.addr<=t.addr;
......
sem.put(1) ; //处理完成时把钥匙返回
endtask
endprogram
白话一刻
这段代码是一个简单的自动测试程序,使用了SystemVerilog(SV)语言。它的主要目的是模拟总线上的事务,并确保这些事务是顺序执行的,通过使用信号量(semaphore)来同步这些事务。
以下是对代码各个部分的解释:
program automatic test(bus_ifc.TB bus);
这一行定义了一个名为test的自动程序,该程序接受一个类型为bus_ifc.TB的接口参数bus。
semaphore sem;
这定义了一个信号量sem。信号量通常用于同步多个线程或任务,确保它们以正确的顺序执行。
initial begin
sem=new(1) ;
fork
sequencer() ;
sequencer() ;
join
end
在initial块中,首先为信号量sem分配了一个初始值1(这通常表示有一个“钥匙”可用)。然后,使用fork和join结构并行启动了两个sequencer任务。这两个任务将并发执行。
task sequencer;
repeat($urandom%10)
@bus.cb;
send Trans() ;
endtask
sequencer任务首先会随机等待0到9个周期($urandom%10生成一个0到9的随机数)。然后,它会等待bus.cb上的某个信号(可能是一个时钟信号或其他同步事件)。最后,它调用sendTrans任务来发送一个总线事务。
task sendTrans;
sem.get(1) ;
@bus.cb;
bus.cb.addr<=t.addr;
......
sem.put(1) ;
endtask
在sendTrans任务中,首先尝试获取信号量sem的一个“钥匙”(如果sem为0,则任务将等待直到有钥匙可用)。然后,它再次等待bus.cb上的某个信号,并执行一些操作(例如,将地址t.addr写入bus.cb.addr)。在任务完成并处理完总线事务后,它释放(放回)信号量的“钥匙”。
这个程序创建了两个并发的sequencer任务,每个任务在随机延迟后尝试发送一个总线事务。通过使用信号量sem,确保了这两个事务是顺序执行的,即一个事务必须等待另一个事务完成后才能开始。这避免了可能的总线冲突或同步问题。
线程之间除了”发球”和接球”这样的打乒乓以外,还有更深入的友谊,比如共用一些资源。
对于线程间共享资源的使用方式, 应该遵循互斥访问(mutex access) 原则。
控制共享资源的原因在于,如果不对其访问做控制,可能会出现多个线程对同一资源的访问,进而导致不可预期的数据损坏和线程的异常,这种现象称之为"线程不安全”。
以这里比亚迪为例:
class car;
semaphore key;
function new();
key=new(1) ;
endfunction
task get_on(string p);
$display("%s is waiting for the key", p);
key.get();
#1ns;
$display("%s got on the car", p);
endtask
task get_off(string p);
$display("%s got off the car", p);
key.put();
#1ns;
$display("%s returned the key", p);
endtask
endclass
module family;
car byd=new();
string p1="husband";
string p2="wife";
initial begin
fork
begin//丈夫开车
byd.get_on(p1);
byd.get_off(p1);
end
begin//妻子开车
byd.get_on(p2);
byd.get_off(p2);
end
join
end
endmodule
//打印结果
// husband is waiting for the key
// wife is waiting for the key
// husband got on the car
// husband got off the car
// husband returned the key
// wife got on the car
// wife got off the car
// wife returned the key
program automatic bounded;
mailbox mbx;
initial begin
mbx=new(1); //容量为1
fork
//Producer线程
for(inti=1; i<4; i++) begin
$display("Producer: before put(%0d)", i);
mbx.put(i);
$display("Producer: after put(%0d)", i);
end
//consumer线程
repeat(3) begin
int j;
#1ns mbx.get(j);
$display("Consumer: after get(%0d)", 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)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。