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
every request is eventually answeredIt 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.
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 PASS3. 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.
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
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
// 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
endmodule9. SystemVerilog Implementation
// 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
endmodule10. VHDL-2008 Implementation
-- 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 cyclesRead 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:
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:
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");
endFIFO order, as a discriminator that passes on a correct engine:
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:
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
// 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
endmodule12.2 SystemVerilog testbench
// 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
endmodule12.3 VHDL testbench
-- 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
| Measure | Verilog | SystemVerilog | VHDL |
|---|---|---|---|
| occupancy x input | 40 / 40 | 40 / 40 | 40 / 40 |
| oldest age x response | 26 / 26 | 26 / 26 | 26 / 26 |
| Steps | 43774 | 43774 | 43774 |
| Checks executed | 435882 | 434953 | 434602 |
| triggers offered | 10955 | 10922 | 10814 |
| obligations discharged cleanly | 6433 | 6667 | 6594 |
| — late | 2013 | 1859 | 2021 |
| — early | 1256 | 1205 | 1324 |
| — spurious | 1653 | 1738 | 1313 |
| — overflow | 916 | 840 | 535 |
| — dangling | 337 | 351 | 340 |
| Result | PASS | PASS | PASS |
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
| # | Mutation | Verilog | SysVer | VHDL |
|---|---|---|---|---|
| A3 | one timer instead of a queue — a response clears them all | 56196 | 56430 | 55695 |
| A1 | the MAX bound is removed — "eventually" semantics | 38584 | 36868 | 47260 |
| A7 | end of test does not drain | 14115 | 14289 | 9862 |
| A4 | LIFO: the window is charged against the newest | 8729 | 9131 | 8955 |
| A2 | the MIN bound is removed | 3775 | 3622 | 3979 |
| A5 | a response with nothing outstanding is ignored | 3309 | 3479 | 2629 |
| A6 | overflow is silently dropped | 1835 | 1683 | 1073 |
| — | unmutated baseline | 0 | 0 | 0 |
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:
// PASSED FOR TWO YEARS. Checks nothing.
property p_in_answered;
@(posedge clk) in_token |-> s_eventually(response);
endpropertyStep 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
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));
endmodule16.2 The drain, which SVA will not do for you
// 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;
endclass17. 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
| Idea | Why it matters |
|---|---|
| "Eventually" has no failing case | in simulation it can only be inconclusive |
| Every real liveness check is bounded | and the bound is the specification, not a detail |
| A window has two edges | MIN_LAT catches a response to the wrong request |
| Obligations overlap | one timer lets one response discharge two |
| Retirement is FIFO | LIFO charges the window against the wrong request |
| Overflow is a failure, not a limit | or the checker stops when the design is busiest |
| An unfinished obligation is not a pass | the run is over; nothing more is coming |
| The upper edge must be stated | or the tie cycle is decided by evaluation order |
| Zero failures and zero successes | is an assertion that was never reached |
$assertkill silences liveness | count and report the leftovers first |
| SVA handles overlap well, capacity not at all | keep the outstanding count in hardware |
| 40/40 and 26/26, 6433 clean discharges | 7 mutations, all killed in 3 languages |
Tooling
| Step | Command |
|---|---|
| Verilog-2005 | iverilog -g2005 -o ae_v.out ae_v.v ae_v_tb.v && ./ae_v.out |
| SystemVerilog | iverilog -g2012 -o ae_sv.out ae_sv.sv ae_sv_tb.sv && ./ae_sv.out |
| VHDL-2008 analyse | nvc --std=2008 -a ae_vhdl.vhd ae_vhdl_tb.vhd |
| VHDL-2008 elaborate | nvc --std=2008 -e tb_ae_vhdl |
| VHDL-2008 run | nvc --std=2008 -r tb_ae_vhdl |
| One mutation | iverilog -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
Related tutorials
- Related topic
USB Protocol Checkers
Every ordering rule says what must happen next, and none of them fires when nothing happens at all — the timeout is a checker's only liveness tool, and a checker with false positives gets switched off.
- Related topic
USB Scoreboards
Two transfers carrying the same bytes are indistinguishable to a scoreboard that matches on value, so a duplicate delivery and a lost transfer cancel out — identity finds the partner, value checks it.
- Related topic
USB Functional Coverage
An unreachable bin and an untested bin both read 0% and demand opposite responses — and an exclusion is a claim about the design, so a bin that is excluded and then hit must fail.
- Related topic
USB VIP Usage
A VIP is a second implementation of the specification, so where it and the design disagree one of them is wrong — and the adapter's job is to make that visible, never to resolve it in a shim.
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.
