Chapte 16 – Using Analysis ports in a testbench
测试平台中所有的组件可以分为两种:驱动DUT运行 & 观察DUT变化。
我们此前的测试平台中,tester_h就是用于驱动仿真平台运行,scoreboard_h和coverage_h用于监控DUT的行为。从UVM的角度来看,scoreboard和coverage组成了测试平台的_analysis layer_.所以,用于传输数据的端口被称作uvm_analysis_port.
16.1 重复代码
如果两段代码在实现一致的功能,就像是露营时打开帐篷:You are inviting bugs in。
此前,我们的测试平台就具有这样的代码,在covereage和scoreboard中都需要从BFM收集cmd信号,但是这样的工作其实做一次就足够了。
我们希望可以在BFM的pins上,检测cmd的变化,并将数据送至coverage和scoreboard。因为scoreboard需要计算结果,所以需要在上收集result,并送至scoreboard。
16.2 将analysis port用于测试平台
测试平台的整体框架图如下:
在此前的测试平台,tester_h中包含BFM的句柄,现在BFM有了两个新的句柄,command_monitor和result_monitor. BFM识别出TinyALU的command和results并将其送至monitor,在monitor中调用analysis的write函数,将数据送至coverage_h和scoreboard_h中。
16.2.1 bfm模块的处理
OOP编程最大的优势就是,我们可以通过共享对象句柄为测试平台的各个模块增加功能。现在我们使用tinyalu_bfm将测试平台的其余部分连接起来。
- 以下是tinyalu_bfm模块的部分代码:
interface tinyalu_bfm;
import tinyalu_pkg::*;
command_monitor command_monitor_h; //声明两个Monitor的句柄
result_monitor result_monitor_h;
assign op = op_set;
always @(posedge clk) begin : cmd_monitor
bit new_command;
if (!start)
new_command = 1;
else
if (new_command) begin //监控ALU的commands
command_monitor_h.write_to_monitor(A, B, op); //每当有新的command到来,调用cmd_monitor的write_to_monitor函数将数据送至command_monitor
new_command = (op == 3'b000); // handle no_op
end
end : cmd_monitor
always @(negedge reset_n) begin : rst_monitor //监控ALU的commands
command_monitor_h.write_to_monitor(A, B, rst_op); //当reset_n的信号有效时,调用cmd_monitor的write_to_monitor函数,将reset_op送至command_monitor
end : rst_monitor
//监控ALU的results
always @(posedge clk) begin : rslt_monitor //当操作结束,done信号有效时,调用result_monitor的write_to_monitor函数,将result数据送至result_monitor
if (done)
result_monitor_h.write_to_monitor(result);
end : rslt_monitor
endinterface : tinyalu_bfm
16.2.2 command_monitor模块的处理 (monitor)
在command_monitor的build_phase中,声明virtual interface bfm,在仿真开始时,通过uvm_cfgdb机制get bfm,然后将自己的句柄传输给bfm中的command_monitor_h。
(result_monitor以同样的方式实现此功能,这里就不再贴代码了)
- command_s数据结构如下图:
- 以下是command_monitor模块的代码:(analysis port就在monitor中)
class command_monitor extends uvm_component;
`uvm_component_utils(command_monitor);
uvm_analysis_port #(command_s) ap; //注意,analysis的参数类型是command_s,这是我们自定义的数据结构,如上图。
function void build_phase(uvm_phase phase);
virtual tinyalu_bfm bfm;
if(!uvm_config_db #(virtual tinyalu_bfm)::get(null, "*","bfm", bfm))
$fatal("Failed to get BFM"); //build phase top down开始,先执行bfm的bp,然后执行monitor的build phase
bfm.command_monitor_h = this; //将自己的句柄连接至BFM的cmd_monitor,这样bfm就可以通过command\_monitor向测试平台传递数据
ap = new("ap",this); //analysis port在build phase例化
endfunction : build_phase
function void write_to_monitor(byte A, byte B, bit[2:0] op);
command_s cmd;
cmd.A = A;
cmd.B = B;
cmd.op = op2enum(op);
$display("COMMAND MONITOR: A:0x%2h B:0x%2h op: %s", A, B, cmd.op.name());
ap.write(cmd); //在bfm调用monitor的write_to_monitor函数时,同时调用了analysis port的write函数,将数据送出,
//其他功能模块就可以从analysis_export端口接收到command_s类型的数据
endfunction : write_to_monitor
function new (string name, uvm_component parent);
super.new(name,parent);
endfunction
endclass : command_monitor
16.2.3 coverage模块的处理(analysis layer)
随着我们将复杂的操作分为一个个更小的单元,简单地小单元可以使我们更容易构建其他环境组件。这些拆分的小模块都成为了后续的资源。
作为subscriber(subscriber和analysis port是一对哦)的coverage,就可以利用以上的小功能单元,通过简单地调用实现同样的功能,并且易于debug和重用,其部分代码如下:
class coverage extends uvm_subscriber #(command_s); //subscriber是参数化的类,使用时需要制定analysis export传输的数据类型
`uvm_component_utils(coverage)
byte unsigned A;
byte unsigned B;
operation_t op_set;
covergroup op_cov;
function new (string name, uvm_component parent);
super.new(name, parent);
op_cov = new();
zeros_or_ones_on_ops = new();
endfunction : new
function void write(command_s t); //1. uvm_subscriber提供的analysis export接收analysis port传过来的数据
A = t.A; //2. 但是哪里能只占便宜呢?
B = t.B; //3. 接收到的数据必须在名称为write()的函数中进行处理
op_set = t.op; //4. 使用uvm_subscriber必须要重载write函数。
op_cov.sample(); //5. write函数的参数类型必须和export传输的数据类型一致
zeros_or_ones_on_ops.sample();
endfunction : write
endclass : coverage
coverage函数不再需要自己处理信号级别的数据,如clock,bits等。他可以直接接收monitor通过analysis port传输的整洁的command_s类型的数据。
16.2.4 scoreboard模块的处理(analysis layer)
- 基础的uvm_analysis port机制允许uvm_subscriber监控一个analysis port, 但是在类似scoreboard中我们希望可以在一个uvm_subscriber同时从多个analysis port中获取数据。(当然我们在声明一个subscriber就可以解决问题,但是却增加了环境复杂度)
- UVM的开发人员,为我们提供了uvm_tlm_analysis_fifo来解决这个问题,我们在scoreboard中使用这种机制实现同一个subscriber中获取不同参数类型的analysis port的数据:
- uvm_tlm_analysi_fifo是一个参数化的类,此fifo在一侧实现了analysis_export,在另一侧使用try_get函数。我们通过analysis_export从analysis_port获得数据,通过try_get从FIFO中吧数据取出来
class scoreboard extends uvm_subscriber #(shortint); //subscriber 传入shortint类型
`uvm_component_utils(scoreboard); //1. wirte函数的参数类型必须是shortint
//2. scoreboard继承了shorint类型的export
uvm_tlm_analysis_fifo #(command_s) cmd_f; //声明command_s类型的uvm_tlm_analysis_fifo
//这样就可以实现对两种不同类型的analysis port的订阅
function void build_phase(uvm_phase phase);
cmd_f = new ("cmd_f", this);
endfunction : build_phase
function void write(shortint t); //假设我们确定只有在收到command后才会收到result
shortint predicted_result; //scoreboard通过result analysis port的write函数触发检查
command_s cmd;
cmd.op = no_op; //1. 有result到达意味着此前一定有command进入了FIFO
do //2. 接收到result之后,用do while循环找到上次的非rst操作command
if (!cmd_f.try_get(cmd)) $fatal(1, "No command in self checker"); //如果FIFO为空,try_get返回0,但是没有cmd不会有result,所以此时进入fatal错误
while ((cmd.op == no_op) || (cmd.op == rst_op));
case (cmd.op) //3. 找到对应的command_s类型的数据后,根据cmd和A B计算预测值
add_op: predicted_result = cmd.A + cmd.B;
and_op: predicted_result = cmd.A & cmd.B;
xor_op: predicted_result = cmd.A ^ cmd.B;
mul_op: predicted_result = cmd.A * cmd.B;
endcase // case (op_set)
if (predicted_result != t) //4. 如果预测值和收到的result不一致,说明DUT计算错误
$error (
"FAILED: A: %2h B: %2h op: %s actual result: %4h expected: %4h",
cmd.A, cmd.B, cmd.op.name(), t, predicted_result);
endfunction : write
function new (string name, uvm_component parent);
super.new(name, parent);
endfunction : new
endclass : scoreboard
16.2.5 env模块的处理
通过上面的处理,我们已经将测试平台按照功能分割开来,那么在验证环境中需要在env中将个组件连接起来。
- BFM和monitor之间:BFM内部声明monitor类型的句柄,monitor在build phase中将this传递给BFM中对应的monitor句柄;
- monitor和coverage&scoreboard之间:analysis port和subscriber的关系,通过analysis port的write函数和subscriber的export,或FIFO的export连接起来。
在测试环境组件env的connect phase中完成上述连接:
class env extends uvm_env;
`uvm_component_utils(env);
random_tester random_tester_h; //对象声明
coverage coverage_h;
scoreboard scoreboard_h;
command_monitor command_monitor_h;
result_monitor result_monitor_h;
function new (string name, uvm_component parent);
super.new(name,parent);
endfunction : new
function void build_phase(uvm_phase phase); //在build phase例化
random_tester_h = random_tester::type_id::create("random_tester_h",this);
coverage_h = coverage::type_id::create ("coverage_h",this);
scoreboard_h = scoreboard::type_id::create("scoreboard_h",this);
command_monitor_h = command_monitor::type_id::create("command_monitor_h",this);
result_monitor_h= result_monitor::type_id::create("result_monitor_h",this);
endfunction : build_phase
function void connect_phase(uvm_phase phase); //connect phase调用analysis port的connect函数
result_monitor_h.ap.connect(scoreboard_h.analysis_export); //连接至subscriber默认的export
command_monitor_h.ap.connect(scoreboard_h.cmd_f.analysis_export); //连接至subscriber中的FIFO上的export
command_monitor_h.ap.connect(coverage_h.analysis_export); //连接至subscriber默认的export
endfunction : connect_phase
endclass
- 本节中,我们使用uvm_analysis_port实现了testbench中Observer design pattern,我们从信号级监控转至使用monitor class和analysis port实现数据从monitor -> analysis class的传输;
- analysis port是单线程通信的有效机制,所有的功能都发生在单线程内。当monitor中在调用analysis port的write函数时,其实调用了线程内所有subscriber的write函数。
- analysis port在单线程通信中是十分有效的机制。但是当我们需要在多线程间进行数据传输时,UVM为我们提供了新的机制。