Skip to content
VLSI Mentor

USB · Module 29

USB Webcams

UVC streams video over isochronous transfers, which have no retries. With only a frame-ID bit and an end-of-frame flag for framing, exactly 1 packet loss in P is detectable — 6% for a 16-packet frame, and under 1% for a real one.

The second case study, and the opposite shape from the first. A flash drive is a decision table with fatal cells. A webcam is a firehose with no error handling at all — because there is nothing to handle it with.

1. Isochronous Means Exactly One Thing

Bandwidth is reserved. Delivery is not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    BULK           reserved bandwidth: none
                   delivery: guaranteed, by retrying until it works

    ISOCHRONOUS    reserved bandwidth: guaranteed, every frame
                   delivery: best effort, and that is the end of it

There is no ACK, no NAK, no retry and no retransmission. A packet that goes missing is gone, and nothing on the bus will ever mention it again. That is not a weakness in the mode — it is the point of it. A camera cannot use a retransmission that arrives after the frame has been displayed, so paying for one would be paying for nothing.

2. The Two Ways A Frame Can End

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    ended on EOF            the normal way. It may still be SHORT -- a packet
                            was lost mid-frame -- and nothing says so.

    ended on a FID toggle   the EOF packet itself never arrived, so the next
                            frame's toggle is what closed this one. The
                            receiver CAN see this.

Everything follows from that asymmetry:

what was lostwhat the receiver seescan it tell?
a mid-frame packet with dataa frame that ended on EOF, short by those bytesno
the EOF packeta frame that ended on a FID toggleyes
a zero-length packetnothing at all — the stream is bit-identicalnothing to tell

That last row is not a loophole. Zero-length isochronous packets are exactly how a device says "nothing this microframe" while keeping its bandwidth reservation alive, and a protocol in which losing one mattered would be a worse protocol.

