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 withR8.3's own benchA bench built from the brief's corners
the first packet after reset never comparedoutput sameoutput differs at e4
the type compared on 4 bitsoutput sameoutput differs at e16
four other one-line faultsall four change the outputall 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?

One stimulus, two designs, one checkerArrows carry beats. The brief feeds the checker's model; it never reads the design under test.
One stimulus, two designs, one checkerArrows carry beats. The brief feeds the checker's model; it never reads the design under test. source Stimulus packets and stalls block Correct filter FAULT = 0 block Faulty copy FAULT = k logic Compare outputs does the fault show? logic Your checker does it notice? doc The brief the rule, the ports beats inbeats inbeats outbeats outboth portsthe oracle
  1. 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.

  2. 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.

  3. 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.

Fault 6 in the bank: one line, switched by the parameter.
wire hdr = (FAULT == 6) ? (s_tdata[3:0] == DROP_TYPE[3:0])
                       : (s_tdata[7:0] == DROP_TYPE);
FaultLessonWhat exposes itSmokeCorners
1 decision not heldR8.3a dropped packet of two or more beatse8e4
2 skid never fillsR4.4a passed beat that arrives during a stalle5e9
3 new beat to the frontR4.4the samee4e8
4 first_q resets to 0R2.2a dropped packet first after resetsamee4
5 skid drops TKEEPR4.4, R8.3a partial last beat that waits in the skide20e9
6 type compared on 4 bitsR1.4a passed type whose low four bits match 0xd0samee16

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.

CheckerCorrect filterCorners faults flagged
data only, no rule for a beat on offer (the lab)silent5 of 6: misses fault 5
expects each beat one edge after it arrivesfalse alarm at e66 of 6
expects every beat out, forgets the drop rulefalse alarm at e56 of 6
the brief's checkersilent6 of 6

Measured on the corners set. A checker that also raises a false alarm on the correct design proves nothing.

Fault 3: the first bad cycleThe corners set, values just before edges e5 to e9. m good is the correct filter's output beat; m fault 3 and skid belong to the faulty copy.
01234clksp1.1p1.2p2.0m_treadym goodp1.0p1.1p1.2m fault 3p1.0p1.1p1.2p1.1skid
  1. Cycle 1

    Edge e6: the consumer stalls with p1.1 on offer. p1.2 arrives and must wait behind it.

  2. 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.

  3. 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
  1. 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.

  2. 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.

  3. 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.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: 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 5
Read 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

Predict the outputThe smoke set (types 0x11, 0xd0, 0x22, 0xd0, 0x33, 0xd0, 0x55) runs against fault 6 alone, the type compared on its low four bits. The checker is the brief's. What is the summary line?
// filter_check: watches both ports of the filter and counts every edge where the filter breaks its brief.
module filter_check #(parameter logic [7:0] DROP_TYPE = 8'hd0) (
  input  logic        clk,
  input  logic        rst_n,
  input  logic        s_tvalid, s_tready, s_tlast,
  input  logic [31:0] s_tdata,
  input  logic [3:0]  s_tkeep,
  input  logic        m_tvalid, m_tready, m_tlast,
  input  logic [31:0] m_tdata,
  input  logic [3:0]  m_tkeep,
  output int          errors
);
  logic [36:0] want[$];                  // the beats the brief says must leave, oldest first
  logic        first = 1, dropping = 0;  // the rule's packet state, kept by the checker
  logic        held = 0;                 // the output offered a beat that was not taken
  logic [36:0] held_beat, got;
  logic        d;
  initial errors = 0;
  always @(posedge clk or negedge rst_n)
    if (!rst_n) begin
      want.delete(); first = 1; dropping = 0; held = 0;
    end else begin
      // A beat on offer stays on offer, unchanged, until it is taken.
      if (held && (m_tvalid !== 1'b1 || {m_tlast, m_tkeep, m_tdata} !== held_beat)) errors++;
      // A beat that leaves is the next one the rule lets through: last, keep and data.
      if (m_tvalid && m_tready) begin
        if (want.size() == 0) errors++;
        else begin
          got = want.pop_front();
          if ({m_tlast, m_tkeep, m_tdata} !== got) errors++;
        end
      end
      held = m_tvalid && !m_tready;
      held_beat = {m_tlast, m_tkeep, m_tdata};
      // Every beat taken at the input goes through the rule.
      if (s_tvalid && s_tready) begin
        d = first ? (s_tdata[7:0] == DROP_TYPE) : dropping;
        if (!d) want.push_back({s_tlast, s_tkeep, s_tdata});
        first = s_tlast;
        dropping = d && !s_tlast;
      end
    end
endmodule

`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
  end
  function automatic bit stall(input int e);
    if (SET == 0) return e % 3 == 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 = 1;                  // 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 == 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 (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(6), .SET(0)) l0_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_6.n_out, l0_6.alarm_at, 0, 6, "smoke", "6 type compared on 4 bits");
    $display("smoke: %0d of 1 faults change the output; the checker flags %0d", exposed, flagged);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule
Spot the bugEach option is the line that compares a beat that leaves with got, the head of the model's queue ({last, keep, data}). The corners set runs against all six faults. Which line lets a faulty filter through?
ConceptYour checker raises an alarm on all six faulty copies. Why is that not yet enough?
ConceptWhere should the model inside your checker come from?
ConceptA design passes its own bench with no error. What does that show?
Read the waveformFault 3 on the corners set, values just before edges e5 to e9. At which edge does the faulty filter first break its brief?
Fault 3: new beat to the front
01234sp1.1p1.2p2.0m_treadymp1.0p1.1p1.2p1.1m_tlastskid

Edges e5 to e9, left to right; m_tvalid is 1 throughout.

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.