Skip to content
VLSI Mentor

USB · Module 16

Real-Time Data Streams

A sensor stream must know exactly how much data is missing, not just that something is. Sequence numbers, the modular arithmetic that survives a wrap, and the half-space limit beyond which a gap cannot be measured at all.

Chapter 16.2 built a receiver that could always tell that a frame boundary had occurred. One toggling bit, carried by every packet, made the boundary recoverable no matter which packet was lost.

What that bit could never tell it is how many boundaries it missed. A FID toggle looks identical whether one frame was lost or three — it is one bit, and one bit counts to two. For video that is acceptable: the display shows what arrives, the viewer sees a stutter, and the next frame is complete.

This chapter is about streams where it is not acceptable, because the data's position in time is part of the measurement.

1. When Position Is the Measurement

A camera produces images. A sensor produces a time series, and the two fail differently under loss.

Consider a data-acquisition front end sampling a strain gauge at 10 kHz and shipping the samples over an isochronous IN endpoint. The host reconstructs a waveform. Now lose some packets.

The consumer believesIf the gap size is knownIf the gap size is unknown
sample indexderived by countingcorrected by skipping Nwrong from here on
timestampsindex × sample periodstill correctevery later sample mistimed
frequency contentfrom sample spacinga hole, correctly placeda time-base error
the analysisvalid with a marked gapvalidsilently wrong

The last column is the problem, and the word that matters is silently. A waveform reassembled with an unknown gap does not look broken. It looks like a slightly different signal — shifted, or stretched, or carrying a frequency that was never there — and nothing downstream has any reason to doubt it.

Video loses an image and the viewer sees it. A sensor stream loses its time base and the analysis is wrong in a way no one notices. That asymmetry is why this chapter needs a mechanism that 16.2's single bit cannot provide.

2. Numbering the Payloads

The mechanism is the obvious one, and the interesting part is not the idea but its arithmetic. Each payload carries a sequence number, incrementing by one per service interval, modulo some power of two. The receiver keeps the value it expected and subtracts.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
gap = received_sequence − expected_sequence

That is the whole mechanism in one line, and the line is wrong as written — or rather, it is right only if you are careful about what kind of subtraction it is. §3 is that care.

Where the number comes from is worth stating. Nothing in the core USB specification puts a sequence number in an isochronous payload. This is a device-defined header, exactly as Chapter 16.2's FID and EOF bits come from the USB Video Class rather than from ch9.h. A vendor-specific data-acquisition device defines its own payload format, and a sequence counter is the near-universal first field.

The host's own report is not a substitute. The Linux URB provides per-packet status through the isochronous frame descriptor array:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 * ISO transfer status is reported in the status and actual_length fields
 * of the iso_frame_desc array, and the number of errors is reported in
 * error_count.

That tells the driver which service intervals the host controller failed to complete. It is genuinely useful and Chapter 16.2's design consumed it as pkt_missing. But it reports what the host knows, and it cannot report a payload the device never managed to produce — a device whose internal FIFO overran and skipped samples has lost data the host controller has no idea about. A sequence number written by the device covers both.

3. The Wrap: Why a Comparison Is Not a Comparison

Sequence numbers are finite. After 255 comes 0. And the moment they wrap, the obvious implementation stops working permanently.

Suppose the receiver expects 255 and packet 0 arrives — one packet late, a gap of one, the most ordinary event in the stream.

ImplementationWhat it computesVerdict
if (seq > expected) — a magnitude comparison0 > 255 is false"behind" — wrong
seq − expected in full precision0 − 255 = −255a negative gap — meaningless
(seq − expected) mod 2560 − 255 ≡ 1a gap of one — correct

The third is not a trick. Sequence space is modular — the numbers are residues, not magnitudes — so the arithmetic that respects it is modular arithmetic. And in hardware, modular arithmetic in 2^N is what a plain N-bit subtractor already does, for free:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  wire [SEQ_W-1:0] diff = pkt_seq - expect_r;

An N-bit subtraction wraps by construction. No modulo operator, no conditional, no extra logic — the truncation is the modulo. The bug is not in doing something difficult; the bug is in doing something extra, like widening the operands or comparing magnitudes first.

4. The Half-Space Limit

Modular arithmetic solves the wrap and introduces a limit that cannot be engineered away.

If the difference is taken modulo 256, then a gap of 200 and a backward jump of 56 produce the same residue. The receiver has no way to tell them apart, because in modular arithmetic they are the same number.

So the sequence space must be split. The convention is the halfway point:

Modular differenceInterpretationWhy
0in orderexactly what was expected
1 … HALF−1a gap of that many packetsahead by less than half the space
HALF … 2^N−1behind — duplicate, reorder, or an unmeasurable burstahead by more than half is indistinguishable from behind

With an 8-bit sequence number, the largest measurable gap is 127 packets. A burst loss of 128 or more is not merely uncounted — it is unrecognisable as a gap at all, and the receiver must classify it as behind.

And a gap of exactly 256 is completely invisible: the residue is zero, so the receiver reports a perfectly in-order packet. The stream appears healthy and an entire block of samples has vanished.

The sequence width is therefore a design parameter with a hard requirement attached: 2^(N−1) must exceed the largest burst loss the system can plausibly suffer. At 8 bits that is 127 packets; if the device's FIFO can overrun by more than that, 8 bits is the wrong choice and no amount of correct arithmetic will rescue it.

What to do when the limit is exceeded

A receiver that sees behind has three options, and two of them are wrong.

Advance the expectation and count the residue as lost. Wrong: the residue is not the gap size, so the loss total becomes fiction.

Freeze the expectation and wait. Correct for a genuine duplicate — which should simply be ignored — and catastrophic for a large burst, because the source has moved on and every subsequent packet will also read as behind. The stream never recovers.

Freeze, but not forever. Ignore the first few — they are probably duplicates — and after a small number of consecutive stale packets, conclude that the receiver is the one that is lost and re-adopt the incoming sequence number as a new origin.

The third is the only one that survives both cases, and it is honest about what it gives up: after a resync, the size of that particular gap is unknown and is not counted. The stream continues correctly; one measurement is lost. That is strictly better than a stream that never recovers or a loss total that is quietly wrong.

5. The Hardware, Before Any Language

State retained: expect_r (the next sequence number expected), synced (has any packet been seen), a saturating lost_r total, and stale_cnt (consecutive stale packets).

On reset or bus reset: everything clears.

On the first packet ever: adopt its sequence number as the origin and expect the next. Nothing is counted lost — the receiver cannot know what preceded its own arrival, and inventing a loss total from an arbitrary starting residue would be worse than reporting zero.

