A reference model answers one question: given these inputs, what must the DUT output. The right level of detail is a design decision with a clear default.
Level | Cost | Checks | Risk |
|---|---|---|---|
Transaction-level functional | Low | Data integrity, ordering, flags | Misses timing bugs. |
Cycle-approximate | Medium | Adds latency windows, throughput bounds | Some timing escapes. |
Cycle-accurate | Very high | Exact per-cycle pin behavior | Becomes a second RTL; clones spec bugs. |
Start transaction-level. Timing belongs in assertions beside the model, not inside it. Cycle accuracy is justified only when the spec itself is cycle-exact.
The next listing is classes with no top module, so this course's checks do not run it. The model consumes the input-monitor stream and its predict stays a pure function of one transaction.
// not-run: classes only, no top module
typedef enum logic [1:0] { OP_ADD, OP_SUB, OP_AND, OP_XOR } op_e;
class alu_txn;
op_e op;
logic [31:0] a, b;
logic [31:0] result; // filled by DUT (actual) or model (expected)
logic cout; // ADD carry-out, SUB borrow (unsigned, spec 3.2.1)
function string convert2string();
return $sformatf("op=%s a=0x%08h b=0x%08h result=0x%08h cout=%0b",
op.name(), a, b, result, cout);
endfunction
endclass
class alu_ref_model;
mailbox #(alu_txn) mbx_in; // from input monitor
mailbox #(alu_txn) mbx_exp; // to scoreboard
bit stop;
function new(mailbox #(alu_txn) mbx_in, mailbox #(alu_txn) mbx_exp);
this.mbx_in = mbx_in;
this.mbx_exp = mbx_exp;
endfunction
// Spec 3.2: cout is UNSIGNED carry (ADD) or borrow (SUB: 1 when a < b).
// Signed overflow is a different bit: carry-in XOR carry-out of the MSB.
function void predict(alu_txn t);
logic [32:0] wide;
case (t.op)
OP_ADD: begin
wide = {1'b0, t.a} + {1'b0, t.b};
t.result = wide[31:0];
t.cout = wide[32];
end
OP_SUB: begin
wide = {1'b0, t.a} - {1'b0, t.b};
t.result = wide[31:0];
t.cout = wide[32]; // borrow, not signed overflow
end
OP_AND: begin t.result = t.a & t.b; t.cout = 1'b0; end
OP_XOR: begin t.result = t.a ^ t.b; t.cout = 1'b0; end
endcase
endfunction
task run();
while (!stop) begin
alu_txn t;
mbx_in.get(t);
predict(t);
mbx_exp.put(t);
end
endtask
endclassThree habits keep the model honest. Tag each branch with its spec section, feed it from the input monitor rather than driver intent, and reset its state on mid-test reset.
When model and RTL disagree, check both against the spec and trust neither side by default. For algorithm-heavy DUTs, import the existing C model through DPI-C instead of re-implementing it.
Coverage must sample what the DUT did, never what the driver intended. The collector subscribes to the monitor stream like any other consumer.
The next listing is not run in this course's checks: Verilator 5.048 ignores covergroup bins and Icarus 13 cannot parse a covergroup. Here bus_txn is the scoreboard lesson's transaction plus an op field of the model's op_e type. Arguments on sample remove the copy-then-sample dance, and the reset gate keeps reset-phase traffic out of the bins.
// not-run: covergroups: Verilator 5.048 ignores bins (COVERIGN) and Icarus 13 cannot parse them; bus_txn (with an op_e op field) is defined elsewhere
// bus_txn here = the scoreboard lesson's transaction plus: op_e op;
class bus_coverage;
mailbox #(bus_txn) mbx_cov;
bit in_reset;
bit stop;
int unsigned sample_count;
covergroup cg with function sample(op_e op, int unsigned len,
logic [1:0] resp);
cp_op : coverpoint op {
bins add_op = {OP_ADD};
bins sub_op = {OP_SUB};
bins logical[] = {OP_AND, OP_XOR};
}
cp_len : coverpoint len {
bins one_beat = {1};
bins few = {[2:4]}; // "small" is a reserved word, not a bin name
bins many = {[5:16]};
}
cp_resp : coverpoint resp {
bins okay = {0};
bins error = {[1:3]};
}
x_op_len : cross cp_op, cp_len;
endgroup
function new(mailbox #(bus_txn) mbx_cov);
this.mbx_cov = mbx_cov;
cg = new(); // a covergroup handle starts null; construct it here
endfunction
task run();
while (!stop) begin
bus_txn tr;
mbx_cov.get(tr);
if (in_reset) continue; // reset-phase traffic pollutes closure
cg.sample(tr.op, tr.len, tr.resp);
sample_count++;
end
endtask
endclassPlacement | Verdict | Why |
|---|---|---|
Separate collector on the monitor stream | Default | Bins vary per project; coverage stays optional. |
Inside the monitor | Rare exception | Only for protocol-universal bins every user wants. |
In the driver or generator | Never | That samples intent, and intent can lie. |
Neither yet. Recompute the case from the spec by hand first: the model encodes one reading and the RTL another, and often the model or the spec text, not the RTL, is what is wrong.
Coverage measured intent, not behavior. Sample from the monitor stream so dropped or transformed transactions never credit bins they never reached.
An ALU, an independent model function, and manual coverage bins in plain Verilog.
module alu (
input logic [1:0] op, // 0 ADD, 1 SUB, 2 AND, 3 XOR
input logic [31:0] a, b,
output logic [31:0] result,
output logic cout // unsigned carry (ADD) or borrow (SUB)
);
timeunit 1ns; timeprecision 1ps;
wire [32:0] sum = {1'b0, a} + {1'b0, b};
wire [32:0] diff = {1'b0, a} - {1'b0, b};
assign result = (op == 2'd0) ? sum[31:0] :
(op == 2'd1) ? diff[31:0] :
(op == 2'd2) ? (a & b) : (a ^ b);
assign cout = (op == 2'd0) ? sum[32] :
(op == 2'd1) ? diff[32] : 1'b0;
endmodule