路科验证的个人空间 https://blog.eetop.cn/1561828 [收藏] [复制] [分享] [RSS]

空间首页 动态 记录 日志 相册 主题 分享 留言板 个人资料

日志

SV系统集成篇之三:测试场景的生成(下)

已有 1179 次阅读| 2017-6-9 01:00 |个人分类:验证系统思想|系统分类:芯片设计

线程的精细控制
除了知道各个子线程什么时候结束之外,是否可以停止?暂停?恢复呢各个线程呢?
首先我们来看看停止线程的用法。
第一种方式是,给线程先起个名字,譬如“孙行者”或者“行者孙”,然后通过关键词disable来停止线程的运行。来看看下面这个例子:
module fork_case2;
task t1();
#15ns;
$display("t1 is leaving");
endtask

task tkill();
#10ns;
$display("@%0t kill thread_trigger", $time);
disable thread_trigger;
$display("I am still alive");
endtask
initial begin
$display("fork:thread_trigger start");
fork: thread_trigger
t1();
tkill();
join_none
$display("fork:thread_trigger finish");
end
endmodule

输出结果:
# fork:thread_trigger start
# fork:thread_trigger finish
# @10 kill thread_trigger

thread_trigger的子线程tkill()会在10ns之后停止线程thread_trigger。这里,disable的作用在于停止线程thread_trigger以及它的所有子线程t1()和tkill()。因此在10ns时,thread_trigger线程就被直接结束了,因此输出结果没有打印出来t1::"t1 is leaving",也没有打印出tkill::"I am still alive"。
然而在使用按照线程名字起名来停止线程运行的时候,需要注意避免出现多个同名的线程,因为disable一旦停止该名字的线程,即会停止所有该名字的线程。来看看下面这个例子:
module fork_case3;
int i;
int id = 0;
task t1();
$display("t1[%0d] start", id);
id++;
#15ns;
$display("t1[%0d] finish", id);
endtask

task tkill();
#10ns;
$display("@%0t kill thread_trigger", $time);
disable thread_trigger;
endtask

initial begin
for(i=0; i<3; i++) begin
fork: thread_trigger
t1();
join_none
end
tkill();
end
endmodule

输出结果:
# t1[0] start
# t1[1] start
# t1[2] start
# @10 kill thread_trigger

上面这个例子中,在for循环语句中先后调用了三次t1()任务,而在稍后的tkill()中停止了thread_trigger线程,实际上是停止了三个同名的thread_trigger这一禁止的行为是不做任何区别的
再来看看停止线程的另外一种方式“disable fork”。从下面这个例子可以看到,在initial过程语句块中有三个并行的fork线程,每一个fork线程又开辟了子线程分别取调用t1、t2、t3。与这三个fork线程平行的语句“disable fork”会在仿真时间12ns时,停止其可见域以内所有的fork线程和由这些fork线程开辟的所有子线程。这个例子中,disable fork可见的域是initial过程块中的三个fork线程。
module fork_case4;
task t1();
$display("t1 start");
#15ns;
$display("@%0t t1 finish", $time);
endtask
task t2();
$display("t2 start");
#20ns;
$display("t2 finish");
$display("@%0t t2 finish", $time);
endtask
task t3();
$display("t3 start");
#10ns;
$display("@%0t t3 finish", $time);
endtask

initial begin
fork
t1();
join_none
fork
t2();
join_none
fork
t3();
join_none
#12ns $display("@%0t disable all descendant fork threads", $time);
disable fork;
end
endmodule
输出结果:
# t3 start
# t2 start
# t1 start
# @10 t3 finish
# @12 disable all descendant fork threads
接下来看看线程的暂停和恢复功能。用户可以通过SV的内建类process以及它的常用方法来对线程进行终止、暂停、恢复等操作。下面是几种常用的方法:
  • function state status()
  • task await()
  • function void suspend()
  • function void resume()
可以通过status()方法来得知线程目前的状态:
  • FINISHED 表示线程正产结束。
  • RUNNING 表示线程还在执行中。
  • WAITING 表示线程在一个等待语句中。
  • SUSPENDED 表示线程被暂停,等待恢复。
  • KILLED 表示线程被强制终止。
例如,下面这段代码是对线程t1的暂停和恢复的用法。需要注意的是,p1和p2只能用作句柄,无法用来创建新的process对象。在线程内部可以通过self()来返回当前线程的句柄。
module proc_case1;
process p1;
process p2;
task t1();
p1 = process::self();
$display("@%0t t1 started", $time);
#15ns;
$display("@%0t t1 running", $time);
#15ns;
$display("@%0t t1 finished", $time);
endtask
task t2();
p2 = process::self();
$display("@%0t t2 started", $time);
#20ns;
$display("@%0t t2 finished", $time);
endtask