On a packet whose modular difference is zero: in order. Advance.

On a difference in 1 … HALF−1: a gap of exactly that many packets. Report it, add it to the saturating total, and advance the expectation past this packet.

On a difference at or above HALF: behind. Do not advance, do not count — and increment stale_cnt.

On the STALE_LIMIT-th consecutive stale packet: re-adopt this packet as the origin, pulse resync, and clear stale_cnt. Still count nothing, because the gap size is genuinely unknowable.

On any non-stale packet: clear stale_cnt. The limit counts consecutive staleness; an isolated duplicate in an otherwise healthy stream must not accumulate toward a resync.

The loss total saturates. A wrapped total reports a plausible small loss for a stream that has lost a great deal — the direction that makes a consumer trust data it should discard.

6. Verilog

The RTL contract

  • What it models: the gap-quantification logic of an isochronous data-acquisition receiver.
  • Why it exists: because a sensor stream's sample positions are part of its measurement (§1), and a gap of unknown size silently corrupts everything after it.
  • Inputs: pkt_valid, pkt_seq (the device-written sequence number), bus_reset.
  • State retained: expect_r, synced_r, lost_r, stale_cnt.
  • Outputs: in_order, gap_valid + gap_pkts, stale, resync, total_lost, expect_seq, synced.
  • Hardware implied: one SEQ_W subtractor, two comparators, one saturating LOST_W adder, one small counter, four flags.
  • Reset: asynchronous active-low rst_n; bus_reset synchronous and equivalent.
  • Priority: first-packet outranks stale, which outranks the ordinary in-order/gap path.
  • Latency: every output is registered, so a classification is visible the interval after the packet.
  • Boundaries: the largest measurable gap is 2^(SEQ_W−1) − 1; lost_r saturates rather than wrapping.
  • Simultaneous events: exactly one packet is classified per service interval — there is no collision case here, unlike 16.1 and 16.2.
  • Assumptions: the source increments by one per interval; burst losses stay below 2^(SEQ_W−1) — beyond that the gap size is unrecoverable by construction (§4) and the design resynchronises instead.
  • Omissions: no payload parsing, no sample FIFO, no timestamps, no device-side overrun detection.
  • What DV should verify: that a gap straddling the sequence wrap is measured correctly; that the half-space boundary is exclusive; that a duplicate changes nothing; that consecutive staleness resynchronises; that the loss total saturates.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// iso_stream_gap_tracker -- recovers the SIZE of a gap in an isochronous
