Skip to content
VLSI Mentor

USB · Module 24

USB Assertions

“Eventually” has no failing case, so it cannot be checked in a finite run — every real liveness check is bounded, a window has two edges, and an obligation still outstanding at end of test is a failure, not an unknown.

Chapter 24.1 built a checker whose rules were all safety properties — this must never happen — and SVA is excellent at those. What is left is the other half, and it is much harder than it looks.

1. The Property Everybody Wants to Write

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    every request is eventually answered

It is unimplementable, unsynthesisable, and uncheckable in a finite simulation.

"Eventually" has no failing case. At any moment during a run, the answer to "has it been answered yet?" is either yes or not yet — and not yet is not a failure. A simulation that ends with the request outstanding has not disproved the property. It has stopped early.

2. A Window Has Two Edges

MAX_LAT alone accepts a response that arrives impossibly early — one cycle after the request, when the pipeline that produces it is four stages deep.

Such a response did not come from this request. It came from the previous one, or from a signal that is stuck asserted, and either way the check has passed while the design is broken.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   MIN_LAT is not paranoia. It is the half of the window
   that catches a response to the WRONG request.

       age <  MIN_LAT    it cannot be ours
       age >= MAX_LAT    it is too late
       otherwise         PASS

3. Obligations Overlap, So One Timer Is Not Enough

A second request can arrive before the first is answered.

With a single timer there is no way to tell which response belongs to which request, and the usual implementation — clear the timer on any response — lets one response satisfy both obligations. The design then drops a response for every overlapping pair, for ever, and the check never fires.

An engine needs a queue of outstanding obligations, retired in order.

4. Running Out of Tracking Capacity Is a Failure, Not a Limit

The queue is finite. When it is full and another request arrives there are two choices: report it, or drop it.

Dropping it is how a checker silently stops checking exactly when the design is busiest — which is when it is most likely to be wrong. So overflow is a reported failure.

5. An Unfinished Obligation Is Not a Pass

This is the one that is missed most often. At the end of the run, whatever is still outstanding has not been answered. It is not "inconclusive" and it is not "still in flight" — the simulation is over and nothing more is coming.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   A run that ends with obligations outstanding and reports
   zero failures has not verified the property.

   It has run out of time while the property was still being
   tested, and called that success.

So eot drains the queue and every survivor is a dangling failure.

6. The Bound's Upper Edge Must Be Stated, Not Inferred

This block was written with the upper edge left implicit: the timeout retires anything that reaches MAX_LAT, so surely a response can never see an obligation that old.

It can — on the exact cycle the age reaches MAX_LAT, because the response is evaluated first.

7. What We Are Building

usb_assert_engine — a queue of obligations, and five ways one can fail

A bounded liveness engine. A trigger pushes a new obligation with age zero onto a four-entry queue, or reports an overflow failure if the queue is full. A response retires the oldest obligation and is classified as a pass if its age is inside the window, an early failure if it is below MIN_LAT, or a late failure if it has reached MAX_LAT. The timeout retires an obligation that ages out as a late failure, and end-of-test retires every survivor as a dangling failure.triggeran obligation beginsOVERFLOWqueue full: reportedobligation queue4 entries, ages onlyDANGLINGoutstanding at endresponseretires the OLDESTLATEage reached MAXPASSMIN ≤ age < MAXEARLYnot ours: too soonpush, age 0oldest firsttimeoutend of test12
An obligation is nothing but an age: the only question ever asked of it is how long it has been waiting. A response retires the oldest and is judged against the window; the timeout retires anything that ages out; end-of-test retires the rest as failures.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  usb_assert_engine  #(N_OBL = 4, MIN_LAT = 2, MAX_LAT = 12)

  inputs                    outputs
  ------                    -------
  trigger    an obligation  outstanding / oldest_age
             begins         pass_pulse / fail_pulse
  response   one is         fail_code   LATE / EARLY /
             discharged                 SPURIOUS / OVERFLOW /
  eot        drain & report             DANGLING

  n_triggers n_pass n_late n_early
  n_spurious n_overflow n_dangling

  ONE EVENT PER CYCLE. A reporting channel has one slot, and
  an engine that can emit three failures in a cycle cannot
  say which one it was. A drain therefore takes as many
  cycles as there are obligations.

8. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_assert_engine -- what an assertion actually is when you build one, and
// the four things every real liveness check needs that "eventually" does not
// give you.
//
// THE PROPERTY EVERYBODY WANTS TO WRITE
//
//     every request is eventually answered
//
// It is unimplementable, unsynthesisable, and uncheckable in a finite
// simulation. "Eventually" has no failing case: at any moment during a run
// the answer to "has it been answered yet?" is either yes or NOT YET, and not
// yet is not a failure. A simulation that ends with the request outstanding
// has not disproved the property -- it has simply stopped early.
//
// So every liveness check that exists in practice is a BOUNDED one:
//
//     every request is answered within MAX_LAT cycles
//
// and choosing MAX_LAT is the whole job. This block is that check, built as
// hardware, and the four things it needs are the four things a hand-written
// timer usually lacks.
//
// 1. A WINDOW HAS TWO EDGES
//
// MAX_LAT alone accepts a response that arrives IMPOSSIBLY EARLY -- one cycle
// after the request, when the pipeline that produces it is four stages deep.
// Such a response did not come from this request. It came from the previous
// one, or from a signal that is stuck asserted, and either way the check has
// passed while the design is broken.
//
//     MIN_LAT is not paranoia. It is the half of the window that
//     catches a response to the WRONG request.
//
// 2. OBLIGATIONS OVERLAP, SO ONE TIMER IS NOT ENOUGH
//
// A second request can arrive before the first is answered. With a single
// timer there is no way to tell which response belongs to which request, and
// the usual implementation -- clear the timer on any response -- lets ONE
// response satisfy BOTH obligations. The bus then drops a response for every
// overlapping pair, for ever, and the check never fires.
//
// An engine needs a QUEUE of outstanding obligations, retired in order.
//
// 3. RUNNING OUT OF TRACKING CAPACITY IS A FAILURE, NOT A LIMIT
//
// The queue is finite. When it is full and another request arrives, there are
// two choices: report it, or drop it. Dropping it is how a checker silently
// stops checking exactly when the design is at its busiest -- which is when
// it is most likely to be wrong. So overflow is a reported failure.
//
// 4. AN UNFINISHED OBLIGATION IS NOT A PASS
//
// This is the one that is missed most often. At the end of the run, whatever
// is still outstanding has NOT been answered. It is not "inconclusive" and it
// is not "still in flight" -- the simulation is over and nothing more is
// coming.
//
//     A run that ends with obligations outstanding and reports
//     zero failures has not verified the property. It has run
//     out of time while the property was still being tested,
//     and called that success.
//
// So `eot` drains the queue and every survivor is a DANGLING failure.
//
// ONE EVENT PER CYCLE
//
// The engine retires at most one obligation per cycle, because a reporting
// channel has one slot and an engine that can emit three failures in a cycle
// cannot say which one it was. A drain therefore takes as many cycles as
// there are obligations, and the testbench holds `eot` long enough for it.
module usb_assert_engine #(
  parameter integer N_OBL   = 4,   // obligations trackable at once
  parameter integer MIN_LAT = 2,   // a response sooner than this is not ours
  parameter integer MAX_LAT = 12   // a response later than this is a failure
) (
  input  wire       clk,
  input  wire       rst_n,

  input  wire       trigger,    // the antecedent fired: an obligation begins
  input  wire       response,   // the consequent fired: one is discharged
  input  wire       eot,        // end of test: drain and report

  output wire [2:0] outstanding,
  output wire [4:0] oldest_age,
  output wire       pass_pulse,
  output wire       fail_pulse,
  output wire [2:0] fail_code,

  output reg [31:0] n_triggers,
  output reg [31:0] n_pass,
  output reg [31:0] n_late,
  output reg [31:0] n_early,
  output reg [31:0] n_spurious,
  output reg [31:0] n_overflow,
  output reg [31:0] n_dangling
);

  localparam [2:0] F_NONE     = 3'd0,
                   F_LATE     = 3'd1,  // no response within MAX_LAT
                   F_EARLY    = 3'd2,  // a response before MIN_LAT
                   F_SPURIOUS = 3'd3,  // a response with nothing outstanding
                   F_OVERFLOW = 3'd4,  // more obligations than trackable
                   F_DANGLING = 3'd5;  // still outstanding at end of test

  // ---- The queue of outstanding obligations. Just their ages: an
  // ---- obligation has no other content, because the only question ever
  // ---- asked of it is "how long has it been waiting".
  reg [4:0] age_r [0:N_OBL-1];
  reg [2:0] cnt_r;
  reg [2:0] fc_r;
  reg       pass_r, fail_r;

  assign outstanding = cnt_r;
  assign oldest_age  = (cnt_r == 3'd0) ? 5'd0 : age_r[0];
  assign pass_pulse  = pass_r;
  assign fail_pulse  = fail_r;
  assign fail_code   = fc_r;

  integer i;
  reg [4:0] age_n [0:N_OBL-1];
  reg [2:0] cnt_n, fc_n;
  reg       pass_n, fail_n;
  reg       retired;            // an obligation left the queue this cycle

  // Shift the queue down by one: the OLDEST leaves. FIFO order is not a
  // style choice -- responses arrive in the order their requests were made,
  // so retiring the newest would charge the wrong obligation's age against
  // the window and let a genuinely late response pass as a prompt one.
  task retire_oldest;
    integer k;
    begin
      for (k = 0; k < N_OBL - 1; k = k + 1) age_n[k] = age_n[k+1];
      age_n[N_OBL-1] = 5'd0;
      cnt_n = cnt_n - 3'd1;
    end
  endtask

  always @* begin
    for (i = 0; i < N_OBL; i = i + 1) age_n[i] = age_r[i];
    cnt_n   = cnt_r;
    fc_n    = F_NONE;
    pass_n  = 1'b0;
    fail_n  = 1'b0;
    retired = 1'b0;

    // ---- 1. END OF TEST drains first, and every survivor is a failure. ----
    if (eot) begin
      if (cnt_r != 3'd0) begin
        retire_oldest;
        retired = 1'b1;
        fail_n  = 1'b1;
        fc_n    = F_DANGLING;
      end
    end else begin
      // ---- 2. A response discharges the OLDEST outstanding obligation. ----
      if (response) begin
        if (cnt_r == 3'd0) begin
          // Nothing was outstanding. A response to nothing is not harmless:
          // it means the consequent can fire on its own, so a later real
          // obligation could be discharged by a signal that has nothing to
          // do with it.
          fail_n = 1'b1;
          fc_n   = F_SPURIOUS;
        end else begin
          if (age_r[0] < MIN_LAT[4:0]) begin
            // Too soon to be ours. See note 1 in the header.
            fail_n = 1'b1;
            fc_n   = F_EARLY;
          end else if (age_r[0] >= MAX_LAT[4:0]) begin
            // ---- The UPPER edge, stated HERE and not left to the timeout.
            //
            // It is tempting to leave this out: the timeout below retires
            // anything that reaches MAX_LAT, so surely a response can never
            // see an obligation that old. It can -- on the exact cycle the
            // age reaches MAX_LAT, because the response is evaluated first.
            //
            // With the test omitted, that one tie cycle is a PASS, and which
            // way it goes is decided by the order of two `if` statements
            // rather than by the specification. A bound whose boundary case
            // depends on evaluation order is not a bound anybody can quote.
            //
            // The window is MIN_LAT <= age < MAX_LAT, written in one place.
            fail_n = 1'b1;
            fc_n   = F_LATE;
          end else begin
            pass_n = 1'b1;
          end
          retire_oldest;
          retired = 1'b1;
        end
      end

      // ---- 3. The timeout, checked on the oldest, and only if nothing was
      // ---- retired this cycle -- one event per cycle.
      if (!retired && (cnt_r != 3'd0) && (age_r[0] >= MAX_LAT[4:0])) begin
        retire_oldest;
        retired = 1'b1;
        fail_n  = 1'b1;
        fc_n    = F_LATE;
      end

      // ---- 4. Everything still outstanding gets one cycle older. ----
      //
      // Done BEFORE the new obligation is pushed, not after, so that the
      // new one starts at age 0 and is not charged for the cycle it was
      // created in. Ageing after the push needs an exclusion for the
      // just-pushed entry, and that exclusion is exactly the kind of
      // condition that is written once, is wrong by one, and is never
      // noticed because MIN_LAT hides it.
      for (i = 0; i < N_OBL; i = i + 1)
        if ((i[2:0] < cnt_n) && (age_n[i] < MAX_LAT[4:0]))
          age_n[i] = age_n[i] + 5'd1;

      // ---- 5. A new obligation. Overflow is REPORTED, never dropped. ----
      if (trigger) begin
        if (cnt_n >= N_OBL[2:0]) begin
          // The queue is full. Saying so is the point: silently dropping the
          // obligation makes the engine stop checking precisely when the
          // design is busiest.
          fail_n = 1'b1;
          fc_n   = F_OVERFLOW;
        end else begin
          age_n[cnt_n] = 5'd0;
          cnt_n = cnt_n + 3'd1;
        end
      end
    end
  end

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_OBL; i = i + 1) age_r[i] <= 5'd0;
      cnt_r      <= 3'd0;
      fc_r       <= F_NONE;
      pass_r     <= 1'b0;
      fail_r     <= 1'b0;
      n_triggers <= 32'd0;
      n_pass     <= 32'd0;
      n_late     <= 32'd0;
      n_early    <= 32'd0;
      n_spurious <= 32'd0;
      n_overflow <= 32'd0;
      n_dangling <= 32'd0;
    end else begin
      for (i = 0; i < N_OBL; i = i + 1) age_r[i] <= age_n[i];
      cnt_r  <= cnt_n;
      fc_r   <= fc_n;
      pass_r <= pass_n;
      fail_r <= fail_n;

      if (trigger && !eot) n_triggers <= n_triggers + 32'd1;
      if (pass_n)          n_pass     <= n_pass + 32'd1;

      // The per-cause counters are driven by the SAME pulse as the failure,
      // so they sum to the failure total by construction (chapter 23.4).
      if (fail_n) begin
        case (fc_n)
          F_LATE:     n_late     <= n_late     + 32'd1;
          F_EARLY:    n_early    <= n_early    + 32'd1;
          F_SPURIOUS: n_spurious <= n_spurious + 32'd1;
          F_OVERFLOW: n_overflow <= n_overflow + 32'd1;
          F_DANGLING: n_dangling <= n_dangling + 32'd1;
          default: ;
        endcase
      end
    end
  end
endmodule

9. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// usb_assert_engine -- what an assertion actually is when you build one, and
// the four things every real liveness check needs that "eventually" does not
// give you.
//
// THE PROPERTY EVERYBODY WANTS TO WRITE
//
//     every request is eventually answered
//
// It is unimplementable, unsynthesisable, and uncheckable in a finite
// simulation. "Eventually" has no failing case: at any moment during a run
// the answer to "has it been answered yet?" is either yes or NOT YET, and not
// yet is not a failure. A simulation that ends with the request outstanding
// has not disproved the property -- it has simply stopped early.
//
// So every liveness check that exists in practice is a BOUNDED one:
//
//     every request is answered within MAX_LAT cycles
//
// and choosing MAX_LAT is the whole job. This block is that check, built as
// hardware, and the four things it needs are the four things a hand-written
// timer usually lacks.
//
// 1. A WINDOW HAS TWO EDGES
//
// MAX_LAT alone accepts a response that arrives IMPOSSIBLY EARLY -- one cycle
// after the request, when the pipeline that produces it is four stages deep.
// Such a response did not come from this request. It came from the previous
// one, or from a signal that is stuck asserted, and either way the check has
// passed while the design is broken.
//
//     MIN_LAT is not paranoia. It is the half of the window that
//     catches a response to the WRONG request.
//
// 2. OBLIGATIONS OVERLAP, SO ONE TIMER IS NOT ENOUGH
//
// A second request can arrive before the first is answered. With a single
// timer there is no way to tell which response belongs to which request, and
// the usual implementation -- clear the timer on any response -- lets ONE
// response satisfy BOTH obligations. The bus then drops a response for every
// overlapping pair, for ever, and the check never fires.
//
// An engine needs a QUEUE of outstanding obligations, retired in order.
//
// 3. RUNNING OUT OF TRACKING CAPACITY IS A FAILURE, NOT A LIMIT
//
// The queue is finite. When it is full and another request arrives, there are
// two choices: report it, or drop it. Dropping it is how a checker silently
// stops checking exactly when the design is at its busiest -- which is when
// it is most likely to be wrong. So overflow is a reported failure.
//
// 4. AN UNFINISHED OBLIGATION IS NOT A PASS
//
// This is the one that is missed most often. At the end of the run, whatever
// is still outstanding has NOT been answered. It is not "inconclusive" and it
// is not "still in flight" -- the simulation is over and nothing more is
// coming.
//
//     A run that ends with obligations outstanding and reports
//     zero failures has not verified the property. It has run
//     out of time while the property was still being tested,
//     and called that success.
//
// So `eot` drains the queue and every survivor is a DANGLING failure.
//
// ONE EVENT PER CYCLE
//
// The engine retires at most one obligation per cycle, because a reporting
// channel has one slot and an engine that can emit three failures in a cycle
// cannot say which one it was. A drain therefore takes as many cycles as
// there are obligations, and the testbench holds `eot` long enough for it.
package usb_ae_pkg;
  // The five ways an obligation can fail, named. F_DANGLING is the one that
  // is usually missing, and it is the one that decides whether a run that
  // ended early counts as a pass.
  typedef enum logic [2:0] {
    F_NONE     = 3'd0,
    F_LATE     = 3'd1,   // no response within MAX_LAT
    F_EARLY    = 3'd2,   // a response before MIN_LAT
    F_SPURIOUS = 3'd3,   // a response with nothing outstanding
    F_OVERFLOW = 3'd4,   // more obligations than the engine can track
    F_DANGLING = 3'd5    // still outstanding at end of test
  } fail_e;
endpackage

module usb_assert_engine
  import usb_ae_pkg::*;
 #(
  parameter int N_OBL   = 4,   // obligations trackable at once
  parameter int MIN_LAT = 2,   // a response sooner than this is not ours
  parameter int MAX_LAT = 12   // a response later than this is a failure
) (
  input  logic       clk,
  input  logic       rst_n,

  input  logic       trigger,    // the antecedent fired: an obligation begins
  input  logic       response,   // the consequent fired: one is discharged
  input  logic       eot,        // end of test: drain and report

  output logic [2:0] outstanding,
  output logic [4:0] oldest_age,
  output logic       pass_pulse,
  output logic       fail_pulse,
  output fail_e      fail_code,

  output logic [31:0] n_triggers,
  output logic [31:0] n_pass,
  output logic [31:0] n_late,
  output logic [31:0] n_early,
  output logic [31:0] n_spurious,
  output logic [31:0] n_overflow,
  output logic [31:0] n_dangling
);

  // ---- The queue of outstanding obligations. Just their ages: an
  // ---- obligation has no other content, because the only question ever
  // ---- asked of it is "how long has it been waiting".
  logic [4:0] age_r [N_OBL];
  logic [2:0] cnt_r;
  fail_e      fc_r;
  logic       pass_r, fail_r;

  assign outstanding = cnt_r;
  assign oldest_age  = (cnt_r == 3'd0) ? 5'd0 : age_r[0];
  assign pass_pulse  = pass_r;
  assign fail_pulse  = fail_r;
  assign fail_code   = fc_r;

  int         i;
  logic [4:0] age_n [N_OBL];
  logic [2:0] cnt_n;
  fail_e      fc_n;
  logic       pass_n, fail_n;
  logic       retired;          // an obligation left the queue this cycle

  // Shift the queue down by one: the OLDEST leaves. FIFO order is not a
  // style choice -- responses arrive in the order their requests were made,
  // so retiring the newest would charge the wrong obligation's age against
  // the window and let a genuinely late response pass as a prompt one.
  //
  // Written out at each of the three retirement points rather than shared,
  // because a subprogram that mutates module-level state from inside an
  // always_comb is exactly the construct simulators disagree about.
  `define RETIRE_OLDEST                                       \
      for (int k = 0; k < N_OBL - 1; k++) age_n[k] = age_n[k+1]; \
      age_n[N_OBL-1] = 5'd0;                                  \
      cnt_n = cnt_n - 3'd1;

  always_comb begin
    for (i = 0; i < N_OBL; i++) age_n[i] = age_r[i];
    cnt_n   = cnt_r;
    fc_n    = F_NONE;
    pass_n  = 1'b0;
    fail_n  = 1'b0;
    retired = 1'b0;

    // ---- 1. END OF TEST drains first, and every survivor is a failure. ----
    if (eot) begin
      if (cnt_r != 3'd0) begin
        `RETIRE_OLDEST
        retired = 1'b1;
        fail_n  = 1'b1;
        fc_n    = F_DANGLING;
      end
    end else begin
      // ---- 2. A response discharges the OLDEST outstanding obligation. ----
      if (response) begin
        if (cnt_r == 3'd0) begin
          // Nothing was outstanding. A response to nothing is not harmless:
          // it means the consequent can fire on its own, so a later real
          // obligation could be discharged by a signal that has nothing to
          // do with it.
          fail_n = 1'b1;
          fc_n   = F_SPURIOUS;
        end else begin
          if (age_r[0] < 5'(MIN_LAT)) begin
            // Too soon to be ours. See note 1 in the header.
            fail_n = 1'b1;
            fc_n   = F_EARLY;
          end else if (age_r[0] >= 5'(MAX_LAT)) begin
            // ---- The UPPER edge, stated HERE and not left to the timeout.
            //
            // It is tempting to leave this out: the timeout below retires
            // anything that reaches MAX_LAT, so surely a response can never
            // see an obligation that old. It can -- on the exact cycle the
            // age reaches MAX_LAT, because the response is evaluated first.
            //
            // With the test omitted, that one tie cycle is a PASS, and which
            // way it goes is decided by the order of two `if` statements
            // rather than by the specification. A bound whose boundary case
            // depends on evaluation order is not a bound anybody can quote.
            //
            // The window is MIN_LAT <= age < MAX_LAT, written in one place.
            fail_n = 1'b1;
            fc_n   = F_LATE;
          end else begin
            pass_n = 1'b1;
          end
          `RETIRE_OLDEST
          retired = 1'b1;
        end
      end

      // ---- 3. The timeout, checked on the oldest, and only if nothing was
      // ---- retired this cycle -- one event per cycle.
      if (!retired && (cnt_r != 3'd0) && (age_r[0] >= 5'(MAX_LAT))) begin
        `RETIRE_OLDEST
        retired = 1'b1;
        fail_n  = 1'b1;
        fc_n    = F_LATE;
      end

      // ---- 4. Everything still outstanding gets one cycle older. ----
      //
      // Done BEFORE the new obligation is pushed, not after, so that the
      // new one starts at age 0 and is not charged for the cycle it was
      // created in. Ageing after the push needs an exclusion for the
      // just-pushed entry, and that exclusion is exactly the kind of
      // condition that is written once, is wrong by one, and is never
      // noticed because MIN_LAT hides it.
      for (i = 0; i < N_OBL; i++)
        if ((3'(i) < cnt_n) && (age_n[i] < 5'(MAX_LAT)))
          age_n[i] = age_n[i] + 5'd1;

      // ---- 5. A new obligation. Overflow is REPORTED, never dropped. ----
      if (trigger) begin
        if (cnt_n >= 3'(N_OBL)) begin
          // The queue is full. Saying so is the point: silently dropping the
          // obligation makes the engine stop checking precisely when the
          // design is busiest.
          fail_n = 1'b1;
          fc_n   = F_OVERFLOW;
        end else begin
          age_n[cnt_n] = 5'd0;
          cnt_n = cnt_n + 3'd1;
        end
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_OBL; i++) age_r[i] <= 5'd0;
      cnt_r      <= 3'd0;
      fc_r       <= F_NONE;
      pass_r     <= 1'b0;
      fail_r     <= 1'b0;
      n_triggers <= 32'd0;
      n_pass     <= 32'd0;
      n_late     <= 32'd0;
      n_early    <= 32'd0;
      n_spurious <= 32'd0;
      n_overflow <= 32'd0;
      n_dangling <= 32'd0;
    end else begin
      for (i = 0; i < N_OBL; i++) age_r[i] <= age_n[i];
      cnt_r  <= cnt_n;
      fc_r   <= fc_n;
      pass_r <= pass_n;
      fail_r <= fail_n;

      if (trigger && !eot) n_triggers <= n_triggers + 32'd1;
      if (pass_n)          n_pass     <= n_pass + 32'd1;

      // The per-cause counters are driven by the SAME pulse as the failure,
      // so they sum to the failure total by construction (chapter 23.4).
      if (fail_n) begin
        case (fc_n)
          F_LATE:     n_late     <= n_late     + 32'd1;
          F_EARLY:    n_early    <= n_early    + 32'd1;
          F_SPURIOUS: n_spurious <= n_spurious + 32'd1;
          F_OVERFLOW: n_overflow <= n_overflow + 32'd1;
          F_DANGLING: n_dangling <= n_dangling + 32'd1;
          default: ;
        endcase
      end
    end
  end
endmodule

10. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- usb_assert_engine -- what an assertion actually is when you build one, and
-- the four things every real liveness check needs that "eventually" does not
-- give you.
--
-- THE PROPERTY EVERYBODY WANTS TO WRITE
--
--     every request is eventually answered
--
-- It is unimplementable, unsynthesisable, and uncheckable in a finite
-- simulation. "Eventually" has no failing case: at any moment during a run
-- the answer to "has it been answered yet?" is either yes or NOT YET, and not
-- yet is not a failure. A simulation that ends with the request outstanding
-- has not disproved the property -- it has simply stopped early.
--
-- So every liveness check that exists in practice is a BOUNDED one:
--
--     every request is answered within MAX_LAT cycles
--
-- and choosing MAX_LAT is the whole job. This block is that check, built as
-- hardware, and the four things it needs are the four things a hand-written
-- timer usually lacks.
--
-- 1. A WINDOW HAS TWO EDGES
--
-- MAX_LAT alone accepts a response that arrives IMPOSSIBLY EARLY -- one cycle
-- after the request, when the pipeline that produces it is four stages deep.
-- Such a response did not come from this request. It came from the previous
-- one, or from a signal that is stuck asserted, and either way the check has
-- passed while the design is broken.
--
--     MIN_LAT is not paranoia. It is the half of the window that
--     catches a response to the WRONG request.
--
-- 2. OBLIGATIONS OVERLAP, SO ONE TIMER IS NOT ENOUGH
--
-- A second request can arrive before the first is answered. With a single
-- timer there is no way to tell which response belongs to which request, and
-- the usual implementation -- clear the timer on any response -- lets ONE
-- response satisfy BOTH obligations. The bus then drops a response for every
-- overlapping pair, for ever, and the check never fires.
--
-- An engine needs a QUEUE of outstanding obligations, retired in order.
--
-- 3. RUNNING OUT OF TRACKING CAPACITY IS A FAILURE, NOT A LIMIT
--
-- The queue is finite. When it is full and another request arrives, there are
-- two choices: report it, or drop it. Dropping it is how a checker silently
-- stops checking exactly when the design is at its busiest -- which is when
-- it is most likely to be wrong. So overflow is a reported failure.
--
-- 4. AN UNFINISHED OBLIGATION IS NOT A PASS
--
-- This is the one that is missed most often. At the end of the run, whatever
-- is still outstanding has NOT been answered. It is not "inconclusive" and it
-- is not "still in flight" -- the simulation is over and nothing more is
-- coming.
--
--     A run that ends with obligations outstanding and reports
--     zero failures has not verified the property. It has run
--     out of time while the property was still being tested,
--     and called that success.
--
-- So `eot` drains the queue and every survivor is a DANGLING failure.
--
-- ONE EVENT PER CYCLE
--
-- The engine retires at most one obligation per cycle, because a reporting
-- channel has one slot and an engine that can emit three failures in a cycle
-- cannot say which one it was. A drain therefore takes as many cycles as
-- there are obligations, and the testbench holds `eot` long enough for it.
library ieee;
use ieee.std_logic_1164.all;

package usb_ae_pkg is
  -- The five ways an obligation can fail, named. F_DANGLING is the one that
  -- is usually missing, and it is the one that decides whether a run that
  -- ended early counts as a pass.
  type fail_t is (F_NONE, F_LATE, F_EARLY, F_SPURIOUS, F_OVERFLOW, F_DANGLING);
  function fc_code (f : fail_t) return std_logic_vector;
end package usb_ae_pkg;

package body usb_ae_pkg is
  -- Written out rather than derived from position, so the encoding is pinned
  -- to the same numbers the Verilog and SystemVerilog use.
  function fc_code (f : fail_t) return std_logic_vector is
  begin
    case f is
      when F_NONE     => return "000";
      when F_LATE     => return "001";
      when F_EARLY    => return "010";
      when F_SPURIOUS => return "011";
      when F_OVERFLOW => return "100";
      when F_DANGLING => return "101";
    end case;
  end function;
end package body usb_ae_pkg;

library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.usb_ae_pkg.all;

entity usb_assert_engine is
  generic (
    N_OBL   : integer := 4;   -- obligations trackable at once
    MIN_LAT : integer := 2;   -- a response sooner than this is not ours
    MAX_LAT : integer := 12   -- a response later than this is a failure
  );
  port (
    clk         : in  std_logic;
    rst_n       : in  std_logic;

    trigger     : in  std_logic;   -- the antecedent fired
    response    : in  std_logic;   -- the consequent fired
    eot         : in  std_logic;   -- end of test: drain and report

    outstanding : out std_logic_vector(2 downto 0);
    oldest_age  : out std_logic_vector(4 downto 0);
    pass_pulse  : out std_logic;
    fail_pulse  : out std_logic;
    fail_code   : out std_logic_vector(2 downto 0);

    n_triggers  : out std_logic_vector(31 downto 0);
    n_pass      : out std_logic_vector(31 downto 0);
    n_late      : out std_logic_vector(31 downto 0);
    n_early     : out std_logic_vector(31 downto 0);
    n_spurious  : out std_logic_vector(31 downto 0);
    n_overflow  : out std_logic_vector(31 downto 0);
    n_dangling  : out std_logic_vector(31 downto 0)
  );
end entity usb_assert_engine;

architecture rtl of usb_assert_engine is

  type age_arr is array (0 to N_OBL-1) of unsigned(4 downto 0);

  signal age_r  : age_arr := (others => (others => '0'));
  signal cnt_r  : unsigned(2 downto 0) := (others => '0');
  signal fc_r   : fail_t := F_NONE;
  signal pass_r, fail_r : std_logic := '0';

  -- Accumulators are held as unsigned rather than as range-constrained
  -- integers: a constrained integer aborts simulation on overflow, which
  -- turns a mutation into a crash instead of a measured kill.
  signal c_trg, c_p, c_late : unsigned(31 downto 0) := (others => '0');
  signal c_early, c_spur, c_ovf, c_dng : unsigned(31 downto 0) := (others => '0');

begin

  outstanding <= std_logic_vector(cnt_r);
  oldest_age  <= (others => '0') when cnt_r = 0
                 else std_logic_vector(age_r(0));
  pass_pulse  <= pass_r;
  fail_pulse  <= fail_r;
  fail_code   <= fc_code(fc_r);

  n_triggers <= std_logic_vector(c_trg);
  n_pass     <= std_logic_vector(c_p);
  n_late     <= std_logic_vector(c_late);
  n_early    <= std_logic_vector(c_early);
  n_spurious <= std_logic_vector(c_spur);
  n_overflow <= std_logic_vector(c_ovf);
  n_dangling <= std_logic_vector(c_dng);

  process (clk, rst_n)
    variable na  : age_arr;
    variable nc  : unsigned(2 downto 0);
    variable nfc : fail_t;
    variable np, nf, ret : std_logic;

    -- Shift the queue down by one: the OLDEST leaves. FIFO order is not a
    -- style choice -- responses arrive in the order their requests were
    -- made, so retiring the newest would charge the wrong obligation's age
    -- against the window and let a genuinely late response pass as a
    -- prompt one.
    procedure retire_oldest is
    begin
      for k in 0 to N_OBL - 2 loop
        na(k) := na(k+1);
      end loop;
      na(N_OBL-1) := (others => '0');
      nc := nc - 1;
    end procedure;
  begin
    if rst_n = '0' then
      age_r   <= (others => (others => '0'));
      cnt_r   <= (others => '0');
      fc_r    <= F_NONE;
      pass_r  <= '0';
      fail_r  <= '0';
      c_trg   <= (others => '0');
      c_p     <= (others => '0');
      c_late  <= (others => '0');
      c_early <= (others => '0');
      c_spur  <= (others => '0');
      c_ovf   <= (others => '0');
      c_dng   <= (others => '0');
    elsif rising_edge(clk) then
      na  := age_r;
      nc  := cnt_r;
      nfc := F_NONE;
      np  := '0'; nf := '0'; ret := '0';

      -- ---- 1. END OF TEST drains first, and every survivor is a failure. --
      if eot = '1' then
        if cnt_r /= 0 then
          retire_oldest;
          ret := '1'; nf := '1'; nfc := F_DANGLING;
        end if;
      else
        -- ---- 2. A response discharges the OLDEST outstanding obligation. --
        if response = '1' then
          if cnt_r = 0 then
            -- Nothing was outstanding. A response to nothing is not
            -- harmless: it means the consequent can fire on its own, so a
            -- later real obligation could be discharged by a signal that
            -- has nothing to do with it.
            nf := '1'; nfc := F_SPURIOUS;
          else
            if age_r(0) < to_unsigned(MIN_LAT, 5) then
              -- Too soon to be ours. See note 1 in the header.
              nf := '1'; nfc := F_EARLY;
            elsif age_r(0) >= to_unsigned(MAX_LAT, 5) then
              -- ---- The UPPER edge, stated HERE and not left to the
              -- ---- timeout.
              --
              -- It is tempting to leave this out: the timeout below retires
              -- anything that reaches MAX_LAT, so surely a response can
              -- never see an obligation that old. It can -- on the exact
              -- cycle the age reaches MAX_LAT, because the response is
              -- evaluated first.
              --
              -- With the test omitted, that one tie cycle is a PASS, and
              -- which way it goes is decided by the order of two branches
              -- rather than by the specification. A bound whose boundary
              -- case depends on evaluation order is not a bound anybody
              -- can quote.
              --
              -- The window is MIN_LAT <= age < MAX_LAT, written in one
              -- place.
              nf := '1'; nfc := F_LATE;
            else
              np := '1';
            end if;
            retire_oldest;
            ret := '1';
          end if;
        end if;

        -- ---- 3. The timeout, checked on the oldest, and only if nothing
        -- ---- was retired this cycle -- one event per cycle.
        if ret = '0' and cnt_r /= 0
           and age_r(0) >= to_unsigned(MAX_LAT, 5) then
          retire_oldest;
          ret := '1'; nf := '1'; nfc := F_LATE;
        end if;

        -- ---- 4. Everything still outstanding gets one cycle older. ----
        --
        -- Done BEFORE the new obligation is pushed, not after, so that the
        -- new one starts at age 0 and is not charged for the cycle it was
        -- created in. Ageing after the push needs an exclusion for the
        -- just-pushed entry, and that exclusion is exactly the kind of
        -- condition that is written once, is wrong by one, and is never
        -- noticed because MIN_LAT hides it.
        for i in 0 to N_OBL - 1 loop
          if to_unsigned(i, 3) < nc and na(i) < to_unsigned(MAX_LAT, 5) then
            na(i) := na(i) + 1;
          end if;
        end loop;

        -- ---- 5. A new obligation. Overflow is REPORTED, never dropped. ---
        if trigger = '1' then
          if nc >= to_unsigned(N_OBL, 3) then
            -- The queue is full. Saying so is the point: silently dropping
            -- the obligation makes the engine stop checking precisely when
            -- the design is busiest.
            nf := '1'; nfc := F_OVERFLOW;
          else
            na(to_integer(nc)) := (others => '0');
            nc := nc + 1;
          end if;
        end if;
      end if;

      age_r  <= na;
      cnt_r  <= nc;
      fc_r   <= nfc;
      pass_r <= np;
      fail_r <= nf;

      if trigger = '1' and eot = '0' then c_trg <= c_trg + 1; end if;
      if np = '1' then c_p <= c_p + 1; end if;

      -- The per-cause counters are driven by the SAME pulse as the failure,
      -- so they sum to the failure total by construction (chapter 23.4).
      if nf = '1' then
        case nfc is
          when F_LATE     => c_late  <= c_late  + 1;
          when F_EARLY    => c_early <= c_early + 1;
          when F_SPURIOUS => c_spur  <= c_spur  + 1;
          when F_OVERFLOW => c_ovf   <= c_ovf   + 1;
          when F_DANGLING => c_dng   <= c_dng   + 1;
          when others     => null;
        end case;
      end if;
    end if;
  end process;

end architecture rtl;

VHDL's nested procedure inside the clocked process is the one place where the three languages genuinely differ in comfort: it can mutate the process's own variables with no ambiguity at all, which is what the Verilog task was reaching for and what the SystemVerilog macro works around.

11. Seeing Obligations Overlap

Two obligations in flight, discharged in order, then two failures

usb_assert_engine — overlap, FIFO order, and two failure kinds

10 cycles
A ten-cycle waveform. A trigger at cycle 0 creates an obligation; a second trigger at cycle 2 creates another while the first is still outstanding, so outstanding reads 2. Responses at cycles 4 and 5 discharge them oldest-first and both pass. A response at cycle 6 with nothing outstanding raises a spurious failure. A trigger at cycle 7 followed immediately by a response at cycle 8 raises an early failure because the age is below MIN_LAT.two outstanding: ages 2 and 0two outstanding: ages 2 and0charged against the OLDER: passcharged against the OLDER:passa response to nothinga response to nothingage 0: too soon to be oursage 0: too soon to be oursclktriggerresponseoutstanding0112210010oldest_age0012320000pass_pulsefail_pulsefail_codeNONENONENONENONENONENONENONESPURNONEEARLYt0t1t2t3t4t5t6t7t8t9
Cycle 2 adds a second obligation while the first is still waiting, so the queue holds ages 2 and 0. The response at cycle 4 is charged against the OLDER one and passes. A response with nothing outstanding is spurious; a response one cycle after a trigger is too soon to be that trigger's.

Read oldest_age at cycle 3: it is 2, the age of the first obligation, while a second one sits behind it at age 0. That one number is the whole difference between a queue and a timer.

12. The Testbenches

Two exhaustive sweeps:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   1.  every queue occupancy 0..N_OBL  x  all 8 combinations of
       {trigger, response, eot}                    =  40 pairs

   2.  every age of the oldest obligation 0..MAX_LAT
       x  {a response arrived, none did}            =  26 pairs

   Both required complete, and every occupancy reached by
   real triggers.

And three things that are not sweeps at all.

Both edges of the window, at every latency from 0 to MAX_LAT + 2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      if (lat < MIN_LAT) begin
        check(n_early == before_f + 1,
              "a response that arrived sooner than MIN_LAT was accepted -- it cannot have been a response to this request");
        check(n_pass == before_p,
              "an impossibly early response was counted as a pass");
      end else if (lat < MAX_LAT) begin
        check(n_pass == before_p + 1,
              "a response inside the window was not accepted");
        check(n_early == before_f && n_late == before_l,
              "a response inside the window was reported as a failure");
      end else begin
        check(n_late == before_l + 1,
              "a response later than MAX_LAT was accepted -- the bound is not a bound");
        check(n_pass == before_p,
              "a response later than MAX_LAT was counted as a pass");
      end

FIFO order, as a discriminator that passes on a correct engine:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      step(1'b1, 1'b0, 1'b0);          // obligation 1
      idle(MIN_LAT + 2);
      step(1'b1, 1'b0, 1'b0);          // obligation 2, much younger
      step(1'b0, 1'b1, 1'b0);          // one response
      idle(1);
      check(n_pass == before_p + 1,
            "a response was not charged against the OLDEST outstanding obligation -- the engine is not FIFO");
      check(n_early == before_f,
            "a response was charged against a younger obligation and reported as early");

The drain, with the check that nobody writes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      check(n_dangling == before_f + oc,
            "obligations outstanding at end of test were not reported -- a run that stops while the property is still being tested has not verified it");
      check(n_pass == before_p,
            "an obligation that was never answered was counted as a pass");

12.1 Verilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Testbench for usb_assert_engine (Verilog-2005).
//
// WHAT IS EXHAUSTIVE HERE
//
//   1. Every occupancy of the obligation queue, 0 to N_OBL, crossed with
//      all eight combinations of {trigger, response, eot} = 40 pairs, each
//      reached by real triggers and real responses.
//
//   2. Every age of the oldest obligation, 0 to MAX_LAT, crossed with
//      "a response arrived" and "none did" = 26 pairs.
//
// AND THE THINGS THAT ARE NOT SWEEPS
//
//   BOTH EDGES OF THE WINDOW. A response at MAX_LAT-1 must PASS and one at
//   MAX_LAT must FAIL. Checking only the second accepts an engine with no
//   window at all; checking only the first accepts one that never fires.
//
//   FIFO ORDER. Two obligations of different ages, one response: it must be
//   charged against the OLDER. A LIFO engine charges the younger, which is
//   below MIN_LAT, so the discriminator is that a correct engine PASSES here
//   and a LIFO one reports an EARLY failure on perfectly good traffic.
//
//   THE DRAIN. Triggers, then end-of-test, and every survivor must be
//   reported. An engine that simply stops has not passed -- it has run out
//   of time while the property was still being tested.
`timescale 1ns/1ps
module tb_ae_v;

  localparam integer N_OBL   = 4;
  localparam integer MIN_LAT = 2;
  localparam integer MAX_LAT = 12;

  localparam [2:0] F_NONE=3'd0, F_LATE=3'd1, F_EARLY=3'd2,
                   F_SPURIOUS=3'd3, F_OVERFLOW=3'd4, F_DANGLING=3'd5;

  reg clk = 1'b0, rst_n = 1'b0;
  reg trigger = 1'b0, response = 1'b0, eot = 1'b0;

  wire [2:0] outstanding, fail_code;
  wire [4:0] oldest_age;
  wire pass_pulse, fail_pulse;
  wire [31:0] n_triggers, n_pass, n_late, n_early, n_spurious,
              n_overflow, n_dangling;

  usb_assert_engine #(.N_OBL(N_OBL), .MIN_LAT(MIN_LAT), .MAX_LAT(MAX_LAT)) dut (
    .clk(clk), .rst_n(rst_n),
    .trigger(trigger), .response(response), .eot(eot),
    .outstanding(outstanding), .oldest_age(oldest_age),
    .pass_pulse(pass_pulse), .fail_pulse(fail_pulse), .fail_code(fail_code),
    .n_triggers(n_triggers), .n_pass(n_pass), .n_late(n_late),
    .n_early(n_early), .n_spurious(n_spurious), .n_overflow(n_overflow),
    .n_dangling(n_dangling)
  );

  always #5 clk = ~clk;

  integer errors = 0, checks = 0;
  task check(input cond, input [1023:0] msg);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 25)
          $display("FAIL @%0t: %0s | out=%0d age=%0d pass=%b fail=%b code=%0d",
                   $time, msg, outstanding, oldest_age, pass_pulse,
                   fail_pulse, fail_code);
      end
    end
  endtask

  // ------------------------------------------------------------------
  // The shadow engine. Its own queue, its own ages.
  // ------------------------------------------------------------------
  reg [4:0] m_age [0:N_OBL-1];
  reg [2:0] m_cnt, m_fc;
  reg       m_pass, m_fail;
  integer   m_trg, m_p, m_late, m_early, m_spur, m_ovf, m_dng;

  integer seen_oc [0:39];       // (N_OBL+1) x 8 input combinations
  integer seen_ag [0:25];       // (MAX_LAT+1) x {response, none}
  integer n_oc, n_ag, n_steps;

  task model_reset;
    integer i;
    begin
      for (i = 0; i < N_OBL; i = i + 1) m_age[i] = 5'd0;
      m_cnt = 3'd0; m_fc = F_NONE; m_pass = 1'b0; m_fail = 1'b0;
      m_trg = 0; m_p = 0; m_late = 0; m_early = 0;
      m_spur = 0; m_ovf = 0; m_dng = 0;
      for (i = 0; i < 40; i = i + 1) seen_oc[i] = 0;
      for (i = 0; i < 26; i = i + 1) seen_ag[i] = 0;
      n_oc = 0; n_ag = 0; n_steps = 0;
    end
  endtask

  integer q, ia, ic;
  task step(input tg, input rs, input et);
    reg [4:0] na [0:N_OBL-1];
    reg [2:0] nc, nfc;
    reg np, nf, ret;
    begin
      trigger = tg; response = rs; eot = et;
      #1;

      check(outstanding === m_cnt, "outstanding disagrees with the shadow engine");
      check(oldest_age  === ((m_cnt == 3'd0) ? 5'd0 : m_age[0]),
            "oldest_age disagrees -- the queue is not being aged the way the model says");
      check(pass_pulse  === m_pass, "the pass pulse disagrees");
      check(fail_pulse  === m_fail, "the fail pulse disagrees");
      check(fail_code   === m_fc,   "fail_code disagrees");
      check(!(pass_pulse && fail_pulse),
            "an obligation was reported as both a pass and a failure");
      check(outstanding <= N_OBL[2:0],
            "more obligations are outstanding than the engine can track");
      check(oldest_age <= MAX_LAT[4:0],
            "an obligation aged past MAX_LAT without being reported -- the bound is not a bound");
      check(!((m_cnt == 3'd0) && (oldest_age != 5'd0)),
            "an age is being reported with nothing outstanding");

      // ---- ages are MONOTONIC down the queue: the oldest is at the front.
      // ---- If this is ever false the engine is not FIFO and every window
      // ---- decision after it is charged against the wrong obligation.
      for (q = 0; q + 1 < N_OBL; q = q + 1)
        if (q + 1 < m_cnt)
          check(m_age[q] >= m_age[q+1],
                "the obligation queue is out of order -- it is not FIFO, and the window is being applied to the wrong request");

      ic = m_cnt * 8 + (tg ? 4 : 0) + (rs ? 2 : 0) + (et ? 1 : 0);
      if (seen_oc[ic] == 0) begin seen_oc[ic] = 1; n_oc = n_oc + 1; end
      ia = ((m_cnt == 3'd0) ? 0 : m_age[0]) * 2 + (rs ? 1 : 0);
      if (seen_ag[ia] == 0) begin seen_ag[ia] = 1; n_ag = n_ag + 1; end
      n_steps = n_steps + 1;

      // ---- advance the shadow engine ----
      for (q = 0; q < N_OBL; q = q + 1) na[q] = m_age[q];
      nc = m_cnt; nfc = F_NONE; np = 1'b0; nf = 1'b0; ret = 1'b0;

      if (et) begin
        if (m_cnt != 3'd0) begin
          for (q = 0; q < N_OBL - 1; q = q + 1) na[q] = na[q+1];
          na[N_OBL-1] = 5'd0;
          nc = nc - 3'd1;
          ret = 1'b1; nf = 1'b1; nfc = F_DANGLING;
        end
      end else begin
        if (rs) begin
          if (m_cnt == 3'd0) begin
            nf = 1'b1; nfc = F_SPURIOUS;
          end else begin
            if (m_age[0] < MIN_LAT[4:0]) begin nf = 1'b1; nfc = F_EARLY; end
            else if (m_age[0] >= MAX_LAT[4:0]) begin nf = 1'b1; nfc = F_LATE; end
            else np = 1'b1;
            for (q = 0; q < N_OBL - 1; q = q + 1) na[q] = na[q+1];
            na[N_OBL-1] = 5'd0;
            nc = nc - 3'd1;
            ret = 1'b1;
          end
        end
        if (!ret && (m_cnt != 3'd0) && (m_age[0] >= MAX_LAT[4:0])) begin
          for (q = 0; q < N_OBL - 1; q = q + 1) na[q] = na[q+1];
          na[N_OBL-1] = 5'd0;
          nc = nc - 3'd1;
          ret = 1'b1; nf = 1'b1; nfc = F_LATE;
        end
        for (q = 0; q < N_OBL; q = q + 1)
          if ((q[2:0] < nc) && (na[q] < MAX_LAT[4:0])) na[q] = na[q] + 5'd1;
        if (tg) begin
          if (nc >= N_OBL[2:0]) begin nf = 1'b1; nfc = F_OVERFLOW; end
          else begin na[nc] = 5'd0; nc = nc + 3'd1; end
        end
      end

      for (q = 0; q < N_OBL; q = q + 1) m_age[q] = na[q];
      m_cnt = nc; m_fc = nfc; m_pass = np; m_fail = nf;
      if (tg && !et) m_trg = m_trg + 1;
      if (np) m_p = m_p + 1;
      if (nf) begin
        case (nfc)
          F_LATE:     m_late  = m_late + 1;
          F_EARLY:    m_early = m_early + 1;
          F_SPURIOUS: m_spur  = m_spur + 1;
          F_OVERFLOW: m_ovf   = m_ovf + 1;
          F_DANGLING: m_dng   = m_dng + 1;
          default: ;
        endcase
      end

      @(posedge clk); #1;
      trigger = 1'b0; response = 1'b0; eot = 1'b0;
    end
  endtask

  task idle(input integer n);
    integer i;
    begin for (i = 0; i < n; i = i + 1) step(1'b0, 1'b0, 1'b0); end
  endtask

  // Drain the queue the way the engine provides for -- by asserting eot
  // until it is empty -- never by forcing it.
  task drain;
    integer g;
    begin
      for (g = 0; g < N_OBL + 2; g = g + 1) step(1'b0, 1'b0, 1'b1);
      check(outstanding === 3'd0, "the drain did not empty the obligation queue");
      idle(2);
    end
  endtask

  integer before_p, before_f, before_l, k, lat, oc, cb, a;

  initial begin
    model_reset;
    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(posedge clk); #1;

    // ---- Phase A: the state after reset ----
    check(outstanding === 3'd0, "reset left obligations outstanding");
    check(oldest_age  === 5'd0, "reset left an age set");
    check(pass_pulse  === 1'b0, "reset asserted a pass");
    check(fail_pulse  === 1'b0, "reset asserted a failure");

    // ---- Phase B: BOTH EDGES OF THE WINDOW, at every latency. ----
    for (lat = 0; lat <= MAX_LAT + 2; lat = lat + 1) begin
      before_p = n_pass;
      before_f = n_early;
      before_l = n_late;
      step(1'b1, 1'b0, 1'b0);         // trigger
      idle(lat);                       // wait `lat` cycles
      step(1'b0, 1'b1, 1'b0);         // respond
      idle(1);
      if (lat < MIN_LAT) begin
        check(n_early == before_f + 1,
              "a response that arrived sooner than MIN_LAT was accepted -- it cannot have been a response to this request");
        check(n_pass == before_p,
              "an impossibly early response was counted as a pass");
      end else if (lat < MAX_LAT) begin
        check(n_pass == before_p + 1,
              "a response inside the window was not accepted");
        check(n_early == before_f && n_late == before_l,
              "a response inside the window was reported as a failure");
      end else begin
        check(n_late == before_l + 1,
              "a response later than MAX_LAT was accepted -- the bound is not a bound");
        check(n_pass == before_p,
              "a response later than MAX_LAT was counted as a pass");
      end
      drain;
    end

    // ---- Phase C: FIFO ORDER. Two obligations, different ages, one
    // ---- response. It must be charged against the OLDER one -- which is
    // ---- mature, so a correct engine PASSES. A LIFO engine charges the
    // ---- younger one, which is below MIN_LAT, and reports EARLY on
    // ---- perfectly good traffic.
    for (k = 0; k < 40; k = k + 1) begin
      before_p = n_pass;
      before_f = n_early;
      step(1'b1, 1'b0, 1'b0);          // obligation 1
      idle(MIN_LAT + 2);
      step(1'b1, 1'b0, 1'b0);          // obligation 2, much younger
      step(1'b0, 1'b1, 1'b0);          // one response
      idle(1);
      check(n_pass == before_p + 1,
            "a response was not charged against the OLDEST outstanding obligation -- the engine is not FIFO");
      check(n_early == before_f,
            "a response was charged against a younger obligation and reported as early");
      drain;
    end

    // ---- Phase D: OVERLAP. Fill the queue and discharge it in order. ----
    for (oc = 1; oc <= N_OBL; oc = oc + 1) begin
      before_p = n_pass;
      for (k = 0; k < oc; k = k + 1) step(1'b1, 1'b0, 1'b0);
      check(outstanding === oc[2:0],
            "the queue did not accept the obligations offered to it");
      idle(MIN_LAT + 1);
      for (k = 0; k < oc; k = k + 1) step(1'b0, 1'b1, 1'b0);
      idle(1);
      check(n_pass == before_p + oc,
            "one response discharged more than one obligation -- a single timer cannot tell overlapping obligations apart");
      check(outstanding === 3'd0, "obligations were left outstanding");
      drain;
    end

    // ---- Phase E: OVERFLOW is reported, never dropped. ----
    before_f = n_overflow;
    for (k = 0; k < N_OBL; k = k + 1) step(1'b1, 1'b0, 1'b0);
    check(outstanding === N_OBL[2:0], "the queue is not full when it should be");
    step(1'b1, 1'b0, 1'b0);
    idle(1);
    check(n_overflow == before_f + 1,
          "a trigger was silently dropped when the queue was full -- the engine stopped checking exactly when the design was busiest");
    check(outstanding === N_OBL[2:0],
          "the overflowing trigger displaced an obligation already being tracked");
    drain;

    // ---- Phase F: a response with nothing outstanding. ----
    before_f = n_spurious;
    step(1'b0, 1'b1, 1'b0);
    idle(1);
    check(n_spurious == before_f + 1,
          "a response arrived with nothing outstanding and was ignored -- the consequent can fire on its own, so a later real obligation could be discharged by something unrelated");

    // ---- Phase G: THE DRAIN. An unfinished obligation is not a pass. ----
    for (oc = 1; oc <= N_OBL; oc = oc + 1) begin
      before_f = n_dangling;
      before_p = n_pass;
      for (k = 0; k < oc; k = k + 1) step(1'b1, 1'b0, 1'b0);
      idle(2);
      for (k = 0; k < N_OBL + 2; k = k + 1) step(1'b0, 1'b0, 1'b1);
      check(n_dangling == before_f + oc,
            "obligations outstanding at end of test were not reported -- a run that stops while the property is still being tested has not verified it");
      check(n_pass == before_p,
            "an obligation that was never answered was counted as a pass");
      check(outstanding === 3'd0, "the drain left obligations outstanding");
      idle(2);
    end

    // ---- Phase H: EXHAUSTIVE. Every occupancy x every input combination. ----
    for (oc = 0; oc <= N_OBL; oc = oc + 1) begin
      for (cb = 0; cb < 8; cb = cb + 1) begin
        drain;
        for (k = 0; k < oc; k = k + 1) step(1'b1, 1'b0, 1'b0);
        check(outstanding === oc[2:0],
              "the sweep could not reach the occupancy it meant to reach");
        step(cb[2], cb[1], cb[0]);
        step(cb[2], cb[1], cb[0]);
      end
    end

    // ---- Phase I: every age of the oldest obligation, with and without a
    // ---- response, so the window is swept rather than sampled.
    for (a = 0; a <= MAX_LAT; a = a + 1) begin
      for (k = 0; k < 2; k = k + 1) begin
        drain;
        step(1'b1, 1'b0, 1'b0);
        idle(a);
        step(1'b0, k[0], 1'b0);
        idle(1);
      end
    end

    // ---- Phase J: random ----
    for (k = 0; k < 40000; k = k + 1)
      step(($unsigned($random) % 100) < 26,
           ($unsigned($random) % 100) < 24,
           ($unsigned($random) % 1000) < 6);

    // ---- Phase K: and clean traffic afterwards, so the engine is shown to
    // ---- still work rather than merely to have stopped.
    drain;
    before_p = n_pass;
    before_f = n_pass + n_late + n_early + n_spurious + n_overflow + n_dangling;
    for (k = 0; k < 300; k = k + 1) begin
      step(1'b1, 1'b0, 1'b0);
      idle(MIN_LAT + 1);
      step(1'b0, 1'b1, 1'b0);
      idle(1);
    end
    check(n_pass == before_p + 300,
          "the engine stopped accepting clean traffic after the random phase");

    // ---- Final agreement ----
    check(n_triggers === m_trg[31:0],  "n_triggers disagrees with the model");
    check(n_pass     === m_p[31:0],    "n_pass disagrees with the model");
    check(n_late     === m_late[31:0], "n_late disagrees");
    check(n_early    === m_early[31:0],"n_early disagrees");
    check(n_spurious === m_spur[31:0], "n_spurious disagrees");
    check(n_overflow === m_ovf[31:0],  "n_overflow disagrees");
    check(n_dangling === m_dng[31:0],  "n_dangling disagrees");

    check(n_oc == 40, "not every queue occupancy was crossed with every input combination");
    check(n_ag == 26, "not every age of the oldest obligation was seen with and without a response");
    check(n_pass     > 32'd0, "no obligation was ever discharged cleanly");
    check(n_late     > 32'd0, "the MAX_LAT bound was never exercised");
    check(n_early    > 32'd0, "the MIN_LAT bound was never exercised");
    check(n_spurious > 32'd0, "a response with nothing outstanding was never seen");
    check(n_overflow > 32'd0, "the tracking capacity was never exceeded");
    check(n_dangling > 32'd0, "the end-of-test drain was never exercised");

    $display("REACH occupancy-x-input=%0d/40 age-x-response=%0d/26 steps=%0d",
             n_oc, n_ag, n_steps);
    $display("COUNTERS triggers=%0d pass=%0d late=%0d early=%0d spurious=%0d overflow=%0d dangling=%0d",
             n_triggers, n_pass, n_late, n_early, n_spurious, n_overflow, n_dangling);
    $display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
    $finish;
  end
endmodule

12.2 SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Testbench for usb_assert_engine (SystemVerilog).
//
// WHAT IS EXHAUSTIVE HERE
//
//   1. Every occupancy of the obligation queue, 0 to N_OBL, crossed with
//      all eight combinations of {trigger, response, eot} = 40 pairs, each
//      reached by real triggers and real responses.
//
//   2. Every age of the oldest obligation, 0 to MAX_LAT, crossed with
//      "a response arrived" and "none did" = 26 pairs.
//
// AND THE THINGS THAT ARE NOT SWEEPS
//
//   BOTH EDGES OF THE WINDOW. A response at MAX_LAT-1 must PASS and one at
//   MAX_LAT must FAIL. Checking only the second accepts an engine with no
//   window at all; checking only the first accepts one that never fires.
//
//   FIFO ORDER. Two obligations of different ages, one response: it must be
//   charged against the OLDER. A LIFO engine charges the younger, which is
//   below MIN_LAT, so the discriminator is that a correct engine PASSES here
//   and a LIFO one reports an EARLY failure on perfectly good traffic.
//
//   THE DRAIN. Triggers, then end-of-test, and every survivor must be
//   reported. An engine that simply stops has not passed -- it has run out
//   of time while the property was still being tested.
`timescale 1ns/1ps
module tb_ae_sv;
  import usb_ae_pkg::*;

  localparam int N_OBL   = 4;
  localparam int MIN_LAT = 2;
  localparam int MAX_LAT = 12;

  logic clk = 1'b0, rst_n = 1'b0;
  logic trigger = 1'b0, response = 1'b0, eot = 1'b0;

  logic [2:0] outstanding;
  fail_e      fail_code;
  logic [4:0] oldest_age;
  logic pass_pulse, fail_pulse;
  logic [31:0] n_triggers, n_pass, n_late, n_early, n_spurious,
               n_overflow, n_dangling;

  usb_assert_engine #(.N_OBL(N_OBL), .MIN_LAT(MIN_LAT), .MAX_LAT(MAX_LAT))
    dut (.*);

  always #5 clk = ~clk;

  int errors = 0, checks = 0;
  task automatic check(input logic cond, input string msg);
    checks++;
    if (!cond) begin
      errors++;
      if (errors <= 25)
        $display("FAIL @%0t: %0s | out=%0d age=%0d pass=%b fail=%b code=%0d",
                 $time, msg, outstanding, oldest_age, pass_pulse,
                 fail_pulse, fail_code);
    end
  endtask

  // ------------------------------------------------------------------
  // The shadow engine. Its own queue, its own ages.
  // ------------------------------------------------------------------
  logic [4:0] m_age [N_OBL];
  logic [2:0] m_cnt;
  fail_e      m_fc;
  logic       m_pass, m_fail;
  int         m_trg, m_p, m_late, m_early, m_spur, m_ovf, m_dng;

  int seen_oc [40];             // (N_OBL+1) x 8 input combinations
  int seen_ag [26];             // (MAX_LAT+1) x {response, none}
  int n_oc, n_ag, n_steps;

  task automatic model_reset();
    foreach (m_age[i]) m_age[i] = '0;
    m_cnt = '0; m_fc = F_NONE; m_pass = 1'b0; m_fail = 1'b0;
    m_trg = 0; m_p = 0; m_late = 0; m_early = 0;
    m_spur = 0; m_ovf = 0; m_dng = 0;
    foreach (seen_oc[i]) seen_oc[i] = 0;
    foreach (seen_ag[i]) seen_ag[i] = 0;
    n_oc = 0; n_ag = 0; n_steps = 0;
  endtask

  int q, ia, ic;
  task automatic step(input logic tg, input logic rs, input logic et);
    logic [4:0] na [N_OBL];
    logic [2:0] nc;
    fail_e      nfc;
    logic np, nf, ret;
    begin
      trigger = tg; response = rs; eot = et;
      #1;

      check(outstanding === m_cnt, "outstanding disagrees with the shadow engine");
      check(oldest_age  === ((m_cnt == 3'd0) ? 5'd0 : m_age[0]),
            "oldest_age disagrees -- the queue is not being aged the way the model says");
      check(pass_pulse  === m_pass, "the pass pulse disagrees");
      check(fail_pulse  === m_fail, "the fail pulse disagrees");
      check(fail_code   === m_fc,   "fail_code disagrees");
      check(!(pass_pulse && fail_pulse),
            "an obligation was reported as both a pass and a failure");
      check(outstanding <= 3'(N_OBL),
            "more obligations are outstanding than the engine can track");
      check(oldest_age <= 5'(MAX_LAT),
            "an obligation aged past MAX_LAT without being reported -- the bound is not a bound");
      check(!((m_cnt == 3'd0) && (oldest_age != 5'd0)),
            "an age is being reported with nothing outstanding");

      // ---- ages are MONOTONIC down the queue: the oldest is at the front.
      // ---- If this is ever false the engine is not FIFO and every window
      // ---- decision after it is charged against the wrong obligation.
      for (q = 0; q + 1 < N_OBL; q++)
        if (q + 1 < m_cnt)
          check(m_age[q] >= m_age[q+1],
                "the obligation queue is out of order -- it is not FIFO, and the window is being applied to the wrong request");

      ic = int'(m_cnt) * 8 + (tg ? 4 : 0) + (rs ? 2 : 0) + (et ? 1 : 0);
      if (seen_oc[ic] == 0) begin seen_oc[ic] = 1; n_oc = n_oc + 1; end
      ia = int'((m_cnt == 3'd0) ? 5'd0 : m_age[0]) * 2 + (rs ? 1 : 0);
      if (seen_ag[ia] == 0) begin seen_ag[ia] = 1; n_ag = n_ag + 1; end
      n_steps = n_steps + 1;

      // ---- advance the shadow engine ----
      for (q = 0; q < N_OBL; q++) na[q] = m_age[q];
      nc = m_cnt; nfc = F_NONE; np = 1'b0; nf = 1'b0; ret = 1'b0;

      if (et) begin
        if (m_cnt != 3'd0) begin
          for (q = 0; q < N_OBL - 1; q++) na[q] = na[q+1];
          na[N_OBL-1] = 5'd0;
          nc = nc - 3'd1;
          ret = 1'b1; nf = 1'b1; nfc = F_DANGLING;
        end
      end else begin
        if (rs) begin
          if (m_cnt == 3'd0) begin
            nf = 1'b1; nfc = F_SPURIOUS;
          end else begin
            if (m_age[0] < 5'(MIN_LAT)) begin nf = 1'b1; nfc = F_EARLY; end
            else if (m_age[0] >= 5'(MAX_LAT)) begin nf = 1'b1; nfc = F_LATE; end
            else np = 1'b1;
            for (q = 0; q < N_OBL - 1; q++) na[q] = na[q+1];
            na[N_OBL-1] = 5'd0;
            nc = nc - 3'd1;
            ret = 1'b1;
          end
        end
        if (!ret && (m_cnt != 3'd0) && (m_age[0] >= 5'(MAX_LAT))) begin
          for (q = 0; q < N_OBL - 1; q++) na[q] = na[q+1];
          na[N_OBL-1] = 5'd0;
          nc = nc - 3'd1;
          ret = 1'b1; nf = 1'b1; nfc = F_LATE;
        end
        for (q = 0; q < N_OBL; q++)
          if ((3'(q) < nc) && (na[q] < 5'(MAX_LAT))) na[q] = na[q] + 5'd1;
        if (tg) begin
          if (nc >= 3'(N_OBL)) begin nf = 1'b1; nfc = F_OVERFLOW; end
          else begin na[nc] = 5'd0; nc = nc + 3'd1; end
        end
      end

      for (q = 0; q < N_OBL; q++) m_age[q] = na[q];
      m_cnt = nc; m_fc = nfc; m_pass = np; m_fail = nf;
      if (tg && !et) m_trg = m_trg + 1;
      if (np) m_p = m_p + 1;
      if (nf) begin
        case (nfc)
          F_LATE:     m_late  = m_late + 1;
          F_EARLY:    m_early = m_early + 1;
          F_SPURIOUS: m_spur  = m_spur + 1;
          F_OVERFLOW: m_ovf   = m_ovf + 1;
          F_DANGLING: m_dng   = m_dng + 1;
          default: ;
        endcase
      end

      @(posedge clk); #1;
      trigger = 1'b0; response = 1'b0; eot = 1'b0;
    end
  endtask

  task automatic idle(input int n);
    repeat (n) step(1'b0, 1'b0, 1'b0);
  endtask

  // Drain the queue the way the engine provides for -- by asserting eot
  // until it is empty -- never by forcing it.
  task automatic drain();
    repeat (N_OBL + 2) step(1'b0, 1'b0, 1'b1);
    check(outstanding === 3'd0, "the drain did not empty the obligation queue");
    idle(2);
  endtask

  int before_p, before_f, before_l, k, lat, oc, cb, a;

  initial begin
    model_reset();
    repeat (3) @(posedge clk);
    rst_n = 1'b1;
    @(posedge clk); #1;

    // ---- Phase A: the state after reset ----
    check(outstanding === 3'd0, "reset left obligations outstanding");
    check(oldest_age  === 5'd0, "reset left an age set");
    check(pass_pulse  === 1'b0, "reset asserted a pass");
    check(fail_pulse  === 1'b0, "reset asserted a failure");

    // ---- Phase B: BOTH EDGES OF THE WINDOW, at every latency. ----
    for (lat = 0; lat <= MAX_LAT + 2; lat++) begin
      before_p = n_pass;
      before_f = n_early;
      before_l = n_late;
      step(1'b1, 1'b0, 1'b0);         // trigger
      idle(lat);                       // wait `lat` cycles
      step(1'b0, 1'b1, 1'b0);         // respond
      idle(1);
      if (lat < MIN_LAT) begin
        check(n_early == before_f + 1,
              "a response that arrived sooner than MIN_LAT was accepted -- it cannot have been a response to this request");
        check(n_pass == before_p,
              "an impossibly early response was counted as a pass");
      end else if (lat < MAX_LAT) begin
        check(n_pass == before_p + 1,
              "a response inside the window was not accepted");
        check(n_early == before_f && n_late == before_l,
              "a response inside the window was reported as a failure");
      end else begin
        check(n_late == before_l + 1,
              "a response later than MAX_LAT was accepted -- the bound is not a bound");
        check(n_pass == before_p,
              "a response later than MAX_LAT was counted as a pass");
      end
      drain();
    end

    // ---- Phase C: FIFO ORDER. Two obligations, different ages, one
    // ---- response. It must be charged against the OLDER one -- which is
    // ---- mature, so a correct engine PASSES. A LIFO engine charges the
    // ---- younger one, which is below MIN_LAT, and reports EARLY on
    // ---- perfectly good traffic.
    for (k = 0; k < 40; k++) begin
      before_p = n_pass;
      before_f = n_early;
      step(1'b1, 1'b0, 1'b0);          // obligation 1
      idle(MIN_LAT + 2);
      step(1'b1, 1'b0, 1'b0);          // obligation 2, much younger
      step(1'b0, 1'b1, 1'b0);          // one response
      idle(1);
      check(n_pass == before_p + 1,
            "a response was not charged against the OLDEST outstanding obligation -- the engine is not FIFO");
      check(n_early == before_f,
            "a response was charged against a younger obligation and reported as early");
      drain();
    end

    // ---- Phase D: OVERLAP. Fill the queue and discharge it in order. ----
    for (oc = 1; oc <= N_OBL; oc++) begin
      before_p = n_pass;
      for (k = 0; k < oc; k++) step(1'b1, 1'b0, 1'b0);
      check(outstanding === 3'(oc),
            "the queue did not accept the obligations offered to it");
      idle(MIN_LAT + 1);
      for (k = 0; k < oc; k++) step(1'b0, 1'b1, 1'b0);
      idle(1);
      check(n_pass == before_p + oc,
            "one response discharged more than one obligation -- a single timer cannot tell overlapping obligations apart");
      check(outstanding === 3'd0, "obligations were left outstanding");
      drain();
    end

    // ---- Phase E: OVERFLOW is reported, never dropped. ----
    before_f = n_overflow;
    for (k = 0; k < N_OBL; k++) step(1'b1, 1'b0, 1'b0);
    check(outstanding === 3'(N_OBL), "the queue is not full when it should be");
    step(1'b1, 1'b0, 1'b0);
    idle(1);
    check(n_overflow == before_f + 1,
          "a trigger was silently dropped when the queue was full -- the engine stopped checking exactly when the design was busiest");
    check(outstanding === 3'(N_OBL),
          "the overflowing trigger displaced an obligation already being tracked");
    drain();

    // ---- Phase F: a response with nothing outstanding. ----
    before_f = n_spurious;
    step(1'b0, 1'b1, 1'b0);
    idle(1);
    check(n_spurious == before_f + 1,
          "a response arrived with nothing outstanding and was ignored -- the consequent can fire on its own, so a later real obligation could be discharged by something unrelated");

    // ---- Phase G: THE DRAIN. An unfinished obligation is not a pass. ----
    for (oc = 1; oc <= N_OBL; oc++) begin
      before_f = n_dangling;
      before_p = n_pass;
      for (k = 0; k < oc; k++) step(1'b1, 1'b0, 1'b0);
      idle(2);
      for (k = 0; k < N_OBL + 2; k++) step(1'b0, 1'b0, 1'b1);
      check(n_dangling == before_f + oc,
            "obligations outstanding at end of test were not reported -- a run that stops while the property is still being tested has not verified it");
      check(n_pass == before_p,
            "an obligation that was never answered was counted as a pass");
      check(outstanding === 3'd0, "the drain left obligations outstanding");
      idle(2);
    end

    // ---- Phase H: EXHAUSTIVE. Every occupancy x every input combination. ----
    for (oc = 0; oc <= N_OBL; oc++) begin
      for (cb = 0; cb < 8; cb++) begin
        drain();
        for (k = 0; k < oc; k++) step(1'b1, 1'b0, 1'b0);
        check(outstanding === 3'(oc),
              "the sweep could not reach the occupancy it meant to reach");
        step(1'(cb[2]), 1'(cb[1]), 1'(cb[0]));
        step(1'(cb[2]), 1'(cb[1]), 1'(cb[0]));
      end
    end

    // ---- Phase I: every age of the oldest obligation, with and without a
    // ---- response, so the window is swept rather than sampled.
    for (a = 0; a <= MAX_LAT; a++) begin
      for (k = 0; k < 2; k++) begin
        drain();
        step(1'b1, 1'b0, 1'b0);
        idle(a);
        step(1'b0, 1'(k[0]), 1'b0);
        idle(1);
      end
    end

    // ---- Phase J: random ----
    for (k = 0; k < 40000; k++)
      step($urandom_range(0,99) < 26,
           $urandom_range(0,99) < 24,
           $urandom_range(0,999) < 6);

    // ---- Phase K: and clean traffic afterwards, so the engine is shown to
    // ---- still work rather than merely to have stopped.
    drain();
    before_p = n_pass;
    before_f = n_pass + n_late + n_early + n_spurious + n_overflow + n_dangling;
    for (k = 0; k < 300; k++) begin
      step(1'b1, 1'b0, 1'b0);
      idle(MIN_LAT + 1);
      step(1'b0, 1'b1, 1'b0);
      idle(1);
    end
    check(n_pass == before_p + 300,
          "the engine stopped accepting clean traffic after the random phase");

    // ---- Final agreement ----
    check(n_triggers === 32'(m_trg),  "n_triggers disagrees with the model");
    check(n_pass     === 32'(m_p),    "n_pass disagrees with the model");
    check(n_late     === 32'(m_late), "n_late disagrees");
    check(n_early    === 32'(m_early),"n_early disagrees");
    check(n_spurious === 32'(m_spur), "n_spurious disagrees");
    check(n_overflow === 32'(m_ovf),  "n_overflow disagrees");
    check(n_dangling === 32'(m_dng),  "n_dangling disagrees");

    check(n_oc == 40, "not every queue occupancy was crossed with every input combination");
    check(n_ag == 26, "not every age of the oldest obligation was seen with and without a response");
    check(n_pass     > 32'd0, "no obligation was ever discharged cleanly");
    check(n_late     > 32'd0, "the MAX_LAT bound was never exercised");
    check(n_early    > 32'd0, "the MIN_LAT bound was never exercised");
    check(n_spurious > 32'd0, "a response with nothing outstanding was never seen");
    check(n_overflow > 32'd0, "the tracking capacity was never exceeded");
    check(n_dangling > 32'd0, "the end-of-test drain was never exercised");

    $display("REACH occupancy-x-input=%0d/40 age-x-response=%0d/26 steps=%0d",
             n_oc, n_ag, n_steps);
    $display("COUNTERS triggers=%0d pass=%0d late=%0d early=%0d spurious=%0d overflow=%0d dangling=%0d",
             n_triggers, n_pass, n_late, n_early, n_spurious, n_overflow, n_dangling);
    $display("%0s: %0d errors in %0d checks", (errors==0)?"PASS":"FAIL", errors, checks);
    $finish;
  end
endmodule

12.3 VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- Testbench for usb_assert_engine (VHDL-2008).
--
-- WHAT IS EXHAUSTIVE HERE
--
--   1. Every occupancy of the obligation queue, 0 to N_OBL, crossed with
--      all eight combinations of (trigger, response, eot) = 40 pairs, each
--      reached by real triggers and real responses.
--
--   2. Every age of the oldest obligation, 0 to MAX_LAT, crossed with
--      "a response arrived" and "none did" = 26 pairs.
--
-- AND THE THINGS THAT ARE NOT SWEEPS
--
--   BOTH EDGES OF THE WINDOW. A response at MAX_LAT-1 must PASS and one at
--   MAX_LAT must FAIL. Checking only the second accepts an engine with no
--   window at all; checking only the first accepts one that never fires.
--
--   FIFO ORDER. Two obligations of different ages, one response: it must be
--   charged against the OLDER. A LIFO engine charges the younger, which is
--   below MIN_LAT, so the discriminator is that a correct engine PASSES here
--   and a LIFO one reports an EARLY failure on perfectly good traffic.
--
--   THE DRAIN. Triggers, then end-of-test, and every survivor must be
--   reported. An engine that simply stops has not passed -- it has run out
--   of time while the property was still being tested.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;
use work.usb_ae_pkg.all;

entity tb_ae_vhdl is
end entity tb_ae_vhdl;

architecture sim of tb_ae_vhdl is

  constant N_OBL   : integer := 4;
  constant MIN_LAT : integer := 2;
  constant MAX_LAT : integer := 12;

  signal clk   : std_logic := '0';
  signal rst_n : std_logic := '0';
  signal done  : boolean   := false;

  signal trigger, response, eot : std_logic := '0';

  signal outstanding, fail_code : std_logic_vector(2 downto 0);
  signal oldest_age : std_logic_vector(4 downto 0);
  signal pass_pulse, fail_pulse : std_logic;
  signal n_triggers, n_pass, n_late, n_early : std_logic_vector(31 downto 0);
  signal n_spurious, n_overflow, n_dangling  : std_logic_vector(31 downto 0);

begin

  dut : entity work.usb_assert_engine
    generic map (N_OBL => N_OBL, MIN_LAT => MIN_LAT, MAX_LAT => MAX_LAT)
    port map (
      clk => clk, rst_n => rst_n,
      trigger => trigger, response => response, eot => eot,
      outstanding => outstanding, oldest_age => oldest_age,
      pass_pulse => pass_pulse, fail_pulse => fail_pulse,
      fail_code => fail_code,
      n_triggers => n_triggers, n_pass => n_pass, n_late => n_late,
      n_early => n_early, n_spurious => n_spurious,
      n_overflow => n_overflow, n_dangling => n_dangling
    );

  clk <= (not clk) after 5 ns when not done else '0';

  stim : process
    type age_arr is array (0 to N_OBL-1) of unsigned(4 downto 0);
    type oc_arr  is array (0 to 39) of integer;
    type ag_arr  is array (0 to 25) of integer;

    variable errors, checks : integer := 0;

    -- ---- The shadow engine. Its own queue, its own ages. ----
    variable m_age : age_arr := (others => (others => '0'));
    variable m_cnt : unsigned(2 downto 0) := (others => '0');
    variable m_fc  : fail_t := F_NONE;
    variable m_pass, m_fail : std_logic := '0';
    variable m_trg, m_p, m_late, m_early : integer := 0;
    variable m_spur, m_ovf, m_dng : integer := 0;

    variable seen_oc : oc_arr := (others => 0);
    variable seen_ag : ag_arr := (others => 0);
    variable n_oc, n_ag, n_steps : integer := 0;

    -- A deterministic LFSR, so a rerun reproduces exactly the same traffic.
    variable lfsr : unsigned(31 downto 0) := x"0BADC0DE";

    impure function rnd32 return unsigned is
    begin
      lfsr := lfsr(30 downto 0) &
              (lfsr(31) xor lfsr(21) xor lfsr(1) xor lfsr(0));
      return lfsr;
    end function;

    -- Only the low 30 bits are converted: a full 32-bit unsigned does not
    -- fit in VHDL's INTEGER, and to_integer aborts the run rather than
    -- wrapping.
    impure function rnd_nat return integer is
      variable u : unsigned(31 downto 0);
    begin
      u := rnd32;
      return to_integer(u(29 downto 0));
    end function;

    impure function rnd_lt (pct, base : integer) return std_logic is
    begin
      if (rnd_nat mod base) < pct then return '1'; else return '0'; end if;
    end function;

    procedure chk (cond : boolean; msg : string) is
    begin
      checks := checks + 1;
      if not cond then
        errors := errors + 1;
        if errors <= 25 then
          report "FAIL: " & msg &
                 " | out=" & integer'image(to_integer(m_cnt)) &
                 " code=" & integer'image(fail_t'pos(m_fc))
            severity note;
        end if;
      end if;
    end procedure;

    procedure step (tg, rs, et : std_logic) is
      variable na  : age_arr;
      variable nc  : unsigned(2 downto 0);
      variable nfc : fail_t;
      variable np, nf, ret : std_logic;
      variable ic, ia : integer;

      procedure retire_oldest is
      begin
        for k in 0 to N_OBL - 2 loop
          na(k) := na(k+1);
        end loop;
        na(N_OBL-1) := (others => '0');
        nc := nc - 1;
      end procedure;
    begin
      trigger <= tg; response <= rs; eot <= et;
      wait for 1 ns;

      chk(unsigned(outstanding) = m_cnt,
          "outstanding disagrees with the shadow engine");
      if m_cnt = 0 then
        chk(unsigned(oldest_age) = 0,
            "oldest_age disagrees -- the queue is not being aged the way the model says");
      else
        chk(unsigned(oldest_age) = m_age(0),
            "oldest_age disagrees -- the queue is not being aged the way the model says");
      end if;
      chk(pass_pulse = m_pass, "the pass pulse disagrees");
      chk(fail_pulse = m_fail, "the fail pulse disagrees");
      chk(fail_code  = fc_code(m_fc), "fail_code disagrees");
      chk(not (pass_pulse = '1' and fail_pulse = '1'),
          "an obligation was reported as both a pass and a failure");
      chk(unsigned(outstanding) <= to_unsigned(N_OBL, 3),
          "more obligations are outstanding than the engine can track");
      chk(unsigned(oldest_age) <= to_unsigned(MAX_LAT, 5),
          "an obligation aged past MAX_LAT without being reported -- the bound is not a bound");
      chk(not (m_cnt = 0 and unsigned(oldest_age) /= 0),
          "an age is being reported with nothing outstanding");

      -- ---- ages are MONOTONIC down the queue: the oldest is at the front.
      -- ---- If this is ever false the engine is not FIFO and every window
      -- ---- decision after it is charged against the wrong obligation.
      for q in 0 to N_OBL - 2 loop
        if to_unsigned(q + 1, 3) < m_cnt then
          chk(m_age(q) >= m_age(q+1),
              "the obligation queue is out of order -- it is not FIFO, and the window is being applied to the wrong request");
        end if;
      end loop;

      ic := to_integer(m_cnt) * 8;
      if tg = '1' then ic := ic + 4; end if;
      if rs = '1' then ic := ic + 2; end if;
      if et = '1' then ic := ic + 1; end if;
      if seen_oc(ic) = 0 then seen_oc(ic) := 1; n_oc := n_oc + 1; end if;

      if m_cnt = 0 then ia := 0; else ia := to_integer(m_age(0)); end if;
      ia := ia * 2;
      if rs = '1' then ia := ia + 1; end if;
      if seen_ag(ia) = 0 then seen_ag(ia) := 1; n_ag := n_ag + 1; end if;
      n_steps := n_steps + 1;

      -- ---- advance the shadow engine ----
      na := m_age; nc := m_cnt; nfc := F_NONE;
      np := '0'; nf := '0'; ret := '0';

      if et = '1' then
        if m_cnt /= 0 then
          retire_oldest;
          ret := '1'; nf := '1'; nfc := F_DANGLING;
        end if;
      else
        if rs = '1' then
          if m_cnt = 0 then
            nf := '1'; nfc := F_SPURIOUS;
          else
            if m_age(0) < to_unsigned(MIN_LAT, 5) then
              nf := '1'; nfc := F_EARLY;
            elsif m_age(0) >= to_unsigned(MAX_LAT, 5) then
              nf := '1'; nfc := F_LATE;
            else
              np := '1';
            end if;
            retire_oldest;
            ret := '1';
          end if;
        end if;
        if ret = '0' and m_cnt /= 0
           and m_age(0) >= to_unsigned(MAX_LAT, 5) then
          retire_oldest;
          ret := '1'; nf := '1'; nfc := F_LATE;
        end if;
        for q in 0 to N_OBL - 1 loop
          if to_unsigned(q, 3) < nc and na(q) < to_unsigned(MAX_LAT, 5) then
            na(q) := na(q) + 1;
          end if;
        end loop;
        if tg = '1' then
          if nc >= to_unsigned(N_OBL, 3) then
            nf := '1'; nfc := F_OVERFLOW;
          else
            na(to_integer(nc)) := (others => '0');
            nc := nc + 1;
          end if;
        end if;
      end if;

      m_age := na; m_cnt := nc; m_fc := nfc; m_pass := np; m_fail := nf;
      if tg = '1' and et = '0' then m_trg := m_trg + 1; end if;
      if np = '1' then m_p := m_p + 1; end if;
      if nf = '1' then
        case nfc is
          when F_LATE     => m_late  := m_late + 1;
          when F_EARLY    => m_early := m_early + 1;
          when F_SPURIOUS => m_spur  := m_spur + 1;
          when F_OVERFLOW => m_ovf   := m_ovf + 1;
          when F_DANGLING => m_dng   := m_dng + 1;
          when others     => null;
        end case;
      end if;

      wait until rising_edge(clk);
      wait for 1 ns;
      trigger <= '0'; response <= '0'; eot <= '0';
    end procedure;

    procedure idle (n : integer) is
    begin
      for i in 1 to n loop
        step('0', '0', '0');
      end loop;
    end procedure;

    -- Drain the queue the way the engine provides for -- by asserting eot
    -- until it is empty -- never by forcing it.
    procedure drain is
    begin
      for g in 1 to N_OBL + 2 loop
        step('0', '0', '1');
      end loop;
      chk(unsigned(outstanding) = 0,
          "the drain did not empty the obligation queue");
      idle(2);
    end procedure;

    variable before_p, before_f, before_l : integer := 0;
    variable tg_v, rs_v, et_v : std_logic;
    variable ln : line;

  begin
    wait for 33 ns;
    rst_n <= '1';
    wait until rising_edge(clk);
    wait for 1 ns;

    -- ---- Phase A: the state after reset ----
    chk(unsigned(outstanding) = 0, "reset left obligations outstanding");
    chk(unsigned(oldest_age) = 0,  "reset left an age set");
    chk(pass_pulse = '0', "reset asserted a pass");
    chk(fail_pulse = '0', "reset asserted a failure");

    -- ---- Phase B: BOTH EDGES OF THE WINDOW, at every latency. ----
    for lat in 0 to MAX_LAT + 2 loop
      before_p := to_integer(unsigned(n_pass));
      before_f := to_integer(unsigned(n_early));
      before_l := to_integer(unsigned(n_late));
      step('1', '0', '0');            -- trigger
      idle(lat);                       -- wait `lat` cycles
      step('0', '1', '0');            -- respond
      idle(1);
      if lat < MIN_LAT then
        chk(to_integer(unsigned(n_early)) = before_f + 1,
            "a response that arrived sooner than MIN_LAT was accepted -- it cannot have been a response to this request");
        chk(to_integer(unsigned(n_pass)) = before_p,
            "an impossibly early response was counted as a pass");
      elsif lat < MAX_LAT then
        chk(to_integer(unsigned(n_pass)) = before_p + 1,
            "a response inside the window was not accepted");
        chk(to_integer(unsigned(n_early)) = before_f
            and to_integer(unsigned(n_late)) = before_l,
            "a response inside the window was reported as a failure");
      else
        chk(to_integer(unsigned(n_late)) = before_l + 1,
            "a response later than MAX_LAT was accepted -- the bound is not a bound");
        chk(to_integer(unsigned(n_pass)) = before_p,
            "a response later than MAX_LAT was counted as a pass");
      end if;
      drain;
    end loop;

    -- ---- Phase C: FIFO ORDER. Two obligations, different ages, one
    -- ---- response. It must be charged against the OLDER one -- which is
    -- ---- mature, so a correct engine PASSES. A LIFO engine charges the
    -- ---- younger one, which is below MIN_LAT, and reports EARLY on
    -- ---- perfectly good traffic.
    for k in 0 to 39 loop
      before_p := to_integer(unsigned(n_pass));
      before_f := to_integer(unsigned(n_early));
      step('1', '0', '0');             -- obligation 1
      idle(MIN_LAT + 2);
      step('1', '0', '0');             -- obligation 2, much younger
      step('0', '1', '0');             -- one response
      idle(1);
      chk(to_integer(unsigned(n_pass)) = before_p + 1,
          "a response was not charged against the OLDEST outstanding obligation -- the engine is not FIFO");
      chk(to_integer(unsigned(n_early)) = before_f,
          "a response was charged against a younger obligation and reported as early");
      drain;
    end loop;

    -- ---- Phase D: OVERLAP. Fill the queue and discharge it in order. ----
    for oc in 1 to N_OBL loop
      before_p := to_integer(unsigned(n_pass));
      for k in 1 to oc loop step('1', '0', '0'); end loop;
      chk(unsigned(outstanding) = to_unsigned(oc, 3),
          "the queue did not accept the obligations offered to it");
      idle(MIN_LAT + 1);
      for k in 1 to oc loop step('0', '1', '0'); end loop;
      idle(1);
      chk(to_integer(unsigned(n_pass)) = before_p + oc,
          "one response discharged more than one obligation -- a single timer cannot tell overlapping obligations apart");
      chk(unsigned(outstanding) = 0, "obligations were left outstanding");
      drain;
    end loop;

    -- ---- Phase E: OVERFLOW is reported, never dropped. ----
    before_f := to_integer(unsigned(n_overflow));
    for k in 1 to N_OBL loop step('1', '0', '0'); end loop;
    chk(unsigned(outstanding) = to_unsigned(N_OBL, 3),
        "the queue is not full when it should be");
    step('1', '0', '0');
    idle(1);
    chk(to_integer(unsigned(n_overflow)) = before_f + 1,
        "a trigger was silently dropped when the queue was full -- the engine stopped checking exactly when the design was busiest");
    chk(unsigned(outstanding) = to_unsigned(N_OBL, 3),
        "the overflowing trigger displaced an obligation already being tracked");
    drain;

    -- ---- Phase F: a response with nothing outstanding. ----
    before_f := to_integer(unsigned(n_spurious));
    step('0', '1', '0');
    idle(1);
    chk(to_integer(unsigned(n_spurious)) = before_f + 1,
        "a response arrived with nothing outstanding and was ignored -- the consequent can fire on its own, so a later real obligation could be discharged by something unrelated");

    -- ---- Phase G: THE DRAIN. An unfinished obligation is not a pass. ----
    for oc in 1 to N_OBL loop
      before_f := to_integer(unsigned(n_dangling));
      before_p := to_integer(unsigned(n_pass));
      for k in 1 to oc loop step('1', '0', '0'); end loop;
      idle(2);
      for k in 1 to N_OBL + 2 loop step('0', '0', '1'); end loop;
      chk(to_integer(unsigned(n_dangling)) = before_f + oc,
          "obligations outstanding at end of test were not reported -- a run that stops while the property is still being tested has not verified it");
      chk(to_integer(unsigned(n_pass)) = before_p,
          "an obligation that was never answered was counted as a pass");
      chk(unsigned(outstanding) = 0, "the drain left obligations outstanding");
      idle(2);
    end loop;

    -- ---- Phase H: EXHAUSTIVE. Every occupancy x every input combination. --
    for oc in 0 to N_OBL loop
      for cb in 0 to 7 loop
        drain;
        for k in 1 to oc loop step('1', '0', '0'); end loop;
        chk(unsigned(outstanding) = to_unsigned(oc, 3),
            "the sweep could not reach the occupancy it meant to reach");
        if (cb / 4) mod 2 = 1 then tg_v := '1'; else tg_v := '0'; end if;
        if (cb / 2) mod 2 = 1 then rs_v := '1'; else rs_v := '0'; end if;
        if  cb      mod 2 = 1 then et_v := '1'; else et_v := '0'; end if;
        step(tg_v, rs_v, et_v);
        step(tg_v, rs_v, et_v);
      end loop;
    end loop;

    -- ---- Phase I: every age of the oldest obligation, with and without a
    -- ---- response, so the window is swept rather than sampled.
    for a in 0 to MAX_LAT loop
      for k in 0 to 1 loop
        drain;
        step('1', '0', '0');
        idle(a);
        if k = 1 then step('0', '1', '0'); else step('0', '0', '0'); end if;
        idle(1);
      end loop;
    end loop;

    -- ---- Phase J: random ----
    for k in 0 to 39999 loop
      tg_v := rnd_lt(26, 100);
      rs_v := rnd_lt(24, 100);
      et_v := rnd_lt(6, 1000);
      step(tg_v, rs_v, et_v);
    end loop;

    -- ---- Phase K: and clean traffic afterwards, so the engine is shown to
    -- ---- still work rather than merely to have stopped.
    drain;
    before_p := to_integer(unsigned(n_pass));
    for k in 1 to 300 loop
      step('1', '0', '0');
      idle(MIN_LAT + 1);
      step('0', '1', '0');
      idle(1);
    end loop;
    chk(to_integer(unsigned(n_pass)) = before_p + 300,
        "the engine stopped accepting clean traffic after the random phase");

    -- ---- Final agreement ----
    chk(to_integer(unsigned(n_triggers)) = m_trg,  "n_triggers disagrees with the model");
    chk(to_integer(unsigned(n_pass))     = m_p,    "n_pass disagrees with the model");
    chk(to_integer(unsigned(n_late))     = m_late, "n_late disagrees");
    chk(to_integer(unsigned(n_early))    = m_early,"n_early disagrees");
    chk(to_integer(unsigned(n_spurious)) = m_spur, "n_spurious disagrees");
    chk(to_integer(unsigned(n_overflow)) = m_ovf,  "n_overflow disagrees");
    chk(to_integer(unsigned(n_dangling)) = m_dng,  "n_dangling disagrees");

    chk(n_oc = 40, "not every queue occupancy was crossed with every input combination");
    chk(n_ag = 26, "not every age of the oldest obligation was seen with and without a response");
    chk(to_integer(unsigned(n_pass))     > 0, "no obligation was ever discharged cleanly");
    chk(to_integer(unsigned(n_late))     > 0, "the MAX_LAT bound was never exercised");
    chk(to_integer(unsigned(n_early))    > 0, "the MIN_LAT bound was never exercised");
    chk(to_integer(unsigned(n_spurious)) > 0, "a response with nothing outstanding was never seen");
    chk(to_integer(unsigned(n_overflow)) > 0, "the tracking capacity was never exceeded");
    chk(to_integer(unsigned(n_dangling)) > 0, "the end-of-test drain was never exercised");

    write(ln, string'("REACH occupancy-x-input=") & integer'image(n_oc) &
              "/40 age-x-response=" & integer'image(n_ag) &
              "/26 steps=" & integer'image(n_steps));
    writeline(output, ln);
    write(ln, string'("COUNTERS triggers=") & integer'image(to_integer(unsigned(n_triggers))) &
              " pass=" & integer'image(to_integer(unsigned(n_pass))) &
              " late=" & integer'image(to_integer(unsigned(n_late))) &
              " early=" & integer'image(to_integer(unsigned(n_early))) &
              " spurious=" & integer'image(to_integer(unsigned(n_spurious))) &
              " overflow=" & integer'image(to_integer(unsigned(n_overflow))) &
              " dangling=" & integer'image(to_integer(unsigned(n_dangling))));
    writeline(output, ln);
    if errors = 0 then
      write(ln, string'("PASS: 0 errors in ") & integer'image(checks) & " checks");
    else
      write(ln, string'("FAIL: ") & integer'image(errors) & " errors in " &
                integer'image(checks) & " checks");
    end if;
    writeline(output, ln);

    done <= true;
    wait;
  end process;

end architecture sim;

13. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
occupancy x input40 / 4040 / 4040 / 40
oldest age x response26 / 2626 / 2626 / 26
Steps437744377443774
Checks executed435882434953434602
triggers offered109551092210814
obligations discharged cleanly643366676594
— late201318592021
— early125612051324
— spurious165317381313
— overflow916840535
— dangling337351340
ResultPASSPASSPASS

The age sweep — 26 of 26 — is the row that found the bug in §6. It covers every age from 0 to MAX_LAT with and without a response, which is what put a response on the exact tie cycle.

14. Mutation Testing

#MutationVerilogSysVerVHDL
A3one timer instead of a queue — a response clears them all561965643055695
A1the MAX bound is removed — "eventually" semantics385843686847260
A7end of test does not drain14115142899862
A4LIFO: the window is charged against the newest872991318955
A2the MIN bound is removed377536223979
A5a response with nothing outstanding is ignored330934792629
A6overflow is silently dropped183516831073
—unmutated baseline000

All seven die in all three languages, all counts distinct.

A3 is the largest by a wide margin, and that is the right shape. A single timer does not fail occasionally — every overlapping pair of obligations loses one, permanently, and the engine's whole purpose evaporates. It is also the most common way a hand-written liveness check is wrong.

A1 is the "eventually" mutation. Remove the upper bound and the engine still tracks, still orders, still reports early responses — and never once says a request went unanswered. Note it does not score zero: it is caught, and it is caught by the dangling drain and the age-bound invariant, which is the argument for having both.

15. Debugging Walkthrough: The Assertion That Passed for Two Years

The report. A USB device controller has an SVA property asserting that every IN token is answered. It has never failed. A customer then reports that under a specific load the device stops answering one endpoint entirely, and the property still does not fail.

Step 1 — read the property. It is written as:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // PASSED FOR TWO YEARS. Checks nothing.
  property p_in_answered;
    @(posedge clk) in_token |-> s_eventually(response);
  endproperty

Step 2 — s_eventually in simulation. It can fail only if the simulation proves the response never comes, which a finite run cannot do. At the end of every test the property is left inconclusive, and the tool's default is to report inconclusive properties as neither pass nor fail — so they do not appear in the failure list at all.

Step 3 — check the coverage report. The property shows 0 failures and 0 successes. Nobody had looked at the second number.

Step 4 — replace it with a bound. in_token |-> ##[1:12] response. It fails immediately, on the customer's load, on the second microframe.

Step 5 — but now it also fails on legal traffic. A NAKed IN is answered promptly; a NAKed IN behind three others is answered in 40 cycles. The bound was wrong, which is a real question the original property allowed everybody to avoid answering.

Step 6 — the bound is the work. Deriving 12 from the design meant establishing the maximum queue depth, the arbiter's bounded waiting (23.2), and the CDC latency (23.5). That took a week, and it produced a number the team could defend — which is the actual deliverable.

16. What SVA Does Well Here, and What It Does Not

16.1 The bounded forms, which are what you actually want

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module usb_liveness_sva #(
  parameter int MIN_LAT = 2,
  parameter int MAX_LAT = 12
) (
  input logic clk,
  input logic rst_n,
  input logic trigger,
  input logic response
);
  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // ---- 1. BOUNDED liveness. This is the property; everything else in this
  // ----    file is a refinement of it.
  //
  //         Written with a bounded range and NOT with s_eventually. The
  //         range is the specification: it is the number somebody had to
  //         derive, and putting it in the property is what forces it to be
  //         derived.
  property p_answered_within_window;
    trigger |-> ##[MIN_LAT:MAX_LAT-1] response;
  endproperty
  a_answered_within_window : assert property (p_answered_within_window)
    else $error("a trigger was not answered inside [%0d,%0d)", MIN_LAT, MAX_LAT);

  // ---- 2. The LOWER edge, as its own property. Property 1 already
  // ----    requires the response to be at least MIN_LAT away, but it is
  // ----    satisfied by ANY response in the range -- including one that
  // ----    also arrived early. Stating the lower edge separately is what
  // ----    catches a consequent that is simply stuck asserted.
  property p_not_answered_too_soon;
    trigger |-> ##[1:MIN_LAT-1] !response;
  endproperty
  a_not_answered_too_soon : assert property (p_not_answered_too_soon)
    else $error("a response arrived sooner than the pipeline can produce one -- it belongs to a different request, or the signal is stuck");

  // ---- 3. OVERLAP. SVA handles this correctly and automatically, and it
  // ----    is the one place it is clearly better than hand-written
  // ----    hardware: each trigger starts an INDEPENDENT attempt, so N
  // ----    overlapping obligations become N live threads with no queue to
  // ----    write and no depth to run out of.
  //
  //         The catch is in property 4.
  property p_each_trigger_independent;
    trigger |-> ##[MIN_LAT:MAX_LAT-1] response;
  endproperty

  // ---- 4. ...but SVA has no notion of TRACKING CAPACITY, which cuts both
  // ----    ways. It cannot overflow -- and it also cannot tell you that
  // ----    500 obligations are in flight, which in a real design means the
  // ----    request path has run away and is itself the bug.
  //
  //         So the count is kept explicitly, in hardware, next to the
  //         assertions rather than inside them.
  int unsigned outstanding;
  always_ff @(posedge clk or negedge rst_n)
    if (!rst_n)              outstanding <= 0;
    else if (trigger && !response) outstanding <= outstanding + 1;
    else if (response && !trigger && outstanding) outstanding <= outstanding - 1;

  a_outstanding_bounded : assert property (outstanding <= 8)
    else $error("%0d obligations in flight -- the request path is running away, which no per-trigger property can see",
                outstanding);

  // ---- 5. A response with NOTHING outstanding. SVA cannot express this as
  // ----    a per-trigger property at all: there is no trigger to hang it
  // ----    off. It needs the count from property 4.
  a_no_spurious_response : assert property
    ((response && outstanding == 0) |-> 1'b0)
    else $error("a response arrived with nothing outstanding -- the consequent can fire on its own, so it could later discharge an obligation it has nothing to do with");

  // ---- Cover both edges, because a bound nobody reaches is not a bound
  // ---- anybody has tested.
  c_at_min : cover property ((trigger ##MIN_LAT response));
  c_at_max : cover property ((trigger ##(MAX_LAT-1) response));
  c_overlap: cover property ((trigger ##1 trigger));
endmodule

16.2 The drain, which SVA will not do for you

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// THE code that turns "no failures" into "verified", and the code that is
// missing from most environments.
//
// At the end of a run, every assertion attempt still in flight is
// INCONCLUSIVE. Tools report inconclusive attempts as neither pass nor fail,
// so a test whose last microframe left four obligations outstanding prints a
// clean report.
//
// `$assertkill` makes that silence official. What is wanted is the opposite:
// name them.
class usb_eot_drain extends uvm_component;
  `uvm_component_utils(usb_eot_drain)

  // Mirrors the assertion engine's queue, for the sole purpose of being
  // able to say what was left over.
  int unsigned outstanding;
  int unsigned oldest_age;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  // A UVM objection is held for as long as obligations are outstanding, so
  // the run cannot end in the middle of one. This is the first line of
  // defence and it is better than reporting the leftovers, because there
  // are none.
  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (outstanding > 0 && !objection_raised) begin
        phase.raise_objection(this, "obligations outstanding");
        objection_raised = 1;
      end else if (outstanding == 0 && objection_raised) begin
        phase.drop_objection(this, "all obligations discharged");
        objection_raised = 0;
      end
    end
  endtask

  // ...and the second line of defence, for when the objection cannot be
  // held -- a timeout, a fatal, a deliberately truncated test.
  function void check_phase(uvm_phase phase);
    if (outstanding != 0)
      `uvm_error("DANGLING",
        $sformatf("%0d obligation(s) were still outstanding when the run ended, the oldest for %0d cycles -- these are NOT inconclusive, the simulation is over and nothing more is coming",
                  outstanding, oldest_age))
  endfunction

  // And the check that catches an assertion nobody ever exercised.
  function void report_phase(uvm_phase phase);
    if (n_nonvacuous == 0)
      `uvm_error("VACUOUS",
        "the liveness property was never non-vacuously satisfied -- zero failures and zero successes is not a pass, it is an assertion that was never reached")
  endfunction

  local bit objection_raised;
  local int unsigned n_nonvacuous;
endclass

17. Common Misconceptions

"s_eventually checks liveness." In formal, yes. In simulation it can only be inconclusive, and inconclusive is reported as neither pass nor fail.

"An assertion with no failures is passing." An assertion with no failures and no successes was never reached. Grep for the second number.

"MAX_LAT alone is enough." It accepts a response that arrived before the pipeline could have produced one — a response to a different request, or a stuck signal.

"One timer is enough if requests are rare." They are rare until they are not, and then one response discharges two obligations, permanently.

"Retiring the newest is equivalent." It charges the window against the wrong request. Its symptom is noise on legal traffic, which gets the check switched off.

"Overflow is a tool limit, not a bug." Dropping an obligation makes the checker stop checking exactly when the design is busiest.

"Leftover obligations are inconclusive." The simulation is over. Nothing more is coming. They are failures.

"$assertkill just tidies up the log." It discards the population you most needed to see.

"The bound is an implementation detail." The bound is the specification. Deriving it is the work; an unbounded property exists to avoid doing it.

18. Exercises

1. Show that trigger |-> s_eventually(response) cannot fail in any finite simulation, then say what its coverage report looks like on a design that never responds at all.

2. A3 scores ~56 000 and A4 ~9 000, though both get the queue wrong. Explain the ratio from what each does to a pair of overlapping obligations.

3. A6's score is 2 x n_overflow + 3 in all three languages. Derive the 2 and the 3, then predict A5's score from n_spurious and check it.

4. Delete the explicit upper-edge test (§6) and find the single latency in the sweep that changes its verdict. Then argue whether a coverage-driven suite would have found it.

5. MIN_LAT is 2 here. Derive what it should be for an IN token answered from a FIFO whose read path is three stages deep and which sits behind the CDC handshake of 23.5.

6. Write the report_phase check that fails a run in which a liveness property was satisfied only vacuously, and say why cover property is not sufficient on its own.

19. Summary

IdeaWhy it matters
"Eventually" has no failing casein simulation it can only be inconclusive
Every real liveness check is boundedand the bound is the specification, not a detail
A window has two edgesMIN_LAT catches a response to the wrong request
Obligations overlapone timer lets one response discharge two
Retirement is FIFOLIFO charges the window against the wrong request
Overflow is a failure, not a limitor the checker stops when the design is busiest
An unfinished obligation is not a passthe run is over; nothing more is coming
The upper edge must be statedor the tie cycle is decided by evaluation order
Zero failures and zero successesis an assertion that was never reached
$assertkill silences livenesscount and report the leftovers first
SVA handles overlap well, capacity not at allkeep the outstanding count in hardware
40/40 and 26/26, 6433 clean discharges7 mutations, all killed in 3 languages

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o ae_v.out ae_v.v ae_v_tb.v && ./ae_v.out
SystemVerilogiverilog -g2012 -o ae_sv.out ae_sv.sv ae_sv_tb.sv && ./ae_sv.out
VHDL-2008 analysenvc --std=2008 -a ae_vhdl.vhd ae_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_ae_vhdl
VHDL-2008 runnvc --std=2008 -r tb_ae_vhdl
One mutationiverilog -g2005 -DMUT_A3 -o mm ae_v_mut.v ae_v_tb.v && ./mm

All three implementations pass with 0 errors: every queue occupancy crossed with every input combination, every age of the oldest obligation swept with and without a response, and both edges of the window checked at every latency from 0 to MAX_LAT + 2.


Chapter 24.3 — USB Scoreboards moves from "was that legal?" to "was that mine?" A scoreboard matches what came back against what went out, and the mistake that defines the chapter is matching on value instead of identity: two transfers carrying the same bytes are indistinguishable to a value-matching scoreboard, so a duplicate delivery and a lost one cancel out and the report is clean.

Continue learning

Standards & specifications

Governing standard
USB-IF (Universal Serial Bus Specification)(opens USB Implementers Forum (USB-IF) in a new tab)

Defines the USB bus — its electrical signalling, connectors, packet and transaction model, device framework and the descriptors a device must expose — together with the device-class specifications layered on it. It does not define host-controller register interfaces (xHCI and EHCI are separate documents) nor any operating system's driver architecture.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the USB curriculum.