在搭建UVM验证环境之前,我们先从SystemVerilog验证平台开始,随后一步一步地过渡到完整的UVM验证平台。
TinyALU对验证平台的需求如下:
- 全面测试 TinyALU 的功能
- 仿真RTL的每一行代码和通过这些行代码的路径
因此我们要创建需要覆盖的内容,随后创建验证平台去覆盖代码。并且创建自检的验证平台,这样跑回归时就不需要手动检查了。
TinyALU的功能覆盖率模型
我们使用 SystemVerilog 的 covergroup 来实现(具体可以参考绿皮书SystemVerilog),覆盖率目标如下:
- 测试所有指令
- 所有指令的全”0”输入仿真
- 所有指令的全”1”输入仿真
- 所有指令的复位后运行
- 单周期指令之后运行乘法指令
- 乘法指令之后运行一个单周期指令
- 指令连续两次运行的仿真
所有的上述情况都验证并通过后, 我们就能确认这个TinyALU是可用的。是否达到100%代码覆盖率也是需要检查的。
testbench文件
将验证平台放在同一个仿真文件里,该文件包含三个部分: 激励、自检和覆盖率。在文件里例化 DUT,并为其施加激励并使用自检和覆盖采集的 always 模块来监控它。
testbench变量定义
用一种便于定义激励的形式来定义TinyALU的指令, 验证平台信号定义如下:
module top;
typedef enum bit[2:0] {no_op = 3'b000,
add_op = 3'b001,
and_op = 3'b010,
xor_op = 3'b011,
mul_op = 3'b100,
rst_op = 3'b111} operation_t;
byte unsigned A;
byte unsigned B;
bit clk;
bit reset_n;
wire [2:0] op;
bit start;
wire done;
wire [15:0] result;
operation_t op_set;
assign op = op_set;
tinyalu DUT (.A, .B, .clk, .op, .reset_n, .start, .done, .result);
使用 SystemVerilog 的枚举类型将 TinyALU 的指令定义为枚举变量。为了方便验证平台的使用,我们将DUT 中没有的rst_op指令加入指令枚举类型。assign语句用于将指令输入到 DUT 的指令总线上。
我们使用SystemVerilog的byte和bit来定义激励变量。最后,我们使用端口顺序映射的形式(这样就不用把信号名写两次,这是个很好的特性)来例化DUT。
covergroup模块
用covergroup来捕获功能覆盖。先定义covergroup,再例化并用其进行采集
covergroup op_cov;
coverpoint op_set {
bins single_cycle[] = {[add_op : xor_op], rst_op,no_op};
bins multi_cycle = {mul_op};
bins opn_rst[] = ([add_op:mul_op] => rst_op);
bins rst_opn[] = (rst_op => [add_op:mul_op]);
bins sngl_mul[] = ([add_op:xor_op],no_op => mul_op);
bins mul_sngl[] = (mul_op => [add_op:xor_op], no_op);
bins twoops[] = ([add_op:mul_op] [* 2]);
bins manymult = (mul_op [* 3:5]);
}
endgroup
covergroup zeros_or_ones_on_ops;
all_ops : coverpoint op_set {
ignore_bins null_ops = {rst_op, no_op};}
a_leg: coverpoint A {
bins zeros = {'h00};
bins others= {['h01:'hFE]};
bins ones = {'hFF};
}
b_leg: coverpoint B {
bins zeros = {'h00};
bins others= {['h01:'hFE]};
bins ones = {'hFF};
}
op_cov 这个 covergroup 用来保证我们覆盖了所有的指令以及这些指令间可能的组合。zeros_or_ones_on_ops 这个 covergroup 用来检查我们是否在所有的指令上用全 0 和全 1 操作数进行测试。
定义完这些 covergroup 之后,我们需要将其声明,例化并采集:
initial begin : coverage
oc = new();
c_00_FF = new();
forever begin @(negedge clk);
oc.sample();
c_00_FF.sample();
end
end : coverage
这是个非常简单的覆盖率模型。我们在每个时钟的下降沿检查 TinyALU 的指令和数据输入并进行记录。要达到完全覆盖需要借助带约束随机的激励。
tester模块
可以为每个覆盖目标写一个直接测试用例来验证,对于小型的DUT是可行的。更加高效的办法是使用随机的激励,并且是带有约束的随机激励。这里使用get_op( )和get_data( )两个函数来实现带约束的随机激励:
initial begin : tester
reset_n = 1'b0;
@(negedge clk);
@(negedge clk);
reset_n = 1'b1;
start = 1'b0;
repeat (1000) begin
@(negedge clk);
op_set = get_op();
A = get_data();
B = get_data();
start = 1'b1;
case (op_set) // handle the start signal
no_op: begin
@(posedge clk);
start = 1'b0;
end
rst_op: begin
reset_n = 1'b0;
start = 1'b0;
@(negedge clk);
reset_n = 1'b1;
end
default: begin
wait(done);
start = 1'b0;
end
endcase // case (op_set)
end
$stop;
end : tester
这个循环为 TinyALU 生成了 1000 个 transaction。每次循环我们从 get_op()里得到随机的指令,从 get_data( )里得到随机操作数,随后将他们驱动到 TinyALU,并且执行了 start 信号的协议。这两个函数保证了我们得到的是合法的指令和有效的数据。
scoreboard循环做自检
Scoreboard循环使用预期结果来对比检查TinyALU的结果。循环会监控 done 信号,每当 done 信号拉高,scoreboard 根据 TinyALU 的输入预测其结果,并检查其是否正确。
always @(posedge done) begin : scoreboard
shortint predicted_result;
#1;
case (op_set)
add_op: predicted_result = A + B;
and_op: predicted_result = A & B;
xor_op: predicted_result = A ^ B;
mul_op: predicted_result = A * B;
endcase // case (op_set)
if ((op_set != no_op) && (op_set != rst_op))
if (predicted_result != result)
$error ("FAILED: A: %0h B: %0h op: %s result: %0h",
A, B, op_set.name(), result);
end : scoreboard
简单的验证平台
到这已经的完成了TinyALU的验证平台,传递功能覆盖率,检查所有的指令是否工作正常,很少的工作来产生激励,只用一个模块和文件就完成了。
但这验证平台不是一个好的例子,所有的行为都在一个文件里混到一起了。这
些使得这个验证平台难以复用。那要怎么改进这个验证平台呢?我们最先注意到的是 scoreboard 模块, coverage 模块和 tester 模块是在完全不同的功能之上定义的,它们没必要在同一个文件里。把他们分开后,我们就可以把这个验证平台中的模块复用到其他验证平台,方便于修改其他 Test 模块中的其他模块,比如激励。