// data-acquisition stream.
//
// Chapter 16.2's FID bit could say THAT a boundary occurred but never HOW
// MANY were missed. A sensor stream cannot live with that: the position of a
// sample in time is part of the measurement, so a gap of unknown size
// corrupts every sample after it. The device therefore numbers its payloads,
// and the receiver recovers the gap by subtraction.
//
// The trap is that sequence numbers WRAP. After 255 comes 0, so "is this
// packet ahead of what I expected" is NOT a magnitude comparison -- 0 is
// ahead of 255. The comparison must be done on the MODULAR DIFFERENCE, and
// the halfway point of the sequence space is what splits "ahead" from
// "behind". Writing `pkt_seq > expected` works perfectly until the counter
// wraps and then fails permanently.
module iso_stream_gap_tracker #(
  parameter integer SEQ_W       = 8,  // sequence numbers are modulo 2**SEQ_W
  parameter integer LOST_W      = 16, // saturating count of lost packets
  parameter integer STALE_LIMIT = 4   // consecutive stales before resync
) (
  input  wire              clk,
  input  wire              rst_n,
  input  wire              bus_reset,
  input  wire              pkt_valid,
  input  wire [SEQ_W-1:0]  pkt_seq,
  output wire              in_order,     // exactly the expected packet
  output wire              gap_valid,    // packets were lost before this one
  output wire [SEQ_W-1:0]  gap_pkts,     // ...this many of them
  output wire              stale,        // a duplicate or a reordered packet
  output wire [LOST_W-1:0] total_lost,   // saturating running total
  output wire [SEQ_W-1:0]  expect_seq,
  output wire              resync,       // the receiver re-adopted an origin
  output wire              synced
);
  localparam [LOST_W-1:0] LOST_MAX = {LOST_W{1'b1}};
  // The halfway point of the sequence space. A modular difference below this
  // means the packet is AHEAD (a gap); at or above it means BEHIND (stale).
  localparam [SEQ_W-1:0]  HALF = {1'b1, {(SEQ_W-1){1'b0}}};

  reg [SEQ_W-1:0]  expect_r;
  reg              synced_r;
  reg [LOST_W-1:0] lost_r;
  reg              inorder_r, gap_r, stale_r, resync_r;
  reg [SEQ_W-1:0]  gap_r_n;
  // Consecutive stale packets. Without this the receiver would freeze its
  // expectation forever after a single over-large burst -- see below.
  reg [$clog2(STALE_LIMIT+1)-1:0] stale_cnt;

  assign in_order   = inorder_r;
  assign gap_valid  = gap_r;
  assign gap_pkts   = gap_r_n;
  assign stale      = stale_r;
  assign total_lost = lost_r;
  assign expect_seq = expect_r;
  assign resync     = resync_r;
  assign synced     = synced_r;

  // THE MODULAR DIFFERENCE. Subtraction in SEQ_W bits wraps by construction,
  // which is exactly what is wanted: (0 - 255) mod 256 = 1, so packet 0
  // arriving when 255 was expected is correctly "one ahead", not "255 behind".
  wire [SEQ_W-1:0] diff = pkt_seq - expect_r;

  wire is_inorder = (diff == {SEQ_W{1'b0}});
  wire is_stale   = (diff >= HALF);          // behind: duplicate or reorder
  wire is_gap     = !is_inorder && !is_stale;

  // Saturating loss total. A wrapped total reports a PLAUSIBLE small loss
  // for a stream that has lost a great deal, which is the direction that
  // makes a consumer trust data it should discard.
  wire [LOST_W:0] lost_sum =
    {1'b0, lost_r} + {{(LOST_W-SEQ_W+1){1'b0}}, diff};
  wire [LOST_W-1:0] lost_next =
    lost_sum[LOST_W] ? LOST_MAX : lost_sum[LOST_W-1:0];

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      expect_r <= {SEQ_W{1'b0}}; synced_r <= 1'b0; lost_r <= {LOST_W{1'b0}};
      inorder_r <= 1'b0; gap_r <= 1'b0; stale_r <= 1'b0; resync_r <= 1'b0;
      gap_r_n <= {SEQ_W{1'b0}}; stale_cnt <= 0;
    end else if (bus_reset) begin
      expect_r <= {SEQ_W{1'b0}}; synced_r <= 1'b0; lost_r <= {LOST_W{1'b0}};
      inorder_r <= 1'b0; gap_r <= 1'b0; stale_r <= 1'b0; resync_r <= 1'b0;
      gap_r_n <= {SEQ_W{1'b0}}; stale_cnt <= 0;
    end else begin
      inorder_r <= 1'b0;
      gap_r     <= 1'b0;
      stale_r   <= 1'b0;
      resync_r  <= 1'b0;

      if (pkt_valid) begin
        if (!synced_r) begin
          // The first packet defines the origin. The receiver cannot know
          // how many packets preceded its arrival, so nothing is counted
          // as lost here -- see section 5 on why that matters.
          synced_r  <= 1'b1;
          expect_r  <= pkt_seq + {{(SEQ_W-1){1'b0}}, 1'b1};
          inorder_r <= 1'b1;
          stale_cnt <= 0;
        end else if (is_stale) begin
          // Behind the expectation: a duplicate, a reordering, or a burst
          // loss so large it wrapped past the halfway point. The expectation
          // is NOT advanced and nothing is counted lost, because the true
          // gap is unknowable -- but freezing forever is not an option
          // either, so after STALE_LIMIT consecutive stales the receiver
          // RE-ADOPTS this packet as a new origin. The stream continues;
          // the size of that particular gap is simply lost, which is the
          // honest outcome and is why SEQ_W must be chosen so that
          // plausible burst losses stay below 2**(SEQ_W-1).
          if (stale_cnt == STALE_LIMIT[$clog2(STALE_LIMIT+1)-1:0] - 1) begin
            expect_r  <= pkt_seq + {{(SEQ_W-1){1'b0}}, 1'b1};
            resync_r  <= 1'b1;
            stale_cnt <= 0;
          end else begin
            stale_r   <= 1'b1;
            stale_cnt <= stale_cnt + 1'b1;
          end
        end else begin
          expect_r  <= pkt_seq + {{(SEQ_W-1){1'b0}}, 1'b1};
          inorder_r <= is_inorder;
          stale_cnt <= 0;
          if (is_gap) begin
            gap_r   <= 1'b1;
            gap_r_n <= diff;
            lost_r  <= lost_next;
          end
        end
      end
    end
  end
endmodule

Three details are worth naming.

diff is the entire mechanism and it costs one subtractor. §3's modular arithmetic is not implemented — it is inherited, because an SEQ_W-bit subtraction already wraps. The temptation this design resists is doing anything more elaborate.

HALF is {1'b1, {(SEQ_W-1){1'b0}}}, the top bit set and nothing else. So diff >= HALF is really is the top bit of diff set — a single wire, not a comparator. A synthesis tool will find that; writing it as a comparison keeps the intent readable while costing nothing.

Nothing is counted lost on the first packet or on a resync. Both are cases where the receiver's expectation was arbitrary, and a loss total computed from an arbitrary origin is a fabricated number. Reporting zero is not a simplification; it is the only honest answer, and §12's mutation R3 measures what happens when that discipline slips.

7. SystemVerilog

Same hardware, with the five outcomes named.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
package iso_gap_pkg;
  // How this packet relates to what the receiver expected. Naming the five
  // outcomes makes them exhaustive and mutually exclusive by construction;
  // in the Verilog they are the positions of an if/else chain.
  typedef enum logic [2:0] {
    C_NONE,     // no packet this interval
    C_FIRST,    // the first packet since reset -- it defines the origin
    C_INORDER,  // exactly the expected sequence number
    C_GAP,      // ahead by 1..2**(SEQ_W-1)-1: that many packets were lost
    C_STALE,    // behind: a duplicate, a reorder, or an unrecoverable burst
    C_RESYNC    // enough consecutive stales that an origin is re-adopted
  } cls_e;
endpackage

module iso_stream_gap_tracker_sv
  import iso_gap_pkg::*;
#(
  parameter int unsigned SEQ_W       = 8,
  parameter int unsigned LOST_W      = 16,
  parameter int unsigned STALE_LIMIT = 4
) (
  input  logic              clk,
  input  logic              rst_n,
  input  logic              bus_reset,
  input  logic              pkt_valid,
  input  logic [SEQ_W-1:0]  pkt_seq,
  output logic              in_order,
  output logic              gap_valid,
  output logic [SEQ_W-1:0]  gap_pkts,
  output logic              stale,
  output logic [LOST_W-1:0] total_lost,
  output logic [SEQ_W-1:0]  expect_seq,
  output logic              resync,
  output logic              synced
);
  initial begin
    if (SEQ_W < 2)
      $fatal(1, "SEQ_W=%0d leaves no room for a halfway point", SEQ_W);
    if (LOST_W < SEQ_W)
      $fatal(1, "LOST_W=%0d cannot hold even one maximal gap", LOST_W);
    if (STALE_LIMIT < 1)
      $fatal(1, "STALE_LIMIT=%0d would never resync", STALE_LIMIT);
  end

  localparam logic [LOST_W-1:0] LOST_MAX = '1;
  // The halfway point of the sequence space: below it the packet is AHEAD
  // (a measurable gap), at or above it the packet is BEHIND.
  localparam logic [SEQ_W-1:0]  HALF = {1'b1, {(SEQ_W-1){1'b0}}};

  logic [SEQ_W-1:0]  expect_r;
  logic [LOST_W-1:0] lost_r;
  logic [$clog2(STALE_LIMIT+1)-1:0] stale_cnt;

  assign total_lost = lost_r;
  assign expect_seq = expect_r;

  // THE MODULAR DIFFERENCE. Subtraction in SEQ_W bits wraps by construction,
  // which is precisely what is wanted: (0 - 255) mod 256 = 1, so packet 0
  // arriving when 255 was expected is one AHEAD, not 255 behind.
  wire [SEQ_W-1:0] diff = pkt_seq - expect_r;

  cls_e cls;
  always_comb begin
    if      (!pkt_valid)          cls = C_NONE;
    else if (!synced)             cls = C_FIRST;
    else if (diff == '0)          cls = C_INORDER;
    else if (diff < HALF)         cls = C_GAP;
    else if (stale_cnt == ($clog2(STALE_LIMIT+1))'(STALE_LIMIT) - 1)
                                  cls = C_RESYNC;
    else                          cls = C_STALE;
  end

  // Saturating loss total, widened BEFORE the add so the carry is real.
  logic [LOST_W:0]   lost_sum;
  logic [LOST_W-1:0] lost_next;
  always_comb begin
    lost_sum  = {1'b0, lost_r} + {1'b0, LOST_W'(diff)};
    lost_next = lost_sum[LOST_W] ? LOST_MAX : lost_sum[LOST_W-1:0];
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || bus_reset) begin
      expect_r <= '0; synced <= 1'b0; lost_r <= '0; stale_cnt <= '0;
      in_order <= 1'b0; gap_valid <= 1'b0; stale <= 1'b0; resync <= 1'b0;
      gap_pkts <= '0;
    end else begin
      in_order  <= 1'b0;
      gap_valid <= 1'b0;
      stale     <= 1'b0;
      resync    <= 1'b0;

      unique case (cls)
        C_NONE: ;

        C_FIRST: begin
          // The first packet defines the origin. Nothing is counted lost:
          // the receiver cannot know what preceded its own arrival.
          synced <= 1'b1; expect_r <= pkt_seq + 1'b1;
          in_order <= 1'b1; stale_cnt <= '0;
        end

        C_INORDER: begin
          expect_r <= pkt_seq + 1'b1; in_order <= 1'b1; stale_cnt <= '0;
        end

        C_GAP: begin
          expect_r <= pkt_seq + 1'b1;
          gap_valid <= 1'b1; gap_pkts <= diff;
          lost_r <= lost_next; stale_cnt <= '0;
        end

        C_STALE: begin
          // Behind the expectation. The true gap is unknowable, so nothing
          // is counted and the expectation does NOT move.
          stale <= 1'b1; stale_cnt <= stale_cnt + 1'b1;
        end

        C_RESYNC: begin
          // Enough consecutive stales that freezing is worse than guessing.
          // Re-adopt this packet as the origin: the stream continues and the
          // size of that one gap is lost, which is the honest outcome.
          expect_r <= pkt_seq + 1'b1; resync <= 1'b1; stale_cnt <= '0;
        end
      endcase
    end
  end
endmodule

cls_e is the real gain here, and not for readability. The Verilog expresses first / in-order / gap / stale / resync as the positions of a nested if/else chain, in which the resync case is buried inside the stale branch. Naming the five outcomes makes them a flat, exhaustive, mutually exclusive set — and it moves §4's decision, which of these is this packet, into one always_comb block that can be read in isolation from the state updates.

The three $fatal guards catch parameterisations that quietly disable the design. SEQ_W < 2 leaves no halfway point. LOST_W < SEQ_W makes the loss counter unable to hold a single maximal gap, so it saturates on the first one. STALE_LIMIT < 1 never resyncs — restoring precisely the deadlock §4 describes.

8. VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package iso_gap_pkg is
  -- How this packet relates to what the receiver expected. In VHDL this is a
  -- genuine distinct type with no numeric encoding underneath, so a case over
  -- it must be exhaustive and cannot be compared against a raw integer.
  type cls_t is (C_NONE, C_FIRST, C_INORDER, C_GAP, C_STALE, C_RESYNC);
end package;

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

entity iso_stream_gap_tracker_vhdl is
  generic (
    SEQ_W       : positive := 8;
    LOST_W      : positive := 16;
    STALE_LIMIT : positive := 4
  );
  port (
    clk        : in  std_logic;
    rst_n      : in  std_logic;
    bus_reset  : in  std_logic;
    pkt_valid  : in  std_logic;
    pkt_seq    : in  unsigned(SEQ_W-1 downto 0);
    in_order   : out std_logic;
    gap_valid  : out std_logic;
    gap_pkts   : out unsigned(SEQ_W-1 downto 0);
    stale      : out std_logic;
    total_lost : out unsigned(LOST_W-1 downto 0);
    expect_seq : out unsigned(SEQ_W-1 downto 0);
    resync     : out std_logic;
    synced     : out std_logic
  );
end entity;

architecture rtl of iso_stream_gap_tracker_vhdl is
  constant LOST_MAX : unsigned(LOST_W-1 downto 0) := (others => '1');
  -- The halfway point of the sequence space: below it the packet is AHEAD
  -- (a measurable gap), at or above it the packet is BEHIND.
  constant HALF : unsigned(SEQ_W-1 downto 0) :=
    to_unsigned(2**(SEQ_W-1), SEQ_W);

  signal expect_r  : unsigned(SEQ_W-1 downto 0) := (others => '0');
  signal lost_r    : unsigned(LOST_W-1 downto 0) := (others => '0');
  signal synced_r  : std_logic := '0';
  signal stale_cnt : integer range 0 to STALE_LIMIT := 0;

  signal io_r, gap_r, stale_r, resync_r : std_logic := '0';
  signal gapn_r : unsigned(SEQ_W-1 downto 0) := (others => '0');

  signal diff      : unsigned(SEQ_W-1 downto 0) := (others => '0');
  signal cls       : cls_t;
  signal lost_sum  : unsigned(LOST_W downto 0) := (others => '0');
  signal lost_next : unsigned(LOST_W-1 downto 0) := (others => '0');
begin
  assert SEQ_W >= 2
    report "SEQ_W leaves no room for a halfway point" severity failure;
  assert LOST_W >= SEQ_W
    report "LOST_W cannot hold even one maximal gap" severity failure;

  in_order   <= io_r;
  gap_valid  <= gap_r;
  gap_pkts   <= gapn_r;
  stale      <= stale_r;
  total_lost <= lost_r;
  expect_seq <= expect_r;
  resync     <= resync_r;
  synced     <= synced_r;

  -- THE MODULAR DIFFERENCE. numeric_std subtraction on UNSIGNED is defined
  -- modulo 2**SEQ_W, which is exactly what is wanted: (0 - 255) mod 256 = 1,
  -- so packet 0 arriving when 255 was expected is one AHEAD, not 255 behind.
  diff <= pkt_seq - expect_r;

  cls <= C_NONE    when pkt_valid = '0' else
         C_FIRST   when synced_r = '0' else
         C_INORDER when diff = 0 else
         C_GAP     when diff < HALF else
         C_RESYNC  when stale_cnt = STALE_LIMIT - 1 else
         C_STALE;

  -- Saturating loss total, widened before the add so the carry is a real bit.
  lost_sum  <= resize(lost_r, LOST_W+1) + resize(diff, LOST_W+1);
  lost_next <= LOST_MAX when lost_sum(LOST_W) = '1'
               else lost_sum(LOST_W-1 downto 0);

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      expect_r <= (others => '0'); lost_r <= (others => '0');
      synced_r <= '0'; stale_cnt <= 0;
      io_r <= '0'; gap_r <= '0'; stale_r <= '0'; resync_r <= '0';
      gapn_r <= (others => '0');
    elsif rising_edge(clk) then
      if bus_reset = '1' then
        expect_r <= (others => '0'); lost_r <= (others => '0');
        synced_r <= '0'; stale_cnt <= 0;
        io_r <= '0'; gap_r <= '0'; stale_r <= '0'; resync_r <= '0';
        gapn_r <= (others => '0');
      else
        io_r     <= '0';
        gap_r    <= '0';
        stale_r  <= '0';
        resync_r <= '0';

        case cls is
          when C_NONE =>
            null;                     -- nothing arrived; hold everything

          when C_FIRST =>
            -- The first packet defines the origin. Nothing is counted lost:
            -- the receiver cannot know what preceded its own arrival.
            synced_r  <= '1';
            expect_r  <= pkt_seq + 1;
            io_r      <= '1';
            stale_cnt <= 0;

          when C_INORDER =>
            expect_r  <= pkt_seq + 1;
            io_r      <= '1';
            stale_cnt <= 0;

          when C_GAP =>
            expect_r  <= pkt_seq + 1;
            gap_r     <= '1';
            gapn_r    <= diff;
            lost_r    <= lost_next;
            stale_cnt <= 0;

          when C_STALE =>
            -- Behind the expectation. The true gap is unknowable, so nothing
            -- is counted and the expectation does NOT move.
            stale_r   <= '1';
            stale_cnt <= stale_cnt + 1;

          when C_RESYNC =>
            -- Enough consecutive stales that freezing is worse than guessing.
            -- Re-adopt this packet as the origin: the stream continues and
            -- the size of that one gap is lost, which is the honest outcome.
            expect_r  <= pkt_seq + 1;
            resync_r  <= '1';
            stale_cnt <= 0;
        end case;
      end if;
    end if;
  end process;
end architecture;

cls_t is a genuine distinct type with no numeric encoding at all. There is no underlying logic [2:0], so there is no way to compare a classification against an integer, and the case must cover all six values or fail analysis. The SystemVerilog enum has an encoding that can be inspected — which is occasionally useful and occasionally how a bug gets in.

stale_cnt is an integer range 0 to STALE_LIMIT. The range is part of the type, so a value outside it is an error the simulator reports rather than a silent wrap. The Verilog uses $clog2 to size a vector and gets no such check; the SystemVerilog does the same.

diff <= pkt_seq - expect_r is modular for the same reason as the Verilog — numeric_std defines subtraction on unsigned as modulo 2^N. The difference is that here it is specified by the package rather than inherited from bit-vector truncation, so the behaviour is a documented property of the operator rather than a consequence of the wire width.

9. Comparing the Three

ConcernVerilogSystemVerilogVHDL
Modular differencetruncation of a subtractionsamenumeric_std defines it on unsigned
The five outcomesnested if/else, resync inside staletypedef enum + unique casetype cls_t — no numeric encoding
The stale counter$clog2-sized vector, uncheckedsameinteger range 0 to N — range-checked
Illegal parameterisationundetectedthree $fatal guardstwo assert ... severity failure
Case exhaustivenessnot checkedunique — but see §20enforced by the type
"Do nothing this interval"no branchno branchexplicit null

All three describe the same hardware, and after the corrections described in §12 the Verilog and SystemVerilog mutation counts agree exactly.

10. The Testbenches

All three benches share one decision, and it is the strongest form of independence this module has used: the bench creates the gap, so it knows the true answer without computing it.

The design recovers the gap by modular subtraction of the sequence field. The bench never performs a modular subtraction at all. It maintains the source's next sequence number, skips n packets when it wants a gap of n, and predicts from n directly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  task automatic tx(input int n);
    tx_seq = (tx_seq + n) % SEQ_MOD;        // the SOURCE skips n packets
    pkt_seq = SEQ_W'(tx_seq); pkt_valid = 1;
    @(posedge clk); #1; pkt_valid = 0;

    if (!m_synced)      begin m_synced=1; e_inorder=1; end
    else if (n == 0)    begin e_inorder=1; end
    else if (n < HALF)  begin e_gap=1; e_gapn=n; m_lost += n; end
    else                begin e_stale=1; end     // predicted from n, not diff
    tx_seq = (tx_seq + 1) % SEQ_MOD;

There is no modular arithmetic in the model, so a modular-arithmetic bug in the design cannot be reproduced by it. That is the property that makes mutation R1 — the magnitude comparison — die by 10 608 failures rather than passing unnoticed.

The directed sequence covers the boundaries §3 and §4 identify:

ScenarioWhat it pins down
the first packetsyncs, is in-order, and counts nothing lost
an unbroken runloses nothing
a gap of 1, then 9the smallest gap, and accumulation
240 packets, then a gap of 5a gap straddling the sequence wrap
a gap of HALF−1the largest measurable gap
a skip of exactly HALFambiguous → stale, never a gap
three more consecutive stalesthe STALE_LIMIT-th resyncs
a duplicatestale; expectation and total both unmoved
bus resetsync and total cleared
600 maximal gapsthe loss total saturates
5000 randomised intervalstwo independent draws each

Two deliberate stimulus decisions deserve stating, because both were arrived at by measurement rather than by intent.

The random phase stays inside the design's stated operating range. §6's contract says burst losses below 2^(SEQ_W−1); the randomiser generates gaps in 1 … HALF−1 and never beyond. Everything outside that range is therefore directed-only by construction — which §13 shows has consequences that must be reported rather than assumed away.

The loss total is cleared periodically. Chapter 16.1 §13 measured what a saturated output does to detectability: once pinned, it cannot report a difference smaller than the distance to the pin. An early version of this bench left total_lost saturated for most of the run — 749 of its gap events landed on an already-pinned counter. Clearing it every 400 packets dropped that to 84 and put the counter back in its linear region, where defects in it are visible:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  REACH: in_order=3614 gaps=2260 stale=4 resyncs=1
         wrap-straddling=720 max_gap=127 saturations=84

720 of the 2260 gaps straddled the sequence wrap. That number is the reason R1 is detectable at all, and it is not an accident — the stimulus walks the counter continuously through the full space rather than resetting it.

11. Mutation Testing — Across All Three Languages

IDMutationVerilogSystemVerilogVHDLKilled
—baseline, no mutation000—
R1magnitude comparison instead of modular106081060810524✅ all three
R2gap size off by one226422642303✅ all three
R3a stale packet resynchronises immediately666✅ all three
R4the loss total wraps instead of saturating858585✅ all three
R5the expectation is not advanced on a gap147941479414835✅ all three
R6the halfway boundary is off by one555✅ all three

R5 is the largest at 14 794 and for a structural reason: failing to advance the expectation means every subsequent packet is misclassified, so one defect produces a permanent cascade rather than one wrong answer. R1 is second at 10 608 for the same kind of reason — it is correct until the first wrap and wrong forever after.

R4's count of 85 is worth reading against §10. The loss total is cleared every 400 packets, so the saturation boundary is crossed 84 times in the run; R4 is detected essentially once per crossing. Under the earlier bench, which left the total pinned, this mutation would have been far harder to see — the wrapped and saturated values agree whenever the counter is already at maximum.

12. Two Corrections to the Mutations Themselves

The first run of this matrix produced a compile failure and an asymmetry, and both are worth recording because neither was a defect in the design.

R3 failed to compile in SystemVerilog. The mutation replaced a guarded else if with a bare else, leaving two else clauses in the same chain. A mutation that does not compile is not a surviving mutation and must not be recorded as one — it is a broken experiment. Rewriting it as else if (1'b1) preserved the intent and compiled.

R5 initially read V=16275, SV=14794, VHDL=14835. The Verilog looked more detectable, which is the opposite of the usual direction and therefore worth investigating rather than celebrating. The cause:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        end else begin                       // Verilog: ONE arm serves BOTH
          expect_r  <= pkt_seq + 1;          //   in-order AND gap
          inorder_r <= is_inorder;

The Verilog shares the expectation update between the in-order and gap paths, while the SystemVerilog and VHDL give each its own case arm. So an unqualified removal in the Verilog broke both paths — a strictly broader mutation than the other two, not a more detectable design. Qualifying it to the gap path only brought it to 14 794, exactly matching SystemVerilog.

13. Two Mutations That Only Directed Tests Could Kill

R3 and R6 were killed by 6 and 5 failures, out of a run containing 5000 randomised intervals. Both were killed entirely by the directed phase, and the reason is a deliberate design decision in the stimulus rather than a defect:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  FAIL: a skip of exactly 128 is ambiguous: stale, never a gap   (t=2577000)
  FAIL: one stale is not yet a resync                            (t=2577000)
  FAIL: the second consecutive stale                             (t=2587000)
  FAIL: the third consecutive stale                              (t=2597000)
  FAIL: the fourth consecutive stale RE-ADOPTS an origin         (t=2607000)
  FAIL: a duplicate is stale                                     (t=2677000)

Every failure is in a 100 µs window — the directed stale-and-resync sequence — and nothing in the following 5000 random packets adds one.

This is a direct consequence of §10's decision to confine random stimulus to the design's operating range. R3 and R6 both live at or beyond the half-space boundary, and the randomiser never goes there. The stale path, the resync path and the boundary itself are directed-only by construction.

That is a defensible choice and an unacceptable thing to leave unstated. "It passed 5000 randomised intervals" is true and says nothing about R3 or R6. The randomised phase tests the design inside its contract; the directed phase is the only thing testing what happens at and beyond the contract's edge.

The alternative — randomising beyond the boundary — was tried and rejected. It requires the model to track the receiver's frozen expectation in order to predict anything, which reintroduces exactly the modular reasoning §10 removed from the model on purpose. The bench would have become a second implementation of the design, and mutation R1 would then have died against a model that shared its arithmetic.

Catalogue to date across Modules 11–16: unreachable state, shared misunderstanding, empty negative space, missing external anchor, compressed timeline, ungeneratable illegal input, unobserved layer, canonical-flow blind spot, printed-not-checked quantity, asserted-not-observed precondition, correlated stimulus variables, unrandomised secondary signal, unreachable-by-distribution boundary, sticky flag masking, clamp masking, and now the contract-edge blind spot — stimulus confined to the specified operating range cannot test behaviour outside it, and the confinement is usually invisible in the results.

14. The Stream, End to End

A sequence diagram showing how packet loss is quantified in an isochronous data acquisition stream. The sensor produces samples into the device's FIFO. The device packetises them, writing an incrementing sequence number into each payload header, and hands them to its isochronous IN endpoint. Two independent things can then go wrong. First, the device's own FIFO can overrun, so the device skips samples and its sequence number jumps; the host controller has no knowledge of this at all. Second, the host controller can fail to complete a service interval, which it records in the isochronous frame descriptor status and the error count, but which the device never learns about. The receiver takes the modular difference between the sequence number that arrived and the one it expected, and that single subtraction accounts for both kinds of loss together, producing an exact gap size. The consumer then uses the gap size to place the following samples at their correct positions in time rather than shifting the entire remaining time series.Two sources of loss, one measurementSensorDeviceHostReceiversamples at a fixedratepacketise; write seq= N, N+1, N+2 …IN payload, seq = NFIFO OVERRUN —samples skipped, seqjumpsIN payload, seq =N+9 (8 neverexisted)service intervalmissed — logged aserror_countpayload delivered,seq = N+9diff = (N+9) − (N+1)= 8 packets lostONE subtractioncovers BOTH losssources
Figure 1 — how a gap becomes a number. The device numbers every payload; the host controller reports only what it knows it dropped; the receiver's subtraction covers both that loss and losses the host never saw.

The diagram's two red and orange events are the point. A device FIFO overrun is invisible to the host; a missed service interval is invisible to the device. Neither party can report the total, and the sequence number — written by the device, read by the receiver — is the only thing that spans both.

And the final arrow is what §1 asked for. The receiver does not merely know that data is missing; it knows that exactly eight packets are missing, so the next sample goes at index i + 9 rather than i + 1. The time base survives.

15. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // A1. SAFETY, and the property §3 exists for: the reported gap equals the
  //     modular difference, computed INDEPENDENTLY in the bench from the
  //     sequence numbers observed on the interface.
  property p_gap_is_modular_difference;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      gap_valid |-> (gap_pkts == obs_modular_diff);
  endproperty
  a_gap_is_modular_difference: assert property (p_gap_is_modular_difference);

  // A2. SAFETY: the classifications are mutually exclusive. At most one of
  //     the four outcome strobes may be high in any cycle.
  property p_one_hot_outcome;
    @(posedge clk) disable iff (!rst_n)
      $onehot0({in_order, gap_valid, stale, resync});
  endproperty
  a_one_hot_outcome: assert property (p_one_hot_outcome);

  // A3. SAFETY: a stale packet moves nothing. Both the expectation and the
  //     loss total must be unchanged -- this is the property mutation R3
  //     violates, and it cannot be expressed as a check on one signal.
  property p_stale_changes_nothing;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      stale |=> $stable(expect_seq) && $stable(total_lost);
  endproperty
  a_stale_changes_nothing: assert property (p_stale_changes_nothing);

  // A4. SAFETY: a resync counts nothing lost. The gap size is unknowable, so
  //     any increment here would be a fabricated measurement.
  property p_resync_counts_nothing;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      resync |=> $stable(total_lost);
  endproperty
  a_resync_counts_nothing: assert property (p_resync_counts_nothing);

  // A5. PROGRESS: consecutive staleness cannot continue for ever. This is
  //     the property the ORIGINAL design violated -- and note that every
  //     safety property above held perfectly while it did.
  property p_stale_eventually_resyncs;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      stale |-> ##[1:STALE_LIMIT] (resync || in_order || gap_valid);
  endproperty
  a_stale_eventually_resyncs: assert property (p_stale_eventually_resyncs);

  // A6. BOUNDEDNESS: the loss total never decreases except through a reset.
  //     A wrapping counter violates this on its first wrap.
  property p_lost_monotonic;
    @(posedge clk) disable iff (!rst_n || bus_reset)
      1'b1 |=> (total_lost >= $past(total_lost));
  endproperty
  a_lost_monotonic: assert property (p_lost_monotonic);

Assertion contracts

ClaimSafety / progressVacuity riskHow non-vacuity is established
A1the gap equals the observed modular differencesafetymoderate — no gaps means vacuous2260 gaps, 720 across the wrap
A2the outcomes are mutually exclusivesafetynone — $onehot0 has no antecedentholds every cycle by construction
A3a stale packet moves neither expectation nor totalsafetyhigh — 4 stale events in the entire rundirected only; see §13
A4a resync counts nothing lostsafetyvery high — 1 resync in the entire rundirected only
A5staleness cannot persist for everprogresshigh — same antecedent as A3directed only
A6the loss total never decreasessafetylowevery gap exercises it

A5 is the assertion this chapter was written around. §4's callout describes a design that satisfied A1, A2, A3, A4 and A6 perfectly and was still unusable, because it could freeze for ever. Every safety property held; the progress property was the only one that failed — and it failed only because a test continued past the anomaly instead of stopping at it.

A3 and A4's vacuity rows are §13's finding restated. Four activations and one activation respectively, in a run of over 5000 intervals. Both properties are correct, non-trivial, and almost entirely untested by the random stimulus — and a vacuity report that merely confirmed the antecedent had occurred would have obscured exactly that.

A1 is phrased against obs_modular_diff — a difference the bench computes from sequence numbers it observed on the interface — and this is the one place in the module where the bench must do modular arithmetic. §10's tx() model deliberately avoids it; this property deliberately requires it, because the property being stated is about the arithmetic. The two are different checks with different independence properties, and the design needs both.

16. Verification: a Narrower Case for UVM Than 16.2

Chapter 16.2 made a strong case for UVM: a frame is a structure spanning hundreds of packets, loss injection is natural, and the coverage crosses are genuinely multi-dimensional.

This block is smaller than that, and the honest assessment is that UVM buys less here — but not nothing, and the part it does buy is specific.

ComponentVerdict
Sequence itemthin — one interval is a single sequence number
Driverthin — drive a number
Loss-injection sequencejustified — burst-size distributions, including deliberate excursions past HALF, are the whole test
Monitorjustified but simple — observe pkt_seq on the interface
Reference modeljustified — must track expectation and staleness without modular arithmetic, exactly as §10's bench does
Coveragethe strongest case — §13's blind spot is a coverage question

The coverage model is where this environment earns its place, and the bins follow directly from §13:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  bin:   gap_size        { 0, 1, 2..126, 127, 128, 129..255 }
  bin:   gap_straddles_sequence_wrap   { yes, no }
  cross: gap_size_class × straddles_wrap
  bin:   consecutive_stale_run_length  { 1, 2, .. STALE_LIMIT }
  bin:   resync_events
  bin:   total_lost_region { linear, saturated }

The last bin is Chapter 16.1's clamp-masking lesson turned into a standing measurement. §10 fixed the saturation problem by hand, after noticing it once. A coverage bin that distinguishes the linear region from the saturated one reports it in every regression — and would have flagged the original bench's 749 pinned events immediately.

The reference model must not do modular arithmetic, for the reason §10 gives: a model that computed (seq − expect) mod 2^N would agree with mutation R1 in every case the bench happened not to wrap through. It must derive the expected gap from the generator's own record of what it skipped. That is the same principle as Chapter 15.3's wide-integer accumulator and 16.2's packet list: the model's method must differ from the design's, not merely its code.

17. Debugging: the Instrument That Reads Correctly for Twenty-Five Seconds

A data-acquisition device streams 10 kHz samples over isochronous. The captured waveform is correct for the first twenty to thirty seconds of every session. After that the reconstructed signal shows a frequency slightly different from the known input, and the error grows. Restarting the capture restores correct behaviour — for another twenty-five seconds.

Convert the time into a count before opening anything. At 10 kHz with one packet per 125 µs microframe, 25 seconds is 200 000 packets. If the sequence field is 8 bits, that is 781 wraps — but the first wrap happened at 32 milliseconds, which is not what the symptom says.

So test the other hypothesis: what counter reaches its limit at 200 000? A 16-bit loss total saturates at 65 535; a 18-bit counter wraps at 262 144. The arithmetic points at a counter sized in the region of 2^17–2^18, not at the sequence field — and that is a very different search than "the sequence logic is broken".

This is the method §17 of Chapter 16.1 introduced, applied to a different quantity: the period of a periodic failure is a measurement. Here it does not merely confirm the diagnosis, it redirects it.

The chain, from the outside in:

Protocol analyser — are the sequence numbers in the payloads monotonic? If the device's own numbering has a discontinuity, the fault is device-side and everything downstream is behaving correctly on bad input.

Receiver statistics — does total_lost grow, and does it ever stop growing? A total that stops growing at a round binary number has saturated (correct) or wrapped (mutation R4). A total that stops at 65 535 exactly is saturated; one that returns to a small value has wrapped, and the difference is one comparison in the RTL.

Receiver statistics — does resync ever pulse? A resync means §4's limit was exceeded, and each one is a gap of unknown size that the consumer's time base cannot account for. A stream with periodic resyncs will show exactly this symptom — correct locally, drifting globally — and the fix is a wider sequence field, not different arithmetic.

RTL — the first divergence. Compare the bench's independent skip count against gap_pkts, packet by packet. The first interval where they disagree is the bug, and in practice it is one of three: a magnitude comparison that has just crossed its first wrap (R1), a boundary off by one at HALF (R6), or an expectation that failed to advance (R5).

18. Common Misconceptions

"The host's error_count tells you how much data was lost." It reports intervals the host controller failed to complete (§2). A device-side FIFO overrun is invisible to it.

"seq > expected detects a gap." It works for exactly one pass through the sequence space and then fails permanently (§3). Mutation R1 costs 10 608 failures — but only against a bench that walks through the wrap.

"Modular arithmetic needs a modulo operator." An N-bit subtraction already is modulo 2^N (§3). The defect comes from doing more, not less.

"A wider sequence field is always safer." It is safer against burst loss and costs payload bandwidth on every packet. The requirement is specific: 2^(N−1) must exceed the largest plausible burst (§4).

"A gap of 200 packets is measured as 200." With an 8-bit field it is indistinguishable from being 56 behind (§4) and must be reported as stale.

"A receiver should never resynchronise — it loses information." It loses the size of one gap. A receiver that refuses loses the entire remainder of the stream (§4), which the original design demonstrated.

"Counting the residue as lost when behind is a reasonable approximation." It is a fabricated number (§5), and mutation R3 measures the behaviour it produces.

"Passing 5000 randomised intervals means the boundary cases are covered." §13: R3 and R6 were killed entirely by directed tests, and the random phase contributed nothing by construction.

19. Exercises

1. A device sends one payload per 125 µs microframe. Compute how long an 8-bit sequence field takes to wrap, and how long a 16-bit one takes. Then determine the minimum field width for a device whose FIFO can overrun by up to 500 packets.

2. Work out what the design reports for a gap of exactly 2^SEQ_W packets, and explain why no choice of HALF can fix it. State the implication for how SEQ_W must be chosen.

3. §13 shows R3 and R6 are directed-only. Extend the randomiser to generate excursions past HALF without making the model perform modular arithmetic. If you conclude it cannot be done, say precisely which property of the model prevents it.

4. Implement A5 from §15 as a standing procedural check, then re-run against the original freeze-for-ever design described in §4. Confirm it fires and that A1–A4 and A6 do not.

5. The loss total saturates. §10 clears it periodically to keep it in its linear region. Design a mechanism the hardware could provide — not the bench — that preserves the information a saturated counter discards, and say what it costs.

6. Combine this chapter's sequence number with 16.2's FID. Determine whether a device streaming video needs both, and what each still provides that the other does not.

7. §17's debugging case converts 25 seconds into 200 000 packets and concludes the sequence field is not the culprit. Work out which counters in this design have a modulus near 200 000, and what you would inspect first.

20. Summary

A video receiver needs to know that a boundary occurred; a sensor receiver needs to know how large the gap was (§1), because sample position is part of the measurement and an unquantified gap corrupts the time base silently.

The mechanism is a device-written sequence number and a subtraction (§2) — and it covers both loss sources, the host-reported missed interval and the device-side FIFO overrun the host never sees.

The subtraction must be modular, and in hardware it already is (§3). An N-bit subtractor wraps by construction; the defect is doing something extra, like comparing magnitudes. That defect works perfectly until the first wrap and then fails permanently — mutation R1, 10 608 failures, detectable only because the bench walks through the wrap 720 times.

Modular arithmetic imposes a limit that cannot be engineered away (§4). A difference at or beyond half the sequence space is indistinguishable from being behind, so the largest measurable gap is 2^(N−1) − 1, and a gap of exactly 2^N is invisible.

Beyond that limit, freezing is not an option (§4). The original design classified over-large bursts as stale and froze its expectation — and satisfied every safety property while being unusable, because the stream never recovered. The testbench found it by continuing past the anomaly; a resync escape hatch was added, and §15's A5 is the progress property that states it.

All three HDL implementations were simulated, including the VHDL (§20). Six mutations died in all three languages (§11), and two apparent asymmetries were investigated rather than recorded: one was a mutation that did not compile, one was a mutation placed more broadly in the Verilog because that design shares a branch the others separate (§12). Neither was a language difference; neither was a design defect — unlike 16.2, where the same kind of investigation found one.

And two mutations were killed entirely by directed tests (§13), because the random stimulus is deliberately confined to the design's stated operating range. That is a defensible choice and an unacceptable thing to leave unstated — the stale path, the resync path, and the half-space boundary are directed-only by construction, and "5000 randomised intervals passed" says nothing whatever about them.

21. Tooling, Honestly

LanguageDesignTestbenchAnalysed / compiledSimulatedMutations
Verilog-2005iso_stream_gap_trackergap_v_tb.v✅ Icarus -g2005✅ 0 errors✅ all six
SystemVerilogiso_stream_gap_tracker_svgap_sv_tb.sv✅ Icarus -g2012✅ 0 errors✅ all six
VHDL-2008iso_stream_gap_tracker_vhdlgap_vhdl_tb.vhd✅ nvc 1.23.0✅ 0 errors✅ all six
SVA (§15)——❌ unsupported by Icarus❌—
unique case——✅ accepted⚠️ quality ignored—

One mutation was discarded and rewritten rather than recorded (§12): R3's first SystemVerilog form did not compile. A mutation that fails to build is a broken experiment, not a surviving mutation, and recording it as either "killed" or "survived" would have been false.

22. What Comes Next

The last three chapters have all assumed the same thing without examining it: that the endpoint gets its slot. 16.1's feedback loop, 16.2's frame assembly and this chapter's sequence arithmetic all begin after the host has already agreed to carry the stream.

Chapter 16.4 is where that agreement is made or refused. An isochronous endpoint does not ask politely at run time — it declares a requirement in its descriptors, and the host either admits it or fails the configuration outright. There is no degraded mode, no best-effort fallback, and no retry: a device whose reservation cannot be met does not run slowly, it does not run.

That makes the admission decision a piece of arithmetic with a hard consequence, and the arithmetic is more involved than the packet size suggests — because what is reserved is time on the wire, not bytes in a buffer.

Browse the full path on the USB tutorials index.

Continue learning

Standards & specifications

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

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

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

Where this fits

Part of the USB curriculum.