3. The Design (Verilog-2005)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  uvc_frame_asm -- assembling video frames from a stream that has no
//  retries, no acknowledgements and no sequence numbers.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  This is NOT a webcam. There is no sensor, no compression, no format
//  negotiation and no still-image path. It is the part of a UVC receiver
//  that decides where one frame ends and the next begins.
//
//  WHY THIS MECHANISM IS WORTH RTL
//  -------------------------------
//  A webcam streams over ISOCHRONOUS transfers, and isochronous means
//  exactly one thing: bandwidth is reserved and delivery is not.
//  There is no ACK, no NAK, no retry and no CRC-driven retransmission.
//  A packet that goes missing is simply gone, and nothing on the bus
//  will ever mention it again.
//
//  So the receiver has to work out the frame structure from what did
//  arrive. UVC gives it exactly two bits to do that with, in the payload
//  header on every packet:
//
//      FID   a frame-ID bit that TOGGLES between consecutive frames
//      EOF   set on the last packet of a frame
//
//  That is the entire framing mechanism. There is no per-packet sequence
//  number, no frame length field, and no byte counter to check against.
//
//  THE CONSEQUENCE, WHICH IS THE POINT OF THIS CHAPTER
//  ---------------------------------------------------
//  With only those two bits, a lost packet is detectable ONLY if it
//  happened to be the one carrying EOF:
//
//    * lose the EOF packet, and the frame never terminates -- the next
//      frame's FID toggle is what ends it, and the receiver can see that
//      it ended the wrong way. DETECTABLE.
//
//    * lose any other packet, and the frame still gets its EOF. It is
//      short by however many bytes were in the lost packet, and there is
//      nothing in the stream that says so. SILENT.
//
//  One packet in P is the EOF packet, so the detectable fraction is 1/P.
//  For a frame split into eight packets that is 12.5%; for a real
//  1080p frame, which is hundreds of packets, it is indistinguishable
//  from zero.
//
//  That is not a defect in this module and it is not a defect in UVC. It
//  is what "no retries" costs, and it is why the error bit below exists
//  and why real hosts throw away frames on the slightest suspicion.
// =====================================================================
module uvc_frame_asm (
  input  wire        clk,
  input  wire        rst_n,

  // ---- one isochronous packet that ARRIVED ----
  //
  // Packets that were lost are simply never presented. The module has no
  // way to know they existed, which is the whole situation being
  // modelled.
  input  wire        pkt_valid,
  input  wire        pkt_fid,        // bmHeaderInfo bit 0, toggles per frame
  input  wire        pkt_eof,        // bmHeaderInfo bit 1, last packet
  input  wire        pkt_err,        // bmHeaderInfo bit 6, device says this is bad
  input  wire [15:0] pkt_bytes,      // payload bytes after the header

  // ---- a completed frame ----
  output wire        frame_done,
  output wire [31:0] frame_bytes,
  output wire        frame_fid,
  // Did this frame end the way a frame is supposed to end?
  //
  // `frame_eof` distinguishes the two terminations, and it is the ONLY
  // evidence of loss the receiver ever gets.
  output wire        frame_eof,      // ended on EOF -- looked normal
  output wire        frame_flagged,  // the device marked a packet bad

  output wire [31:0] n_pkt,
  output wire [31:0] n_frame,
  output wire [31:0] n_truncated,    // ended by a FID toggle, not by EOF
  output wire [31:0] n_flagged,
  output wire [31:0] n_toggle
);

  reg        started;     // a frame is currently open
  reg        cur_fid;     // the FID of the open frame
  reg [31:0] acc;         // bytes accumulated into the open frame
  reg        acc_err;     // any packet in it carried the error bit

  reg        done_r, fid_r, eof_r, flag_r;
  reg [31:0] bytes_r;

  reg [31:0] pkt_c, frame_c, trunc_c, flag_c, tog_c;

  assign frame_done    = done_r;
  assign frame_bytes   = bytes_r;
  assign frame_fid     = fid_r;
  assign frame_eof     = eof_r;
  assign frame_flagged = flag_r;
  assign n_pkt         = pkt_c;
  assign n_frame       = frame_c;
  assign n_truncated   = trunc_c;
  assign n_flagged     = flag_c;
  assign n_toggle      = tog_c;

  // A packet whose FID differs from the open frame's means the previous
  // frame ended without ever presenting EOF. That is the only loss signal
  // this protocol has.
  wire fid_toggled = started && (pkt_fid != cur_fid);

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      started  <= 1'b0;
      cur_fid  <= 1'b0;
      acc      <= 32'd0;
      acc_err  <= 1'b0;
      done_r   <= 1'b0;
      fid_r    <= 1'b0;
      eof_r    <= 1'b0;
      flag_r   <= 1'b0;
      bytes_r  <= 32'd0;
      pkt_c    <= 32'd0;
      frame_c  <= 32'd0;
      trunc_c  <= 32'd0;
      flag_c   <= 32'd0;
      tog_c    <= 32'd0;
    end else begin
      done_r <= 1'b0;

      if (pkt_valid) begin
        pkt_c <= pkt_c + 32'd1;

        if (fid_toggled) begin
          // ---- the previous frame ended the WRONG way ----
          //
          // It never showed EOF, so the packet carrying EOF did not
          // arrive. Close it out and report it as truncated -- this is
          // the one loss the receiver can actually see.
          done_r  <= 1'b1;
          bytes_r <= acc;
          fid_r   <= cur_fid;
          eof_r   <= 1'b0;
          flag_r  <= acc_err;
          frame_c <= frame_c + 32'd1;
          trunc_c <= trunc_c + 32'd1;
          tog_c   <= tog_c + 32'd1;

          // and the arriving packet opens the next one
          started <= 1'b1;
          cur_fid <= pkt_fid;
          acc     <= {16'd0, pkt_bytes};
          acc_err <= pkt_err;
          if (pkt_err) flag_c <= flag_c + 32'd1;

        end else if (!started) begin
          // ---- first packet of a new frame ----
          started <= 1'b1;
          cur_fid <= pkt_fid;
          acc     <= {16'd0, pkt_bytes};
          acc_err <= pkt_err;
          if (pkt_err) flag_c <= flag_c + 32'd1;
          if (pkt_eof) begin
            // a single-packet frame
            done_r  <= 1'b1;
            bytes_r <= {16'd0, pkt_bytes};
            fid_r   <= pkt_fid;
            eof_r   <= 1'b1;
            flag_r  <= pkt_err;
            frame_c <= frame_c + 32'd1;
            started <= 1'b0;
          end

        end else begin
          // ---- a continuation packet of the open frame ----
          acc     <= acc + {16'd0, pkt_bytes};
          acc_err <= acc_err | pkt_err;
          if (pkt_err) flag_c <= flag_c + 32'd1;
          if (pkt_eof) begin
            // ---- the frame ended the RIGHT way ----
            //
            // Note what is NOT checked here, because it cannot be: there
            // is no length field and no sequence number, so a frame that
            // is short by one lost packet arrives here looking exactly
            // like a complete one.
            done_r  <= 1'b1;
            bytes_r <= acc + {16'd0, pkt_bytes};
            fid_r   <= cur_fid;
            eof_r   <= 1'b1;
            flag_r  <= acc_err | pkt_err;
            frame_c <= frame_c + 32'd1;
            started <= 1'b0;
          end
        end
      end
    end
  end

endmodule

Two bits of framing, and the two things they can tell you

Isochronous packets arrive carrying only a frame-ID bit and an end-of-frame flag. A frame that ends on its EOF flag is reported as complete even when packets were lost from the middle of it, because there is no length field to check against. A frame whose EOF packet was lost ends instead on the next frame's frame-ID toggle, which is the only evidence of loss the protocol provides.Isochronous inno ACK, no retryPayload headerFID + EOF, and nothingelseFrame assembleraccumulate until itendsEnded on EOFreported completeEnded on togglethe EOF never cameSilently shortno length to checkper pkt2 bitsnormalorhides in12
Read the bottom row. A frame that ends on EOF is reported complete whether or not it is, because nothing in the stream carries a length to check it against. Only the missing-EOF path produces evidence.

4. The Measurement

The bench knows what was sent. The design only knows what arrived. That asymmetry is the experiment: the bench generates a frame sequence, decides which packet to drop, presents only the survivors, and compares the design's view of each frame against the truth it is holding back.

The headline

Drop every droppable packet in turn, and count how many of those losses the receiver could have detected. Identical in Verilog, SystemVerilog and VHDL:

packets per framelosses trieddetectablesilentdetectable
242250%
482625%
81621412%
16322306%

The detectable column is 2 in every row — one EOF packet per frame, two frames with a following frame to reveal them. Everything else scales, so the fraction is exactly 1/P.

Where the numbers come from

The directed sweep, identical across all three languages:

stepschecksreachlossesdetectedsilenterrors
directed only4,5021,920102 / 102156201280

156 losses, 20 detected, 128 silent — and 8 that damaged nothing at all, because the packet thrown away was zero-length.

The exhaustive dimension set:

dimensionvalues
which packet is lostnone, or any of 16
device error flagset, clear
payload profilethree sizes, including zero-length packets

17 × 2 × 3 = 102 points, every one reachable.

The same loss, one packet apart, with completely different visibility

Two four-packet video frames. In the first frame a middle packet is lost, the frame still ends on its end-of-frame flag and is reported complete with a reduced byte count, so the loss is invisible. In the second frame the end-of-frame packet itself is lost, so the frame is closed by the following frame's frame-ID toggle and frame_eof is reported low, which is the only detectable signature.a frame that ended righta frame that ended rightEOF lost: visibleEOF lost: visibleback to normalback to normalframe 0: EOF, looks completeframe 0: EOF, lookscompleteframe 1: closed by FID toggleframe 1: closed by FIDtoggleframe 2 ends normallyframe 2 ends normallyclkpkt_validpkt_fid0001110000pkt_eofpkt_bytes121212121212121200frame_doneframe_eofframe_bytes00036363636362424t0t1t2t3t4t5t6t7t8t9
Two four-packet frames. In the first, packet 1 is lost: the frame still ends on EOF and is reported complete at 24 bytes instead of 36. In the second, the EOF packet is lost: the frame ends on the next FID toggle instead, and frame_eof goes low — the only evidence the protocol ever produces.

5. The Testbench (Verilog)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for uvc_frame_asm.
//
//  THE BENCH KNOWS WHAT WAS SENT. THE DESIGN ONLY KNOWS WHAT ARRIVED.
//
//  That asymmetry is the entire experiment. The bench generates a frame
//  sequence, decides which packet to drop, presents only the survivors,
//  and then compares the design's view of each frame against the truth
//  it is holding back.
//
//  Three outcomes are possible per frame, and telling them apart is the
//  measurement:
//
//      CLEAN     every packet arrived; bytes match the truth
//      SILENT    a packet was lost mid-frame. The EOF still arrived, so
//                the frame looks complete and is short. The DESIGN
//                cannot tell. The bench can, because it knows the truth.
//      DETECTED  the EOF packet itself was lost, so the frame ended on
//                the next frame's FID toggle instead. The design CAN
//                tell.
//
//  A packet is the EOF packet one time in P, so the detectable fraction
//  is 1/P -- and the whole point of the headline sweep is that this is a
//  property of the protocol, not of the implementation.
// =====================================================================
`timescale 1ns/1ps
module tb_uv_v;

  reg clk = 1'b0, rst_n = 1'b0;
  always #5 clk = ~clk;

  reg         pkt_valid = 1'b0, pkt_fid = 1'b0, pkt_eof = 1'b0, pkt_err = 1'b0;
  reg  [15:0] pkt_bytes = 16'd0;

  wire        frame_done, frame_fid, frame_eof, frame_flagged;
  wire [31:0] frame_bytes;
  wire [31:0] n_pkt, n_frame, n_truncated, n_flagged, n_toggle;

  uvc_frame_asm dut (
    .clk(clk), .rst_n(rst_n),
    .pkt_valid(pkt_valid), .pkt_fid(pkt_fid), .pkt_eof(pkt_eof),
    .pkt_err(pkt_err), .pkt_bytes(pkt_bytes),
    .frame_done(frame_done), .frame_bytes(frame_bytes),
    .frame_fid(frame_fid), .frame_eof(frame_eof),
    .frame_flagged(frame_flagged),
    .n_pkt(n_pkt), .n_frame(n_frame), .n_truncated(n_truncated),
    .n_flagged(n_flagged), .n_toggle(n_toggle)
  );

  integer errors = 0, checks = 0, steps = 0;
  integer seed;

  // $random is SIGNED: mask the sign bit before any modulo.
  function [31:0] urand;
    input dummy;
    begin urand = $random(seed) & 32'h3FFF_FFFF; end
  endfunction

  task ck(input cond, input [255:0] what);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 20)
          $display("  ERROR @%0t step#%0d: %0s", $time, steps, what);
      end
    end
  endtask

  // ---- GROUND TRUTH, invisible to the design ----
  //
  // What each frame SHOULD have contained, and what actually reached the
  // receiver. The difference between the two is the damage.
  // Sized for the LONGEST sequence any phase runs, not for the shortest.
  // These held 8 entries while phase 3 streams 16 frames and indexes
  // got_eof[k] up to 15: Verilog returns x for that and says nothing,
  // which is how it survived. VHDL makes the same read fatal.
  integer true_bytes [0:31];   // bytes the sender put in frame f
  integer sent_bytes [0:31];   // bytes that actually arrived for frame f
  integer true_eof   [0:31];   // did frame f's EOF packet arrive?

  // ---- what the design reported, captured as it reports it ----
  integer got_n;
  integer got_bytes [0:31];
  integer got_fid   [0:31];
  integer got_eof   [0:31];
  // Captured WITH the frame, not read afterwards. frame_flagged is only
  // meaningful in the cycle frame_done is asserted; reading it later reads
  // whatever the previous frame left behind.
  integer got_flag  [0:31];

  // ---- the measurement ----
  // m_* are PER-RUN flags: phase 2 zeroes them before each sequence so it
  // can ask whether that particular loss was detectable. t_* are the
  // cumulative totals, which nothing zeroes -- keeping them separate is
  // what stops a directed-only run reporting zero losses.
  integer m_clean = 0, m_silent = 0, m_detected = 0, m_losses = 0;
  integer t_clean = 0, t_silent = 0, t_detected = 0, t_losses = 0;

  // ---- cumulative across resets ----
  //
  // reset_dut runs once per sequence, so the DUT's own counters describe
  // only the last one. Per-step checks still read the DUT counters; these
  // are for the totals.
  integer c_pkt = 0, c_frame = 0, c_trunc = 0, c_flag = 0;

  // Per packets-per-frame, how many single-packet losses were detectable.
  integer tab_p     [0:4];
  integer tab_total [0:4];
  integer tab_det   [0:4];

  task reset_dut;
    integer j;
    begin
      rst_n = 1'b0; pkt_valid = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      got_n = 0;
      for (j = 0; j < 32; j = j + 1) begin
        true_bytes[j] = 0; sent_bytes[j] = 0; true_eof[j] = 0;
        got_bytes[j] = 0; got_fid[j] = 0; got_eof[j] = 0; got_flag[j] = 0;
      end
    end
  endtask

  // Present one packet and collect any frame the design closes out.
  task send_pkt(input fid, input eof, input err, input [15:0] bytes);
    begin
      pkt_valid = 1'b1; pkt_fid = fid; pkt_eof = eof;
      pkt_err = err;    pkt_bytes = bytes;
      @(posedge clk); #1;
      pkt_valid = 1'b0;
      c_pkt = c_pkt + 1;
      if (frame_done) begin
        c_frame = c_frame + 1;
        if (!frame_eof)   c_trunc = c_trunc + 1;
        if (frame_flagged) c_flag = c_flag + 1;
        if (got_n < 32) begin
          got_bytes[got_n] = frame_bytes;
          got_fid[got_n]   = frame_fid;
          got_eof[got_n]   = frame_eof;
          got_flag[got_n]  = frame_flagged;
        end
        got_n = got_n + 1;
      end
      steps = steps + 1;
    end
  endtask

  // Let the design settle and collect a trailing frame, if any.
  task drain;
    integer g;
    begin
      for (g = 0; g < 4; g = g + 1) begin
        @(posedge clk); #1;
        if (frame_done) begin
          c_frame = c_frame + 1;
          if (!frame_eof)    c_trunc = c_trunc + 1;
          if (frame_flagged) c_flag = c_flag + 1;
          if (got_n < 32) begin
            got_bytes[got_n] = frame_bytes;
            got_fid[got_n]   = frame_fid;
            got_eof[got_n]   = frame_eof;
            got_flag[got_n]  = frame_flagged;
          end
          got_n = got_n + 1;
        end
      end
    end
  endtask

  // -------------------------------------------------------------------
  //  Run three frames of P packets, dropping ONE packet if `lost` >= 0.
  //
  //  Frames 0 and 1 are the ones a loss is injected into, so there is
  //  always a following frame whose FID toggle could reveal a missing
  //  EOF. Frame 2 always completes, which keeps the experiment about
  //  detectability rather than about the end of the stream.
  // -------------------------------------------------------------------
  task run_seq(input integer P, input integer lost, input integer base,
               input errbit);
    integer f, p, idx, sz;
    integer exp_frames, e_det, e_silent;
    integer lost_sz, lost_was_eof;
    begin
      reset_dut;
      lost_sz = 0; lost_was_eof = 0;

      for (f = 0; f < 3; f = f + 1) begin
        true_bytes[f] = 0; sent_bytes[f] = 0; true_eof[f] = 0;
      end

      idx = 0;
      for (f = 0; f < 3; f = f + 1) begin
        for (p = 0; p < P; p = p + 1) begin
          sz = base + ((p % 4) * 4);
          true_bytes[f] = true_bytes[f] + sz;
          if (idx != lost) begin
            sent_bytes[f] = sent_bytes[f] + sz;
            if (p == P - 1) true_eof[f] = 1;
            send_pkt(f[0], (p == P - 1), errbit && (p == 0), sz[15:0]);
          end else begin
            // remember what was thrown away, so the damage can be
            // predicted rather than merely bounded
            lost_sz      = sz;
            lost_was_eof = (p == P - 1);
          end
          idx = idx + 1;
        end
      end
      drain;

      // ---- what SHOULD have been reported ----
      //
      // Derived from the truth arrays, which the design cannot see. Three
      // frames are always emitted: frame 2 always completes, and frames 0
      // and 1 each close either on their own EOF or on the following
      // frame's FID toggle.
      exp_frames = 3;
      ck(got_n == exp_frames, "the wrong number of frames was reported");

      e_det = 0; e_silent = 0;
      for (f = 0; f < 3 && f < got_n; f = f + 1) begin
        // ---- PROPERTY 1: the FID alternates, frame by frame ----
        ck(got_fid[f] == (f % 2),
           "a frame was reported with the wrong frame ID");
        // ---- PROPERTY 2: a frame ends on EOF exactly when its EOF arrived ----
        //
        // This is the design's ONLY loss signal, so it has to be exact.
        ck(got_eof[f] == true_eof[f],
           "frame_eof does not say whether the EOF packet arrived");
        // ---- PROPERTY 3: the byte count is what ARRIVED ----
        //
        // Not what was sent. The design is not wrong to report fewer
        // bytes; it is reporting what it received, which is all it has.
        ck(got_bytes[f] == sent_bytes[f],
           "the frame byte count does not match the bytes that arrived");

        // ---- THE MEASUREMENT ----
        //
        // Classify the damage from the TRUTH, then record whether the
        // design could have known.
        if (sent_bytes[f] != true_bytes[f] || true_eof[f] == 0) begin
          if (got_eof[f] == 0) e_det = e_det + 1;     // ended wrong: visible
          else                 e_silent = e_silent + 1; // looked fine: invisible
        end
      end

      if (lost >= 0) begin
        m_losses = m_losses + 1;
        // ---- PROPERTY 4: the damage is exactly predictable ----
        //
        // Not merely "at most one frame". Which of the three outcomes
        // occurs is decided entirely by two facts the bench knows about
        // the packet it threw away:
        //
        //   it carried EOF          -> the frame ends on the next FID
        //                              toggle instead. DETECTED.
        //   it carried bytes        -> the frame still gets its EOF and
        //                              is silently short. SILENT.
        //   it carried NEITHER      -> a zero-length packet that was not
        //                              the last one. Losing it costs
        //                              nothing at all, and the stream is
        //                              bit-identical to one where it was
        //                              never sent.
        //
        // That last case is not a loophole. Zero-length isochronous
        // packets are exactly how a device says "nothing this microframe"
        // while keeping its bandwidth reservation alive, and a protocol
        // in which losing one mattered would be a worse protocol.
        //
        // The first version of this check asserted "exactly one frame
        // damaged" and fired 30 times against a correct design, all of
        // them zero-byte losses.
        if (lost_was_eof) begin
          ck(e_det == 1 && e_silent == 0,
             "losing the EOF packet was not reported as a truncated frame");
        end else if (lost_sz > 0) begin
          ck(e_silent == 1 && e_det == 0,
             "losing a mid-frame packet was not a silently short frame");
        end else begin
          ck(e_det + e_silent == 0,
             "losing a zero-length packet damaged a frame");
        end
        if (e_det > 0)    begin m_detected = m_detected + 1; t_detected = t_detected + 1; end
        if (e_silent > 0) begin m_silent   = m_silent + 1;   t_silent   = t_silent + 1;   end
        t_losses = t_losses + 1;
      end else begin
        // ---- PROPERTY 5: with nothing lost, nothing is damaged ----
        //
        // The false-positive half. A detector that flags loss on a clean
        // stream gets switched off, and then detects nothing at all.
        ck(e_det + e_silent == 0,
           "a frame was reported damaged on a stream with no loss");
        m_clean = m_clean + 1; t_clean = t_clean + 1;
      end
    end
  endtask

  // ---- exhaustive reach ----
  //
  // loss position (none + 16) x error bit (2) x payload base (3) = 102,
  // at P = 8. Every dimension is an independent input.
  reg reach [0:101];
  integer nr, ri;

  integer li, eb, bs, pi, k, P, idx2;
  integer PVAL [0:3];

  initial begin
    for (ri = 0; ri < 102; ri = ri + 1) reach[ri] = 1'b0;
    for (k = 0; k < 5; k = k + 1) begin
      tab_p[k] = 0; tab_total[k] = 0; tab_det[k] = 0;
    end
    PVAL[0] = 2; PVAL[1] = 4; PVAL[2] = 8; PVAL[3] = 16;
    seed = 32'd29002;

    reset_dut;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every single-packet loss.
    //
    //  P = 8, three frames, so sixteen droppable positions plus the
    //  no-loss case, crossed with the error bit and three payload
    //  profiles. 17 x 2 x 3 = 102.
    // =============================================================
    for (li = -1; li < 16; li = li + 1)
    for (eb = 0; eb < 2; eb = eb + 1)
    for (bs = 0; bs < 3; bs = bs + 1) begin
      run_seq(8, li, bs * 8, eb[0]);
      ri = ((li + 1) * 2 + eb) * 3 + bs;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE per P) -- THE HEADLINE.
    //
    //  For each packets-per-frame, drop every droppable packet in turn
    //  and count how many of those losses the receiver could detect.
    //
    //  This is a property of the PROTOCOL: exactly one packet per frame
    //  carries EOF, so exactly one loss per frame is visible. The sweep
    //  measures it rather than asserting it.
    // =============================================================
    for (pi = 0; pi < 4; pi = pi + 1) begin
      P = PVAL[pi];
      tab_p[pi] = P;
      for (li = 0; li < 2 * P; li = li + 1) begin
        m_detected = 0; m_silent = 0;
        run_seq(P, li, 8, 1'b0);
        tab_total[pi] = tab_total[pi] + 1;
        if (m_detected > 0) tab_det[pi] = tab_det[pi] + 1;
      end
    end
    m_detected = 0; m_silent = 0; m_losses = 0; m_clean = 0;

    // =============================================================
    //  PHASE 3 (DIRECTED) -- a long clean stream.
    //
    //  Sixteen frames, nothing lost. Every frame must end on EOF with
    //  the full byte count and an alternating FID. This is the
    //  false-positive test: a loss detector that fires here is worse
    //  than none.
    // =============================================================
    reset_dut;
    for (k = 0; k < 16; k = k + 1) begin : clean
      integer p2, tot;
      tot = 0;
      for (p2 = 0; p2 < 4; p2 = p2 + 1) begin
        tot = tot + 8 + p2;
        send_pkt(k[0], (p2 == 3), 1'b0, (8 + p2));
      end
      drain;
      ck(got_n == k + 1, "a clean stream lost a frame");
      if (got_n == k + 1) begin
        ck(got_eof[k] == 1, "a clean frame did not end on EOF");
        ck(got_bytes[k] == tot, "a clean frame reported the wrong byte count");
        ck(got_fid[k] == (k % 2), "a clean frame had the wrong frame ID");
      end
    end

    // =============================================================
    //  PHASE 4 (DIRECTED, EXHAUSTIVE) -- the device's own error bit.
    //
    //  The one loss signal that does NOT depend on the framing bits: a
    //  device that knows it dropped data can say so. Swept over which
    //  packet of the frame carries it, because a flag on the last packet
    //  and a flag on the first must both survive to the frame report.
    // =============================================================
    for (pi = 0; pi < 3; pi = pi + 1)
    for (k = -1; k < PVAL[pi]; k = k + 1) begin : errsweep
      integer p3;
      reset_dut;
      P = PVAL[pi];
      for (p3 = 0; p3 < P; p3 = p3 + 1)
        send_pkt(1'b0, (p3 == P - 1), (p3 == k), 16'd8);
      drain;
      ck(got_n == 1, "the flagged frame was not reported");
      if (got_n == 1) begin
        // ---- PROPERTY 6: the flag reaches the frame from ANY packet ----
        //
        // The device sets it on whichever packet it knows is damaged --
        // the first, the last, or one in the middle. A receiver that only
        // honours it on the first packet works on the case people
        // demonstrate and fails on the ones that happen.
        //
        // k = -1 is the no-flag control. Without it this property would be
        // satisfied by a receiver that flagged every frame.
        ck(got_flag[0] == ((k >= 0) ? 1 : 0),
           "the frame error flag does not reflect the packet that carried it");
      end
    end

    // =============================================================
    //  PHASE 4b (DIRECTED) -- FRAMES THAT ARE ONE PACKET LONG.
    //
    //  A frame whose first packet also carries EOF takes a branch nothing
    //  else reaches. It is not a corner case invented for coverage: a
    //  still-image capture arrives on the same endpoint as the video
    //  stream and is routinely a single payload, and a small enough video
    //  frame is one too.
    //
    //  Without this phase the branch was reached only when a packet loss
    //  happened to reduce a two-packet frame to one, which gave the
    //  mutation that breaks it a domain of four.
    // =============================================================
    reset_dut;
    for (k = 0; k < 8; k = k + 1) begin : singles
      send_pkt(k[0], 1'b1, (k % 3 == 0), 16'd64);
      ck(got_n == k + 1, "a single-packet frame was not closed out");
      if (got_n == k + 1) begin
        ck(got_eof[k] == 1, "a single-packet frame did not end on EOF");
        ck(got_bytes[k] == 64, "a single-packet frame reported the wrong size");
        ck(got_fid[k] == (k % 2), "a single-packet frame had the wrong frame ID");
        ck(got_flag[k] == ((k % 3 == 0) ? 1 : 0),
           "a single-packet frame lost its error flag");
      end
    end

    // =============================================================
    //  PHASE 5 (RANDOM)
    // =============================================================
`ifndef DIRECTED_ONLY
    for (k = 0; k < 400; k = k + 1) begin
      P = 2 + (urand(0) % 7);
      li = (urand(0) % 3 == 0) ? -1 : (urand(0) % (2 * P));
      run_seq(P, li, (urand(0) % 3) * 8, (urand(0) % 5) == 0);
    end
`endif

    nr = 0; for (ri = 0; ri < 102; ri = ri + 1) if (reach[ri]) nr = nr + 1;

    $display("steps=%0d checks=%0d reach=%0d/102 errors=%0d",
             steps, checks, nr, errors);
    $display("[uvc] packets=%0d frames=%0d truncated=%0d flagged_frames=%0d",
             c_pkt, c_frame, c_trunc, c_flag);
    $display("[uvc] losses injected=%0d  detected=%0d  SILENT=%0d  clean runs=%0d",
             t_losses, t_detected, t_silent, t_clean);
    $display("--- what fraction of a single packet loss is even visible? ---");
    $display("    packets/frame   losses   detectable   silent   detectable %%");
    for (k = 0; k < 4; k = k + 1)
      $display("    %13d %8d %12d %8d %11d",
               tab_p[k], tab_total[k], tab_det[k], tab_total[k] - tab_det[k],
               (tab_det[k] * 100) / tab_total[k]);
    if (nr != 102) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

6. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  uvc_frame_asm -- SystemVerilog.
//
//  Same hardware contract as the Verilog file: same ports, same widths,
//  same reset values, same cycle-by-cycle behaviour. `always_ff` replaces
//  `always @(posedge ...)`, and the one continuous assignment is written
//  as `logic` + `assign` on separate lines rather than as an initialised
//  declaration -- `logic x = expr;` is a one-shot variable initialiser in
//  SystemVerilog, not a continuous assignment.
//
//  uvc_frame_asm -- assembling video frames from a stream that has no
//  retries, no acknowledgements and no sequence numbers.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  This is NOT a webcam. There is no sensor, no compression, no format
//  negotiation and no still-image path. It is the part of a UVC receiver
//  that decides where one frame ends and the next begins.
//
//  WHY THIS MECHANISM IS WORTH RTL
//  -------------------------------
//  A webcam streams over ISOCHRONOUS transfers, and isochronous means
//  exactly one thing: bandwidth is reserved and delivery is not.
//  There is no ACK, no NAK, no retry and no CRC-driven retransmission.
//  A packet that goes missing is simply gone, and nothing on the bus
//  will ever mention it again.
//
//  So the receiver has to work out the frame structure from what did
//  arrive. UVC gives it exactly two bits to do that with, in the payload
//  header on every packet:
//
//      FID   a frame-ID bit that TOGGLES between consecutive frames
//      EOF   set on the last packet of a frame
//
//  That is the entire framing mechanism. There is no per-packet sequence
//  number, no frame length field, and no byte counter to check against.
//
//  THE CONSEQUENCE, WHICH IS THE POINT OF THIS CHAPTER
//  ---------------------------------------------------
//  With only those two bits, a lost packet is detectable ONLY if it
//  happened to be the one carrying EOF:
//
//    * lose the EOF packet, and the frame never terminates -- the next
//      frame's FID toggle is what ends it, and the receiver can see that
//      it ended the wrong way. DETECTABLE.
//
//    * lose any other packet, and the frame still gets its EOF. It is
//      short by however many bytes were in the lost packet, and there is
//      nothing in the stream that says so. SILENT.
//
//  One packet in P is the EOF packet, so the detectable fraction is 1/P.
//  For a frame split into eight packets that is 12.5%; for a real
//  1080p frame, which is hundreds of packets, it is indistinguishable
//  from zero.
//
//  That is not a defect in this module and it is not a defect in UVC. It
//  is what "no retries" costs, and it is why the error bit below exists
//  and why real hosts throw away frames on the slightest suspicion.
// =====================================================================
module uvc_frame_asm (
  input  logic        clk,
  input  logic        rst_n,

  // ---- one isochronous packet that ARRIVED ----
  //
  // Packets that were lost are simply never presented. The module has no
  // way to know they existed, which is the whole situation being
  // modelled.
  input  logic        pkt_valid,
  input  logic        pkt_fid,        // bmHeaderInfo bit 0, toggles per frame
  input  logic        pkt_eof,        // bmHeaderInfo bit 1, last packet
  input  logic        pkt_err,        // bmHeaderInfo bit 6, device says this is bad
  input  logic [15:0] pkt_bytes,      // payload bytes after the header

  // ---- a completed frame ----
  output logic        frame_done,
  output logic [31:0] frame_bytes,
  output logic        frame_fid,
  // Did this frame end the way a frame is supposed to end?
  //
  // `frame_eof` distinguishes the two terminations, and it is the ONLY
  // evidence of loss the receiver ever gets.
  output logic        frame_eof,      // ended on EOF -- looked normal
  output logic        frame_flagged,  // the device marked a packet bad

  output logic [31:0] n_pkt,
  output logic [31:0] n_frame,
  output logic [31:0] n_truncated,    // ended by a FID toggle, not by EOF
  output logic [31:0] n_flagged,
  output logic [31:0] n_toggle
);

  logic        started;     // a frame is currently open
  logic        cur_fid;     // the FID of the open frame
  logic [31:0] acc;         // bytes accumulated into the open frame
  logic        acc_err;     // any packet in it carried the error bit

  logic        done_r, fid_r, eof_r, flag_r;
  logic [31:0] bytes_r;

  logic [31:0] pkt_c, frame_c, trunc_c, flag_c, tog_c;

  assign frame_done    = done_r;
  assign frame_bytes   = bytes_r;
  assign frame_fid     = fid_r;
  assign frame_eof     = eof_r;
  assign frame_flagged = flag_r;
  assign n_pkt         = pkt_c;
  assign n_frame       = frame_c;
  assign n_truncated   = trunc_c;
  assign n_flagged     = flag_c;
  assign n_toggle      = tog_c;

  // A packet whose FID differs from the open frame's means the previous
  // frame ended without ever presenting EOF. That is the only loss signal
  // this protocol has.
  logic fid_toggled;
  assign fid_toggled = started && (pkt_fid != cur_fid);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      started  <= 1'b0;
      cur_fid  <= 1'b0;
      acc      <= 32'd0;
      acc_err  <= 1'b0;
      done_r   <= 1'b0;
      fid_r    <= 1'b0;
      eof_r    <= 1'b0;
      flag_r   <= 1'b0;
      bytes_r  <= 32'd0;
      pkt_c    <= 32'd0;
      frame_c  <= 32'd0;
      trunc_c  <= 32'd0;
      flag_c   <= 32'd0;
      tog_c    <= 32'd0;
    end else begin
      done_r <= 1'b0;

      if (pkt_valid) begin
        pkt_c <= pkt_c + 32'd1;

        if (fid_toggled) begin
          // ---- the previous frame ended the WRONG way ----
          //
          // It never showed EOF, so the packet carrying EOF did not
          // arrive. Close it out and report it as truncated -- this is
          // the one loss the receiver can actually see.
          done_r  <= 1'b1;
          bytes_r <= acc;
          fid_r   <= cur_fid;
          eof_r   <= 1'b0;
          flag_r  <= acc_err;
          frame_c <= frame_c + 32'd1;
          trunc_c <= trunc_c + 32'd1;
          tog_c   <= tog_c + 32'd1;

          // and the arriving packet opens the next one
          started <= 1'b1;
          cur_fid <= pkt_fid;
          acc     <= {16'd0, pkt_bytes};
          acc_err <= pkt_err;
          if (pkt_err) flag_c <= flag_c + 32'd1;

        end else if (!started) begin
          // ---- first packet of a new frame ----
          started <= 1'b1;
          cur_fid <= pkt_fid;
          acc     <= {16'd0, pkt_bytes};
          acc_err <= pkt_err;
          if (pkt_err) flag_c <= flag_c + 32'd1;
          if (pkt_eof) begin
            // a single-packet frame
            done_r  <= 1'b1;
            bytes_r <= {16'd0, pkt_bytes};
            fid_r   <= pkt_fid;
            eof_r   <= 1'b1;
            flag_r  <= pkt_err;
            frame_c <= frame_c + 32'd1;
            started <= 1'b0;
          end

        end else begin
          // ---- a continuation packet of the open frame ----
          acc     <= acc + {16'd0, pkt_bytes};
          acc_err <= acc_err | pkt_err;
          if (pkt_err) flag_c <= flag_c + 32'd1;
          if (pkt_eof) begin
            // ---- the frame ended the RIGHT way ----
            //
            // Note what is NOT checked here, because it cannot be: there
            // is no length field and no sequence number, so a frame that
            // is short by one lost packet arrives here looking exactly
            // like a complete one.
            done_r  <= 1'b1;
            bytes_r <= acc + {16'd0, pkt_bytes};
            fid_r   <= cur_fid;
            eof_r   <= 1'b1;
            flag_r  <= acc_err | pkt_err;
            frame_c <= frame_c + 32'd1;
            started <= 1'b0;
          end
        end
      end
    end
  end

endmodule

The SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for uvc_frame_asm. -- SystemVerilog.
//
//  SAME SEED AND SAME PHASE ORDER AS THE VERILOG BENCH, deliberately.
//  Icarus seeds $random identically, so both drive identical stimulus and
//  any difference between the two mutation columns is a real difference
//  between the two DESIGNS. The independent-stimulus role is VHDL's.
//
//  THE BENCH KNOWS WHAT WAS SENT. THE DESIGN ONLY KNOWS WHAT ARRIVED.
//
//  That asymmetry is the entire experiment. The bench generates a frame
//  sequence, decides which packet to drop, presents only the survivors,
//  and then compares the design's view of each frame against the truth
//  it is holding back.
//
//  Three outcomes are possible per frame, and telling them apart is the
//  measurement:
//
//      CLEAN     every packet arrived; bytes match the truth
//      SILENT    a packet was lost mid-frame. The EOF still arrived, so
//                the frame looks complete and is short. The DESIGN
//                cannot tell. The bench can, because it knows the truth.
//      DETECTED  the EOF packet itself was lost, so the frame ended on
//                the next frame's FID toggle instead. The design CAN
//                tell.
//
//  A packet is the EOF packet one time in P, so the detectable fraction
//  is 1/P -- and the whole point of the headline sweep is that this is a
//  property of the protocol, not of the implementation.
// =====================================================================
`timescale 1ns/1ps
module tb_uv_sv;

  logic clk = 1'b0, rst_n = 1'b0;
  always #5 clk = ~clk;

  logic         pkt_valid = 1'b0, pkt_fid = 1'b0, pkt_eof = 1'b0, pkt_err = 1'b0;
  logic [15:0] pkt_bytes = 16'd0;

  logic        frame_done, frame_fid, frame_eof, frame_flagged;
  logic [31:0] frame_bytes;
  logic [31:0] n_pkt, n_frame, n_truncated, n_flagged, n_toggle;

  uvc_frame_asm dut (
    .clk(clk), .rst_n(rst_n),
    .pkt_valid(pkt_valid), .pkt_fid(pkt_fid), .pkt_eof(pkt_eof),
    .pkt_err(pkt_err), .pkt_bytes(pkt_bytes),
    .frame_done(frame_done), .frame_bytes(frame_bytes),
    .frame_fid(frame_fid), .frame_eof(frame_eof),
    .frame_flagged(frame_flagged),
    .n_pkt(n_pkt), .n_frame(n_frame), .n_truncated(n_truncated),
    .n_flagged(n_flagged), .n_toggle(n_toggle)
  );

  int errors = 0, checks = 0, steps = 0;
  int seed;

  // $random is SIGNED: mask the sign bit before any modulo.
  function automatic logic [31:0] urand();
    return $random(seed) & 32'h3FFF_FFFF;
  endfunction

  task automatic ck(input logic cond, input string what);
    begin
      checks = checks + 1;
      if (!cond) begin
        errors = errors + 1;
        if (errors <= 20)
          $display("  ERROR @%0t step#%0d: %s", $time, steps, what);
      end
    end
  endtask

  // ---- GROUND TRUTH, invisible to the design ----
  //
  // What each frame SHOULD have contained, and what actually reached the
  // receiver. The difference between the two is the damage.
  // Sized for the LONGEST sequence any phase runs, not for the shortest.
  // These held 8 entries while phase 3 streams 16 frames and indexes
  // got_eof[k] up to 15: Verilog returns x for that and says nothing,
  // which is how it survived. VHDL makes the same read fatal.
  int true_bytes [0:31];   // bytes the sender put in frame f
  int sent_bytes [0:31];   // bytes that actually arrived for frame f
  int true_eof [0:31];   // did frame f's EOF packet arrive?

  // ---- what the design reported, captured as it reports it ----
  int got_n;
  int got_bytes [0:31];
  int got_fid [0:31];
  int got_eof [0:31];
  // Captured WITH the frame, not read afterwards. frame_flagged is only
  // meaningful in the cycle frame_done is asserted; reading it later reads
  // whatever the previous frame left behind.
  int got_flag [0:31];

  // ---- the measurement ----
  // m_* are PER-RUN flags: phase 2 zeroes them before each sequence so it
  // can ask whether that particular loss was detectable. t_* are the
  // cumulative totals, which nothing zeroes -- keeping them separate is
  // what stops a directed-only run reporting zero losses.
  int m_clean = 0, m_silent = 0, m_detected = 0, m_losses = 0;
  int t_clean = 0, t_silent = 0, t_detected = 0, t_losses = 0;

  // ---- cumulative across resets ----
  //
  // reset_dut runs once per sequence, so the DUT's own counters describe
  // only the last one. Per-step checks still read the DUT counters; these
  // are for the totals.
  int c_pkt = 0, c_frame = 0, c_trunc = 0, c_flag = 0;

  // Per packets-per-frame, how many single-packet losses were detectable.
  int tab_p [0:4];
  int tab_total [0:4];
  int tab_det [0:4];

  task automatic reset_dut;
    int j;
    begin
      rst_n = 1'b0; pkt_valid = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      got_n = 0;
      for (j = 0; j < 32; j = j + 1) begin
        true_bytes[j] = 0; sent_bytes[j] = 0; true_eof[j] = 0;
        got_bytes[j] = 0; got_fid[j] = 0; got_eof[j] = 0; got_flag[j] = 0;
      end
    end
  endtask

  // Present one packet and collect any frame the design closes out.
  task automatic send_pkt(input fid, input eof, input err, input [15:0] bytes);
    begin
      pkt_valid = 1'b1; pkt_fid = fid; pkt_eof = eof;
      pkt_err = err;    pkt_bytes = bytes;
      @(posedge clk); #1;
      pkt_valid = 1'b0;
      c_pkt = c_pkt + 1;
      if (frame_done) begin
        c_frame = c_frame + 1;
        if (!frame_eof)   c_trunc = c_trunc + 1;
        if (frame_flagged) c_flag = c_flag + 1;
        if (got_n < 32) begin
          got_bytes[got_n] = frame_bytes;
          got_fid[got_n]   = frame_fid;
          got_eof[got_n]   = frame_eof;
          got_flag[got_n]  = frame_flagged;
        end
        got_n = got_n + 1;
      end
      steps = steps + 1;
    end
  endtask

  // Let the design settle and collect a trailing frame, if any.
  task automatic drain;
    int g;
    begin
      for (g = 0; g < 4; g = g + 1) begin
        @(posedge clk); #1;
        if (frame_done) begin
          c_frame = c_frame + 1;
          if (!frame_eof)    c_trunc = c_trunc + 1;
          if (frame_flagged) c_flag = c_flag + 1;
          if (got_n < 32) begin
            got_bytes[got_n] = frame_bytes;
            got_fid[got_n]   = frame_fid;
            got_eof[got_n]   = frame_eof;
            got_flag[got_n]  = frame_flagged;
          end
          got_n = got_n + 1;
        end
      end
    end
  endtask

  // -------------------------------------------------------------------
  //  Run three frames of P packets, dropping ONE packet if `lost` >= 0.
  //
  //  Frames 0 and 1 are the ones a loss is injected into, so there is
  //  always a following frame whose FID toggle could reveal a missing
  //  EOF. Frame 2 always completes, which keeps the experiment about
  //  detectability rather than about the end of the stream.
  // -------------------------------------------------------------------
  task automatic run_seq(input integer P, input integer lost, input integer base,
               input errbit);
    int f, p, idx, sz;
    int exp_frames, e_det, e_silent;
    int lost_sz, lost_was_eof;
    begin
      reset_dut;
      lost_sz = 0; lost_was_eof = 0;

      for (f = 0; f < 3; f = f + 1) begin
        true_bytes[f] = 0; sent_bytes[f] = 0; true_eof[f] = 0;
      end

      idx = 0;
      for (f = 0; f < 3; f = f + 1) begin
        for (p = 0; p < P; p = p + 1) begin
          sz = base + ((p % 4) * 4);
          true_bytes[f] = true_bytes[f] + sz;
          if (idx != lost) begin
            sent_bytes[f] = sent_bytes[f] + sz;
            if (p == P - 1) true_eof[f] = 1;
            send_pkt(f[0], (p == P - 1), errbit && (p == 0), sz[15:0]);
          end else begin
            // remember what was thrown away, so the damage can be
            // predicted rather than merely bounded
            lost_sz      = sz;
            lost_was_eof = (p == P - 1);
          end
          idx = idx + 1;
        end
      end
      drain;

      // ---- what SHOULD have been reported ----
      //
      // Derived from the truth arrays, which the design cannot see. Three
      // frames are always emitted: frame 2 always completes, and frames 0
      // and 1 each close either on their own EOF or on the following
      // frame's FID toggle.
      exp_frames = 3;
      ck(got_n == exp_frames, "the wrong number of frames was reported");

      e_det = 0; e_silent = 0;
      for (f = 0; f < 3 && f < got_n; f = f + 1) begin
        // ---- PROPERTY 1: the FID alternates, frame by frame ----
        ck(got_fid[f] == (f % 2),
           "a frame was reported with the wrong frame ID");
        // ---- PROPERTY 2: a frame ends on EOF exactly when its EOF arrived ----
        //
        // This is the design's ONLY loss signal, so it has to be exact.
        ck(got_eof[f] == true_eof[f],
           "frame_eof does not say whether the EOF packet arrived");
        // ---- PROPERTY 3: the byte count is what ARRIVED ----
        //
        // Not what was sent. The design is not wrong to report fewer
        // bytes; it is reporting what it received, which is all it has.
        ck(got_bytes[f] == sent_bytes[f],
           "the frame byte count does not match the bytes that arrived");

        // ---- THE MEASUREMENT ----
        //
        // Classify the damage from the TRUTH, then record whether the
        // design could have known.
        if (sent_bytes[f] != true_bytes[f] || true_eof[f] == 0) begin
          if (got_eof[f] == 0) e_det = e_det + 1;     // ended wrong: visible
          else                 e_silent = e_silent + 1; // looked fine: invisible
        end
      end

      if (lost >= 0) begin
        m_losses = m_losses + 1;
        // ---- PROPERTY 4: the damage is exactly predictable ----
        //
        // Not merely "at most one frame". Which of the three outcomes
        // occurs is decided entirely by two facts the bench knows about
        // the packet it threw away:
        //
        //   it carried EOF          -> the frame ends on the next FID
        //                              toggle instead. DETECTED.
        //   it carried bytes        -> the frame still gets its EOF and
        //                              is silently short. SILENT.
        //   it carried NEITHER      -> a zero-length packet that was not
        //                              the last one. Losing it costs
        //                              nothing at all, and the stream is
        //                              bit-identical to one where it was
        //                              never sent.
        //
        // That last case is not a loophole. Zero-length isochronous
        // packets are exactly how a device says "nothing this microframe"
        // while keeping its bandwidth reservation alive, and a protocol
        // in which losing one mattered would be a worse protocol.
        //
        // The first version of this check asserted "exactly one frame
        // damaged" and fired 30 times against a correct design, all of
        // them zero-byte losses.
        if (lost_was_eof) begin
          ck(e_det == 1 && e_silent == 0,
             "losing the EOF packet was not reported as a truncated frame");
        end else if (lost_sz > 0) begin
          ck(e_silent == 1 && e_det == 0,
             "losing a mid-frame packet was not a silently short frame");
        end else begin
          ck(e_det + e_silent == 0,
             "losing a zero-length packet damaged a frame");
        end
        if (e_det > 0)    begin m_detected = m_detected + 1; t_detected = t_detected + 1; end
        if (e_silent > 0) begin m_silent   = m_silent + 1;   t_silent   = t_silent + 1;   end
        t_losses = t_losses + 1;
      end else begin
        // ---- PROPERTY 5: with nothing lost, nothing is damaged ----
        //
        // The false-positive half. A detector that flags loss on a clean
        // stream gets switched off, and then detects nothing at all.
        ck(e_det + e_silent == 0,
           "a frame was reported damaged on a stream with no loss");
        m_clean = m_clean + 1; t_clean = t_clean + 1;
      end
    end
  endtask

  // ---- exhaustive reach ----
  //
  // loss position (none + 16) x error bit (2) x payload base (3) = 102,
  // at P = 8. Every dimension is an independent input.
  logic reach [0:101];
  int nr, ri;

  int li, eb, bs, pi, k, P, idx2;
  int PVAL [0:3];

  initial begin
    for (ri = 0; ri < 102; ri = ri + 1) reach[ri] = 1'b0;
    for (k = 0; k < 5; k = k + 1) begin
      tab_p[k] = 0; tab_total[k] = 0; tab_det[k] = 0;
    end
    PVAL[0] = 2; PVAL[1] = 4; PVAL[2] = 8; PVAL[3] = 16;
    seed = 32'd29002;

    reset_dut;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every single-packet loss.
    //
    //  P = 8, three frames, so sixteen droppable positions plus the
    //  no-loss case, crossed with the error bit and three payload
    //  profiles. 17 x 2 x 3 = 102.
    // =============================================================
    for (li = -1; li < 16; li = li + 1)
    for (eb = 0; eb < 2; eb = eb + 1)
    for (bs = 0; bs < 3; bs = bs + 1) begin
      run_seq(8, li, bs * 8, eb[0]);
      ri = ((li + 1) * 2 + eb) * 3 + bs;
      reach[ri] = 1'b1;
    end

    // =============================================================
    //  PHASE 2 (DIRECTED, EXHAUSTIVE per P) -- THE HEADLINE.
    //
    //  For each packets-per-frame, drop every droppable packet in turn
    //  and count how many of those losses the receiver could detect.
    //
    //  This is a property of the PROTOCOL: exactly one packet per frame
    //  carries EOF, so exactly one loss per frame is visible. The sweep
    //  measures it rather than asserting it.
    // =============================================================
    for (pi = 0; pi < 4; pi = pi + 1) begin
      P = PVAL[pi];
      tab_p[pi] = P;
      for (li = 0; li < 2 * P; li = li + 1) begin
        m_detected = 0; m_silent = 0;
        run_seq(P, li, 8, 1'b0);
        tab_total[pi] = tab_total[pi] + 1;
        if (m_detected > 0) tab_det[pi] = tab_det[pi] + 1;
      end
    end
    m_detected = 0; m_silent = 0; m_losses = 0; m_clean = 0;

    // =============================================================
    //  PHASE 3 (DIRECTED) -- a long clean stream.
    //
    //  Sixteen frames, nothing lost. Every frame must end on EOF with
    //  the full byte count and an alternating FID. This is the
    //  false-positive test: a loss detector that fires here is worse
    //  than none.
    // =============================================================
    reset_dut;
    for (k = 0; k < 16; k = k + 1) begin : clean
      int p2, tot;
      tot = 0;
      for (p2 = 0; p2 < 4; p2 = p2 + 1) begin
        tot = tot + 8 + p2;
        send_pkt(k[0], (p2 == 3), 1'b0, (8 + p2));
      end
      drain;
      ck(got_n == k + 1, "a clean stream lost a frame");
      if (got_n == k + 1) begin
        ck(got_eof[k] == 1, "a clean frame did not end on EOF");
        ck(got_bytes[k] == tot, "a clean frame reported the wrong byte count");
        ck(got_fid[k] == (k % 2), "a clean frame had the wrong frame ID");
      end
    end

    // =============================================================
    //  PHASE 4 (DIRECTED, EXHAUSTIVE) -- the device's own error bit.
    //
    //  The one loss signal that does NOT depend on the framing bits: a
    //  device that knows it dropped data can say so. Swept over which
    //  packet of the frame carries it, because a flag on the last packet
    //  and a flag on the first must both survive to the frame report.
    // =============================================================
    for (pi = 0; pi < 3; pi = pi + 1)
    for (k = -1; k < PVAL[pi]; k = k + 1) begin : errsweep
      int p3;
      reset_dut;
      P = PVAL[pi];
      for (p3 = 0; p3 < P; p3 = p3 + 1)
        send_pkt(1'b0, (p3 == P - 1), (p3 == k), 16'd8);
      drain;
      ck(got_n == 1, "the flagged frame was not reported");
      if (got_n == 1) begin
        // ---- PROPERTY 6: the flag reaches the frame from ANY packet ----
        //
        // The device sets it on whichever packet it knows is damaged --
        // the first, the last, or one in the middle. A receiver that only
        // honours it on the first packet works on the case people
        // demonstrate and fails on the ones that happen.
        //
        // k = -1 is the no-flag control. Without it this property would be
        // satisfied by a receiver that flagged every frame.
        ck(got_flag[0] == ((k >= 0) ? 1 : 0),
           "the frame error flag does not reflect the packet that carried it");
      end
    end

    // =============================================================
    //  PHASE 4b (DIRECTED) -- FRAMES THAT ARE ONE PACKET LONG.
    //
    //  A frame whose first packet also carries EOF takes a branch nothing
    //  else reaches. It is not a corner case invented for coverage: a
    //  still-image capture arrives on the same endpoint as the video
    //  stream and is routinely a single payload, and a small enough video
    //  frame is one too.
    //
    //  Without this phase the branch was reached only when a packet loss
    //  happened to reduce a two-packet frame to one, which gave the
    //  mutation that breaks it a domain of four.
    // =============================================================
    reset_dut;
    for (k = 0; k < 8; k = k + 1) begin : singles
      send_pkt(k[0], 1'b1, (k % 3 == 0), 16'd64);
      ck(got_n == k + 1, "a single-packet frame was not closed out");
      if (got_n == k + 1) begin
        ck(got_eof[k] == 1, "a single-packet frame did not end on EOF");
        ck(got_bytes[k] == 64, "a single-packet frame reported the wrong size");
        ck(got_fid[k] == (k % 2), "a single-packet frame had the wrong frame ID");
        ck(got_flag[k] == ((k % 3 == 0) ? 1 : 0),
           "a single-packet frame lost its error flag");
      end
    end

    // =============================================================
    //  PHASE 5 (RANDOM)
    // =============================================================
`ifndef DIRECTED_ONLY
    for (k = 0; k < 400; k = k + 1) begin
      P = 2 + (urand() % 7);
      li = (urand() % 3 == 0) ? -1 : (urand() % (2 * P));
      run_seq(P, li, (urand() % 3) * 8, (urand() % 5) == 0);
    end
`endif

    nr = 0; for (ri = 0; ri < 102; ri = ri + 1) if (reach[ri]) nr = nr + 1;

    $display("steps=%0d checks=%0d reach=%0d/102 errors=%0d",
             steps, checks, nr, errors);
    $display("[uvc] packets=%0d frames=%0d truncated=%0d flagged_frames=%0d",
             c_pkt, c_frame, c_trunc, c_flag);
    $display("[uvc] losses injected=%0d  detected=%0d  SILENT=%0d  clean runs=%0d",
             t_losses, t_detected, t_silent, t_clean);
    $display("--- what fraction of a single packet loss is even visible? ---");
    $display("    packets/frame   losses   detectable   silent   detectable %%");
    for (k = 0; k < 4; k = k + 1)
      $display("    %13d %8d %12d %8d %11d",
               tab_p[k], tab_total[k], tab_det[k], tab_total[k] - tab_det[k],
               (tab_det[k] * 100) / tab_total[k]);
    if (nr != 102) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

7. VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  uvc_frame_asm -- VHDL-2008.
--
--  CLASSIFICATION: simplified synthesisable teaching RTL.
--  Same hardware contract as the Verilog and SystemVerilog files: same
--  ports, same widths, same reset values, same cycle-by-cycle behaviour.
--
--  This is NOT a webcam. There is no sensor, no compression and no format
--  negotiation. It is the part of a UVC receiver that decides where one
--  frame ends and the next begins.
--
--  A webcam streams over ISOCHRONOUS transfers, and isochronous means
--  exactly one thing: bandwidth is reserved and delivery is not. No ACK,
--  no NAK, no retry. A packet that goes missing is gone, and nothing on
--  the bus will ever mention it again.
--
--  UVC gives the receiver two bits to reconstruct the frame structure:
--
--      FID   a frame-ID bit that TOGGLES between consecutive frames
--      EOF   set on the last packet of a frame
--
--  There is no per-packet sequence number and no frame length field. So a
--  lost packet is detectable ONLY if it happened to carry EOF:
--
--    * lose the EOF packet -> the frame never terminates, and the next
--      frame's FID toggle ends it instead. DETECTABLE.
--    * lose any other packet -> the frame still gets its EOF, is short by
--      the lost bytes, and nothing says so. SILENT.
--
--  One packet in P carries EOF, so the detectable fraction is 1/P.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uvc_frame_asm is
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    -- one isochronous packet that ARRIVED. Packets that were lost are
    -- simply never presented; the module has no way to know they existed,
    -- which is the whole situation being modelled.
    pkt_valid : in std_logic;
    pkt_fid   : in std_logic;                      -- toggles per frame
    pkt_eof   : in std_logic;                      -- last packet
    pkt_err   : in std_logic;                      -- device says this is bad
    pkt_bytes : in std_logic_vector(15 downto 0);

    frame_done    : out std_logic;
    frame_bytes   : out std_logic_vector(31 downto 0);
    frame_fid     : out std_logic;
    -- Did this frame end the way a frame is supposed to end? The ONLY
    -- evidence of loss the receiver ever gets.
    frame_eof     : out std_logic;
    frame_flagged : out std_logic;

    n_pkt       : out std_logic_vector(31 downto 0);
    n_frame     : out std_logic_vector(31 downto 0);
    n_truncated : out std_logic_vector(31 downto 0);
    n_flagged   : out std_logic_vector(31 downto 0);
    n_toggle    : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of uvc_frame_asm is

  signal started : std_logic := '0';   -- a frame is currently open
  signal cur_fid : std_logic := '0';   -- the FID of the open frame
  signal acc     : unsigned(31 downto 0) := (others => '0');
  signal acc_err : std_logic := '0';

  signal done_r, fid_r, eof_r, flag_r : std_logic := '0';
  signal bytes_r : unsigned(31 downto 0) := (others => '0');

  signal pkt_c, frame_c, trunc_c, flag_c, tog_c
         : unsigned(31 downto 0) := (others => '0');

  -- A packet whose FID differs from the open frame's means the previous
  -- frame ended without ever presenting EOF. That is the only loss signal
  -- this protocol has.
  signal fid_toggled : std_logic;

begin

  fid_toggled <= '1' when (started = '1' and pkt_fid /= cur_fid) else '0';

  frame_done    <= done_r;
  frame_bytes   <= std_logic_vector(bytes_r);
  frame_fid     <= fid_r;
  frame_eof     <= eof_r;
  frame_flagged <= flag_r;
  n_pkt         <= std_logic_vector(pkt_c);
  n_frame       <= std_logic_vector(frame_c);
  n_truncated   <= std_logic_vector(trunc_c);
  n_flagged     <= std_logic_vector(flag_c);
  n_toggle      <= std_logic_vector(tog_c);

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      started <= '0';
      cur_fid <= '0';
      acc     <= (others => '0');
      acc_err <= '0';
      done_r  <= '0';
      fid_r   <= '0';
      eof_r   <= '0';
      flag_r  <= '0';
      bytes_r <= (others => '0');
      pkt_c   <= (others => '0');
      frame_c <= (others => '0');
      trunc_c <= (others => '0');
      flag_c  <= (others => '0');
      tog_c   <= (others => '0');
    elsif rising_edge(clk) then
      done_r <= '0';

      if pkt_valid = '1' then
        pkt_c <= pkt_c + 1;

        if fid_toggled = '1' then
          -- ---- the previous frame ended the WRONG way ----
          --
          -- It never showed EOF, so the packet carrying EOF did not
          -- arrive. Close it out and report it as truncated: this is the
          -- one loss the receiver can actually see.
          done_r  <= '1';
          bytes_r <= acc;
          fid_r   <= cur_fid;
          eof_r   <= '0';
          flag_r  <= acc_err;
          frame_c <= frame_c + 1;
          trunc_c <= trunc_c + 1;
          tog_c   <= tog_c + 1;

          -- and the arriving packet opens the next one
          started <= '1';
          cur_fid <= pkt_fid;
          acc     <= resize(unsigned(pkt_bytes), 32);
          acc_err <= pkt_err;
          if pkt_err = '1' then flag_c <= flag_c + 1; end if;

        elsif started = '0' then
          -- ---- first packet of a new frame ----
          started <= '1';
          cur_fid <= pkt_fid;
          acc     <= resize(unsigned(pkt_bytes), 32);
          acc_err <= pkt_err;
          if pkt_err = '1' then flag_c <= flag_c + 1; end if;
          if pkt_eof = '1' then
            -- a single-packet frame
            done_r  <= '1';
            bytes_r <= resize(unsigned(pkt_bytes), 32);
            fid_r   <= pkt_fid;
            eof_r   <= '1';
            flag_r  <= pkt_err;
            frame_c <= frame_c + 1;
            started <= '0';
          end if;

        else
          -- ---- a continuation packet of the open frame ----
          acc     <= acc + resize(unsigned(pkt_bytes), 32);
          acc_err <= acc_err or pkt_err;
          if pkt_err = '1' then flag_c <= flag_c + 1; end if;
          if pkt_eof = '1' then
            -- ---- the frame ended the RIGHT way ----
            --
            -- Note what is NOT checked here, because it cannot be: there
            -- is no length field and no sequence number, so a frame short
            -- by one lost packet arrives here looking exactly like a
            -- complete one.
            done_r  <= '1';
            bytes_r <= acc + resize(unsigned(pkt_bytes), 32);
            fid_r   <= cur_fid;
            eof_r   <= '1';
            flag_r  <= acc_err or pkt_err;
            frame_c <= frame_c + 1;
            started <= '0';
          end if;
        end if;
      end if;
    end if;
  end process;

end architecture;

The VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  Testbench for uvc_frame_asm -- VHDL-2008.
--
--  THE BENCH KNOWS WHAT WAS SENT. THE DESIGN ONLY KNOWS WHAT ARRIVED.
--
--  That asymmetry is the experiment. The bench generates a frame
--  sequence, decides which packet to drop, presents only the survivors,
--  and compares the design's view of each frame against the truth it is
--  holding back. Three outcomes per frame:
--
--      CLEAN     every packet arrived; bytes match the truth
--      SILENT    a packet was lost mid-frame; the EOF still arrived, so
--                the frame looks complete and is short. The DESIGN cannot
--                tell. The bench can.
--      DETECTED  the EOF packet itself was lost, so the frame ended on
--                the next frame's FID toggle. The design CAN tell.
--
--  THIS IS THE INDEPENDENT BENCH. The directed phases are structurally
--  identical to the Verilog and SystemVerilog benches, so the DIRECTED
--  mutation columns must agree EXACTLY. The random phase uses a
--  VHDL-native generator.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

entity tb_uv_vhdl is
  generic (
    DIRECTED_ONLY : boolean := false
  );
end entity;

architecture sim of tb_uv_vhdl is

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

  signal pkt_valid : std_logic := '0';
  signal pkt_fid   : std_logic := '0';
  signal pkt_eof   : std_logic := '0';
  signal pkt_err   : std_logic := '0';
  signal pkt_bytes : std_logic_vector(15 downto 0) := (others => '0');

  signal frame_done    : std_logic;
  signal frame_bytes   : std_logic_vector(31 downto 0);
  signal frame_fid     : std_logic;
  signal frame_eof     : std_logic;
  signal frame_flagged : std_logic;
  signal n_pkt, n_frame, n_truncated, n_flagged, n_toggle
         : std_logic_vector(31 downto 0);

begin

  dut : entity work.uvc_frame_asm
    port map (
      clk => clk, rst_n => rst_n,
      pkt_valid => pkt_valid, pkt_fid => pkt_fid, pkt_eof => pkt_eof,
      pkt_err => pkt_err, pkt_bytes => pkt_bytes,
      frame_done => frame_done, frame_bytes => frame_bytes,
      frame_fid => frame_fid, frame_eof => frame_eof,
      frame_flagged => frame_flagged,
      n_pkt => n_pkt, n_frame => n_frame, n_truncated => n_truncated,
      n_flagged => n_flagged, n_toggle => n_toggle
    );

  clkgen : process
  begin
    while not done loop
      clk <= '0'; wait for 5 ns;
      clk <= '1'; wait for 5 ns;
    end loop;
    wait;
  end process;

  main : process

    variable errors : integer := 0;
    variable checks : integer := 0;
    variable steps  : integer := 0;
    variable lo     : line;

    -- GROUND TRUTH, invisible to the design
    -- Sized for the longest sequence any phase runs. Phase 3 streams
    -- sixteen frames; an array of eight made that a fatal index error.
    type i8_t is array (0 to 31) of integer;
    variable true_bytes : i8_t := (others => 0);
    variable sent_bytes : i8_t := (others => 0);
    variable true_eof   : i8_t := (others => 0);

    -- what the design reported, captured AS it reports it
    variable got_n : integer := 0;
    variable got_bytes, got_fid, got_eof, got_flag : i8_t := (others => 0);

    -- the measurement
    -- m_* are PER-RUN flags: phase 2 zeroes them before each sequence so
    -- it can ask whether that particular loss was detectable. t_* are the
    -- cumulative totals, which nothing zeroes.
    variable m_clean, m_silent, m_detected, m_losses : integer := 0;
    variable t_clean, t_silent, t_detected, t_losses : integer := 0;

    type i5_t is array (0 to 4) of integer;
    variable tab_p, tab_total, tab_det : i5_t := (others => 0);

    -- cumulative across resets
    variable c_pkt, c_frame, c_trunc, c_flag : integer := 0;

    type reach_t is array (0 to 101) of boolean;
    variable reach : reach_t := (others => false);
    variable nr : integer := 0;

    type i4_t is array (0 to 3) of integer;
    constant PVAL : i4_t := (2, 4, 8, 16);

    variable rnd_state : unsigned(31 downto 0) := x"00120702";
    impure function urand return integer is
    begin
      rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
                   + to_unsigned(12345, 32);
      -- HIGH bits: in an LCG with a power-of-two modulus bit i has period
      -- 2**(i+1), so the low bits cycle in lockstep with the calling loop
      -- while their histogram stays perfectly uniform.
      return to_integer(rnd_state(30 downto 15));
    end function;

    -- The frame-ID bit for frame k, as a named function. The obvious
    -- alternative -- slicing bit 0 out of a one-bit to_unsigned conversion
    -- -- is a TRUNCATING conversion for every k above 1, and nvc warns on
    -- every call.
    function fid_of (k : integer) return std_logic is
    begin
      if (k mod 2) = 1 then return '1'; else return '0'; end if;
    end function;

    procedure ck (cond : boolean; what : string) is
    begin
      checks := checks + 1;
      if not cond then
        errors := errors + 1;
        if errors <= 20 then
          write(lo, string'("  ERROR @") & time'image(now) &
                    string'(" step#") & integer'image(steps) &
                    string'(": ") & what);
          writeline(output, lo);
        end if;
      end if;
    end procedure;

    procedure reset_dut is
    begin
      rst_n <= '0';
      pkt_valid <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      wait until rising_edge(clk);
      wait for 1 ns;
      got_n := 0;
      true_bytes := (others => 0);
      sent_bytes := (others => 0);
      true_eof   := (others => 0);
      got_bytes  := (others => 0);
      got_fid    := (others => 0);
      got_eof    := (others => 0);
      got_flag   := (others => 0);
    end procedure;

    -- Present one packet and collect any frame the design closes out.
    -- The frame's fields are captured HERE, in the cycle frame_done is
    -- asserted; reading them later reads whatever the next frame leaves.
    procedure send_pkt (fid, eof, err : std_logic; nbytes : integer) is
    begin
      pkt_valid <= '1';
      pkt_fid   <= fid;
      pkt_eof   <= eof;
      pkt_err   <= err;
      pkt_bytes <= std_logic_vector(to_unsigned(nbytes, 16));
      wait until rising_edge(clk);
      wait for 1 ns;
      pkt_valid <= '0';
      c_pkt := c_pkt + 1;
      if frame_done = '1' then
        c_frame := c_frame + 1;
        if frame_eof = '0'     then c_trunc := c_trunc + 1; end if;
        if frame_flagged = '1' then c_flag  := c_flag + 1; end if;
        if got_n < 32 then
          got_bytes(got_n) := to_integer(unsigned(frame_bytes));
          if frame_fid = '1'     then got_fid(got_n)  := 1; else got_fid(got_n)  := 0; end if;
          if frame_eof = '1'     then got_eof(got_n)  := 1; else got_eof(got_n)  := 0; end if;
          if frame_flagged = '1' then got_flag(got_n) := 1; else got_flag(got_n) := 0; end if;
        end if;
        got_n := got_n + 1;
      end if;
      steps := steps + 1;
    end procedure;

    procedure drain is
    begin
      for g in 0 to 3 loop
        wait until rising_edge(clk);
        wait for 1 ns;
        if frame_done = '1' then
          c_frame := c_frame + 1;
          if frame_eof = '0'     then c_trunc := c_trunc + 1; end if;
          if frame_flagged = '1' then c_flag  := c_flag + 1; end if;
          if got_n < 32 then
            got_bytes(got_n) := to_integer(unsigned(frame_bytes));
            if frame_fid = '1'     then got_fid(got_n)  := 1; else got_fid(got_n)  := 0; end if;
            if frame_eof = '1'     then got_eof(got_n)  := 1; else got_eof(got_n)  := 0; end if;
            if frame_flagged = '1' then got_flag(got_n) := 1; else got_flag(got_n) := 0; end if;
          end if;
          got_n := got_n + 1;
        end if;
      end loop;
    end procedure;

    -- -----------------------------------------------------------------
    --  Three frames of P packets, dropping ONE if `lost` >= 0.
    --
    --  Frames 0 and 1 are the ones a loss is injected into, so there is
    --  always a following frame whose FID toggle could reveal a missing
    --  EOF. Frame 2 always completes.
    -- -----------------------------------------------------------------
    procedure run_seq (P : integer; lost : integer; base : integer;
                       errbit : boolean) is
      variable idx, sz : integer;
      variable e_det, e_silent : integer;
      variable lost_sz, lost_was_eof : integer;
      variable eb : std_logic;
    begin
      reset_dut;
      lost_sz := 0; lost_was_eof := 0;

      idx := 0;
      for f in 0 to 2 loop
        -- `pp`, not `p`: VHDL is case-insensitive, so a loop variable `p`
        -- hides the procedure's own parameter `P`.
        for pp in 0 to P - 1 loop
          sz := base + ((pp mod 4) * 4);
          true_bytes(f) := true_bytes(f) + sz;
          if idx /= lost then
            sent_bytes(f) := sent_bytes(f) + sz;
            if pp = P - 1 then true_eof(f) := 1; end if;
            if errbit and pp = 0 then eb := '1'; else eb := '0'; end if;
            if pp = P - 1 then
              send_pkt(fid_of(f), '1', eb, sz);
            else
              send_pkt(fid_of(f), '0', eb, sz);
            end if;
          else
            -- remember what was thrown away, so the damage can be
            -- predicted rather than merely bounded
            lost_sz := sz;
            if pp = P - 1 then lost_was_eof := 1; end if;
          end if;
          idx := idx + 1;
        end loop;
      end loop;
      drain;

      ck(got_n = 3, "the wrong number of frames was reported");

      e_det := 0; e_silent := 0;
      for f in 0 to 2 loop
        if f < got_n then
          -- ---- PROPERTY 1: the FID alternates, frame by frame ----
          ck(got_fid(f) = (f mod 2),
             "a frame was reported with the wrong frame ID");
          -- ---- PROPERTY 2: frame_eof says whether the EOF arrived ----
          ck(got_eof(f) = true_eof(f),
             "frame_eof does not say whether the EOF packet arrived");
          -- ---- PROPERTY 3: the byte count is what ARRIVED ----
          ck(got_bytes(f) = sent_bytes(f),
             "the frame byte count does not match the bytes that arrived");

          if sent_bytes(f) /= true_bytes(f) or true_eof(f) = 0 then
            if got_eof(f) = 0 then e_det := e_det + 1;
            else                   e_silent := e_silent + 1; end if;
          end if;
        end if;
      end loop;

      if lost >= 0 then
        m_losses := m_losses + 1;
        -- ---- PROPERTY 4: the damage is exactly predictable ----
        --
        -- Which of the three outcomes occurs is decided by two facts the
        -- bench knows about the packet it threw away: whether it carried
        -- EOF, and whether it carried any bytes. A zero-length packet that
        -- was not the last one costs nothing at all -- which is not a
        -- loophole, it is how a device says "nothing this microframe"
        -- while keeping its bandwidth reservation alive.
        if lost_was_eof = 1 then
          ck(e_det = 1 and e_silent = 0,
             "losing the EOF packet was not reported as a truncated frame");
        elsif lost_sz > 0 then
          ck(e_silent = 1 and e_det = 0,
             "losing a mid-frame packet was not a silently short frame");
        else
          ck(e_det + e_silent = 0, "losing a zero-length packet damaged a frame");
        end if;
        if e_det > 0    then m_detected := m_detected + 1; t_detected := t_detected + 1; end if;
        if e_silent > 0 then m_silent   := m_silent + 1;   t_silent   := t_silent + 1;   end if;
        t_losses := t_losses + 1;
      else
        -- ---- PROPERTY 5: with nothing lost, nothing is damaged ----
        --
        -- The false-positive half. A detector that flags loss on a clean
        -- stream gets switched off, and then detects nothing at all.
        ck(e_det + e_silent = 0,
           "a frame was reported damaged on a stream with no loss");
        m_clean := m_clean + 1; t_clean := t_clean + 1;
      end if;
    end procedure;

    variable ri, P, li, tot : integer;

  begin
    reset_dut;

    -- ===============================================================
    --  PHASE 1 (DIRECTED, EXHAUSTIVE) -- every single-packet loss.
    --  17 loss positions x 2 error bits x 3 payload profiles = 102.
    -- ===============================================================
    for lx in -1 to 15 loop
      for eb in 0 to 1 loop
        for bs in 0 to 2 loop
          run_seq(8, lx, bs * 8, eb = 1);
          ri := ((lx + 1) * 2 + eb) * 3 + bs;
          reach(ri) := true;
        end loop;
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 2 (DIRECTED, EXHAUSTIVE per P) -- THE HEADLINE.
    --
    --  For each packets-per-frame, drop every droppable packet in turn
    --  and count how many of those losses the receiver could detect.
    --  Exactly one packet per frame carries EOF, so exactly one loss per
    --  frame is visible: the sweep measures that rather than asserting it.
    -- ===============================================================
    for pi in 0 to 3 loop
      P := PVAL(pi);
      tab_p(pi) := P;
      for lx in 0 to 2 * P - 1 loop
        m_detected := 0; m_silent := 0;
        run_seq(P, lx, 8, false);
        tab_total(pi) := tab_total(pi) + 1;
        if m_detected > 0 then tab_det(pi) := tab_det(pi) + 1; end if;
      end loop;
    end loop;
    m_detected := 0; m_silent := 0; m_losses := 0; m_clean := 0;

    -- ===============================================================
    --  PHASE 3 (DIRECTED) -- a long clean stream, the false-positive
    --  test. A loss detector that fires here is worse than none.
    -- ===============================================================
    reset_dut;
    for k in 0 to 15 loop
      tot := 0;
      for p2 in 0 to 3 loop
        tot := tot + 8 + p2;
        if p2 = 3 then
          send_pkt(fid_of(k), '1', '0', 8 + p2);
        else
          send_pkt(fid_of(k), '0', '0', 8 + p2);
        end if;
      end loop;
      drain;
      ck(got_n = k + 1, "a clean stream lost a frame");
      if got_n = k + 1 then
        ck(got_eof(k) = 1, "a clean frame did not end on EOF");
        ck(got_bytes(k) = tot, "a clean frame reported the wrong byte count");
        ck(got_fid(k) = (k mod 2), "a clean frame had the wrong frame ID");
      end if;
    end loop;

    -- ===============================================================
    --  PHASE 4 (DIRECTED, EXHAUSTIVE) -- the device's own error bit.
    --
    --  The one loss signal that does NOT depend on the framing bits: a
    --  device that knows it dropped data can say so. Swept over which
    --  packet carries it, with a no-flag control -- without the control
    --  the property would be satisfied by a receiver that flagged
    --  everything.
    -- ===============================================================
    for pi in 0 to 2 loop
      for k in -1 to PVAL(pi) - 1 loop
        reset_dut;
        P := PVAL(pi);
        for p3 in 0 to P - 1 loop
          if p3 = P - 1 then
            if p3 = k then send_pkt('0', '1', '1', 8);
            else           send_pkt('0', '1', '0', 8); end if;
          else
            if p3 = k then send_pkt('0', '0', '1', 8);
            else           send_pkt('0', '0', '0', 8); end if;
          end if;
        end loop;
        drain;
        ck(got_n = 1, "the flagged frame was not reported");
        if got_n = 1 then
          -- ---- PROPERTY 6: the flag reaches the frame from ANY packet ----
          if k >= 0 then
            ck(got_flag(0) = 1,
               "the frame error flag does not reflect the packet that carried it");
          else
            ck(got_flag(0) = 0,
               "the frame error flag does not reflect the packet that carried it");
          end if;
        end if;
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 4b (DIRECTED) -- FRAMES THAT ARE ONE PACKET LONG.
    --
    --  A frame whose first packet also carries EOF takes a branch nothing
    --  else reaches. Not a corner case invented for coverage: a
    --  still-image capture arrives on the same endpoint and is routinely
    --  a single payload.
    -- ===============================================================
    reset_dut;
    for k in 0 to 7 loop
      if k mod 3 = 0 then send_pkt(fid_of(k), '1', '1', 64);
      else                send_pkt(fid_of(k), '1', '0', 64); end if;
      ck(got_n = k + 1, "a single-packet frame was not closed out");
      if got_n = k + 1 then
        ck(got_eof(k) = 1, "a single-packet frame did not end on EOF");
        ck(got_bytes(k) = 64, "a single-packet frame reported the wrong size");
        ck(got_fid(k) = (k mod 2), "a single-packet frame had the wrong frame ID");
        if k mod 3 = 0 then
          ck(got_flag(k) = 1, "a single-packet frame lost its error flag");
        else
          ck(got_flag(k) = 0, "a single-packet frame lost its error flag");
        end if;
      end if;
    end loop;

    -- ===============================================================
    --  PHASE 5 (RANDOM)
    -- ===============================================================
    if not DIRECTED_ONLY then
      for k in 0 to 399 loop
        P := 2 + (urand mod 7);
        if (urand mod 3) = 0 then li := -1; else li := urand mod (2 * P); end if;
        run_seq(P, li, (urand mod 3) * 8, (urand mod 5) = 0);
      end loop;
    end if;

    nr := 0;
    for i in 0 to 101 loop
      if reach(i) then nr := nr + 1; end if;
    end loop;

    write(lo, string'("steps=") & integer'image(steps) &
              string'(" checks=") & integer'image(checks) &
              string'(" reach=") & integer'image(nr) &
              string'("/102 errors=") & integer'image(errors));
    writeline(output, lo);
    write(lo, string'("[uvc] packets=") & integer'image(c_pkt) &
              string'(" frames=") & integer'image(c_frame) &
              string'(" truncated=") & integer'image(c_trunc) &
              string'(" flagged_frames=") & integer'image(c_flag));
    writeline(output, lo);
    write(lo, string'("[uvc] losses injected=") & integer'image(t_losses) &
              string'("  detected=") & integer'image(t_detected) &
              string'("  SILENT=") & integer'image(t_silent) &
              string'("  clean runs=") & integer'image(t_clean));
    writeline(output, lo);
    write(lo, string'("--- what fraction of a single packet loss is even visible? ---"));
    writeline(output, lo);
    write(lo, string'("    packets/frame   losses   detectable   silent   detectable %"));
    writeline(output, lo);
    for k in 0 to 3 loop
      write(lo, string'("    "));
      write(lo, tab_p(k), right, 13);
      write(lo, tab_total(k), right, 9);
      write(lo, tab_det(k), right, 13);
      write(lo, tab_total(k) - tab_det(k), right, 9);
      write(lo, (tab_det(k) * 100) / tab_total(k), right, 12);
      writeline(output, lo);
    end loop;

    if nr /= 102 then
      write(lo, string'("FAIL: exhaustive sweep incomplete")); writeline(output, lo);
      errors := errors + 1;
    end if;
    if errors = 0 then
      write(lo, string'("PASS: 0 errors in ") & integer'image(checks) & string'(" checks"));
    else
      write(lo, string'("FAIL: ") & integer'image(errors) &
                string'(" errors in ") & integer'image(checks) & string'(" checks"));
    end if;
    writeline(output, lo);

    done <= true;
    wait;
  end process;

end architecture;

8. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  Properties for uvc_frame_asm.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus Verilog does not support
//  concurrent assertions, so every number published here comes from the
//  procedural checks in the testbenches.
//
//  Read the list and notice what is MISSING: there is no property saying
//  a frame is complete. There cannot be one. The module has no length to
//  check against, and inventing an assertion that pretends otherwise
//  would be asserting something the protocol does not support.
// ---------------------------------------------------------------------
module uvc_frame_sva (
  input logic        clk,
  input logic        rst_n,
  input logic        pkt_valid,
  input logic        pkt_fid,
  input logic        pkt_eof,
  input logic        pkt_err,
  input logic [15:0] pkt_bytes,
  input logic        frame_done,
  input logic [31:0] frame_bytes,
  input logic        frame_fid,
  input logic        frame_eof,
  input logic        frame_flagged,
  input logic [31:0] n_frame,
  input logic [31:0] n_truncated
);

  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // ---- 1. frame_done is a single-cycle pulse ----
  // A frame announced for two cycles is counted twice by any consumer
  // that samples the flag.
  a_done_pulse : assert property (frame_done |=> !frame_done);

  // ---- 2. a frame only ever closes on a packet ----
  // Nothing closes a frame except an arriving packet: there is no
  // timeout in this module, because a timeout would need a clock the
  // protocol does not define.
  a_done_needs_pkt : assert property (frame_done |-> $past(pkt_valid));

  // ---- 3. a frame that ends on EOF ends because EOF arrived ----
  //
  // THE property. frame_eof is the receiver's only loss signal, so it
  // must mean exactly one thing and never approximate it.
  a_eof_honest : assert property
    ((frame_done && frame_eof) |-> $past(pkt_eof));

  // ---- 4. a frame that ends WITHOUT EOF ends on a FID toggle ----
  // The converse half. Together, 3 and 4 say frame_eof is exactly the
  // distinction between the two terminations and nothing else.
  a_trunc_honest : assert property
    ((frame_done && !frame_eof) |-> ($past(pkt_valid) && ($past(pkt_fid) != frame_fid)));

  // ---- 5. the truncated counter tracks the truncated frames ----
  // Stated in this direction: the counter may not move for any other
  // reason. The published loss figures are read off these counters, so a
  // counter that drifted would make every measurement unfalsifiable.
  a_trunc_count : assert property
    ((n_truncated != $past(n_truncated)) |-> ($past(frame_done) && !$past(frame_eof)));

  // ---- 6. a flagged frame contained a flagged packet ----
  // The device's own error bit is the one loss signal that does NOT
  // depend on the framing bits, so it must survive from any packet of the
  // frame to the frame report.
  a_flag_grounded : assert property
    ((frame_done && frame_flagged) |-> $past(pkt_err) or 1'b1);
  // (Written loosely here: the real obligation is over the whole frame
  //  and needs an auxiliary accumulator, which a formal tool would carry.)

  // ---- 7. the byte count never exceeds what arrived ----
  // A frame reporting more bytes than were delivered would be inventing
  // pixels. Bounded against a running total a formal tool would maintain.

  // ---- COVER: both terminations, and a zero-length packet ----
  // A suite that only ever sees clean frames has tested nothing about
  // loss, and one that never sees a zero-length packet has missed the
  // case where losing a packet costs nothing.
  c_eof      : cover property (frame_done && frame_eof);
  c_trunc    : cover property (frame_done && !frame_eof);
  c_flagged  : cover property (frame_done && frame_flagged);
  c_zlp      : cover property (pkt_valid && (pkt_bytes == 0));
  c_single   : cover property (pkt_valid && pkt_eof && frame_done);

endmodule

9. Where UVM Fits

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  UVM structure for UVC frame assembly.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks
//  on virtual method dispatch -- so every number comes from the
//  procedural benches.
//
//  THE DESIGN DECISION: loss is injected by the AGENT, not by the
//  sequence. The sequence describes a perfect video stream; the agent
//  decides what fails to arrive. That separation is what lets the
//  scoreboard hold ground truth -- it is fed the sequence's intent, while
//  the monitor sees only the survivors.
// ---------------------------------------------------------------------

class uvc_packet extends uvm_sequence_item;
  `uvm_object_utils(uvc_packet)

  rand bit        fid;
  rand bit        eof;
  rand bit        err;
  rand int        nbytes;

  constraint c_size {
    // Zero-length packets are a real and frequent case: they are how a
    // device holds its bandwidth reservation with nothing to send. A
    // constraint of nbytes > 0 would delete the one loss that costs
    // nothing, and with it the reason the bench's damage prediction has
    // three arms instead of two.
    nbytes inside {0, [8:1024]};
  }

  function new(string name = "uvc_packet");
    super.new(name);
  endfunction
endclass

// ---- the agent drops packets; the sequence never knows ----
//
// A driver that can decide not to drive is unusual and is the whole point
// here: the DUT must experience a gap with no marker of any kind, which
// is exactly what a real isochronous loss looks like.
class iso_driver extends uvm_driver #(uvc_packet);
  `uvm_component_utils(iso_driver)
  virtual uvc_if vif;

  // Loss rate as a per-mille figure, so a realistic 0.1% is expressible.
  // A regression run at 10% loss measures a broken link, not a webcam.
  int unsigned loss_per_mille = 1;

  uvm_analysis_port #(uvc_packet) sent_ap;   // what was INTENDED
  uvm_analysis_port #(uvc_packet) wire_ap;   // what was DRIVEN

  task run_phase(uvm_phase phase);
    forever begin
      uvc_packet p;
      seq_item_port.get_next_item(p);
      sent_ap.write(p);                       // truth, always
      if (($urandom_range(999) >= loss_per_mille)) begin
        drive(p);
        wire_ap.write(p);                     // only the survivors
      end
      seq_item_port.item_done();
    end
  endtask

  task drive(uvc_packet p);
    vif.pkt_valid <= 1'b1;
    vif.pkt_fid   <= p.fid;
    vif.pkt_eof   <= p.eof;
    vif.pkt_err   <= p.err;
    vif.pkt_bytes <= p.nbytes[15:0];
    @(posedge vif.clk);
    vif.pkt_valid <= 1'b0;
  endtask
endclass

// ---- the scoreboard holds both streams and measures the difference ----
class uvc_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(uvc_scoreboard)

  // fed from sent_ap: what the camera meant to send
  int unsigned truth_bytes[$];
  // fed from the DUT's frame reports: what the receiver believes
  int unsigned n_clean, n_silent, n_detected;

  function void report_phase(uvm_phase phase);
    `uvm_info("UVC", $sformatf(
      "frames: %0d clean, %0d SILENTLY SHORT, %0d detected as truncated",
      n_clean, n_silent, n_detected), UVM_LOW)

    // THE measurement, and the reason this environment exists. A
    // regression that reports only a pass/fail has thrown away the number
    // the chapter is about.
    if (n_silent + n_detected > 0)
      `uvm_info("UVC", $sformatf("detectable fraction: %0d%%",
        (n_detected * 100) / (n_silent + n_detected)), UVM_LOW)

    // A silently short frame is NOT a DUT error -- the DUT reported what
    // it received. Flagging it as one would make the environment fail on
    // correct behaviour, and it would be switched off within a week.
  endfunction
endclass

// ---- coverage: the loss POSITION, not the loss rate ----
class uvc_coverage extends uvm_subscriber #(uvc_packet);
  `uvm_component_utils(uvc_coverage)

  int pkt_index_in_frame;
  int frame_length_pkts;

  covergroup cg with function sample(int idx, int len, bit was_lost);
    // Which packet of the frame was lost. THE coverpoint: losing the last
    // one is a different event from losing any other, and a regression
    // that never lost a last packet has never exercised the detection
    // path at all.
    cp_pos : coverpoint idx { bins first = {0}; bins middle = {[1:30]}; bins last = {31}; }
    cp_len : coverpoint len { bins short = {[1:4]}; bins med = {[5:32]}; bins long = {[33:1000]}; }
    cp_lost : coverpoint was_lost;
    x_which : cross cp_pos, cp_lost;
  endgroup

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

  function void write(uvc_packet t);
    cg.sample(pkt_index_in_frame, frame_length_pkts, 1'b0);
  endfunction
endclass

10. Mutation Testing

Eight defects, injected one at a time into all three languages. Every replacement asserted; each mutation generated as its own file.

#the injected defectV-allV-dirSV-allSV-dirVHDL-allVHDL-dir
BASEunmodified design000000
Q1a truncated frame is reported as normal182401824017840
Q2the FID is never compared411904119039890
Q3a truncated frame reports the wrong bytes651865186318
Q4the closing packet's payload is dropped153248015324801543480
Q5the accumulator is not reloaded on a toggle802080207820
Q6the frame stays open after EOF291292829129282875928
Q7the device's error flag is not carried888888
Q8a single-packet frame is not recognised281228122812

Every mutation is killed, and every one by directed stimulus alone. The directed column is identical across all three languages at all eight rows.

Q1 is the mutation this chapter was built for

Q1 changes one bit: a frame closed by a FID toggle reports frame_eof = 1 instead of 0. The assembler still works perfectly — right frames, right boundaries, right byte counts. All it stops doing is saying that a frame ended the wrong way.

Since that is the only loss signal the protocol has, Q1 takes the detectable fraction from 1/P to zero. A receiver with this defect reports every frame as complete, forever, under any loss rate.

It scores 40, entirely directed. That number is small, and it is small for the right reason: only 2 losses per sweep are detectable in the first place, so there are only so many opportunities to get it wrong. Divide the score by its domain before calling it weak — 40 failures against 20 detectable losses is every single one of them, caught twice.

Run totals

stepschecksreachlossesdetectedsilenterrors
Verilog, full10,2176,320102 / 102426913050
Verilog, directed only4,5021,920102 / 102156201280
SystemVerilog, full10,2176,320102 / 102426913050
SystemVerilog, directed only4,5021,920102 / 102156201280
VHDL, full10,2746,320102 / 102420892900
VHDL, directed only4,5021,920102 / 102156201280

The directed-only rows are identical across all three languages in every column, including every loss measurement. The full runs differ only in the VHDL stream's own random choices — and note that even there the check count is the same 6,320, because the number of checks per sequence does not depend on what the sequence contains.

11. What This Does Not Cover

No sensor, no compression, no format negotiation. A real camera spends most of its silicon on those. None of them changes the framing.

The payload header is reduced to its two framing bits. Real UVC headers also carry a presentation timestamp, a source clock reference and an end-of-header flag. The timestamp in particular would let a receiver detect some losses — and leaving it out is deliberate, because this chapter is about what FID and EOF alone can tell you.

No bandwidth negotiation. The reserved bandwidth that makes isochronous isochronous is set up at SET_INTERFACE time by choosing an alternate setting. That is module 16's subject.

Single losses only. The exhaustive sweep drops exactly one packet. Two losses in one frame behave the same way unless one of them is the EOF packet, but the sweep is single-loss and the numbers should not be read as a multi-loss model.

No still-image path. A UVC device can carry stills on the same endpoint with a different header bit. The single-packet frame case in phase 4b is the shape that path takes, but the negotiation around it is out of scope.

Frames are 2 to 16 packets. Real frames are hundreds. The 1/P relationship is exact and extrapolates directly; the table stops at 16 because an exhaustive single-loss sweep at P = 300 is 600 sequences for one row.

12. The Interview Answer

"How does a USB webcam stream video?" — three sentences.

1. Name the mode and what it costs. "Isochronous transfers: bandwidth is reserved every frame, and delivery is not guaranteed. No ACK, no retry — a lost packet is gone, because a retransmitted video packet would arrive after the frame was displayed anyway."

2. Name the framing. "The receiver reconstructs frames from two bits in the UVC payload header: a frame-ID bit that toggles between frames, and an end-of-frame flag. There is no sequence number and no length field."

3. Name the consequence. "So a lost packet is only detectable if it was the one carrying EOF — one in P. For a frame that is hundreds of packets, better than 99% of losses produce a frame the receiver reports as complete with a hole in it."

The follow-up worth having ready is why that is acceptable: the failure mode is a smeared frame at 30 fps, which a viewer barely registers. Put the same mechanism under a filesystem and it is unusable — which is exactly why chapter 29.1's flash drive uses bulk with a thirteen-case error contract instead.

13. What Carries Forward

Two case studies, two opposite shapes:

chapterthe shapethe cost
29.1, flash drivea decision table with fatal cellssix of thirteen cases unrecoverable
29.2, webcama firehose with no error handling1/P of losses detectable

The next chapter keeps isochronous and changes what is being protected. A webcam can afford to lose a frame. An audio device cannot afford to drift — if the host's clock and the device's differ by one part in a million, a buffer somewhere fills or empties, and it does so relentlessly.

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.