Design it, then break it
Test your own design the way a verification engineer will: write its fault list, the stimulus that exposes every fault, and a checker that reads the brief.
- Lesson
- R8.4
- Time
- 35 min
- Runs on
- Your browser
- Check
- 6 questions
A designer shipped a FIFO whose only test was the one sequence that happened to work. The verification team's first random test broke it in ten cycles. The designer's answer, "the design is fine, the test is wrong", took a week to settle.
| R8.3 filter with | R8.3's own bench | A bench built from the brief's corners |
|---|---|---|
| the first packet after reset never compared | output same | output differs at e4 |
| the type compared on 4 bits | output same | output differs at e16 |
| four other one-line faults | all four change the output | all four change the output |
Measured on this lesson's bench. The filter's own lab lets two faulty copies through.
Two questions for every fault
The block under test is R8.3's AXI-Stream filter. Its brief has three rules. Drop every packet whose header type is 0xd0, whole. Pass every other packet with TKEEP and TLAST untouched. Drive the output from a skid register, so a stall never loses a beat.
A fault is one realistic change to a design: a line a tired designer could write. Testing your own block asks two questions of each fault. Does some stimulus make the faulty copy's output differ from the correct one? And does your checker notice when it does?
- Break it
The site's Break-it problems give you a correct design and ask for the stimulus. A faulty copy is caught when its output differs from the correct one on your inputs.
- Verify
The site's Verify problems give you the stimulus and ask for the checker. It must stay silent on the correct design and raise an alarm on every faulty copy.
- Debug
A Debug problem gives you a failing waveform and asks for the first bad cycle and its cause. A good checker points at that cycle for you.
Read the diagram as text
- Stimulus: packets and stalls
- Correct filter: FAULT = 0
- Faulty copy: FAULT = k
- Compare outputs: does the fault show?
- Your checker: does it notice?
- The brief: the rule, the ports
- Stimulus → Correct filter: beats in
- Stimulus → Faulty copy: beats in
- Correct filter → Compare outputs: beats out
- Faulty copy → Compare outputs: beats out
- Faulty copy → Your checker: both ports
- The brief → Your checker: the oracle
The oracle is whatever decides the right answer. Here it is the brief, read by a model inside the checker.
Write the fault list
Start from the bugs this track taught you to fear, and write each as a one-line change to your own RTL. The bench builds every fault into one module with a FAULT parameter, so all copies run side by side.
wire hdr = (FAULT == 6) ? (s_tdata[3:0] == DROP_TYPE[3:0])
: (s_tdata[7:0] == DROP_TYPE);| Fault | Lesson | What exposes it | Smoke | Corners |
|---|---|---|---|---|
| 1 decision not held | R8.3 | a dropped packet of two or more beats | e8 | e4 |
| 2 skid never fills | R4.4 | a passed beat that arrives during a stall | e5 | e9 |
| 3 new beat to the front | R4.4 | the same | e4 | e8 |
| 4 first_q resets to 0 | R2.2 | a dropped packet first after reset | same | e4 |
| 5 skid drops TKEEP | R4.4, R8.3 | a partial last beat that waits in the skid | e20 | e9 |
| 6 type compared on 4 bits | R1.4 | a passed type whose low four bits match 0xd0 | same | e16 |
Measured: the edge where each faulty copy's output first differs from the correct filter's, or "same".
The smoke set is R8.3's lab: a smoke test, which shows the block works at all. Its first packet has type 0x11, and no type it passes ends in hex 0, so faults 4 and 6 never show. The corners set reads the brief row by row instead. It starts with a dropped packet, includes type 0x30, and stalls the consumer for two edges at a time.
Write the checker from the brief
The checker keeps a model: a queue of the beats the brief says must leave. Each beat taken at the input goes through the rule, written again from the brief and never copied from the RTL. Each beat that leaves must match the head of the queue in TLAST, TKEEP and TDATA.
The model compares order, not timing. The brief allows stalls, so a beat may leave any number of edges after it arrives. One more rule comes from AXI-Stream: a beat on offer stays on offer, unchanged, until it is taken.
| Checker | Correct filter | Corners faults flagged |
|---|---|---|
| data only, no rule for a beat on offer (the lab) | silent | 5 of 6: misses fault 5 |
| expects each beat one edge after it arrives | false alarm at e6 | 6 of 6 |
| expects every beat out, forgets the drop rule | false alarm at e5 | 6 of 6 |
| the brief's checker | silent | 6 of 6 |
Measured on the corners set. A checker that also raises a false alarm on the correct design proves nothing.
m good is the correct filter's output beat; m fault 3 and skid belong to the faulty copy.- Cycle 1
Edge e6: the consumer stalls with p1.1 on offer. p1.2 arrives and must wait behind it.
- Cycle 2
Edge e7: the faulty copy offers p1.2 in place of p1.1, which was never taken. The rule for a beat on offer fails here.
- Cycle 3
Edge e8: p1.2 leaves before p1.1. Only now does a data compare see a wrong beat.
Measured. The brief's checker raises its alarm at e7; the same checker without the rule for a beat on offer raises it at e8.
The lab
Run the lab. The bench runs the correct filter and the six faulty copies under both sets, with one copy of your checker on each. Every line says whether the output differs and whether the checker raised an alarm.
The shipped checker compares TDATA alone, so fault 5 slips through on both sets. Change its compare to all of {m_tlast, m_tkeep, m_tdata}. Then add the rule for a beat on offer, using held and held_beat. Run again: six of six, and fault 3 is flagged at e7.
Asked in interviews
How would you test your own design?
List the faults I could have made and write each as a one-line change. Build stimulus from every corner of the brief until each faulty copy behaves differently. Write a checker whose model comes from the brief, not from my RTL.
What is the difference between a test passing and a design being verified?
A pass says this stimulus found nothing this checker looks for. Verified means the stimulus reaches the corners and the checker would notice the faults that matter, which you show by running it against faulty copies.
Which of your own bugs would a random test find first?
The ones on common paths: data and order. Reset values, rare types and stalls at an exact moment need stimulus aimed at them. On this filter the smoke test missed the reset value and the rare type.
Try it
The "output" half of each line is the Break-it question: did this stimulus expose the fault? The "checker" half is the Verify question: did your checker notice? On the smoke set, faults 4 and 6 never change the output, so no checker could catch them.
With the full compare and the rule for a beat on offer, both sets end the checker flags 4 and the checker flags 6. Fault 3 on the corners set reads output differs at e8, alarm at e7.
smoke: correct filter: checker silent smoke: fault 1 decision not held: output differs at e8, alarm at e8 smoke: fault 2 skid never fills: output differs at e5, alarm at e5 smoke: fault 3 new beat to the front: output differs at e4, alarm at e4 smoke: fault 4 first_q resets to 0: output same, checker silent smoke: fault 5 skid drops TKEEP: output differs at e20, checker silent smoke: fault 6 type compared on 4 bits: output same, checker silent smoke: 4 of 6 faults change the output; the checker flags 3 corners: correct filter: checker silent corners: fault 1 decision not held: output differs at e4, alarm at e4 corners: fault 2 skid never fills: output differs at e9, alarm at e9 corners: fault 3 new beat to the front: output differs at e8, alarm at e8 corners: fault 4 first_q resets to 0: output differs at e4, alarm at e4 corners: fault 5 skid drops TKEEP: output differs at e9, checker silent corners: fault 6 type compared on 4 bits: output differs at e16, alarm at e16 corners: 6 of 6 faults change the output; the checker flags 5Read included file: tb.sv
`timescale 1ns/1ps
// axis_filter_f: R8.3's filter with its fault list (read-only). FAULT = 0 is the correct design;
// FAULT = k makes the one-line change of fault k, marked "fault k" below.
module axis_filter_f #(parameter int FAULT = 0, parameter logic [7:0] DROP_TYPE = 8'hd0) (
input logic clk,
input logic rst_n, // active-low, asynchronous
input logic s_tvalid, // the stream in
output logic s_tready,
input logic [31:0] s_tdata, // byte lane l is s_tdata[8l+7:8l]; lane 0 comes first
input logic [3:0] s_tkeep, // 1: this lane holds a byte of the packet
input logic s_tlast,
output logic m_tvalid, // the stream out
input logic m_tready,
output logic [31:0] m_tdata,
output logic [3:0] m_tkeep,
output logic m_tlast
);
// The packet state.
logic first_q; // the next beat to arrive is a header beat
logic drop_q; // the packet in progress is being dropped
wire take = s_tvalid && s_tready; // a beat arrives at this edge
wire hdr = (FAULT == 6) ? (s_tdata[3:0] == DROP_TYPE[3:0]) // fault 6
: (s_tdata[7:0] == DROP_TYPE);
wire drop = (FAULT == 1) ? (first_q && hdr) // fault 1
: (first_q ? hdr : drop_q); // decided once per packet
wire fwd = take && !drop; // the beat arriving goes on to the output
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
first_q <= (FAULT != 4); drop_q <= 1'b0; // fault 4
end else if (take) begin
first_q <= s_tlast; // after a last beat comes a header beat
drop_q <= drop && !s_tlast; // hold the decision until the last beat
end
// The output stage: a main register and a skid register (R4.4). A dropped beat is taken
// from the input and never reaches them.
logic skid_valid;
logic [36:0] main_q, skid_q; // {last, keep, data}
wire move = m_tready || !m_tvalid; // the main register may load at this edge
assign s_tready = !skid_valid; // from a flop
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
m_tvalid <= 1'b0; skid_valid <= 1'b0;
end else if (move) begin
if (skid_valid) begin m_tvalid <= 1'b1; skid_valid <= 1'b0; end // drain the skid first
else m_tvalid <= fwd;
end else if (fwd) begin
skid_valid <= (FAULT != 2); // main is stalled: the beat waits in the skid (fault 2)
end
always_ff @(posedge clk) // the payload registers have no reset
if (move) begin
if (skid_valid) main_q <= (FAULT == 5) ? {skid_q[36], 4'b1111, skid_q[31:0]} : skid_q; // fault 5
else if (fwd) main_q <= {s_tlast, s_tkeep, s_tdata};
end else if (fwd) begin
if (FAULT == 3) begin // fault 3
skid_q <= main_q; main_q <= {s_tlast, s_tkeep, s_tdata};
end else begin
skid_q <= {s_tlast, s_tkeep, s_tdata};
end
end
assign {m_tlast, m_tkeep, m_tdata} = main_q;
endmodule
// lane: one source, one filter from the bank, one consumer and one copy of your checker.
module lane #(parameter int FAULT = 0, parameter int SET = 0) (input logic clk, input logic rst_n);
logic s_tvalid = 0, s_tlast = 0, m_tready = 0;
logic [31:0] s_tdata = 0;
logic [3:0] s_tkeep = 0;
logic s_tready, m_tvalid, m_tlast;
logic [31:0] m_tdata;
logic [3:0] m_tkeep;
int ptype[$], plen[$];
int p = 0, b = 0, e = -1;
bit taken = 0;
int errors, alarm_at = -1, n_out = 0;
logic [36:0] out_beat [0:63]; // every beat that left, {last, keep, data}
int out_edge [0:63];
initial begin
if (SET == 0) begin ptype = '{17, 208, 34, 208, 51, 208, 85}; plen = '{10, 7, 4, 13, 6, 2, 9}; end
if (SET == 1) begin ptype = '{208, 17, 48, 208, 34, 51}; plen = '{6, 10, 7, 13, 6, 1}; end
end
function automatic bit stall(input int e);
if (SET == 0) return e % 3 == 2;
if (SET == 1) return e % 4 == 1 || e % 4 == 2;
return 0;
endfunction
function automatic logic [7:0] byte_of(input int p, input int k);
if (k == 0) return 8'(ptype[p]);
return {4'(p), 4'(k)};
endfunction
axis_filter_f #(.FAULT(FAULT)) dut (.clk(clk), .rst_n(rst_n), .s_tvalid(s_tvalid), .s_tready(s_tready),
.s_tdata(s_tdata), .s_tkeep(s_tkeep), .s_tlast(s_tlast), .m_tvalid(m_tvalid), .m_tready(m_tready),
.m_tdata(m_tdata), .m_tkeep(m_tkeep), .m_tlast(m_tlast));
filter_check chk (.clk(clk), .rst_n(rst_n), .s_tvalid(s_tvalid), .s_tready(s_tready), .s_tdata(s_tdata),
.s_tkeep(s_tkeep), .s_tlast(s_tlast), .m_tvalid(m_tvalid), .m_tready(m_tready), .m_tdata(m_tdata),
.m_tkeep(m_tkeep), .m_tlast(m_tlast), .errors(errors));
always @(posedge clk) begin
e = e + 1;
taken = rst_n && s_tvalid && s_tready;
if (rst_n && m_tvalid && m_tready && n_out < 64) begin
out_beat[n_out] = {m_tlast, m_tkeep, m_tdata}; out_edge[n_out] = e; n_out++;
end
#1; // drive 1 ns after the edge
if (e >= 1) begin
if (taken) begin b += 4; if (b >= plen[p]) begin p++; b = 0; end end
s_tvalid = (p < ptype.size());
if (p < ptype.size()) begin
for (int l = 0; l < 4; l++) begin
s_tdata[8*l +: 8] = byte_of(p, b + l);
s_tkeep[l] = (b + l < plen[p]);
end
s_tlast = (b + 4 >= plen[p]);
end
m_tready = !stall(e);
end
end
always @(negedge clk) if (errors != 0 && alarm_at < 0) alarm_at = e; // the edge of the first alarm
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic rst_n = 1; // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
int edge_n = -1;
always @(posedge clk) edge_n = edge_n + 1;
int exposed = 0, flagged = 0;
bit quiet = 0; // 1: print only the summary line of each set
// The first beat where a faulty copy's output differs from the correct filter's.
task automatic diff_at(input int n0, input int nf, input int alarm, input int s, input int f, input string set, input string label);
logic [36:0] x, y;
int ex, ey, at;
string out_s, chk_s;
at = -1;
for (int k = 0; k < 64 && at < 0 && (k < n0 || k < nf); k++) begin
if (s == 0 && f == 1) begin x = l0_1.out_beat[k]; y = l0_0.out_beat[k]; ex = l0_1.out_edge[k]; ey = l0_0.out_edge[k]; end
if (s == 0 && f == 2) begin x = l0_2.out_beat[k]; y = l0_0.out_beat[k]; ex = l0_2.out_edge[k]; ey = l0_0.out_edge[k]; end
if (s == 0 && f == 3) begin x = l0_3.out_beat[k]; y = l0_0.out_beat[k]; ex = l0_3.out_edge[k]; ey = l0_0.out_edge[k]; end
if (s == 0 && f == 4) begin x = l0_4.out_beat[k]; y = l0_0.out_beat[k]; ex = l0_4.out_edge[k]; ey = l0_0.out_edge[k]; end
if (s == 0 && f == 5) begin x = l0_5.out_beat[k]; y = l0_0.out_beat[k]; ex = l0_5.out_edge[k]; ey = l0_0.out_edge[k]; end
if (s == 0 && f == 6) begin x = l0_6.out_beat[k]; y = l0_0.out_beat[k]; ex = l0_6.out_edge[k]; ey = l0_0.out_edge[k]; end
if (s == 1 && f == 1) begin x = l1_1.out_beat[k]; y = l1_0.out_beat[k]; ex = l1_1.out_edge[k]; ey = l1_0.out_edge[k]; end
if (s == 1 && f == 2) begin x = l1_2.out_beat[k]; y = l1_0.out_beat[k]; ex = l1_2.out_edge[k]; ey = l1_0.out_edge[k]; end
if (s == 1 && f == 3) begin x = l1_3.out_beat[k]; y = l1_0.out_beat[k]; ex = l1_3.out_edge[k]; ey = l1_0.out_edge[k]; end
if (s == 1 && f == 4) begin x = l1_4.out_beat[k]; y = l1_0.out_beat[k]; ex = l1_4.out_edge[k]; ey = l1_0.out_edge[k]; end
if (s == 1 && f == 5) begin x = l1_5.out_beat[k]; y = l1_0.out_beat[k]; ex = l1_5.out_edge[k]; ey = l1_0.out_edge[k]; end
if (s == 1 && f == 6) begin x = l1_6.out_beat[k]; y = l1_0.out_beat[k]; ex = l1_6.out_edge[k]; ey = l1_0.out_edge[k]; end
if (k >= nf) at = ey;
else if (k >= n0 || x !== y) at = ex;
end
if (at >= 0) begin exposed++; out_s = $sformatf("output differs at e%0d", at); end
else out_s = "output same";
if (alarm >= 0) begin flagged++; chk_s = $sformatf("alarm at e%0d", alarm); end
else chk_s = "checker silent";
if (!quiet) $display("%s: fault %s: %s, %s", set, label, out_s, chk_s);
endtask
lane #(.FAULT(0), .SET(0)) l0_0 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(1), .SET(0)) l0_1 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(2), .SET(0)) l0_2 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(3), .SET(0)) l0_3 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(4), .SET(0)) l0_4 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(5), .SET(0)) l0_5 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(6), .SET(0)) l0_6 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(0), .SET(1)) l1_0 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(1), .SET(1)) l1_1 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(2), .SET(1)) l1_2 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(3), .SET(1)) l1_3 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(4), .SET(1)) l1_4 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(5), .SET(1)) l1_5 (.clk(clk), .rst_n(rst_n));
lane #(.FAULT(6), .SET(1)) l1_6 (.clk(clk), .rst_n(rst_n));
// Advance to the next rising edge, wait for the design to settle, then print one line.
task automatic tick();
@(posedge clk); #1;
endtask
initial begin
#1 rst_n = 0; tick(); tick(); rst_n = 1; // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
repeat (40) @(posedge clk);
#2;
exposed = 0; flagged = 0;
if (l0_0.alarm_at >= 0 && !quiet) $display("smoke: correct filter: false alarm at e%0d", l0_0.alarm_at);
else if (!quiet) $display("smoke: correct filter: checker silent");
diff_at(l0_0.n_out, l0_1.n_out, l0_1.alarm_at, 0, 1, "smoke", "1 decision not held");
diff_at(l0_0.n_out, l0_2.n_out, l0_2.alarm_at, 0, 2, "smoke", "2 skid never fills");
diff_at(l0_0.n_out, l0_3.n_out, l0_3.alarm_at, 0, 3, "smoke", "3 new beat to the front");
diff_at(l0_0.n_out, l0_4.n_out, l0_4.alarm_at, 0, 4, "smoke", "4 first_q resets to 0");
diff_at(l0_0.n_out, l0_5.n_out, l0_5.alarm_at, 0, 5, "smoke", "5 skid drops TKEEP");
diff_at(l0_0.n_out, l0_6.n_out, l0_6.alarm_at, 0, 6, "smoke", "6 type compared on 4 bits");
$display("smoke: %0d of 6 faults change the output; the checker flags %0d", exposed, flagged);
exposed = 0; flagged = 0;
if (l1_0.alarm_at >= 0 && !quiet) $display("corners: correct filter: false alarm at e%0d", l1_0.alarm_at);
else if (!quiet) $display("corners: correct filter: checker silent");
diff_at(l1_0.n_out, l1_1.n_out, l1_1.alarm_at, 1, 1, "corners", "1 decision not held");
diff_at(l1_0.n_out, l1_2.n_out, l1_2.alarm_at, 1, 2, "corners", "2 skid never fills");
diff_at(l1_0.n_out, l1_3.n_out, l1_3.alarm_at, 1, 3, "corners", "3 new beat to the front");
diff_at(l1_0.n_out, l1_4.n_out, l1_4.alarm_at, 1, 4, "corners", "4 first_q resets to 0");
diff_at(l1_0.n_out, l1_5.n_out, l1_5.alarm_at, 1, 5, "corners", "5 skid drops TKEEP");
diff_at(l1_0.n_out, l1_6.n_out, l1_6.alarm_at, 1, 6, "corners", "6 type compared on 4 bits");
$display("corners: %0d of 6 faults change the output; the checker flags %0d", exposed, flagged);
$finish(0); // quiet: no "$finish called" line
end
endmodule
Check yourself
Remember
- A fault list is the bugs you could have made, each as a one-line change; this track's lessons are the place to start.
- Stimulus exposes a fault only if it reaches the corner: on this filter the smoke test missed two of six faults.
- A checker's model comes from the brief, compares order and not timing, and checks every field the brief names.
- A checker passes only if it is silent on the correct design and raises an alarm on every faulty copy.