Most verification courses teach techniques on toy examples where every test finds the bug it was written for. Real verification is the opposite: you do not know where the bugs are, how many there are, or which technique will find them. This course gives you a design with exactly that property, and one thing real projects never have: the answer key.
What will you verify in this course, and how much does a first test really prove?
The obvious first step on a new design is a smoke test: send traffic through, see it come out, call the design alive. It is a necessary step and a common trap. A design that passes a smoke test is alive, not correct, and teams that report "basic tests passing" as progress learn this late. This lesson measures exactly how little a smoke test proves.
DCRouter: a 4-port router with valid/ready inputs, credit-based outputs, two priorities, a CRC check, APB registers and an idle low-power mode. The full specification (rules R1 to R13), register map and RTL are on the DCRouter page.
The Bug Zoo: 24 bugs, each switched on by a define such as ` define BUG_8``, each breaking exactly one rule. A reference regression written for the course catches all 24 and passes on the clean design, so every bug is detectable at the pins.
The bug-catch matrix: in each project step you apply one technique and record which bugs it catches. By Part 14 the matrix is measured evidence of which technique finds which kind of bug.
Step | Part | Technique |
|---|---|---|
1 | 0 | A smoke test on every port (this lesson) |
2 | 1 | Classify the bugs and predict which technique will find each |
3 | 2 | A reference model that predicts every output |
4, 5 | 4 | Directed tests, then a constrained-random hunt |
6, 7 | 5 | Assertions, then a scoreboard |
8 | 6 | A coverage model; correlate holes with missed bugs |
9 | 7 | Formal proofs of the arbiter and credits |
10 | 9 | A full verification plan |
11 | 14 | The evidence ledger and sign-off |
Each input sends one short packet to the next output, all four at once, and each output's sink returns a credit for every flit. The first line, // uses: dcrouter, means the DCRouter RTL is placed in front of this testbench when it runs.
// uses: dcrouter
// DCRouter step 1: a smoke test on every port. Input i sends one 2-payload packet to
// output (i + 1) % 4, all four at once; each sink returns a credit for every flit.
// The checker compares every output flit with the packet that should arrive there and
// confirms one tail per output and no error. It also reads CTRL once.
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic rst = 1;
logic [3:0] in_vld = 0, in_last = 0, in_rdy, out_vld, out_last, out_err, out_crd;
logic [63:0] in_data = 0, out_data;
logic psel = 0, penable = 0, pwrite = 0, pready, pslverr, sleeping;
logic [7:0] paddr = 0;
logic [31:0] pwdata = 0, prdata;
dcrouter dut (.*);
function automatic logic [7:0] crc8(input logic [7:0] c, input logic [15:0] d);
logic [7:0] x;
x = c;
for (int k = 15; k >= 0; k--) x = (x[7] ^ d[k]) ? {x[6:0], 1'b0} ^ 8'h07 : {x[6:0], 1'b0};
return x;
endfunction
logic [15:0] pkt [4][4]; // pkt[input][flit]
int sent [4], got [4], tails [4], bad = 0;
initial begin
for (int i = 0; i < 4; i++) begin
logic [7:0] c;
pkt[i][0] = {2'((i + 1) % 4), 1'b0, 4'd2, 9'(i)};
pkt[i][1] = 16'h1000 * (i + 1) + 16'h11;
pkt[i][2] = 16'h1000 * (i + 1) + 16'h22;
c = crc8(crc8(crc8(8'h00, pkt[i][0]), pkt[i][1]), pkt[i][2]);
pkt[i][3] = {8'h00, c};
sent[i] = 0; got[i] = 0; tails[i] = 0;
end
repeat (3) @(posedge clk); rst <= 0;
end
for (genvar i = 0; i < 4; i++) begin : g
// source i
always @(posedge clk) if (!rst) begin
int n;
n = sent[i] + ((in_vld[i] && in_rdy[i]) ? 1 : 0);
sent[i] <= n;
in_vld[i] <= n < 4;
in_data[16*i +: 16] <= pkt[i][n % 4];
in_last[i] <= n == 3;
end
// sink on output (i + 1) % 4 receives input i's packet
always @(posedge clk) begin
out_crd[(i + 1) % 4] <= out_vld[(i + 1) % 4];
if (out_vld[(i + 1) % 4]) begin
if (out_data[16*((i + 1) % 4) +: 16] !== pkt[i][got[i]]) bad++;
if (out_last[(i + 1) % 4]) begin tails[i]++; if (got[i] != 3 || out_err[(i + 1) % 4]) bad++; end
got[i] <= got[i] + 1;
end
end
end
logic [31:0] ctrl;
initial begin
#100;
@(posedge clk); psel <= 1; paddr <= 8'h00;
@(posedge clk); penable <= 1;
@(posedge clk); ctrl = prdata; psel <= 0; penable <= 0;
#600;
if (got[0] == 4 && got[1] == 4 && got[2] == 4 && got[3] == 4 && tails[0] + tails[1] + tails[2] + tails[3] == 4 && bad == 0)
$display("PASS: four packets, one per input, each arrived intact at its output with one tail and no error; CTRL reads %h", ctrl);
else $display("FAIL: got %0d %0d %0d %0d flits, %0d mismatches", got[0], got[1], got[2], got[3], bad);
$finish;
end
endmoduleWhen run on the clean design, it prints a PASS line: four packets, one per input, each arrived intact.
The course's Bug Zoo runner compiles the same testbench once with no bug and once with each of the 24 bugs, and reports which runs fail:
clean design: PASS
BUG_2 caught FAIL: got 4 4 4 12 flits, 12 mismatches
BUG_8 caught FAIL: got 3 3 3 4 flits, 9 mismatches
BUG_17 caught FAIL: got 4 4 4 0 flits, 0 mismatches
caught 3 of 24 bugsHow to read it: the clean design passes, so the test itself is sound. With BUG_2 (a FIFO counting error) and BUG_8 (an output lock released one flit early), packets arrive mangled. With BUG_17, port 3 is disabled after reset, so its packet never arrives. The other 21 bugs pass this test: none of them is both triggered and observed by four short, well-behaved packets with fast sinks. Some may well fire (four equal packets finishing together can trigger the simultaneous-tail counting bug), but nothing reads the counters, so nothing sees them.
A smoke test | Is good for | Is not |
|---|---|---|
One clean transfer per port | Proving the testbench, the build and the basic path work | Evidence about corners, errors, registers or concurrency |
Run first, on every change | Catching broken builds and gross mistakes in minutes | A measure of verification progress |
Three of 24 is the baseline every later technique is measured against.
Practice | Purpose |
|---|---|
A smoke test gating every commit | Keep the design and testbench basically alive |
Bring-up tests on a new block | Prove the environment before writing real tests |
Never reporting smoke results as coverage | Keeps "alive" from being mistaken for "verified" |
A project status report (illustrative) listed a new block as "basic tests passing, 90% done" for six weeks. The basic tests were smoke tests, much like the one above. When constrained-random traffic and error injection arrived, the block failed in the first hour on back-pressure, then on errors, then on registers. It was alive in week one and correct in week twelve.
If the clean design fails, the test itself is wrong, and every "caught" bug could just be the test failing for its own reasons.
Anything needing back-pressure, long or illegal packets, contention, errors, register writes, counter reads, sleep or long runs: none of those conditions occur.
DCRouter hides 24 planted bugs; each project step measures which ones a technique catches.
A smoke test proves the design is alive: here it catches 3 of 24.
Measure progress by checked rules, coverage and bugs found, never by smoke results.