本文转自:http://www.eetop.cn/blog/html/28/1561828-2331491.html
在之前的一节《验证环境的组装》中提到了如果将stimulator与monitor封装在一个agent组件中,更易于从模块级到MCDF子系统一级的环境组合复用。而在有的时候,我们并不需要agent中的stimulator,只需要monitor,譬如如果要监视MCDF内arbiter输入和输出信号时,只需要arb_ini_mon和arb_rsp_mon,并不需要arb_ini_stm和arb_rsp_stm。
考虑到面向对象软件设计的OCP原则(开放-封闭原则),我们应当对要修改agent从而只例化monitor,不例化stimulator的行为说不。这符合OCP原则,即“对于扩展是开放的,对于更改是封闭的”。我们不应当修改原有的行为,而需要在原有的基础上添加一些变量来控制是否要例化stimulator。
agent的两面性
对于上述的要求,我们考虑将含有stimulator对象的agent称之为“active agent”,因为其不仅仅需要监视数据,也需要激励端口;而对于只需要监视数据的agent,称之为“passive agent”。任何一个agent根据其进程的环境要求不同,会不要求配置为active模式或者passive模式。因此,我们需要在每一个agent中添加一个变量来对其内部结构进行配置化的控制。
interface arb_ini_if;
endinterface
class arb_ini_stm;
virtual interface arb_ini_if vif;
endclass
class arb_ini_mon;
virtual interface arb_ini_if vif;
endclass
class arb_ini_agent;
bit active;
arb_ini_stm stm;
arb_ini_mon mon;
function new(bit mod = 1);
active = mod;
mon = new();
if(active)
stm = new();
endfunction
function void connect(virtual interface arb_ini_if intf);
mon.vif = intf;
if(active)
stm.vif = intf;
endfunction
endclass
从上面简化的代码来看,可以在arb_ini_agent中添加一个变量active,默认值为1。那么在默认模式下,arb_ini_agent为active agent模式,因此会分别例化mon和stm对象,同时在connect()阶段会分别对两个字对象的虚接口进行赋值操作。如果,我们要例化一个passive agent,则可以通过在例化时传递参数实现,例如:
arb_ini_agent passive_agt = new(0);
通过这个例子可以看到,通过在组件内部预留一些变量,而在例化阶段对这些变量进行初始化操作可以实现改变环境结构的功能。
各个组件的模式配置
既然在agent中设置变量可以用来改变组件的层次组织,便于在仿真时动态调整环境结构,那么也可以在各个组件中设置其它变量来影响组件自身的行为。例如我们在设计fmt_rsp_stm时,可以考虑驱动grant信号的延迟时间,使其在接收到formatter packet request信号时做出或快或慢的响应,以此来模拟一个下行数据通道的状态。
interface fmt_rsp_if;
logic clk;
logic rstn;
logic req;
logic grant;
endinterface
class fmt_rsp_stm;
typedef enum {FMT_BUSY, FMT_BLOCK, FMT_FREE} mode_t;
mode_t mode = FMT_BUSY;
virtual interface fmt_rsp_if vif;
task run();
fork
drive();
join_none
endtask
task drive();
forever begin
int delay;
@(posedge vif.clk iff vif.rstn === 1 && vif.req === 1);
case(mode)
FMT_BUSY: delay = $urandom_range(6, 12);
FMT_BLOCK: delay = $urandom_range(20, 30);
FMT_FREE: delay = $urandom_range(0, 2);
endcase
$display("when mode=%s , grant delay is %0d", mode, delay);
repeat(delay) @(posedge vif.clk);
vif.grant <= 1;
@(posedge vif.clk);
vif.grant <= 0;
end
endtask
endclass
module tb;
bit clk;
logic rstn;
initial begin
forever #5ns clk <= !clk;
end
initial begin
#15ns rstn <= 0;
#5ns rstn <= 1;
end
fmt_rsp_stm stm;
fmt_rsp_if intf();
assign intf.clk = clk;
assign intf.rstn = rstn;
initial begin
stm = new();
stm.vif = intf;
fork
stm.run();
join_none
@(posedge intf.rstn);
@(posedge intf.clk) intf.req <= 1;
@(posedge intf.grant) intf.req <= 0;
stm.mode = fmt_rsp_stm::FMT_BLOCK;
@(posedge intf.clk) intf.req <= 1;
@(posedge intf.grant) intf.req <= 0;
stm.mode = fmt_rsp_stm::FMT_FREE;
@(posedge intf.clk) intf.req <= 1;
@(posedge intf.grant) intf.req <= 0;
end
endmodule
输出结果:
# when mode=FMT_BUSY , grant delay is 10
# when mode=FMT_BLOCK , grant delay is 28
# when mode=FMT_FREE , grant delay is 1
从这段代码中可以看到,在fmt_rsp_stm中添加了模式控制变量mode,而通过调配不同的模式,可以更改fmt_rsp_stm从接收到intf.req到拉高intf.grant的延迟时间。这一变化是在fmt_rsp_stm::drive()方法中完成的。在最后的输出结果中,可以看到不同的模式设置会有不同的组件行为响应,而对于组件的模式配置是可以发生在仿真中的任何时间的,这一点需要同环境结构只能发生在环境建立时加以区别。
验证结构的集成顺序
在介绍了组件的模式配置和agent的结构组合之外,读者还需要考虑在构建高层次的环境时,如何组织环境。之前的例子中,我们的思路是从底层模块级环境到顶层子系统集的集成。这一集成过程符合项目的执行进度,毕竟模块级环境是先完成的,我们称其为bottom-up集成方式(自底向上)。那么与其对应的,有没有可能进行top-down的集成方式(自顶向下)呢?即首先决定顶层的验证结构,而后通过不同的配置情况来对顶层的环境进行裁剪,使其变化为每一个底层的模块验证环境呢?通过这种方式,verfier董则可以在更早期的阶段就对顶层环境做出可配置化的结构设计,而底层的模块环境则不再各自为政为自己各开辟模块环境。这种top-down的验证结构集成顺序使得环境从顶层到底层环境的风格趋于一致,也在整体上减少了环境构建的工作量。
我们将顶层环境变得更便于配置,方便其剪裁成为各个模块环境,我们来看看下面简化过的代码:
typedef enum {MCDF_TB, REGS_TB, SLV_TB, ARB_TB, FMT_TB} tb_mode_t;
class mcdf_env;
regs_ini_agent regs_ini_agt;
regs_rsp_agent regs_rsp_agt;
regs_checker regs_chk;
virtual interface regs_ini_if regs_ini_vif;
virtual interface regs_rsp_if regs_rsp_vif;
slv_ini_agent slv_ini_agt[3];
slv_rsp_agent slv_rsp_agt[3];
slv_checker slv_chk;
virtual interface slv_ini_if slv_ini_vif[3];
virtual interface slv_rsp_if slv_rsp_vif[3];
arb_ini_agent arb_ini_agt[3];
arb_rsp_agent arb_rsp_agt;
arb_checker arb_chk;
virtual interface arb_ini_if arb_ini_vif[3];
virtual interface arb_rsp_if arb_rsp_vif;
fmt_ini_agent fmt_ini_agt;
fmt_rsp_agent fmt_rsp_agt;
fmt_checker fmt_chk;
virtual interface fmt_ini_if fmt_ini_vif;
virtual interface fmt_rsp_if fmt_rsp_vif;
mcdf_checker chk;
virtual interface mcdf_if vif;
tb_mode_t tb_mode;
function new(tb_mode_t m = MCDF_TB);
tb_mode = m;
if(tb_mode == MCDF_TB) begin
regs_ini_agt = new();
foreach(slv_ini_agt[i]) slv_ini_agt[i] = new();
fmt_rsp_agt = new();
chk = new();
end
else if(tb_mode == REGS_TB) begin
regs_ini_agt = new();
regs_rsp_agt = new();
regs_chk = new();
end
else if(tb_mode == SLV_TB) begin
slv_ini_agt[0] = new();
slv_rsp_agt[0] = new();
slv_chk = new();
end
else if(tb_mode == ARB_TB) begin
foreach(arb_ini_agt[i]) arb_ini_agt[i] = new();
arb_rsp_agt = new();
arb_chk = new();
end
else if(tb_mode == FMT_TB) begin
fmt_ini_agt = new();
fmt_rsp_agt = new();
fmt_chk = new();
end
else
$fatal("env:: tb_mode is out of enum type");
endfunction
function void connect();
if(tb_mode == MCDF_TB) begin
chk.connect();
regs_ini_agt.mon.mb = chk.regs_ini_mb;
foreach(slv_ini_agt[i]) slv_ini_agt[i].mon.mb = chk.slv_ini_mb[i];
fmt_rsp_agt.mon.mb = chk.rsp_mb;
end
else if(tb_mode == REGS_TB) begin
regs_ini_agt.mon.mb = regs_chk.ini_mb;
regs_rsp_agt.mon.mb = regs_chk.rsp_mb;
end
else if(tb_mode == SLV_TB) begin
slv_ini_agt[0].mon.mb = slv_chk.ini_mb;
slv_rsp_agt[0].mon.mb = slv_chk.rsp_mb;
end
else if(tb_mode == ARB_TB) begin
foreach(arb_ini_agt[i]) arb_ini_agt[i].mon.mb = arb_chk.ini_mb[i];
arb_rsp_agt.mon.mb = arb_chk.rsp_mb;
end
else if(tb_mode == FMT_TB) begin
fmt_ini_agt.mon.mb = fmt_chk.ini_mb;
fmt_rsp_agt.mon.mb = fmt_chk.rsp_mb;
end
endfunction
task run();
if(tb_mode == MCDF_TB) begin
fork
regs_ini_agt.run();
foreach(slv_ini_agt[i]) slv_ini_agt[i].run();
fmt_rsp_agt.run();
chk.run();
join_none
end
else if(tb_mode == REGS_TB) begin
regs_ini_agt.run();
regs_rsp_agt.run();
regs_chk.run();
end
else if(tb_mode == SLV_TB) begin
slv_ini_agt[0].run();
slv_rsp_agt[0].run();
slv_chk.run();
end
else if(tb_mode == ARB_TB) begin
foreach(arb_ini_agt[i]) arb_ini_agt[i].run();
arb_rsp_agt.run();
arb_chk.run();
end
else if(tb_mode == FMT_TB) begin
fmt_ini_agt.run();
fmt_rsp_agt.run();
fmt_chk.run();
end
endtask
function void assign_vi(virtual interface mcdf_if intf);
vif = intf;
chk.vif = intf;
endfunction
endclass
在上面这段代码中,通过添加新的enum类型tb_mode_t,使得MCDF的顶层环境mcdf_env可以支持多种配置模式。例如在默认情况下例化mcdf_env,则其结构应当为MCDF的子系统验证结构模式,mcdf_env::tb_mode为MCDF_TB模式。在这种模式下,将例化相应的子组件,并且进行对应的连接关系和运行模式。而对于其它模块验证环境时,可以在mcdf_env例化时传递相应的数值,即可以更改环境结构为register、slave、arbiter和formatter对应的模块验证环境结构,同时相应的组件连接关系和运行模式也会有变化。从这个例子可以看到,自顶向下的验证环境管理模式,可以通过例化时不同的配置发生结构性的变化。
从上面列出的三个配置实例中,我们可以发现设置配置化的特点,以及存在的一些瑕疵:
- 通过内置新的配置变量,可以保持验证结构的灵活性,以及组件运行模式的实时更新。
- 对于验证结构的改变,如果遵循父一级组件只能在构建函数new()中来例化子一级组件时,那么我们必须将结构配置变量在new()函数调用时进行传递。因此,对于结构的配置存在着局限性。
- 对于组件的模式更新,我们可以通过层次化的配置进行分布式地修改,这可以在顶层TB中实现,也可以考虑在vector类中实现。
配置的灵活性利于验证环境的维护,这也意味着好的结构设计在项目的迭代过程中也能节省环境更新的人力。在下一节《初论环境的复用性》中,我们会接着结构设计的话题,考虑参数化设计在结构设计中的应用。