initial begin
fork: thread_trigger
t1();
t2();
join_none
fork
begin
#5ns;
p1.suspend();
$display("@%0t t1 state is %s", $time, p1.status());
end
begin
#5ns;
p2.kill();
$display("@%0t t2 state is %s", $time, p2.status());
end
join
#20ns;
p1.resume();
end
endmodule

输出结果:
# @0 t1 started
# @0 t2 started
# @5 t1 state is SUSPENDED
# @5 t2 state is KILLED
# @25 t1 running
# @40 t1 finished

在上面的例子中,p1和p2分别是t1()和t2()线程的句柄。在5ns时对t1()进行了暂停,由于此时t1()还在等待一段延迟(#15ns),所以t1进入了暂停状态。读者需要注意的是,只有线程在等待某些事件或者延迟的时候调用suspend()才会让线程在特定的时间片(time-slot)中进入SUSPENDED状态,如果线程本身在RUNNING状态,那么无法预计线程执行到哪一语句时进入SUSPENDED状态。

在5ns时,将t2进行了终止,使得t2的状态显示为KILLED。被KILLED的线程是无法再恢复的。在25ns时,对t1又进行了恢复,这时t1实际上已经经过了从0ns到15ns的延迟(时间点已经是25ns),所以t1会在25ns时显示"t1 running"。紧接着,再经过15ns即40ns时显示的是“t1 finished”。
介绍完线程的停止、暂停、恢复等功能,读者需要注意的是,上面对于fork-join这一线程可以使用上述功能,而对于其它线程,task/function也同样可以使用这些功能。因为调用这些task/function也相当于在开辟新的“子线程”。因此,对于线程的概念,无论是fork-join、task/function还是initial begin-end,都可以看做是开辟新的线程。而一旦有新的线程开辟,我们则可以考虑使用上面的线程终止或者更精细的线程控制方式来实现用户的要求
动态测试向量
在介绍完对线程进行精细控制之外,我们就来看看动态测试向量的生成方式以及多个vector向量的常用控制方式。如果我们创建了一个vector类,它的任务就是产生随机数据,进而交给stimulator,那么它是否该一次性创建多个数据对象?还是按照ping-pong的传输方式来生成一个数据交给stimulator,等其“消化”完以后再生成下一个呢?照我们之前对于多个vector控制的思路来看,为了便于实时对vector群落做整体控制,我们应该使用ping-pong的方式在控制vector与stimulator之间的数据传输。这样的传输方式通过一个定长的FIFO或者mailbox就可以实现。下面这个例子中,vector和stimulator简化了vector中数据生成的过程以及stimulator数据驱动的方法实现,主要就它们之间的数据传输展开讨论。
class trans;
rand bit [7:0] addr;
rand bit [31:0] data_arr[];
rand int size;
constraint c1 {soft size inside {4, 8, 16};
data_arr.size() == size;};
endclass

class vector;
mailbox #(trans) put_port;
trans t;

task run();
fork
forever begin
gen_trans();
put_trans();
end
join_none
endtask
function void gen_trans();
t = new();
t.randomize();
$display("@%0t vector:: generated one trans", $time);
endfunction
task put_trans();
put_port.put(t);
$display("@%0t vector:: put one trans", $time);
endtask
endclass

class stimulator;
mailbox #(trans) get_port;
trans t;

function new();
get_port = new(1);
endfunction

task run();
fork
forever begin
get_trans();
drive();
end
join
endtask
task drive();
#5ns;
$display("@%0t stim:: drive the trans", $time);
endtask
task get_trans();
get_port.get(t);
$display("@%0t stim:: got one trans", $time);
endtask
endclass


module tb;
vector v;
stimulator s;
event build_end_e;
event connect_end_e;
initial begin: build
v = new();
s = new();
-> build_end_e;
end

initial begin: connect
wait(build_end_e.triggered());
v.put_port = s.get_port;
->connect_end_e;
end

initial begin: run
wait(connect_end_e.triggered());
fork
v.run();
s.run();
join_none
end
endmodule
输出结果:
# @0 vector:: generated one trans
# @0 vector:: put one trans
# @0 vector:: generated one trans
# @0 stim:: got one trans
# @0 vector:: put one trans
# @0 vector:: generated one trans
# @5 stim:: drive the trans
# @5 stim:: got one trans
# @5 vector:: put one trans
# @5 vector:: generated one trans
# @10 stim:: drive the trans
# @10 stim:: got one trans
# @10 vector:: put one trans
# @10 vector:: generated one trans
...

从上面的例子中可以看到,vector类通过方法gen_trans()和put_trans()来生成新的trans对象并且传递给stimulator。而stimulator则首先通过get_trans()从vector得到一个trans对象,再通过drive()来将trans对象的数据(内含一个完整的数据包)驱动到接口信号上面。

在顶层tb中,分别例化和连接了vector v和stimulator s,它们之间的数据传输通道通过在stimulator内创建的一个定长为1的mailbox。这使得该mailbox stimulator::get_port只有1个存储trans对象的空间,因此从输出结果来看,vector首先产生一个trans对象,进而传递给stimulator,在stimulator“消化”了该数据对象之后,才会从mailbox中取得下一个trans对象。在这之前,由于mailbox的定长为1,使得vector无法不断将对象句柄写入到stimulator::get_port中。通过这种定长的存储方式,就完成了vector与stimulator之间在传输每个对象时都可以握手的需求
vector群落的并发控制
在介绍完vector同stimulator的数据传输同步机制后,我们最后来看看如何调配vector群落,使其可以奏出美妙的“交响音乐”。为了模拟MCDF实际的激励场景,我们考虑了如下的vector和其对应的stimulator:
  • slv_vec & slv_stm:用来生成slave接口对应的激励向量。
  • reg_vec & reg_stm:用来生成registers接口对应的激励向量。 fmt_vec & fmt_stm:用来生成formatter接口对应的激励向量。下面的tb中,继续简化了验证环境的结构,剔除了monitor、agent和environment,为了说明如何调度vector群,只保留了vector和stimulator的连接和传输关系。 module tb;
slv_vector slv_vec[3];
reg_vector reg_vec;
fmt_vector fmt_vec;
slv_stimulator slv_stm[3];
reg_stimulator reg_stm;
fmt_stimulator fmt_stm;
event build_end_e;
event connect_end_e;
initial begin: build
foreach(slv_vec[i]) slv_vec[i] = new();
reg_vec = new();
fmt_vec = new();
foreach(slv_stm[i]) slv_stm[i] = new();
reg_stm = new();
fmt_stm = new();
-> build_end_e;
end

initial begin: connect
wait(build_end_e.triggered());
foreach(slv_vec[i]) slv_vec[i].put_port = slv_stm[i].get_port;
reg_vec.put_port = reg_stm.get_port;
fmt_vec.put_port = fmt_stm.get_port;
->connect_end_e;
end

initial begin: run
wait(connect_end_e.triggered());
fork
foreach(slv_stm[i]) slv_stm[i].run();
reg_stm.run();
fmt_stm.run();
join_none
reg_vec.run();
fork
fmt_vec.run();
join_none
fork
foreach(slv_vec[i]) slv_vec[i].run();
join_none
end
endmodule

首先声明了多个vector和stimulator,并且在build和connect阶段分别对其例化和进行组件之间的连接。而在run阶段,对于vector的调度即体现在如何使用线程的并行方式。在上面的例子中,首先让stimulator组件都运行起来,这可以保证环境结构中的组件保持在“待命”状态,等待从vector对象传递过来的数据对象。接下来,进入了调度vector对象的阶段了。测试场景需要先等待reg_vec执行完毕,这是为了让DUT进入特定的配置模式。完成寄存器配置之后,等待对MCDF下行stimualtor fmt_stm的控制,使其可以进入预定义的工作模式,例如阻塞状态(blocking),或者是忙碌状态(busy)。最后则命令三个slv_vec向量同时向MCDF发送数据,模拟同时进行输出传输的情景,测试MCDF内部arbiter的仲裁逻辑。
从这个例子可以看出几个值得考虑的地方:
  • 无论是stimualtor、monitor、agent还是environment,都应当属于环境结构组件,因此在vector进行数据生成和传输之前都应当建立起来。这如同一栋房子在通水电之前,应当先修建好一样。
  • 在调度vector测试向量时,需要考虑不同vector之间的时序关系。从MCDF这个例子可以看到,应当首先考虑register vector,再考虑formatter vector,最后才需要考虑slave vector的生成和传输。
  • 上面的例子对reg_vec、slv_vec和fmt_vec进行了概括化。实际上,由于需求的差别,应当存在不同种类的vector向量,通过这些vector类进行组合之后就可以变化出更加丰富的测试场景。
  • 另外,对于在运行过程中的vector,也可以通过外部的变量、条件组合以及外部约束给出保持时刻变化的测试向量,提高覆盖率收敛的效率。这一点,我们会在下一节《灵活化的配置》中进行讨论。
    谢谢你对路科验证的关注,也欢迎你分享和转发真正的技术价值,你的支持是我们保持前行的动力。


点赞

评论 (0 个评论)

facelist

您需要登录后才可以评论 登录 | 注册

  • 关注TA
  • 加好友
  • 联系TA
  • 0

    周排名
  • 0

    月排名
  • 0

    总排名
  • 0

    关注
  • 253

    粉丝
  • 25

    好友
  • 33

    获赞
  • 45

    评论
  • 访问数
关闭

站长推荐 上一条 /1 下一条

小黑屋| 关于我们| 联系我们| 在线咨询| 隐私声明| EETOP 创芯网
( 京ICP备:10050787号 京公网安备:11010502037710 )

GMT+8, 2024-4-25 19:12 , Processed in 0.021791 second(s), 12 queries , Gzip On, Redis On.

eetop公众号 创芯大讲堂 创芯人才网
返回顶部