Skip to content
VLSI Mentor

USB · Module 22

xHCI Overview

xHCI shares a ring between software and hardware using one Cycle bit per TRB and no pointer exchange at all — and an unowned entry holds the previous lap's complete, plausible descriptor.

Chapter 22.1 showed EHCI walking a linked list software edits underneath it. xHCI threw all of that away — and replaced it with a mechanism that fits in one bit.

1. The Producer/Consumer Problem, Stated Honestly

xHCI uses rings: fixed arrays of 16-byte Transfer Request Blocks, written by software and read by hardware. Which immediately raises the oldest question in shared-memory design:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   given ONE array and TWO agents, how does the consumer know
   which entries the producer has finished writing?

The textbook answer is a head pointer and a tail pointer. Software advances the head as it enqueues; hardware advances the tail as it consumes; each reads the other's.

xHCI does not do that, and the reason is where the device lives. A host controller is on the other side of a PCIe link. Every pointer software writes for hardware to read is a posted write that has to traverse the fabric; every pointer hardware writes for software to read is a DMA into host memory that will invalidate a cache line. On the fast path, that is a round trip per transfer.

2. The Cycle Bit

Every TRB carries one bit — the Cycle bit — and each side keeps one bit of its own:

holdscalled
softwareone bitPCS, Producer Cycle State
hardwareone bitCCS, Consumer Cycle State

And the entire ownership rule is one comparison:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   a TRB belongs to HARDWARE   <=>   TRB.Cycle == CCS

Software writes a TRB with Cycle = PCS, and that write is the handover. Hardware consumes TRBs while they match CCS and stops at the first one that does not — because that entry is one software has not written yet.

No pointer is exchanged. No register is written on the fast path. "Is there work?" is answered by reading one bit of one TRB — which the controller was going to fetch anyway.

One ring, two agents, and one bit per entry

A TRB ring shared between software and hardware. Software writes entries with its producer cycle state and rings a doorbell. Hardware compares each entry's cycle bit against its consumer cycle state, consuming while they match and stopping at the first mismatch, which is the previous lap's leftover TRB.Softwarewrites Cycle = PCSThe TRB ringone array, no pointers sharedHardwarecompares Cycle against CCSDoorbellthe only write on enqueueLast lap's TRBstill there, stillvalid-lookingCycle mismatchstop: not yet handed overenqueuefetch12
Software writes a TRB with Cycle = PCS, which hands it over. Hardware consumes while Cycle matches CCS and stops at the first mismatch. Both sides invert their cycle state on wrapping, so this lap's value differs from last lap's leftovers.

3. CCS Resets to One, and That Is Not Arbitrary

Both PCS and CCS initialise to 1. Software zeroes a ring before use, so every TRB in a fresh ring reads Cycle = 0.

0 ≠ 1, so every entry of a fresh ring reads as unowned. The ring is empty by construction, with no initialisation handshake, no "valid entries = 0" register, and nothing for software and hardware to agree about before they start.

Reset CCS to 0 instead and a freshly-zeroed ring reads as entirely owned by hardware — which consumes an array of blank TRBs before software has written anything at all. That is mutation L7 in §11, and at 430 000 failures it is the largest in the module.

4. Wrapping, and Why the Toggle Is Not Automatic

The mechanism only works if the value written on one lap differs from the value left by the previous lap. So both sides invert their cycle state each time they wrap.

And the wrap is not implicit. Every ring ends with a Link TRB carrying the address of the next segment and a Toggle Cycle flag:

Link TRBEffect
TC = 1follow the pointer and invert CCS
TC = 0follow the pointer, cycle state unchanged

TC is set on the link that closes the ring and clear on links that merely join one segment to the next. Both exist because a ring may be several physically-separate segments chained together, and only the last one wraps.

Get this wrongWhat happens
invert on every linkany multi-segment ring desynchronises at the first plain join (L2)
never invertthe consumer runs one lap and stops for ever — after the wrap everything looks unowned (L3)

It has a Cycle bit and obeys the same ownership rule. A Link TRB software has not handed over must not be followed — it is the end of what has been produced, exactly like any other unowned entry.

Following it reads a pointer software never wrote, and that pointer is last lap's link target: a real address, into a real ring segment, that the consumer will then walk through stale TRBs. Mutation L4.

The decision, and every way it can go

The TRB decision. If not running or no TRB is fetched, the action is idle. Otherwise the cycle bit is compared against the consumer cycle state: a mismatch means unowned and the consumer stops. A match means the TRB is consumed, either as a transfer, or as a plain link, or as a link that also toggles the cycle state.TRB fetched16 bytes from the ringCycle == CCS?the whole ownership ruleLink TRB?structural, or real workTRB_TRANSFERrun it; pointer += 1TRB_UNOWNEDlast lap's data: STOPToggle Cycle?set only on the closing linkTRB_LINK_TOGjump, and invert CCSmatchdiffernoyesset12
Five outcomes, three of which look like 'nothing much happened' on a waveform. TRB_UNOWNED is the ring going quiet; TRB_LINK is a structural hop that moved no data; TRB_IDLE is nothing fetched at all — and a design that confuses any two of them is hard to debug from a trace.

6. What We Are Building

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  xhci_trb_ring  #(RING_N = 8, PTRW = 3)

  from the fetch engine         to the transfer engine
  ---------------------         ----------------------
  run                           owned / consume
  trb_valid                     is_transfer / follow_link / toggle
  trb_cycle                     action   IDLE / TRANSFER / LINK /
  trb_is_link                            LINK_TOG / UNOWNED
  trb_toggle_cycle
  trb_link_target [2:0]         deq_ptr [2:0]
  doorbell                      ccs
                                ring_empty / stopped

  n_transfers / n_links / n_toggles / n_empty / n_restarts

7. Verilog-2005 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// xhci_trb_ring -- how two agents share a circular buffer without ever
// telling each other where they have got to.
//
// THE PROBLEM EHCI'S LINKED LIST DOES NOT SOLVE WELL
//
// Chapter 22.1's asynchronous schedule is a ring of Queue Heads that software
// edits while hardware walks it. It works, but every edit is a pointer
// update that has to be safe against a concurrent reader, and finding out
// whether there is anything to do costs a full lap.
//
// xHCI replaced all of it with RINGS: fixed arrays of 16-byte Transfer
// Request Blocks, written by software and read by hardware. Which immediately
// raises the classic producer/consumer question:
//
//     given one array and two agents, how does the consumer know
//     which entries the producer has finished writing?
//
// The textbook answer is a head pointer and a tail pointer, exchanged through
// registers or through memory. xHCI does not do that either. Exchanging
// pointers means every enqueue costs a write the other side has to see, and
// on a PCIe device that write is a round trip.
//
// THE CYCLE BIT
//
// Every TRB carries one bit -- the Cycle bit -- and each side keeps one bit
// of its own:
//
//     software holds PCS, the Producer Cycle State
//     hardware holds CCS, the Consumer Cycle State
//
// and the rule is a single comparison:
//
//     a TRB belongs to HARDWARE  <=>  TRB.Cycle == CCS
//
// Software writes a TRB with Cycle = PCS, which hands it over. Hardware
// consumes TRBs while they match CCS and STOPS at the first one that does
// not -- because that TRB is one software has not written yet, and the stale
// contents of the array are the previous lap's TRBs, which carry the OPPOSITE
// cycle value.
//
// That is the whole mechanism. No pointer is exchanged, no register is
// written on the fast path, and "is there work?" is answered by reading one
// bit of one TRB.
//
// WRAPPING, AND WHY THE TOGGLE IS NOT AUTOMATIC
//
// The trick only works if the value written on one lap differs from the value
// left by the previous lap. So both sides invert their cycle state each time
// they wrap -- and the wrap is not implicit. Every ring ENDS with a LINK TRB,
// which carries the address of the next segment and a Toggle Cycle flag:
//
//     Link TRB with TC = 1   ->  follow the pointer AND invert CCS
//     Link TRB with TC = 0   ->  follow the pointer, cycle state UNCHANGED
//
// TC is set on the link that closes the ring and clear on links that merely
// join one segment to the next. A controller that inverts on every link
// desynchronises any multi-segment ring; one that never inverts stops dead
// after one lap, because everything then looks unowned.
//
// AND THE LINK TRB IS ITSELF A TRB
//
// It has a Cycle bit and it obeys the same ownership rule. A Link TRB that
// software has not yet handed over must NOT be followed -- it is the end of
// what has been produced, exactly like any other unowned TRB. Following it
// reads a pointer software has not written.
module xhci_trb_ring #(
  parameter RING_N = 8,          // TRBs per segment
  parameter PTRW   = 3           // pointer width: must hold 0 .. RING_N-1
) (
  input  wire            clk,
  input  wire            rst_n,

  input  wire            run,              // the endpoint is running
  input  wire            trb_valid,        // a TRB has been fetched
  input  wire            trb_cycle,        // its Cycle bit
  input  wire            trb_is_link,      // it is a Link TRB
  input  wire            trb_toggle_cycle, // ...with the Toggle Cycle flag
  input  wire [PTRW-1:0] trb_link_target,  // ...and this is where it points
  input  wire            doorbell,         // software: "I enqueued something"

  output wire            owned,            // TRB.Cycle == CCS
  output wire            consume,          // take this TRB
  output wire            is_transfer,      // ...and it is real work, not a link
  output wire            follow_link,
  output wire            toggle,           // invert CCS this cycle
  output wire [2:0]      action,           // the same decision, named

  output wire [PTRW-1:0] deq_ptr,
  output wire            ccs,              // Consumer Cycle State
  output wire            ring_empty,       // an unowned TRB: nothing to do
  output wire            stopped,          // ...latched, until the doorbell

  output reg  [31:0]     n_transfers,
  output reg  [31:0]     n_links,
  output reg  [31:0]     n_toggles,
  output reg  [31:0]     n_empty,
  output reg  [31:0]     n_restarts
);
  reg [PTRW-1:0] deq_r;
  reg            ccs_r;
  reg            stopped_r;

  assign deq_ptr = deq_r;
  assign ccs     = ccs_r;
  assign stopped = stopped_r;

  wire running = run && !stopped_r;

  // ---- THE OWNERSHIP TEST. One comparison, and everything follows. ----
  //
  // A TRB whose Cycle bit differs from CCS is one software has not handed
  // over. What is physically in memory there is the PREVIOUS lap's TRB --
  // real-looking data, with a real-looking type and a real-looking pointer,
  // that must not be acted on.
  assign owned      = running && trb_valid && (trb_cycle == ccs_r);
  assign ring_empty = running && trb_valid && (trb_cycle != ccs_r);

  assign consume     = owned;
  assign follow_link = owned &&  trb_is_link;
  assign is_transfer = owned && !trb_is_link;

  // The toggle is NOT implicit in wrapping. Only a Link TRB carrying the
  // Toggle Cycle flag inverts the consumer's cycle state.
  assign toggle = follow_link && trb_toggle_cycle;

  // The same decision as one named value. TRB_IDLE, TRB_UNOWNED and a
  // consumed TRB that happens to be a plain link all look like "the pointer
  // did not move much" on a waveform, and they are three different states of
  // the world.
  localparam [2:0] TRB_IDLE     = 3'd0,  // not running, or no TRB fetched
                   TRB_TRANSFER = 3'd1,  // owned, not a link: real work
                   TRB_LINK     = 3'd2,  // owned link, cycle state unchanged
                   TRB_LINK_TOG = 3'd3,  // owned link with Toggle Cycle
                   TRB_UNOWNED  = 3'd4;  // software has not handed it over

  assign action = (!running || !trb_valid) ? TRB_IDLE
                : !owned                   ? TRB_UNOWNED
                : !trb_is_link             ? TRB_TRANSFER
                : trb_toggle_cycle         ? TRB_LINK_TOG
                                           : TRB_LINK;

  // ---- Next state, as priority chains ----
  //
  // A Link TRB moves the dequeue pointer to the link's target; an ordinary
  // consumed TRB advances it by one. A correct ring always ends in a Link
  // TRB, so the increment never has to wrap -- but it wraps anyway, because
  // a controller must not walk off the end of an array software got wrong.
  // RING_N itself does not fit in PTRW bits -- RING_N-1 does. Comparing
  // against the last index rather than against the count is what keeps this
  // correct for any RING_N, including a power of two where the truncated
  // count reads as zero.
  localparam [PTRW-1:0] LAST_IDX = RING_N - 1;

  wire [PTRW-1:0] deq_inc = (deq_r == LAST_IDX) ? {PTRW{1'b0}}
                                                : deq_r + {{(PTRW-1){1'b0}}, 1'b1};

  wire [PTRW-1:0] deq_next = !run        ? {PTRW{1'b0}}
                           : follow_link ? trb_link_target
                           : consume     ? deq_inc
                                         : deq_r;

  // CCS resets to ONE, not zero. Software's PCS also starts at 1, and a ring
  // is zeroed before use -- so every TRB initially reads Cycle = 0, which is
  // "not owned by hardware". An empty ring is therefore empty by
  // construction, with no initialisation handshake at all.
  wire ccs_next = !run   ? 1'b1
                : toggle ? ~ccs_r
                         :  ccs_r;

  // Same pattern as the EHCI walker in chapter 22.1: the condition that
  // stops the consumer evaporates the moment it stops, so it has to be
  // latched, and software has to ring a doorbell to say the ring is no
  // longer empty.
  wire stopped_next = !run      ? 1'b0
                    : doorbell  ? 1'b0
                    : ring_empty ? 1'b1
                                 : stopped_r;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      deq_r       <= {PTRW{1'b0}};
      ccs_r       <= 1'b1;
      stopped_r   <= 1'b0;
      n_transfers <= 32'd0;
      n_links     <= 32'd0;
      n_toggles   <= 32'd0;
      n_empty     <= 32'd0;
      n_restarts  <= 32'd0;
    end else begin
      deq_r     <= deq_next;
      ccs_r     <= ccs_next;
      stopped_r <= stopped_next;

      if (is_transfer)  n_transfers <= n_transfers + 32'd1;
      if (follow_link)  n_links     <= n_links + 32'd1;
      if (toggle)       n_toggles   <= n_toggles + 32'd1;
      if (ring_empty)   n_empty     <= n_empty + 32'd1;
      // Doorbells that actually RELEASED a stopped consumer -- as distinct
      // from doorbells rung while it was already running, which are the
      // common case and cost nothing. A high restart count against a low
      // transfer count is software enqueueing one TRB at a time.
      if (run && doorbell && stopped_r)
                        n_restarts  <= n_restarts + 32'd1;
    end
  end
endmodule

8. SystemVerilog Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// xhci_trb_ring -- how two agents share a circular buffer without ever
// telling each other where they have got to.
//
// THE PROBLEM EHCI'S LINKED LIST DOES NOT SOLVE WELL
//
// Chapter 22.1's asynchronous schedule is a ring of Queue Heads that software
// edits while hardware walks it. It works, but every edit is a pointer
// update that has to be safe against a concurrent reader, and finding out
// whether there is anything to do costs a full lap.
//
// xHCI replaced all of it with RINGS: fixed arrays of 16-byte Transfer
// Request Blocks, written by software and read by hardware. Which immediately
// raises the classic producer/consumer question:
//
//     given one array and two agents, how does the consumer know
//     which entries the producer has finished writing?
//
// The textbook answer is a head pointer and a tail pointer, exchanged through
// registers or through memory. xHCI does not do that either. Exchanging
// pointers means every enqueue costs a write the other side has to see, and
// on a PCIe device that write is a round trip.
//
// THE CYCLE BIT
//
// Every TRB carries one bit -- the Cycle bit -- and each side keeps one bit
// of its own:
//
//     software holds PCS, the Producer Cycle State
//     hardware holds CCS, the Consumer Cycle State
//
// and the rule is a single comparison:
//
//     a TRB belongs to HARDWARE  <=>  TRB.Cycle == CCS
//
// Software writes a TRB with Cycle = PCS, which hands it over. Hardware
// consumes TRBs while they match CCS and STOPS at the first one that does
// not -- because that TRB is one software has not written yet, and the stale
// contents of the array are the previous lap's TRBs, which carry the OPPOSITE
// cycle value.
//
// That is the whole mechanism. No pointer is exchanged, no register is
// written on the fast path, and "is there work?" is answered by reading one
// bit of one TRB.
//
// WRAPPING, AND WHY THE TOGGLE IS NOT AUTOMATIC
//
// The trick only works if the value written on one lap differs from the value
// left by the previous lap. So both sides invert their cycle state each time
// they wrap -- and the wrap is not implicit. Every ring ENDS with a LINK TRB,
// which carries the address of the next segment and a Toggle Cycle flag:
//
//     Link TRB with TC = 1   ->  follow the pointer AND invert CCS
//     Link TRB with TC = 0   ->  follow the pointer, cycle state UNCHANGED
//
// TC is set on the link that closes the ring and clear on links that merely
// join one segment to the next. A controller that inverts on every link
// desynchronises any multi-segment ring; one that never inverts stops dead
// after one lap, because everything then looks unowned.
//
// AND THE LINK TRB IS ITSELF A TRB
//
// It has a Cycle bit and it obeys the same ownership rule. A Link TRB that
// software has not yet handed over must NOT be followed -- it is the end of
// what has been produced, exactly like any other unowned TRB. Following it
// reads a pointer software has not written.
package xhci_ring_pkg;
  // What the consumer decided about the TRB in front of it. Naming the
  // outcomes matters here because THREE of them look like "nothing
  // happened" on a waveform and mean entirely different things.
  typedef enum logic [2:0] {
    TRB_IDLE     = 3'd0,  // not running, or no TRB fetched
    TRB_TRANSFER = 3'd1,  // owned, not a link: real work
    TRB_LINK     = 3'd2,  // owned Link TRB, cycle state unchanged
    TRB_LINK_TOG = 3'd3,  // owned Link TRB with Toggle Cycle: CCS inverts
    TRB_UNOWNED  = 3'd4   // software has not handed this one over
  } trb_action_e;
endpackage

module xhci_trb_ring
  import xhci_ring_pkg::*;
#(
  parameter int RING_N = 8,      // TRBs per segment
  parameter int PTRW   = 3       // pointer width: must hold 0 .. RING_N-1
) (
  input  logic            clk,
  input  logic            rst_n,

  input  logic            run,              // the endpoint is running
  input  logic            trb_valid,        // a TRB has been fetched
  input  logic            trb_cycle,        // its Cycle bit
  input  logic            trb_is_link,      // it is a Link TRB
  input  logic            trb_toggle_cycle, // ...with the Toggle Cycle flag
  input  logic [PTRW-1:0] trb_link_target,  // ...and where it points
  input  logic            doorbell,         // software: "I enqueued something"

  output logic            owned,            // TRB.Cycle == CCS
  output logic            consume,          // take this TRB
  output logic            is_transfer,      // ...real work, not a link
  output logic            follow_link,
  output logic            toggle,           // invert CCS this cycle
  output trb_action_e     action,           // the same decision, named

  output logic [PTRW-1:0] deq_ptr,
  output logic            ccs,              // Consumer Cycle State
  output logic            ring_empty,       // an unowned TRB: nothing to do
  output logic            stopped,          // ...latched, until the doorbell

  output logic [31:0]     n_transfers,
  output logic [31:0]     n_links,
  output logic [31:0]     n_toggles,
  output logic [31:0]     n_empty,
  output logic [31:0]     n_restarts
);
  logic [PTRW-1:0] deq_r;
  logic            ccs_r;
  logic            stopped_r;

  assign deq_ptr = deq_r;
  assign ccs     = ccs_r;
  assign stopped = stopped_r;

  logic running;
  assign running = run && !stopped_r;

  // ---- THE OWNERSHIP TEST. One comparison, and everything follows. ----
  //
  // A TRB whose Cycle bit differs from CCS is one software has not handed
  // over. What is physically in memory there is the PREVIOUS lap's TRB --
  // real-looking data, with a real-looking type and a real-looking pointer,
  // that must not be acted on.
  assign owned      = running && trb_valid && (trb_cycle == ccs_r);
  assign ring_empty = running && trb_valid && (trb_cycle != ccs_r);

  assign consume     = owned;
  assign follow_link = owned &&  trb_is_link;
  assign is_transfer = owned && !trb_is_link;

  // The toggle is NOT implicit in wrapping. Only a Link TRB carrying the
  // Toggle Cycle flag inverts the consumer's cycle state.
  assign toggle = follow_link && trb_toggle_cycle;

  // The same decision as one named value. TRB_IDLE, TRB_UNOWNED and a
  // consumed TRB that happens to be a plain link all look like "the pointer
  // did not move much" on a waveform, and they are three different states of
  // the world.
  always_comb begin
    if      (!running || !trb_valid)  action = TRB_IDLE;
    else if (!owned)                  action = TRB_UNOWNED;
    else if (!trb_is_link)            action = TRB_TRANSFER;
    else if (trb_toggle_cycle)        action = TRB_LINK_TOG;
    else                              action = TRB_LINK;
  end

  // ---- Next state, as priority chains ----
  //
  // A Link TRB moves the dequeue pointer to the link's target; an ordinary
  // consumed TRB advances it by one. A correct ring always ends in a Link
  // TRB, so the increment never has to wrap -- but it wraps anyway, because
  // a controller must not walk off the end of an array software got wrong.
  // RING_N itself does not fit in PTRW bits -- RING_N-1 does. Comparing
  // against the last index rather than against the count is what keeps this
  // correct for any RING_N, including a power of two where the truncated
  // count reads as zero.
  localparam logic [PTRW-1:0] LAST_IDX = PTRW'(RING_N - 1);

  logic [PTRW-1:0] deq_inc;
  assign deq_inc = (deq_r == LAST_IDX) ? {PTRW{1'b0}}
                                                : deq_r + {{(PTRW-1){1'b0}}, 1'b1};

  logic [PTRW-1:0] deq_next;
  assign deq_next = !run        ? {PTRW{1'b0}}
                           : follow_link ? trb_link_target
                           : consume     ? deq_inc
                                         : deq_r;

  // CCS resets to ONE, not zero. Software's PCS also starts at 1, and a ring
  // is zeroed before use -- so every TRB initially reads Cycle = 0, which is
  // "not owned by hardware". An empty ring is therefore empty by
  // construction, with no initialisation handshake at all.
  logic ccs_next;
  assign ccs_next = !run   ? 1'b1
                : toggle ? ~ccs_r
                         :  ccs_r;

  // Same pattern as the EHCI walker in chapter 22.1: the condition that
  // stops the consumer evaporates the moment it stops, so it has to be
  // latched, and software has to ring a doorbell to say the ring is no
  // longer empty.
  logic stopped_next;
  assign stopped_next = !run      ? 1'b0
                    : doorbell  ? 1'b0
                    : ring_empty ? 1'b1
                                 : stopped_r;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      deq_r       <= {PTRW{1'b0}};
      ccs_r       <= 1'b1;
      stopped_r   <= 1'b0;
      n_transfers <= '0;
      n_links     <= '0;
      n_toggles   <= '0;
      n_empty     <= '0;
      n_restarts  <= '0;
    end else begin
      deq_r     <= deq_next;
      ccs_r     <= ccs_next;
      stopped_r <= stopped_next;

      if (is_transfer)  n_transfers <= n_transfers + 1;
      if (follow_link)  n_links     <= n_links + 1;
      if (toggle)       n_toggles   <= n_toggles + 1;
      if (ring_empty)   n_empty     <= n_empty + 1;
      // Doorbells that actually RELEASED a stopped consumer -- as distinct
      // from doorbells rung while it was already running, which are the
      // common case and cost nothing. A high restart count against a low
      // transfer count is software enqueueing one TRB at a time.
      if (run && doorbell && stopped_r)
                        n_restarts  <= n_restarts + 1;
    end
  end
endmodule

9. VHDL-2008 Implementation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- xhci_trb_ring -- how two agents share a circular buffer without ever
-- telling each other where they have got to.
--
-- THE PROBLEM EHCI'S LINKED LIST DOES NOT SOLVE WELL
--
-- Chapter 22.1's asynchronous schedule is a ring of Queue Heads that software
-- edits while hardware walks it. It works, but every edit is a pointer update
-- that has to be safe against a concurrent reader, and finding out whether
-- there is anything to do costs a full lap.
--
-- xHCI replaced all of it with RINGS: fixed arrays of 16-byte Transfer
-- Request Blocks, written by software and read by hardware. Which immediately
-- raises the classic producer/consumer question:
--
--     given one array and two agents, how does the consumer know
--     which entries the producer has finished writing?
--
-- The textbook answer is a head pointer and a tail pointer, exchanged through
-- registers or through memory. xHCI does not do that either: exchanging
-- pointers means every enqueue costs a write the other side has to see, and
-- on a PCIe device that write is a round trip.
--
-- THE CYCLE BIT
--
-- Every TRB carries one bit -- the Cycle bit -- and each side keeps one bit
-- of its own:
--
--     software holds PCS, the Producer Cycle State
--     hardware holds CCS, the Consumer Cycle State
--
-- and the rule is a single comparison:
--
--     a TRB belongs to HARDWARE  <=>  TRB.Cycle = CCS
--
-- Software writes a TRB with Cycle = PCS, which hands it over. Hardware
-- consumes TRBs while they match CCS and STOPS at the first one that does
-- not -- because that TRB is one software has not written yet, and the stale
-- contents there are the previous lap's TRBs, which carry the OPPOSITE cycle
-- value.
--
-- No pointer is exchanged, no register is written on the fast path, and "is
-- there work?" is answered by reading one bit of one TRB.
--
-- WRAPPING, AND WHY THE TOGGLE IS NOT AUTOMATIC
--
-- The trick only works if the value written on one lap differs from the value
-- left by the previous lap. So both sides invert their cycle state each time
-- they wrap -- and the wrap is not implicit. Every ring ENDS with a LINK TRB,
-- which carries the address of the next segment and a Toggle Cycle flag:
--
--     Link TRB with TC = '1'  ->  follow the pointer AND invert CCS
--     Link TRB with TC = '0'  ->  follow the pointer, cycle state UNCHANGED
--
-- TC is set on the link that closes the ring and clear on links that merely
-- join one segment to the next. A controller that inverts on every link
-- desynchronises any multi-segment ring; one that never inverts stops dead
-- after one lap, because everything then looks unowned.
--
-- AND THE LINK TRB IS ITSELF A TRB
--
-- It has a Cycle bit and obeys the same ownership rule. A Link TRB software
-- has not yet handed over must NOT be followed -- it is the end of what has
-- been produced, exactly like any other unowned TRB, and following it reads a
-- pointer software never wrote.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package xhci_ring_pkg is
  -- What the consumer decided about the TRB in front of it. Naming the
  -- outcomes matters here because THREE of them look like "nothing happened"
  -- on a waveform and mean entirely different things.
  type trb_action_t is (
    TRB_IDLE,      -- not running, or no TRB fetched
    TRB_TRANSFER,  -- owned, not a link: real work
    TRB_LINK,      -- owned Link TRB, cycle state unchanged
    TRB_LINK_TOG,  -- owned Link TRB with Toggle Cycle: CCS inverts
    TRB_UNOWNED    -- software has not handed this one over
  );

  function act_code(a : trb_action_t) return std_logic_vector;
end package;

package body xhci_ring_pkg is
  function act_code(a : trb_action_t) return std_logic_vector is
  begin
    return std_logic_vector(to_unsigned(trb_action_t'pos(a), 3));
  end function;
end package body;

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

entity xhci_trb_ring is
  generic (
    RING_N : natural := 8;          -- TRBs per segment
    PTRW   : natural := 3           -- pointer width: holds 0 .. RING_N-1
  );
  port (
    clk              : in  std_logic;
    rst_n            : in  std_logic;

    run              : in  std_logic;   -- the endpoint is running
    trb_valid        : in  std_logic;   -- a TRB has been fetched
    trb_cycle        : in  std_logic;   -- its Cycle bit
    trb_is_link      : in  std_logic;   -- it is a Link TRB
    trb_toggle_cycle : in  std_logic;   -- ...with the Toggle Cycle flag
    trb_link_target  : in  std_logic_vector(PTRW-1 downto 0);
    doorbell         : in  std_logic;   -- software: "I enqueued something"

    owned            : out std_logic;   -- TRB.Cycle = CCS
    consume          : out std_logic;
    is_transfer      : out std_logic;   -- real work, not a link
    follow_link      : out std_logic;
    toggle           : out std_logic;   -- invert CCS this cycle
    action           : out std_logic_vector(2 downto 0);

    deq_ptr          : out std_logic_vector(PTRW-1 downto 0);
    ccs              : out std_logic;   -- Consumer Cycle State
    ring_empty       : out std_logic;
    stopped          : out std_logic;   -- latched, until the doorbell

    n_transfers      : out std_logic_vector(31 downto 0);
    n_links          : out std_logic_vector(31 downto 0);
    n_toggles        : out std_logic_vector(31 downto 0);
    n_empty          : out std_logic_vector(31 downto 0);
    n_restarts       : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of xhci_trb_ring is
  signal deq_r     : natural range 0 to RING_N-1 := 0;
  signal ccs_r     : std_logic := '1';
  signal stopped_r : std_logic := '0';

  signal running, own_s, empty_s, fol_s, xfer_s, tog_s : std_logic;
  signal act_s : trb_action_t;

  signal deq_n : natural range 0 to RING_N-1;
  signal ccs_n, stopped_n : std_logic;

  signal tr_c, lk_c, tg_c, em_c, rs_c : unsigned(31 downto 0)
       := (others => '0');
begin
  deq_ptr <= std_logic_vector(to_unsigned(deq_r, PTRW));
  ccs     <= ccs_r;
  stopped <= stopped_r;

  running <= run and (not stopped_r);

  -- ---- THE OWNERSHIP TEST. One comparison, and everything follows. ----
  --
  -- A TRB whose Cycle bit differs from CCS is one software has not handed
  -- over. What is physically in memory there is the PREVIOUS lap's TRB --
  -- real-looking data, with a real-looking type and a real-looking pointer,
  -- that must not be acted on.
  own_s   <= '1' when (running = '1' and trb_valid = '1'
                       and trb_cycle = ccs_r) else '0';
  empty_s <= '1' when (running = '1' and trb_valid = '1'
                       and trb_cycle /= ccs_r) else '0';

  fol_s  <= own_s and trb_is_link;
  xfer_s <= own_s and (not trb_is_link);

  -- The toggle is NOT implicit in wrapping. Only a Link TRB carrying the
  -- Toggle Cycle flag inverts the consumer's cycle state.
  tog_s <= fol_s and trb_toggle_cycle;

  owned       <= own_s;
  consume     <= own_s;
  is_transfer <= xfer_s;
  follow_link <= fol_s;
  toggle      <= tog_s;
  ring_empty  <= empty_s;

  -- The same decision as one named value. TRB_IDLE, TRB_UNOWNED and a
  -- consumed TRB that happens to be a plain link all look like "the pointer
  -- did not move much" on a waveform, and they are three different states of
  -- the world.
  classify : process (running, trb_valid, own_s, trb_is_link,
                      trb_toggle_cycle)
  begin
    if running = '0' or trb_valid = '0' then
      act_s <= TRB_IDLE;
    elsif own_s = '0' then
      act_s <= TRB_UNOWNED;
    elsif trb_is_link = '0' then
      act_s <= TRB_TRANSFER;
    elsif trb_toggle_cycle = '1' then
      act_s <= TRB_LINK_TOG;
    else
      act_s <= TRB_LINK;
    end if;
  end process;

  action <= act_code(act_s);

  -- ---- Next state, as priority chains ----
  --
  -- A Link TRB moves the dequeue pointer to the link's target; an ordinary
  -- consumed TRB advances it by one. A correct ring always ends in a Link
  -- TRB, so the increment never has to wrap -- but it wraps anyway, because a
  -- controller must not walk off the end of an array software got wrong.
  nextptr : process (run, fol_s, own_s, deq_r, trb_link_target)
  begin
    if run = '0' then
      deq_n <= 0;
    elsif fol_s = '1' then
      deq_n <= to_integer(unsigned(trb_link_target));
    elsif own_s = '1' then
      if deq_r = RING_N - 1 then
        deq_n <= 0;
      else
        deq_n <= deq_r + 1;
      end if;
    else
      deq_n <= deq_r;
    end if;
  end process;

  -- CCS resets to ONE, not zero. Software's PCS also starts at 1, and a ring
  -- is zeroed before use -- so every TRB initially reads Cycle = '0', which
  -- is "not owned by hardware". An empty ring is therefore empty by
  -- construction, with no initialisation handshake at all.
  ccs_n <= '1' when run = '0'
      else (not ccs_r) when tog_s = '1'
      else ccs_r;

  -- Same pattern as the EHCI walker in chapter 22.1: the condition that stops
  -- the consumer evaporates the moment it stops, so it has to be latched, and
  -- software has to ring a doorbell to say the ring is no longer empty.
  stopped_n <= '0' when run = '0'
          else '0' when doorbell = '1'
          else '1' when empty_s = '1'
          else stopped_r;

  regs : process (clk, rst_n)
  begin
    if rst_n = '0' then
      deq_r     <= 0;
      ccs_r     <= '1';
      stopped_r <= '0';
      tr_c <= (others => '0');
      lk_c <= (others => '0');
      tg_c <= (others => '0');
      em_c <= (others => '0');
      rs_c <= (others => '0');
    elsif rising_edge(clk) then
      deq_r     <= deq_n;
      ccs_r     <= ccs_n;
      stopped_r <= stopped_n;

      if xfer_s = '1' then
        tr_c <= tr_c + 1;
      end if;
      if fol_s = '1' then
        lk_c <= lk_c + 1;
      end if;
      if tog_s = '1' then
        tg_c <= tg_c + 1;
      end if;
      if empty_s = '1' then
        em_c <= em_c + 1;
      end if;
      -- Doorbells that actually RELEASED a stopped consumer -- as distinct
      -- from doorbells rung while it was already running, which are the
      -- common case and cost nothing.
      if run = '1' and doorbell = '1' and stopped_r = '1' then
        rs_c <= rs_c + 1;
      end if;
    end if;
  end process;

  n_transfers <= std_logic_vector(tr_c);
  n_links     <= std_logic_vector(lk_c);
  n_toggles   <= std_logic_vector(tg_c);
  n_empty     <= std_logic_vector(em_c);
  n_restarts  <= std_logic_vector(rs_c);
end architecture;

10. Seeing a Lap and the Toggle

Four transfers, a plain link, the closing link, and the cycle state flipping

xhci_trb_ring — a lap, the toggle, and last lap's leftovers

10 cycles
A ten-cycle waveform. CCS starts at 1. Owned transfer TRBs are consumed at cycles 1 and 2 and the dequeue pointer advances. At cycle 4 a link TRB with toggle cycle clear moves the pointer to 6 without changing CCS. At cycle 6 a link TRB with toggle cycle set moves the pointer to 0 and inverts CCS to 0. At cycle 8 a TRB carrying cycle 1 is no longer owned and ring_empty rises.plain link: CCS unchangedplain link: CCS unchangedclosing link: CCS invertsclosing link: CCS invertslast lap's TRB: unownedlast lap's TRB: unownedclktrb_cycletrb_is_linktrb_toggle_cycleactionIDLEXFERXFERIDLELINKIDLELINKTOGIDLEUNOWNEDIDLEdeq_ptr0012266000ccsring_emptyn_transfers0012222222t0t1t2t3t4t5t6t7t8t9
Cycles 1–2: owned TRBs are consumed and the pointer advances. Cycle 4: a Link TRB with Toggle Cycle CLEAR — the pointer jumps, CCS does not move. Cycle 6: the closing link with Toggle Cycle SET — CCS inverts to 0. Cycle 8: last lap's TRB, still carrying Cycle = 1, now reads as unowned.

Read cycles 6 and 8 together. Nothing about the TRB at cycle 8 changed — it still carries Cycle = 1, exactly as it did when it was consumed a lap ago. What changed is CCS. That is the whole mechanism, and it is why it costs one flip-flop.

11. The Testbenches

Two exhaustive domains, because the ownership decision and the pointer update are independent questions:

DomainWhat it enumeratesSize
A — ownership8 deq_ptr × 2 CCS × run × trb_valid × trb_cycle × is_link × toggle_cycle × doorbell1024
B — link targetevery starting pointer × every link target64

Both latched registers and the pointer are reached through legal transitions only — goto_state consumes owned transfer TRBs to advance the pointer and follows a toggling link to invert CCS, rather than forcing registers.

11.1 Verilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_tr_v;
  localparam RING_N = 8, PTRW = 3;
  reg clk=0, rst_n=0;
  reg run=0, trb_valid=0, trb_cycle=0, trb_is_link=0, trb_toggle_cycle=0;
  reg doorbell=0;
  reg [PTRW-1:0] trb_link_target=0;
  wire owned, consume, is_transfer, follow_link, toggle;
  wire ccs, ring_empty, stopped;
  wire [PTRW-1:0] deq_ptr;
  wire [2:0] action;
  wire [31:0] n_transfers, n_links, n_toggles, n_empty, n_restarts;
  always #5 clk=~clk;

  xhci_trb_ring #(.RING_N(RING_N), .PTRW(PTRW)) dut (
    .clk(clk), .rst_n(rst_n), .run(run), .trb_valid(trb_valid),
    .trb_cycle(trb_cycle), .trb_is_link(trb_is_link),
    .trb_toggle_cycle(trb_toggle_cycle), .trb_link_target(trb_link_target),
    .doorbell(doorbell), .owned(owned), .consume(consume),
    .is_transfer(is_transfer), .follow_link(follow_link), .toggle(toggle),
    .action(action),
    .deq_ptr(deq_ptr), .ccs(ccs), .ring_empty(ring_empty),
    .stopped(stopped), .n_transfers(n_transfers), .n_links(n_links),
    .n_toggles(n_toggles), .n_empty(n_empty), .n_restarts(n_restarts));

  // ---- SHADOW MODEL of all three registers ----
  integer s_deq, s_ccs, s_stopped;
  integer m_tr, m_lk, m_tg, m_em, m_rs;

  integer errors=0, i, a, b, c, d, e, f, g, p;
  integer n_exh=0, n_lnk_exh=0;
  integer n_deq [0:7];
  integer n_owned=0, n_unowned=0, n_link=0, n_tog=0, n_notog=0;

  task check(input cond, input [639:0] msg);
    begin if (!cond) begin errors=errors+1;
      if (errors <= 25)
        $display("  FAIL: %0s (run=%b vld=%b cyc=%b link=%b tc=%b tgt=%0d db=%b | own=%b cons=%b xfer=%b fol=%b tog=%b deq=%0d ccs=%b empty=%b stop=%b || model deq=%0d ccs=%0d stop=%0d, t=%0t)",
                 msg, run, trb_valid, trb_cycle, trb_is_link,
                 trb_toggle_cycle, trb_link_target, doorbell, owned, consume,
                 is_transfer, follow_link, toggle, deq_ptr, ccs, ring_empty,
                 stopped, s_deq, s_ccs, s_stopped, $time);
    end end
  endtask

  localparam [2:0] TRB_IDLE=0, TRB_TRANSFER=1, TRB_LINK=2, TRB_LINK_TOG=3,
                   TRB_UNOWNED=4;

  task check_comb;
    reg e_running, e_own, e_empty, e_fol, e_xfer, e_tog;
    reg [2:0] e_act;
    begin
      // The model states the ownership test as an XNOR of the two bits where
      // the design compares them -- a different route to the same answer.
      e_running = run && (s_stopped == 0);
      e_own   = e_running && trb_valid && ~(trb_cycle ^ s_ccs[0]);
      e_empty = e_running && trb_valid && (trb_cycle ^ s_ccs[0]);
      e_fol   = e_own && trb_is_link;
      e_xfer  = e_own && !trb_is_link;
      e_tog   = e_fol && trb_toggle_cycle;

      check(owned       === e_own,   "owned matches the model");
      check(consume     === e_own,   "consume matches the model");
      check(ring_empty  === e_empty, "ring_empty matches the model");
      check(follow_link === e_fol,   "follow_link matches the model");
      check(is_transfer === e_xfer,  "is_transfer matches the model");
      check(toggle      === e_tog,   "toggle matches the model");

      if      (!e_running || !trb_valid) e_act = TRB_IDLE;
      else if (!e_own)                   e_act = TRB_UNOWNED;
      else if (!trb_is_link)             e_act = TRB_TRANSFER;
      else if (trb_toggle_cycle)         e_act = TRB_LINK_TOG;
      else                               e_act = TRB_LINK;
      check(action === e_act, "action matches the model");
      // The named decision must agree with the booleans it summarises.
      check((action === TRB_TRANSFER) === is_transfer,
            "action disagrees with is_transfer");
      check((action === TRB_LINK_TOG) === toggle,
            "action disagrees with toggle");
      check((action === TRB_UNOWNED) === ring_empty,
            "action disagrees with ring_empty");
      check(deq_ptr     === s_deq[PTRW-1:0], "deq_ptr matches the model");
      check(ccs         === s_ccs[0],        "ccs matches the model");
      check(stopped     === (s_stopped != 0),"stopped matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE property. A TRB whose cycle bit differs from CCS belongs to
      //    software and must not be touched -- what is there is the previous
      //    lap's contents, which look exactly like real TRBs.
      if (trb_valid && (trb_cycle !== ccs)) begin
        check(!consume,
              "an unowned TRB was consumed -- that is last lap's data, not software's");
        check(!follow_link,
              "an unowned Link TRB was followed -- that pointer was never written");
        check(!is_transfer, "an unowned TRB was run as a transfer");
      end
      // 2. Owned and empty are exactly complementary while running.
      check(!(owned && ring_empty),
            "a TRB was both owned and not owned");
      // 3. Consuming and the two kinds of TRB partition cleanly.
      check(!(is_transfer && follow_link),
            "a TRB was both a transfer and a link");
      if (consume) check(is_transfer || follow_link,
                         "a TRB was consumed as neither a transfer nor a link");
      // 4. THE toggle rule. Only a Link TRB carrying Toggle Cycle may invert
      //    the cycle state. Anything else desynchronises the ring for ever.
      if (toggle) begin
        check(follow_link,
              "the cycle state was inverted on something that is not a Link TRB");
        check(trb_toggle_cycle,
              "the cycle state was inverted on a Link TRB with Toggle Cycle CLEAR");
      end
      // 5. A Link TRB without Toggle Cycle never inverts.
      if (follow_link && !trb_toggle_cycle)
        check(!toggle,
              "a plain segment link inverted the cycle state");
      // 6. Nothing happens while the endpoint is not running.
      if (!run) begin
        check(!consume && !ring_empty && !toggle,
              "the ring was consumed with the endpoint stopped");
      end
      // 7. A stopped consumer consumes nothing.
      if (stopped) check(!consume, "a stopped consumer took a TRB");
      // 8. The dequeue pointer moves only for a reason: a consumed TRB
      //    advances it, a followed link relocates it, and nothing else
      //    touches it. (Its RANGE is structural -- a PTRW-bit pointer into a
      //    2**PTRW-entry segment cannot leave it -- so range is not the
      //    property worth asserting here; provenance is.)
      if (!consume && run && (s_stopped == 0))
        check(deq_ptr === s_deq[PTRW-1:0],
              "the dequeue pointer moved with no TRB consumed");

      if (s_deq >= 0 && s_deq < 8) n_deq[s_deq] = n_deq[s_deq] + 1;
      if (e_own)   n_owned   = n_owned + 1;
      if (e_empty) n_unowned = n_unowned + 1;
      if (e_fol)   n_link    = n_link + 1;
      if (e_tog)   n_tog     = n_tog + 1;
      if (e_fol && !trb_toggle_cycle) n_notog = n_notog + 1;
    end
  endtask

  task model_step;
    reg e_running, e_own, e_empty, e_fol, e_xfer, e_tog;
    integer nd;
    begin
      e_running = run && (s_stopped == 0);
      e_own   = e_running && trb_valid && (trb_cycle == s_ccs[0]);
      e_empty = e_running && trb_valid && (trb_cycle != s_ccs[0]);
      e_fol   = e_own && trb_is_link;
      e_xfer  = e_own && !trb_is_link;
      e_tog   = e_fol && trb_toggle_cycle;

      if (e_xfer)  m_tr = m_tr + 1;
      if (e_fol)   m_lk = m_lk + 1;
      if (e_tog)   m_tg = m_tg + 1;
      if (e_empty) m_em = m_em + 1;
      if (run && doorbell && (s_stopped != 0)) m_rs = m_rs + 1;

      // next dequeue pointer
      if (!run)         nd = 0;
      else if (e_fol)   nd = trb_link_target;
      else if (e_own)   nd = (s_deq == RING_N-1) ? 0 : s_deq + 1;
      else              nd = s_deq;
      s_deq = nd;

      if (!run)       s_ccs = 1;
      else if (e_tog) s_ccs = 1 - s_ccs;

      if (!run)        s_stopped = 0;
      else if (doorbell) s_stopped = 0;
      else if (e_empty)  s_stopped = 1;
    end
  endtask

  task step;
    begin
      #1;
      check_comb;
      model_step;
      @(posedge clk); #1;
      check(deq_ptr === s_deq[PTRW-1:0],   "deq_ptr tracked the model");
      check(ccs     === s_ccs[0],          "ccs tracked the model");
      check(stopped === (s_stopped != 0),  "stopped tracked the model");
      check(n_transfers === m_tr[31:0], "n_transfers matches the model");
      check(n_links     === m_lk[31:0], "n_links matches the model");
      check(n_toggles   === m_tg[31:0], "n_toggles matches the model");
      check(n_empty     === m_em[31:0], "n_empty matches the model");
      check(n_restarts  === m_rs[31:0], "n_restarts matches the model");
    end
  endtask

  task idle_in;
    begin
      trb_valid=0; trb_is_link=0; trb_toggle_cycle=0; doorbell=0;
    end
  endtask

  task hard_reset;
    begin
      rst_n=0; run=0; idle_in; trb_cycle=0; trb_link_target=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      s_deq=0; s_ccs=1; s_stopped=0;
      m_tr=0; m_lk=0; m_tg=0; m_em=0; m_rs=0;
    end
  endtask

  // Walk the consumer to a chosen (deq_ptr, ccs, stopped) using only legal
  // transitions: consuming owned transfer TRBs advances the pointer, a Link
  // TRB with Toggle Cycle inverts the cycle state, and an unowned TRB stops.
  task goto_state(input [PTRW-1:0] want_deq, input want_ccs,
                  input want_stopped);
    begin
      hard_reset;
      run=1; step; idle_in;
      if (!want_ccs) begin
        // one Link TRB with Toggle Cycle, pointing back at entry 0
        trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=1;
        trb_link_target=0; step; idle_in;
      end
      // advance by consuming owned transfer TRBs
      for (p=0; p<want_deq; p=p+1) begin
        trb_valid=1; trb_cycle=want_ccs; trb_is_link=0; step; idle_in;
      end
      if (want_stopped) begin
        // an unowned TRB stops the consumer without moving the pointer
        trb_valid=1; trb_cycle=~want_ccs; trb_is_link=0; step; idle_in;
      end
      #1;
      check(deq_ptr === want_deq, "goto_state reached the dequeue pointer");
      check(ccs === want_ccs,     "goto_state reached the cycle state");
      check(stopped === want_stopped, "goto_state reached the stop latch");
    end
  endtask

  initial begin
    for (i=0;i<8;i=i+1) n_deq[i]=0;
    hard_reset;
    check(ccs === 1'b1,
          "CCS resets to ONE -- a zeroed ring then reads as unowned");
    check(deq_ptr === 3'd0, "and the dequeue pointer to zero");

    // ===== A. EXHAUSTIVE sweep of the ownership and toggle rules =====
    // 8 (deq_ptr) x 2 (ccs) x 2 (run) x 2 (trb_valid) x 2 (trb_cycle)
    //   x 2 (is_link) x 2 (toggle_cycle) x 2 (doorbell)
    // = 1024 one-step transitions, with the link target fixed; the target is
    // swept separately below because it only matters on a followed link.
    for (a=0; a<8; a=a+1)          // deq_ptr
     for (b=0; b<2; b=b+1)         // ccs
      for (c=0; c<2; c=c+1)        // run
       for (d=0; d<2; d=d+1)       // trb_valid
        for (e=0; e<2; e=e+1)      // trb_cycle
         for (f=0; f<2; f=f+1)     // is_link
          for (g=0; g<2; g=g+1)    // toggle_cycle
           for (i=0; i<2; i=i+1) begin   // doorbell
             goto_state(a[PTRW-1:0], b[0], 1'b0);
             run=c[0]; trb_valid=d[0]; trb_cycle=e[0];
             trb_is_link=f[0]; trb_toggle_cycle=g[0];
             trb_link_target=3'd5; doorbell=i[0];
             step;
             n_exh = n_exh + 1;
             idle_in;
           end
    $display("  exhaustive ownership sweep: %0d of %0d transitions verified",
             n_exh, 8*2*2*2*2*2*2*2);

    // ===== B. EXHAUSTIVE link-target sweep =====
    // Every starting pointer against every link target: 8 x 8 = 64 followed
    // links, which is the whole of the pointer-update domain.
    for (a=0; a<8; a=a+1)
     for (b=0; b<8; b=b+1) begin
       goto_state(a[PTRW-1:0], 1'b1, 1'b0);
       trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=0;
       trb_link_target=b[PTRW-1:0];
       step; idle_in;
       #1;
       check(deq_ptr === b[PTRW-1:0],
             "a followed Link TRB moves the dequeue pointer to its target");
       check(ccs === 1'b1,
             "and leaves the cycle state alone when Toggle Cycle is clear");
       n_lnk_exh = n_lnk_exh + 1;
     end
    $display("  exhaustive link-target sweep: %0d of 64 followed links verified",
             n_lnk_exh);

    // ===== C. directed: one full lap of a ring =====
    hard_reset; run=1; step; idle_in;
    check(ccs === 1'b1, "CCS starts at 1");

    // 1. A zeroed ring reads as unowned: every TRB has Cycle = 0 and CCS is
    //    1, so an untouched ring is empty with no handshake at all.
    trb_valid=1; trb_cycle=0; trb_is_link=0; #1;
    check(!owned, "a zeroed TRB is NOT owned by hardware");
    check(ring_empty, "so the ring reads empty");
    step; idle_in;
    #1; check(stopped, "and the consumer stops");

    // 2. Software enqueues: writes a TRB with Cycle = PCS = 1, then rings.
    doorbell=1; step; idle_in;
    #1; check(!stopped, "the doorbell restarts the consumer");
    check(n_restarts === 32'd1, "and that restart was counted");

    trb_valid=1; trb_cycle=1; trb_is_link=0; step; idle_in;
    check(n_transfers === 32'd1, "the TRB is taken as a transfer");
    #1; check(deq_ptr === 3'd1, "and the pointer advances by one");

    // 3. Three more transfers.
    for (i=0;i<3;i=i+1) begin
      trb_valid=1; trb_cycle=1; trb_is_link=0; step; idle_in;
    end
    #1; check(deq_ptr === 3'd4, "four transfers consumed");
    check(n_transfers === 32'd4, "and counted");

    // 4. A Link TRB that merely joins segments: follow it, do NOT toggle.
    trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=0;
    trb_link_target=3'd6; #1;
    check(follow_link, "an owned Link TRB is followed");
    check(!is_transfer, "and is NOT counted as a transfer");
    check(!toggle, "and with Toggle Cycle clear it does not invert CCS");
    step; idle_in;
    #1;
    check(deq_ptr === 3'd6, "the pointer moved to the link's target");
    check(ccs === 1'b1, "and the cycle state is unchanged");
    check(n_links === 32'd1, "one link followed");
    check(n_transfers === 32'd4, "and still four transfers");

    // 5. THE case. The Link TRB that closes the ring carries Toggle Cycle.
    trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=1;
    trb_link_target=3'd0; #1;
    check(toggle, "the closing link inverts the cycle state");
    step; idle_in;
    #1;
    check(deq_ptr === 3'd0, "back to the start of the segment");
    check(ccs === 1'b0, "with CCS now ZERO -- the next lap uses the other value");
    check(n_toggles === 32'd1, "one toggle");

    // 6. And now the OLD TRBs -- the ones from the first lap, still
    //    physically in memory with Cycle = 1 -- read as unowned.
    trb_valid=1; trb_cycle=1; trb_is_link=0; #1;
    check(!owned,
          "last lap's TRB is not owned this lap -- that is the whole mechanism");
    check(ring_empty, "so the ring reads empty");
    step; idle_in;

    // 7. Software's next enqueue writes Cycle = 0, matching the new CCS.
    doorbell=1; step; idle_in;
    trb_valid=1; trb_cycle=0; trb_is_link=0; #1;
    check(owned, "a TRB written with the NEW cycle value is owned");
    step; idle_in;
    check(n_transfers === 32'd5, "and consumed");

    // 8. An unowned LINK TRB is not followed either -- it is the end of what
    //    software has produced, exactly like any other unowned TRB.
    trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=1;
    trb_link_target=3'd3; #1;
    check(!owned, "an unowned Link TRB is still unowned");
    check(!follow_link, "so it is NOT followed");
    check(!toggle, "and does NOT invert the cycle state");
    step; idle_in;
    #1; check(ccs === 1'b0, "the cycle state survived intact");

    // ===== D. randomised =====
    hard_reset; run=1;
    for (i=0;i<40000;i=i+1) begin
      run              = ({$random}%32)!=0;
      trb_valid        = ({$random}%4)!=0;
      // bias toward OWNED TRBs -- a ring that is mostly unowned never
      // exercises the consume path
      trb_cycle        = (({$random}%4)!=0) ? ccs : ~ccs;
      trb_is_link      = ({$random}%6)==0;
      trb_toggle_cycle = ({$random}%2);
      trb_link_target  = {$random}%8;
      doorbell         = ({$random}%8)==0;
      step;
    end

    for (i=0;i<8;i=i+1)
      check(n_deq[i] > 200, "every dequeue position was occupied many times");
    check(n_owned   > 5000, "owned TRBs were seen many times");
    check(n_unowned > 1000, "unowned TRBs were seen many times");
    check(n_link    > 1000, "Link TRBs were followed many times");
    check(n_tog     > 400,  "the cycle state was toggled many times");
    check(n_notog   > 400,  "and plain segment links were followed many times");

    $display("");
    $display("  REACH: ownership=%0d link-target=%0d | dequeue positions: %0d %0d %0d %0d %0d %0d %0d %0d",
             n_exh, n_lnk_exh, n_deq[0], n_deq[1], n_deq[2], n_deq[3], n_deq[4],
             n_deq[5], n_deq[6], n_deq[7]);
    $display("  CASES: owned=%0d unowned=%0d links-followed=%0d toggling=%0d plain=%0d",
             n_owned, n_unowned, n_link, n_tog, n_notog);
    $display("  COUNTERS: transfers=%0d links=%0d toggles=%0d empty=%0d restarts=%0d",
             n_transfers, n_links, n_toggles, n_empty, n_restarts);
    $display("  [Verilog] xhci_trb_ring: %0d errors", errors);
    $display("  [Verilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

11.2 SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
module tb_tr_sv;
  import xhci_ring_pkg::*;

  localparam RING_N = 8, PTRW = 3;
  logic clk=0, rst_n=0;
  logic run=0, trb_valid=0, trb_cycle=0, trb_is_link=0, trb_toggle_cycle=0;
  logic doorbell=0;
  logic [PTRW-1:0] trb_link_target=0;
  logic owned, consume, is_transfer, follow_link, toggle;
  logic ccs, ring_empty, stopped;
  logic [PTRW-1:0] deq_ptr;
  trb_action_e action;
  logic [31:0] n_transfers, n_links, n_toggles, n_empty, n_restarts;

  // Icarus seeds $random and $urandom identically, so an unseeded run would
  // replay the Verilog suite's stimulus exactly. See chapter 20.5 section 9.2.
  int urandom_seed = 22202;
  always #5 clk=~clk;

  xhci_trb_ring #(.RING_N(RING_N), .PTRW(PTRW)) dut (
    .clk, .rst_n, .run, .trb_valid, .trb_cycle, .trb_is_link,
    .trb_toggle_cycle, .trb_link_target, .doorbell, .owned, .consume,
    .is_transfer, .follow_link, .toggle, .action, .deq_ptr, .ccs,
    .ring_empty, .stopped, .n_transfers, .n_links, .n_toggles, .n_empty,
    .n_restarts);

  // ---- SHADOW MODEL of all three registers ----
  int s_deq, s_ccs, s_stopped;
  int m_tr, m_lk, m_tg, m_em, m_rs;

  int errors=0, i, a, b, c, d, e, f, g, p;
  int n_exh=0, n_lnk_exh=0;
  int n_deq [8];
  int n_owned=0, n_unowned=0, n_link=0, n_tog=0, n_notog=0;

  task automatic check(input bit cond, input string msg);
    // Icarus will not call .name() on a net, so the enum output is copied
    // into a variable of the same type before being printed.
    trb_action_e ac_v;
    if (!cond) begin
      errors++;
      ac_v = action;
      if (errors <= 25)
        $display("  FAIL: %0s (run=%b vld=%b cyc=%b link=%b tc=%b tgt=%0d db=%b | act=%s own=%b tog=%b deq=%0d ccs=%b empty=%b stop=%b || model deq=%0d ccs=%0d stop=%0d, t=%0t)",
                 msg, run, trb_valid, trb_cycle, trb_is_link,
                 trb_toggle_cycle, trb_link_target, doorbell, ac_v.name(),
                 owned, toggle, deq_ptr, ccs, ring_empty, stopped, s_deq,
                 s_ccs, s_stopped, $time);
    end
  endtask

  task automatic check_comb;
    bit e_running, e_own, e_empty, e_fol, e_xfer, e_tog;
    trb_action_e e_act;
    begin
      // The model states the ownership test as an XNOR of the two bits where
      // the design compares them -- a different route to the same answer.
      e_running = run && (s_stopped == 0);
      e_own   = e_running && trb_valid && ~(trb_cycle ^ 1'(s_ccs));
      e_empty = e_running && trb_valid && (trb_cycle ^ 1'(s_ccs));
      e_fol   = e_own && trb_is_link;
      e_xfer  = e_own && !trb_is_link;
      e_tog   = e_fol && trb_toggle_cycle;

      check(owned       === e_own,   "owned matches the model");
      check(consume     === e_own,   "consume matches the model");
      check(ring_empty  === e_empty, "ring_empty matches the model");
      check(follow_link === e_fol,   "follow_link matches the model");
      check(is_transfer === e_xfer,  "is_transfer matches the model");
      check(toggle      === e_tog,   "toggle matches the model");

      if      (!e_running || !trb_valid) e_act = TRB_IDLE;
      else if (!e_own)                   e_act = TRB_UNOWNED;
      else if (!trb_is_link)             e_act = TRB_TRANSFER;
      else if (trb_toggle_cycle)         e_act = TRB_LINK_TOG;
      else                               e_act = TRB_LINK;
      check(action === e_act, "action matches the model");
      // The named decision must agree with the booleans it summarises.
      check((action === TRB_TRANSFER) === is_transfer,
            "action disagrees with is_transfer");
      check((action === TRB_LINK_TOG) === toggle,
            "action disagrees with toggle");
      check((action === TRB_UNOWNED) === ring_empty,
            "action disagrees with ring_empty");
      check(deq_ptr     === PTRW'(s_deq), "deq_ptr matches the model");
      check(ccs         === 1'(s_ccs),        "ccs matches the model");
      check(stopped     === (s_stopped != 0),"stopped matches the model");

      // ---- SAFETY PROPERTIES, independent of the model ----
      // 1. THE property. A TRB whose cycle bit differs from CCS belongs to
      //    software and must not be touched -- what is there is the previous
      //    lap's contents, which look exactly like real TRBs.
      if (trb_valid && (trb_cycle !== ccs)) begin
        check(!consume,
              "an unowned TRB was consumed -- that is last lap's data, not software's");
        check(!follow_link,
              "an unowned Link TRB was followed -- that pointer was never written");
        check(!is_transfer, "an unowned TRB was run as a transfer");
      end
      // 2. Owned and empty are exactly complementary while running.
      check(!(owned && ring_empty),
            "a TRB was both owned and not owned");
      // 3. Consuming and the two kinds of TRB partition cleanly.
      check(!(is_transfer && follow_link),
            "a TRB was both a transfer and a link");
      if (consume) check(is_transfer || follow_link,
                         "a TRB was consumed as neither a transfer nor a link");
      // 4. THE toggle rule. Only a Link TRB carrying Toggle Cycle may invert
      //    the cycle state. Anything else desynchronises the ring for ever.
      if (toggle) begin
        check(follow_link,
              "the cycle state was inverted on something that is not a Link TRB");
        check(trb_toggle_cycle,
              "the cycle state was inverted on a Link TRB with Toggle Cycle CLEAR");
      end
      // 5. A Link TRB without Toggle Cycle never inverts.
      if (follow_link && !trb_toggle_cycle)
        check(!toggle,
              "a plain segment link inverted the cycle state");
      // 6. Nothing happens while the endpoint is not running.
      if (!run) begin
        check(!consume && !ring_empty && !toggle,
              "the ring was consumed with the endpoint stopped");
      end
      // 7. A stopped consumer consumes nothing.
      if (stopped) check(!consume, "a stopped consumer took a TRB");
      // 8. The dequeue pointer moves only for a reason: a consumed TRB
      //    advances it, a followed link relocates it, and nothing else
      //    touches it. (Its RANGE is structural -- a PTRW-bit pointer into a
      //    2**PTRW-entry segment cannot leave it -- so range is not the
      //    property worth asserting here; provenance is.)
      if (!consume && run && (s_stopped == 0))
        check(deq_ptr === PTRW'(s_deq),
              "the dequeue pointer moved with no TRB consumed");

      n_deq[s_deq] = n_deq[s_deq] + 1;
      if (e_own)   n_owned   = n_owned + 1;
      if (e_empty) n_unowned = n_unowned + 1;
      if (e_fol)   n_link    = n_link + 1;
      if (e_tog)   n_tog     = n_tog + 1;
      if (e_fol && !trb_toggle_cycle) n_notog = n_notog + 1;
    end
  endtask

  task automatic model_step;
    bit e_running, e_own, e_empty, e_fol, e_xfer, e_tog;
    int nd;
    begin
      e_running = run && (s_stopped == 0);
      e_own   = e_running && trb_valid && (trb_cycle == 1'(s_ccs));
      e_empty = e_running && trb_valid && (trb_cycle != 1'(s_ccs));
      e_fol   = e_own && trb_is_link;
      e_xfer  = e_own && !trb_is_link;
      e_tog   = e_fol && trb_toggle_cycle;

      if (e_xfer)  m_tr = m_tr + 1;
      if (e_fol)   m_lk = m_lk + 1;
      if (e_tog)   m_tg = m_tg + 1;
      if (e_empty) m_em = m_em + 1;
      if (run && doorbell && (s_stopped != 0)) m_rs = m_rs + 1;

      // next dequeue pointer
      if (!run)         nd = 0;
      else if (e_fol)   nd = trb_link_target;
      else if (e_own)   nd = (s_deq == RING_N-1) ? 0 : s_deq + 1;
      else              nd = s_deq;
      s_deq = nd;

      if (!run)       s_ccs = 1;
      else if (e_tog) s_ccs = 1 - s_ccs;

      if (!run)        s_stopped = 0;
      else if (doorbell) s_stopped = 0;
      else if (e_empty)  s_stopped = 1;
    end
  endtask

  task automatic step;
    begin
      #1;
      check_comb;
      model_step;
      @(posedge clk); #1;
      check(deq_ptr === PTRW'(s_deq),   "deq_ptr tracked the model");
      check(ccs     === 1'(s_ccs),          "ccs tracked the model");
      check(stopped === (s_stopped != 0),  "stopped tracked the model");
      check(n_transfers === 32'(m_tr), "n_transfers matches the model");
      check(n_links     === 32'(m_lk), "n_links matches the model");
      check(n_toggles   === 32'(m_tg), "n_toggles matches the model");
      check(n_empty     === 32'(m_em), "n_empty matches the model");
      check(n_restarts  === 32'(m_rs), "n_restarts matches the model");
    end
  endtask

  task automatic idle_in;
    begin
      trb_valid=0; trb_is_link=0; trb_toggle_cycle=0; doorbell=0;
    end
  endtask

  task automatic hard_reset;
    begin
      rst_n=0; run=0; idle_in; trb_cycle=0; trb_link_target=0;
      @(posedge clk); #1; @(posedge clk); #1; rst_n=1; #1;
      s_deq=0; s_ccs=1; s_stopped=0;
      m_tr=0; m_lk=0; m_tg=0; m_em=0; m_rs=0;
    end
  endtask

  // Walk the consumer to a chosen (deq_ptr, ccs, stopped) using only legal
  // transitions: consuming owned transfer TRBs advances the pointer, a Link
  // TRB with Toggle Cycle inverts the cycle state, and an unowned TRB stops.
  task automatic goto_state(input logic [PTRW-1:0] want_deq,
                            input bit want_ccs, input bit want_stopped);
    begin
      hard_reset;
      run=1; step; idle_in;
      if (!want_ccs) begin
        // one Link TRB with Toggle Cycle, pointing back at entry 0
        trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=1;
        trb_link_target=0; step; idle_in;
      end
      // advance by consuming owned transfer TRBs
      for (p=0; p<want_deq; p=p+1) begin
        trb_valid=1; trb_cycle=want_ccs; trb_is_link=0; step; idle_in;
      end
      if (want_stopped) begin
        // an unowned TRB stops the consumer without moving the pointer
        trb_valid=1; trb_cycle=~want_ccs; trb_is_link=0; step; idle_in;
      end
      #1;
      check(deq_ptr === want_deq, "goto_state reached the dequeue pointer");
      check(ccs === want_ccs,     "goto_state reached the cycle state");
      check(stopped === want_stopped, "goto_state reached the stop latch");
    end
  endtask

  initial begin
    void'($urandom(urandom_seed));
    foreach (n_deq[i]) n_deq[i]=0;
    hard_reset;
    check(ccs === 1'b1,
          "CCS resets to ONE -- a zeroed ring then reads as unowned");
    check(deq_ptr === 3'd0, "and the dequeue pointer to zero");

    // ===== A. EXHAUSTIVE sweep of the ownership and toggle rules =====
    // 8 (deq_ptr) x 2 (ccs) x 2 (run) x 2 (trb_valid) x 2 (trb_cycle)
    //   x 2 (is_link) x 2 (toggle_cycle) x 2 (doorbell)
    // = 1024 one-step transitions, with the link target fixed; the target is
    // swept separately below because it only matters on a followed link.
    for (a=0; a<8; a=a+1)          // deq_ptr
     for (b=0; b<2; b=b+1)         // ccs
      for (c=0; c<2; c=c+1)        // run
       for (d=0; d<2; d=d+1)       // trb_valid
        for (e=0; e<2; e=e+1)      // trb_cycle
         for (f=0; f<2; f=f+1)     // is_link
          for (g=0; g<2; g=g+1)    // toggle_cycle
           for (i=0; i<2; i=i+1) begin   // doorbell
             goto_state(PTRW'(a), b[0], 1'b0);
             run=c[0]; trb_valid=d[0]; trb_cycle=e[0];
             trb_is_link=f[0]; trb_toggle_cycle=g[0];
             trb_link_target=3'd5; doorbell=i[0];
             step;
             n_exh = n_exh + 1;
             idle_in;
           end
    $display("  exhaustive ownership sweep: %0d of %0d transitions verified",
             n_exh, 8*2*2*2*2*2*2*2);

    // ===== B. EXHAUSTIVE link-target sweep =====
    // Every starting pointer against every link target: 8 x 8 = 64 followed
    // links, which is the whole of the pointer-update domain.
    for (a=0; a<8; a=a+1)
     for (b=0; b<8; b=b+1) begin
       goto_state(PTRW'(a), 1'b1, 1'b0);
       trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=0;
       trb_link_target=PTRW'(b);
       step; idle_in;
       #1;
       check(deq_ptr === PTRW'(b),
             "a followed Link TRB moves the dequeue pointer to its target");
       check(ccs === 1'b1,
             "and leaves the cycle state alone when Toggle Cycle is clear");
       n_lnk_exh = n_lnk_exh + 1;
     end
    $display("  exhaustive link-target sweep: %0d of 64 followed links verified",
             n_lnk_exh);

    // ===== C. directed: one full lap of a ring =====
    hard_reset; run=1; step; idle_in;
    check(ccs === 1'b1, "CCS starts at 1");

    // 1. A zeroed ring reads as unowned: every TRB has Cycle = 0 and CCS is
    //    1, so an untouched ring is empty with no handshake at all.
    trb_valid=1; trb_cycle=0; trb_is_link=0; #1;
    check(!owned, "a zeroed TRB is NOT owned by hardware");
    check(ring_empty, "so the ring reads empty");
    step; idle_in;
    #1; check(stopped, "and the consumer stops");

    // 2. Software enqueues: writes a TRB with Cycle = PCS = 1, then rings.
    doorbell=1; step; idle_in;
    #1; check(!stopped, "the doorbell restarts the consumer");
    check(n_restarts === 32'd1, "and that restart was counted");

    trb_valid=1; trb_cycle=1; trb_is_link=0; step; idle_in;
    check(n_transfers === 32'd1, "the TRB is taken as a transfer");
    #1; check(deq_ptr === 3'd1, "and the pointer advances by one");

    // 3. Three more transfers.
    for (i=0;i<3;i=i+1) begin
      trb_valid=1; trb_cycle=1; trb_is_link=0; step; idle_in;
    end
    #1; check(deq_ptr === 3'd4, "four transfers consumed");
    check(n_transfers === 32'd4, "and counted");

    // 4. A Link TRB that merely joins segments: follow it, do NOT toggle.
    trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=0;
    trb_link_target=3'd6; #1;
    check(follow_link, "an owned Link TRB is followed");
    check(!is_transfer, "and is NOT counted as a transfer");
    check(!toggle, "and with Toggle Cycle clear it does not invert CCS");
    step; idle_in;
    #1;
    check(deq_ptr === 3'd6, "the pointer moved to the link's target");
    check(ccs === 1'b1, "and the cycle state is unchanged");
    check(n_links === 32'd1, "one link followed");
    check(n_transfers === 32'd4, "and still four transfers");

    // 5. THE case. The Link TRB that closes the ring carries Toggle Cycle.
    trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=1;
    trb_link_target=3'd0; #1;
    check(toggle, "the closing link inverts the cycle state");
    step; idle_in;
    #1;
    check(deq_ptr === 3'd0, "back to the start of the segment");
    check(ccs === 1'b0, "with CCS now ZERO -- the next lap uses the other value");
    check(n_toggles === 32'd1, "one toggle");

    // 6. And now the OLD TRBs -- the ones from the first lap, still
    //    physically in memory with Cycle = 1 -- read as unowned.
    trb_valid=1; trb_cycle=1; trb_is_link=0; #1;
    check(!owned,
          "last lap's TRB is not owned this lap -- that is the whole mechanism");
    check(ring_empty, "so the ring reads empty");
    step; idle_in;

    // 7. Software's next enqueue writes Cycle = 0, matching the new CCS.
    doorbell=1; step; idle_in;
    trb_valid=1; trb_cycle=0; trb_is_link=0; #1;
    check(owned, "a TRB written with the NEW cycle value is owned");
    step; idle_in;
    check(n_transfers === 32'd5, "and consumed");

    // 8. An unowned LINK TRB is not followed either -- it is the end of what
    //    software has produced, exactly like any other unowned TRB.
    trb_valid=1; trb_cycle=1; trb_is_link=1; trb_toggle_cycle=1;
    trb_link_target=3'd3; #1;
    check(!owned, "an unowned Link TRB is still unowned");
    check(!follow_link, "so it is NOT followed");
    check(!toggle, "and does NOT invert the cycle state");
    step; idle_in;
    #1; check(ccs === 1'b0, "the cycle state survived intact");

    // ===== D. randomised =====
    hard_reset; run=1;
    for (i=0;i<40000;i=i+1) begin
      run              = ($urandom%32)!=0;
      trb_valid        = ($urandom%4)!=0;
      // bias toward OWNED TRBs -- a ring that is mostly unowned never
      // exercises the consume path
      trb_cycle        = (($urandom%4)!=0) ? ccs : ~ccs;
      trb_is_link      = ($urandom%6)==0;
      trb_toggle_cycle = $urandom%2;
      trb_link_target  = PTRW'($urandom%8);
      doorbell         = ($urandom%8)==0;
      step;
    end

    foreach (n_deq[i])
      check(n_deq[i] > 200, "every dequeue position was occupied many times");
    check(n_owned   > 5000, "owned TRBs were seen many times");
    check(n_unowned > 1000, "unowned TRBs were seen many times");
    check(n_link    > 1000, "Link TRBs were followed many times");
    check(n_tog     > 400,  "the cycle state was toggled many times");
    check(n_notog   > 400,  "and plain segment links were followed many times");

    $display("");
    $display("  REACH: ownership=%0d link-target=%0d | dequeue positions: %0d %0d %0d %0d %0d %0d %0d %0d",
             n_exh, n_lnk_exh, n_deq[0], n_deq[1], n_deq[2], n_deq[3], n_deq[4],
             n_deq[5], n_deq[6], n_deq[7]);
    $display("  CASES: owned=%0d unowned=%0d links-followed=%0d toggling=%0d plain=%0d",
             n_owned, n_unowned, n_link, n_tog, n_notog);
    $display("  COUNTERS: transfers=%0d links=%0d toggles=%0d empty=%0d restarts=%0d",
             n_transfers, n_links, n_toggles, n_empty, n_restarts);
    $display("  [SystemVerilog] xhci_trb_ring: %0d errors", errors);
    $display("  [SystemVerilog] %0s", errors==0 ? "PASS" : "FAIL");
    $display("");
    $finish;
  end
endmodule

11.3 VHDL testbench

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

entity tb_tr_vhdl is
end entity;

architecture sim of tb_tr_vhdl is
  constant RING_N : natural := 8;
  constant PTRW   : natural := 3;

  signal clk   : std_logic := '0';
  signal rst_n : std_logic := '0';
  signal run, trb_valid, trb_cycle, trb_is_link : std_logic := '0';
  signal trb_toggle_cycle, doorbell : std_logic := '0';
  signal trb_link_target : std_logic_vector(PTRW-1 downto 0)
       := (others => '0');

  signal owned, consume, is_transfer, follow_link, toggle : std_logic;
  signal ccs, ring_empty, stopped : std_logic;
  signal deq_ptr : std_logic_vector(PTRW-1 downto 0);
  signal action  : std_logic_vector(2 downto 0);
  signal n_transfers, n_links, n_toggles, n_empty, n_restarts
       : std_logic_vector(31 downto 0);

  signal running_sim : boolean := true;

  type cnt8_t is array (0 to 7) of integer;
begin
  clk <= not clk after 5 ns when running_sim else '0';

  dut : entity work.xhci_trb_ring
    generic map (RING_N => RING_N, PTRW => PTRW)
    port map (clk => clk, rst_n => rst_n, run => run,
              trb_valid => trb_valid, trb_cycle => trb_cycle,
              trb_is_link => trb_is_link,
              trb_toggle_cycle => trb_toggle_cycle,
              trb_link_target => trb_link_target, doorbell => doorbell,
              owned => owned, consume => consume, is_transfer => is_transfer,
              follow_link => follow_link, toggle => toggle, action => action,
              deq_ptr => deq_ptr, ccs => ccs, ring_empty => ring_empty,
              stopped => stopped, n_transfers => n_transfers,
              n_links => n_links, n_toggles => n_toggles, n_empty => n_empty,
              n_restarts => n_restarts);

  stim : process
    variable seed1 : positive := 6473;
    variable seed2 : positive := 1987;
    variable r1    : real;

    -- VHDL-2008 requires a shared variable to have a protected type, so the
    -- bookkeeping lives inside the single stimulus process instead.
    variable errors : integer := 0;

    -- SHADOW MODEL of all three registers.
    variable s_deq : integer := 0;
    variable s_ccs : std_logic := '1';
    variable s_stopped : integer := 0;
    variable m_tr, m_lk, m_tg, m_em, m_rs : integer := 0;

    variable n_exh, n_lnk_exh : integer := 0;
    variable n_deq : cnt8_t := (others => 0);
    variable n_owned, n_unowned, n_link, n_tog, n_notog : integer := 0;

    procedure check(cond : boolean; msg : string) is
    begin
      if not cond then
        errors := errors + 1;
        if errors <= 25 then
          report "  FAIL: " & msg
               & " (run=" & std_logic'image(run)(2)
               & " vld=" & std_logic'image(trb_valid)(2)
               & " cyc=" & std_logic'image(trb_cycle)(2)
               & " link=" & std_logic'image(trb_is_link)(2)
               & " tc=" & std_logic'image(trb_toggle_cycle)(2)
               & " tgt=" & integer'image(to_integer(unsigned(trb_link_target)))
               & " db=" & std_logic'image(doorbell)(2)
               & " | act=" & integer'image(to_integer(unsigned(action)))
               & " own=" & std_logic'image(owned)(2)
               & " tog=" & std_logic'image(toggle)(2)
               & " deq=" & integer'image(to_integer(unsigned(deq_ptr)))
               & " ccs=" & std_logic'image(ccs)(2)
               & " empty=" & std_logic'image(ring_empty)(2)
               & " stop=" & std_logic'image(stopped)(2)
               & " || model deq=" & integer'image(s_deq)
               & " ccs=" & std_logic'image(s_ccs)(2)
               & " stop=" & integer'image(s_stopped)
               & ")" severity note;
        end if;
      end if;
    end procedure;

    procedure rnd(variable v : out integer; m : integer) is
    begin
      uniform(seed1, seed2, r1);
      v := integer(floor(r1 * real(m)));
    end procedure;

    procedure check_comb is
      variable e_running, e_own, e_empty, e_fol, e_xfer, e_tog : boolean;
      variable e_act : trb_action_t;
    begin
      -- The model states the ownership test as "not different" where the
      -- design compares for equality -- a different route to the same answer.
      e_running := run = '1' and s_stopped = 0;
      e_own   := e_running and trb_valid = '1' and not (trb_cycle /= s_ccs);
      e_empty := e_running and trb_valid = '1' and (trb_cycle /= s_ccs);
      e_fol   := e_own and trb_is_link = '1';
      e_xfer  := e_own and trb_is_link = '0';
      e_tog   := e_fol and trb_toggle_cycle = '1';

      check((owned = '1') = e_own,          "owned matches the model");
      check((consume = '1') = e_own,        "consume matches the model");
      check((ring_empty = '1') = e_empty,   "ring_empty matches the model");
      check((follow_link = '1') = e_fol,    "follow_link matches the model");
      check((is_transfer = '1') = e_xfer,   "is_transfer matches the model");
      check((toggle = '1') = e_tog,         "toggle matches the model");
      check(to_integer(unsigned(deq_ptr)) = s_deq, "deq_ptr matches the model");
      check(ccs = s_ccs,                    "ccs matches the model");
      check((stopped = '1') = (s_stopped /= 0), "stopped matches the model");

      if not e_running or trb_valid = '0' then e_act := TRB_IDLE;
      elsif not e_own then                     e_act := TRB_UNOWNED;
      elsif trb_is_link = '0' then             e_act := TRB_TRANSFER;
      elsif trb_toggle_cycle = '1' then        e_act := TRB_LINK_TOG;
      else                                     e_act := TRB_LINK;
      end if;
      check(action = act_code(e_act), "action matches the model");
      -- The named decision must agree with the booleans it summarises.
      check((action = act_code(TRB_TRANSFER)) = (is_transfer = '1'),
            "action disagrees with is_transfer");
      check((action = act_code(TRB_LINK_TOG)) = (toggle = '1'),
            "action disagrees with toggle");
      check((action = act_code(TRB_UNOWNED)) = (ring_empty = '1'),
            "action disagrees with ring_empty");

      -- ---- SAFETY PROPERTIES, independent of the model ----
      -- 1. THE property. A TRB whose cycle bit differs from CCS belongs to
      --    software and must not be touched.
      if trb_valid = '1' and trb_cycle /= ccs then
        check(consume = '0',
              "an unowned TRB was consumed -- that is last lap's data, not software's");
        check(follow_link = '0',
              "an unowned Link TRB was followed -- that pointer was never written");
        check(is_transfer = '0', "an unowned TRB was run as a transfer");
      end if;
      -- 2. Owned and empty are exactly complementary while running.
      check(not (owned = '1' and ring_empty = '1'),
            "a TRB was both owned and not owned");
      -- 3. Consuming and the two kinds of TRB partition cleanly.
      check(not (is_transfer = '1' and follow_link = '1'),
            "a TRB was both a transfer and a link");
      if consume = '1' then
        check(is_transfer = '1' or follow_link = '1',
              "a TRB was consumed as neither a transfer nor a link");
      end if;
      -- 4. THE toggle rule. Only a Link TRB carrying Toggle Cycle may invert
      --    the cycle state.
      if toggle = '1' then
        check(follow_link = '1',
              "the cycle state was inverted on something that is not a Link TRB");
        check(trb_toggle_cycle = '1',
              "the cycle state was inverted on a Link TRB with Toggle Cycle CLEAR");
      end if;
      -- 5. A Link TRB without Toggle Cycle never inverts.
      if follow_link = '1' and trb_toggle_cycle = '0' then
        check(toggle = '0', "a plain segment link inverted the cycle state");
      end if;
      -- 6. Nothing happens while the endpoint is not running.
      if run = '0' then
        check(consume = '0' and ring_empty = '0' and toggle = '0',
              "the ring was consumed with the endpoint stopped");
      end if;
      -- 7. A stopped consumer consumes nothing.
      if stopped = '1' then
        check(consume = '0', "a stopped consumer took a TRB");
      end if;
      -- 8. The dequeue pointer moves only for a reason.
      if consume = '0' and run = '1' and s_stopped = 0 then
        check(to_integer(unsigned(deq_ptr)) = s_deq,
              "the dequeue pointer moved with no TRB consumed");
      end if;

      n_deq(s_deq) := n_deq(s_deq) + 1;
      if e_own   then n_owned   := n_owned + 1;   end if;
      if e_empty then n_unowned := n_unowned + 1; end if;
      if e_fol   then n_link    := n_link + 1;    end if;
      if e_tog   then n_tog     := n_tog + 1;     end if;
      if e_fol and trb_toggle_cycle = '0' then n_notog := n_notog + 1; end if;
    end procedure;

    procedure model_step is
      variable e_running, e_own, e_empty, e_fol, e_xfer, e_tog : boolean;
      variable nd : integer;
    begin
      e_running := run = '1' and s_stopped = 0;
      e_own   := e_running and trb_valid = '1' and trb_cycle = s_ccs;
      e_empty := e_running and trb_valid = '1' and trb_cycle /= s_ccs;
      e_fol   := e_own and trb_is_link = '1';
      e_xfer  := e_own and trb_is_link = '0';
      e_tog   := e_fol and trb_toggle_cycle = '1';

      if e_xfer  then m_tr := m_tr + 1; end if;
      if e_fol   then m_lk := m_lk + 1; end if;
      if e_tog   then m_tg := m_tg + 1; end if;
      if e_empty then m_em := m_em + 1; end if;
      if run = '1' and doorbell = '1' and s_stopped /= 0 then
        m_rs := m_rs + 1;
      end if;

      if run = '0' then    nd := 0;
      elsif e_fol then     nd := to_integer(unsigned(trb_link_target));
      elsif e_own then
        if s_deq = RING_N - 1 then nd := 0; else nd := s_deq + 1; end if;
      else                 nd := s_deq;
      end if;
      s_deq := nd;

      if run = '0' then    s_ccs := '1';
      elsif e_tog then     s_ccs := not s_ccs;
      end if;

      if run = '0' then        s_stopped := 0;
      elsif doorbell = '1' then s_stopped := 0;
      elsif e_empty then       s_stopped := 1;
      end if;
    end procedure;

    procedure step is
    begin
      wait for 1 ns;
      check_comb;
      model_step;
      wait until rising_edge(clk);
      wait for 1 ns;
      check(to_integer(unsigned(deq_ptr)) = s_deq, "deq_ptr tracked the model");
      check(ccs = s_ccs, "ccs tracked the model");
      check((stopped = '1') = (s_stopped /= 0), "stopped tracked the model");
      check(n_transfers = std_logic_vector(to_unsigned(m_tr, 32)),
            "n_transfers matches the model");
      check(n_links = std_logic_vector(to_unsigned(m_lk, 32)),
            "n_links matches the model");
      check(n_toggles = std_logic_vector(to_unsigned(m_tg, 32)),
            "n_toggles matches the model");
      check(n_empty = std_logic_vector(to_unsigned(m_em, 32)),
            "n_empty matches the model");
      check(n_restarts = std_logic_vector(to_unsigned(m_rs, 32)),
            "n_restarts matches the model");
    end procedure;

    procedure idle_in is
    begin
      trb_valid <= '0'; trb_is_link <= '0'; trb_toggle_cycle <= '0';
      doorbell <= '0';
    end procedure;

    procedure hard_reset is
    begin
      rst_n <= '0'; run <= '0'; idle_in; trb_cycle <= '0';
      trb_link_target <= (others => '0');
      wait until rising_edge(clk); wait for 1 ns;
      wait until rising_edge(clk); wait for 1 ns;
      rst_n <= '1'; wait for 1 ns;
      s_deq := 0; s_ccs := '1'; s_stopped := 0;
      m_tr := 0; m_lk := 0; m_tg := 0; m_em := 0; m_rs := 0;
    end procedure;

    -- Walk the consumer to a chosen (deq_ptr, ccs, stopped) using only legal
    -- transitions: consuming owned transfer TRBs advances the pointer, a Link
    -- TRB with Toggle Cycle inverts the cycle state, and an unowned TRB stops.
    procedure goto_state(want_deq : integer; want_ccs : std_logic;
                         want_stopped : std_logic) is
    begin
      hard_reset;
      run <= '1'; step; idle_in;
      if want_ccs = '0' then
        trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '1';
        trb_toggle_cycle <= '1'; trb_link_target <= (others => '0');
        step; idle_in;
      end if;
      for p in 0 to want_deq - 1 loop
        trb_valid <= '1'; trb_cycle <= want_ccs; trb_is_link <= '0';
        step; idle_in;
      end loop;
      if want_stopped = '1' then
        trb_valid <= '1'; trb_cycle <= not want_ccs; trb_is_link <= '0';
        step; idle_in;
      end if;
      wait for 1 ns;
      check(to_integer(unsigned(deq_ptr)) = want_deq,
            "goto_state reached the dequeue pointer");
      check(ccs = want_ccs, "goto_state reached the cycle state");
      check(stopped = want_stopped, "goto_state reached the stop latch");
    end procedure;

    variable iv : integer;
    variable bb, bc, bd, be, bf, bg, bi : std_logic;
  begin
    hard_reset;
    check(ccs = '1',
          "CCS resets to ONE -- a zeroed ring then reads as unowned");
    check(to_integer(unsigned(deq_ptr)) = 0,
          "and the dequeue pointer to zero");

    -- ===== A. EXHAUSTIVE sweep of the ownership and toggle rules =====
    -- 8 (deq_ptr) x 2 (ccs) x 2 (run) x 2 (trb_valid) x 2 (trb_cycle)
    --   x 2 (is_link) x 2 (toggle_cycle) x 2 (doorbell)
    -- = 1024 one-step transitions, with the link target fixed; the target is
    -- swept separately below because it only matters on a followed link.
    for a in 0 to 7 loop
      for b in 0 to 1 loop
        if b = 1 then bb := '1'; else bb := '0'; end if;
        for c in 0 to 1 loop
          if c = 1 then bc := '1'; else bc := '0'; end if;
          for d in 0 to 1 loop
            if d = 1 then bd := '1'; else bd := '0'; end if;
            for e in 0 to 1 loop
              if e = 1 then be := '1'; else be := '0'; end if;
              for f in 0 to 1 loop
                if f = 1 then bf := '1'; else bf := '0'; end if;
                for g in 0 to 1 loop
                  if g = 1 then bg := '1'; else bg := '0'; end if;
                  for i2 in 0 to 1 loop
                    if i2 = 1 then bi := '1'; else bi := '0'; end if;
                    goto_state(a, bb, '0');
                    run <= bc; trb_valid <= bd; trb_cycle <= be;
                    trb_is_link <= bf; trb_toggle_cycle <= bg;
                    trb_link_target <= std_logic_vector(to_unsigned(5, PTRW));
                    doorbell <= bi;
                    step;
                    n_exh := n_exh + 1;
                    idle_in;
                  end loop;
                end loop;
              end loop;
            end loop;
          end loop;
        end loop;
      end loop;
    end loop;
    report "  exhaustive ownership sweep: " & integer'image(n_exh)
         & " of 1024 transitions verified" severity note;

    -- ===== B. EXHAUSTIVE link-target sweep =====
    -- Every starting pointer against every link target: 8 x 8 = 64 followed
    -- links, which is the whole of the pointer-update domain.
    for a in 0 to 7 loop
      for b in 0 to 7 loop
        goto_state(a, '1', '0');
        trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '1';
        trb_toggle_cycle <= '0';
        trb_link_target <= std_logic_vector(to_unsigned(b, PTRW));
        step; idle_in;
        wait for 1 ns;
        check(to_integer(unsigned(deq_ptr)) = b,
              "a followed Link TRB moves the dequeue pointer to its target");
        check(ccs = '1',
              "and leaves the cycle state alone when Toggle Cycle is clear");
        n_lnk_exh := n_lnk_exh + 1;
      end loop;
    end loop;
    report "  exhaustive link-target sweep: " & integer'image(n_lnk_exh)
         & " of 64 followed links verified" severity note;

    -- ===== C. directed: one full lap of a ring =====
    hard_reset; run <= '1'; step; idle_in;
    check(ccs = '1', "CCS starts at 1");

    -- 1. A zeroed ring reads as unowned.
    trb_valid <= '1'; trb_cycle <= '0'; trb_is_link <= '0';
    wait for 1 ns;
    check(owned = '0', "a zeroed TRB is NOT owned by hardware");
    check(ring_empty = '1', "so the ring reads empty");
    step; idle_in;
    wait for 1 ns;
    check(stopped = '1', "and the consumer stops");

    -- 2. Software enqueues: writes a TRB with Cycle = PCS = 1, then rings.
    doorbell <= '1'; step; idle_in;
    wait for 1 ns;
    check(stopped = '0', "the doorbell restarts the consumer");
    check(n_restarts = std_logic_vector(to_unsigned(1, 32)),
          "and that restart was counted");

    trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '0'; step; idle_in;
    check(n_transfers = std_logic_vector(to_unsigned(1, 32)),
          "the TRB is taken as a transfer");
    wait for 1 ns;
    check(to_integer(unsigned(deq_ptr)) = 1,
          "and the pointer advances by one");

    -- 3. Three more transfers.
    for i in 0 to 2 loop
      trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '0'; step; idle_in;
    end loop;
    wait for 1 ns;
    check(to_integer(unsigned(deq_ptr)) = 4, "four transfers consumed");
    check(n_transfers = std_logic_vector(to_unsigned(4, 32)), "and counted");

    -- 4. A Link TRB that merely joins segments: follow it, do NOT toggle.
    trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '1';
    trb_toggle_cycle <= '0';
    trb_link_target <= std_logic_vector(to_unsigned(6, PTRW));
    wait for 1 ns;
    check(follow_link = '1', "an owned Link TRB is followed");
    check(is_transfer = '0', "and is NOT counted as a transfer");
    check(toggle = '0',
          "and with Toggle Cycle clear it does not invert CCS");
    step; idle_in;
    wait for 1 ns;
    check(to_integer(unsigned(deq_ptr)) = 6,
          "the pointer moved to the link's target");
    check(ccs = '1', "and the cycle state is unchanged");
    check(n_links = std_logic_vector(to_unsigned(1, 32)),
          "one link followed");
    check(n_transfers = std_logic_vector(to_unsigned(4, 32)),
          "and still four transfers");

    -- 5. THE case. The Link TRB that closes the ring carries Toggle Cycle.
    trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '1';
    trb_toggle_cycle <= '1';
    trb_link_target <= (others => '0');
    wait for 1 ns;
    check(toggle = '1', "the closing link inverts the cycle state");
    step; idle_in;
    wait for 1 ns;
    check(to_integer(unsigned(deq_ptr)) = 0,
          "back to the start of the segment");
    check(ccs = '0',
          "with CCS now ZERO -- the next lap uses the other value");
    check(n_toggles = std_logic_vector(to_unsigned(1, 32)), "one toggle");

    -- 6. And now the OLD TRBs read as unowned.
    trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '0';
    wait for 1 ns;
    check(owned = '0',
          "last lap's TRB is not owned this lap -- that is the whole mechanism");
    check(ring_empty = '1', "so the ring reads empty");
    step; idle_in;

    -- 7. Software's next enqueue writes Cycle = 0, matching the new CCS.
    doorbell <= '1'; step; idle_in;
    trb_valid <= '1'; trb_cycle <= '0'; trb_is_link <= '0';
    wait for 1 ns;
    check(owned = '1', "a TRB written with the NEW cycle value is owned");
    step; idle_in;
    check(n_transfers = std_logic_vector(to_unsigned(5, 32)), "and consumed");

    -- 8. An unowned LINK TRB is not followed either.
    trb_valid <= '1'; trb_cycle <= '1'; trb_is_link <= '1';
    trb_toggle_cycle <= '1';
    trb_link_target <= std_logic_vector(to_unsigned(3, PTRW));
    wait for 1 ns;
    check(owned = '0', "an unowned Link TRB is still unowned");
    check(follow_link = '0', "so it is NOT followed");
    check(toggle = '0', "and does NOT invert the cycle state");
    step; idle_in;
    wait for 1 ns;
    check(ccs = '0', "the cycle state survived intact");

    -- ===== D. randomised =====
    -- ieee.math_real.uniform is a genuinely different generator from either
    -- Verilog builtin, which is what makes this column independent evidence.
    hard_reset; run <= '1';
    for i in 0 to 39999 loop
      rnd(iv, 32); if iv /= 0 then run <= '1'; else run <= '0'; end if;
      rnd(iv, 4);  if iv /= 0 then trb_valid <= '1';
                   else trb_valid <= '0'; end if;
      -- bias toward OWNED TRBs -- a ring that is mostly unowned never
      -- exercises the consume path
      rnd(iv, 4);
      if iv /= 0 then trb_cycle <= ccs; else trb_cycle <= not ccs; end if;
      rnd(iv, 6);  if iv = 0 then trb_is_link <= '1';
                   else trb_is_link <= '0'; end if;
      rnd(iv, 2);  if iv = 1 then trb_toggle_cycle <= '1';
                   else trb_toggle_cycle <= '0'; end if;
      rnd(iv, 8);  trb_link_target <= std_logic_vector(to_unsigned(iv, PTRW));
      rnd(iv, 8);  if iv = 0 then doorbell <= '1';
                   else doorbell <= '0'; end if;
      step;
    end loop;

    for i in 0 to 7 loop
      check(n_deq(i) > 200, "every dequeue position was occupied many times");
    end loop;
    check(n_owned   > 5000, "owned TRBs were seen many times");
    check(n_unowned > 1000, "unowned TRBs were seen many times");
    check(n_link    > 1000, "Link TRBs were followed many times");
    check(n_tog     > 400,  "the cycle state was toggled many times");
    check(n_notog   > 400,  "and plain segment links were followed many times");

    report "  REACH: ownership=" & integer'image(n_exh)
         & " link-target=" & integer'image(n_lnk_exh)
         & " | dequeue positions: " & integer'image(n_deq(0))
         & " " & integer'image(n_deq(1)) & " " & integer'image(n_deq(2))
         & " " & integer'image(n_deq(3)) & " " & integer'image(n_deq(4))
         & " " & integer'image(n_deq(5)) & " " & integer'image(n_deq(6))
         & " " & integer'image(n_deq(7)) severity note;
    report "  CASES: owned=" & integer'image(n_owned)
         & " unowned=" & integer'image(n_unowned)
         & " links-followed=" & integer'image(n_link)
         & " toggling=" & integer'image(n_tog)
         & " plain=" & integer'image(n_notog) severity note;
    report "  COUNTERS: transfers="
         & integer'image(to_integer(unsigned(n_transfers)))
         & " links=" & integer'image(to_integer(unsigned(n_links)))
         & " toggles=" & integer'image(to_integer(unsigned(n_toggles)))
         & " empty=" & integer'image(to_integer(unsigned(n_empty)))
         & " restarts=" & integer'image(to_integer(unsigned(n_restarts)))
         severity note;
    report "  [VHDL] xhci_trb_ring: " & integer'image(errors) & " errors"
         severity note;
    if errors = 0 then
      report "  [VHDL] PASS" severity note;
    else
      report "  [VHDL] FAIL" severity failure;
    end if;
    running_sim <= false;
    wait;
  end process;
end architecture;

12. Exhaustive Verification

MeasureVerilogSystemVerilogVHDL
Ownership sweep1024 / 10241024 / 10241024 / 1024
Link-target sweep64 / 6464 / 6464 / 64
owned TRBs153041496815117
unowned TRBs371236643668
links followed246023892401
toggling links143914171430
plain links1021972971
transfers consumed896787028839
cycle-state toggles894872885
ring-empty events358135333537
doorbell restarts248324382457
ResultPASSPASSPASS

Every dequeue position was occupied at least 3300 times in each run, and the testbenches assert it.

The rows that matter are toggling links (~1430) and plain links (~990), generated in comparable numbers. A suite in which every followed link carried Toggle Cycle would pass identically against mutation L2 — invert on every link — and a suite in which none did would pass against L3. Having both populations, in quantity, is what makes the toggle rule tested rather than merely exercised.

13. Mutation Testing

#MutationVerilogSysVerVHDL
L1the ownership test is dropped310670310843311107
L2every followed link inverts the cycle state277281272283275535
L3the cycle state is never inverted330657326589330699
L4an unowned Link TRB is followed242629242991243207
L5a Link TRB is counted as a transfer503005007650115
L6a followed link advances by one, not to its target537444954251548
L7CCS resets to 0 instead of 1430309430141424885
—unmutated baseline000

All seven die, all counts distinct.

L7 is the largest at ~430 000, and it is one character. CCS resetting to 0 makes a freshly-zeroed ring read as entirely owned by hardware, so the controller consumes an array of blank TRBs before software has written anything. Everything downstream is wrong from the first cycle of every run — which is why it scores higher than dropping the ownership test altogether.

L1, L2, L3 and L4 all break the ring permanently (242 000–330 000) because once CCS and PCS disagree, they never re-agree: the ring is desynchronised for the life of the endpoint. That is the shape of every cycle-bit bug, and the reason this mechanism is so unforgiving — there is no resynchronisation event short of tearing down the ring.

L5 and L6 are the informative ones at ~50 000. Both leave the ownership machinery intact and damage something narrower:

  • L5 counts a Link TRB as a transfer. The ring still works; the endpoint's completion count is inflated by one per lap. In the field that is a performance counter that reads high and a driver that thinks it moved data it did not.
  • L6 advances the pointer by one instead of jumping to the link's target. The consumer walks into the next entry of the old segment rather than into the new one — and since that entry still holds last lap's TRB, it will be unowned, so the ring simply goes quiet.

14. Debugging Walkthrough: The Endpoint That Works Once

The report. A newly-brought-up xHCI endpoint transfers its first few TRBs correctly and then goes permanently silent. Re-enabling it does not help. Tearing the ring down and rebuilding it works — once — and then it goes silent again at the same point.

Step 1 — how many TRBs before it stops? Count them. It stops after exactly as many transfers as there are entries in the ring segment, every time. That is a wrap.

Step 2 — is the Link TRB being followed? Trace follow_link. Yes: the pointer jumps back to the start of the segment correctly.

Step 3 — so what is different after the wrap? Read CCS. It is still 1. The ring wrapped and the cycle state did not invert.

Step 4 — why that is fatal. Software also wrapped, and its PCS inverted to 0. Every TRB software now writes carries Cycle = 0. Hardware is still looking for 1. Nothing will ever be owned again, and no amount of doorbell ringing helps — the doorbell says "look again", and hardware looks again at a TRB that still does not match.

Step 5 — why rebuilding the ring works once. A fresh ring resets CCS to 1 and PCS to 1, and they agree again until the next wrap.

Step 6 — the cause. The Toggle Cycle flag was not being read: the design followed the link and left CCS alone unconditionally. Mutation L3, in production.

15. UVM: Producing a Ring, Not a Stream of Bits

15.1 The transaction

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class xhci_trb_item extends uvm_sequence_item;
  `uvm_object_utils(xhci_trb_item)

  // ONE FETCHED TRB, as the consumer sees it.
  rand bit       trb_valid;
  rand bit       trb_cycle;
  rand bit       trb_is_link;
  rand bit       trb_toggle_cycle;
  rand bit [2:0] trb_link_target;
  rand bit       doorbell;
  rand bit       run;

  // Link TRBs are structural: one per segment, so roughly one entry in eight.
  constraint c_ring_shape {
    trb_is_link dist {0 := 7, 1 := 1};
    trb_valid   dist {1 := 3, 0 := 1};
    run         dist {1 := 31, 0 := 1};
    doorbell    dist {0 := 7, 1 := 1};
  }

  // THE bias that matters. Toggle Cycle is set on the ONE link that closes
  // the ring and clear on links that merely join segments -- so a realistic
  // multi-segment ring produces both, and a suite that sees only one of them
  // cannot distinguish "invert on every link" from "invert on the right one".
  constraint c_both_link_kinds {
    trb_toggle_cycle dist {0 := 1, 1 := 1};
  }

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

  function string convert2string();
    return $sformatf("cyc=%0b link=%0b tc=%0b tgt=%0d db=%0b",
                     trb_cycle, trb_is_link, trb_toggle_cycle,
                     trb_link_target, doorbell);
  endfunction
endclass

15.2 Sequences

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// THE sequence for this chapter. It produces a ring the way SOFTWARE does:
// N owned TRBs at the current producer cycle state, then a closing Link TRB
// with Toggle Cycle, then INVERTS its own PCS and does it again. Anything
// that gets the toggle wrong desynchronises here and never recovers.
class ring_laps_seq extends uvm_sequence #(xhci_trb_item);
  `uvm_object_utils(ring_laps_seq)
  function new(string name = "ring_laps_seq"); super.new(name); endfunction

  rand int unsigned n_laps;
  constraint c_laps { n_laps inside {[4:12]}; }

  task body();
    bit pcs = 1;                       // software's cycle state starts at 1
    repeat (n_laps) begin
      xhci_trb_item it;
      int unsigned n = $urandom_range(2, 6);

      // n ordinary transfer TRBs, all handed over at the current PCS
      repeat (n) begin
        it = xhci_trb_item::type_id::create("it");
        start_item(it);
        it.c_ring_shape.constraint_mode(0);
        it.c_both_link_kinds.constraint_mode(0);
        if (!it.randomize() with { run == 1; trb_valid == 1;
                                   trb_cycle == pcs;
                                   trb_is_link == 0; doorbell == 0; })
          `uvm_error("RAND", "transfer randomize failed")
        finish_item(it);
      end

      // the link that CLOSES the ring: Toggle Cycle set, target = 0
      it = xhci_trb_item::type_id::create("it");
      start_item(it);
      it.c_ring_shape.constraint_mode(0);
      it.c_both_link_kinds.constraint_mode(0);
      if (!it.randomize() with { run == 1; trb_valid == 1;
                                 trb_cycle == pcs;
                                 trb_is_link == 1;
                                 trb_toggle_cycle == 1;
                                 trb_link_target == 0; doorbell == 0; })
        `uvm_error("RAND", "closing-link randomize failed")
      finish_item(it);

      // software inverts its own cycle state on the wrap, exactly as
      // hardware is supposed to
      pcs = ~pcs;
    end
  endtask
endclass

// A MULTI-SEGMENT ring: segments joined by plain links (Toggle Cycle CLEAR)
// and closed by one toggling link. This is the population that separates
// "invert on every link" from "invert on the closing one".
class multi_segment_seq extends uvm_sequence #(xhci_trb_item);
  `uvm_object_utils(multi_segment_seq)
  function new(string name = "multi_segment_seq"); super.new(name); endfunction

  task body();
    bit pcs = 1;
    repeat (150) begin
      xhci_trb_item it;
      int unsigned segs = $urandom_range(2, 3);

      for (int s = 0; s < segs; s++) begin
        // a couple of transfers in this segment
        repeat (2) begin
          it = xhci_trb_item::type_id::create("it");
          start_item(it);
          it.c_ring_shape.constraint_mode(0);
          it.c_both_link_kinds.constraint_mode(0);
          if (!it.randomize() with { run == 1; trb_valid == 1;
                                     trb_cycle == pcs; trb_is_link == 0;
                                     doorbell == 0; })
            `uvm_error("RAND", "segment randomize failed")
          finish_item(it);
        end
        // the join: Toggle Cycle CLEAR on every link but the last
        it = xhci_trb_item::type_id::create("it");
        start_item(it);
        it.c_ring_shape.constraint_mode(0);
        it.c_both_link_kinds.constraint_mode(0);
        if (!it.randomize() with { run == 1; trb_valid == 1;
                                   trb_cycle == pcs; trb_is_link == 1;
                                   trb_toggle_cycle == (s == segs - 1);
                                   doorbell == 0; })
          `uvm_error("RAND", "join randomize failed")
        finish_item(it);
      end
      pcs = ~pcs;
    end
  endtask
endclass

// The adversarial one: an UNOWNED Link TRB -- the end of what software has
// produced, which happens to be a link. Following it reads a pointer
// software never wrote, and the pointer that is there is last lap's.
class unowned_link_seq extends uvm_sequence #(xhci_trb_item);
  `uvm_object_utils(unowned_link_seq)
  function new(string name = "unowned_link_seq"); super.new(name); endfunction

  task body();
    repeat (400) begin
      xhci_trb_item it = xhci_trb_item::type_id::create("it");
      start_item(it);
      it.c_ring_shape.constraint_mode(0);
      // cycle bit deliberately opposite to whatever CCS will be: the driver
      // reads the DUT's ccs and inverts it, which is the only way to
      // construct "unowned" without duplicating the design's own rule.
      if (!it.randomize() with { run == 1; trb_valid == 1;
                                 trb_is_link == 1; doorbell == 0; })
        `uvm_error("RAND", "unowned-link randomize failed")
      it.trb_cycle = ~p_sequencer.cfg.observed_ccs;
      finish_item(it);
    end
  endtask
endclass

15.3 The scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class xhci_ring_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(xhci_ring_scoreboard)

  uvm_analysis_imp #(xhci_ring_mon_item, xhci_ring_scoreboard) ap;

  localparam int RING_N = 8;

  // The scoreboard keeps its OWN cycle state and dequeue pointer. Comparing
  // the DUT's ccs against the DUT's own ownership decision would be circular.
  bit          sb_ccs;
  int unsigned sb_deq;
  bit          sb_stopped;

  int unsigned n_owned, n_unowned, n_tog_links, n_plain_links, n_transfers;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
    sb_ccs = 1;              // CCS initialises to ONE
  endfunction

  function void write(xhci_ring_mon_item t);
    bit running = t.run && !sb_stopped;
    bit owned   = running && t.trb_valid && (t.trb_cycle == sb_ccs);
    bit unowned = running && t.trb_valid && (t.trb_cycle != sb_ccs);
    bit fol     = owned && t.trb_is_link;
    bit xfer    = owned && !t.trb_is_link;
    bit tog     = fol && t.trb_toggle_cycle;

    // ---- THE property. An unowned TRB is not touched, in any way. ----
    if (unowned) begin
      if (t.consume)
        `uvm_error("OWNERSHIP",
          "an unowned TRB was consumed -- that entry still holds LAST LAP's TRB, a complete and plausible descriptor that is not live work")
      if (t.follow_link)
        `uvm_error("OWNERSHIP",
          "an unowned Link TRB was followed -- that pointer was never written this lap")
      if (t.toggle)
        `uvm_error("OWNERSHIP",
          "the cycle state was inverted from an unowned TRB")
      n_unowned++;
    end

    // ---- THE toggle rule. ONLY a Link TRB with Toggle Cycle inverts. ----
    if (t.toggle) begin
      if (!fol)
        `uvm_error("TOGGLE",
          "the cycle state was inverted on something that is not an owned Link TRB")
      if (!t.trb_toggle_cycle)
        `uvm_error("TOGGLE",
          "the cycle state was inverted on a Link TRB with Toggle Cycle CLEAR -- any multi-segment ring is now desynchronised")
    end
    if (fol && !t.trb_toggle_cycle && t.toggle)
      `uvm_error("TOGGLE", "a plain segment join inverted the cycle state");
    if (fol && t.trb_toggle_cycle && !t.toggle)
      `uvm_error("TOGGLE",
        "the closing link did NOT invert the cycle state -- the ring will go silent after this lap and never recover");

    // ---- A Link TRB is structural, not a transfer ----
    if (fol && t.is_transfer)
      `uvm_error("ACCOUNTING",
        "a Link TRB was reported as a transfer -- it moved no data")

    if (owned)  n_owned++;
    if (xfer)   n_transfers++;
    if (tog)    n_tog_links++;
    if (fol && !t.trb_toggle_cycle) n_plain_links++;

    // ---- Advance the scoreboard's own state ----
    if (!t.run) begin
      sb_deq = 0; sb_ccs = 1; sb_stopped = 0;
    end else begin
      if (fol)       sb_deq = t.trb_link_target;
      else if (owned) sb_deq = (sb_deq == RING_N-1) ? 0 : sb_deq + 1;
      if (tog)       sb_ccs = ~sb_ccs;
      if (t.doorbell) sb_stopped = 0;
      else if (unowned) sb_stopped = 1;
    end

    if (t.ccs !== sb_ccs)
      `uvm_error("CCS", $sformatf("ccs=%0b, scoreboard=%0b -- the ring is desynchronised and will not recover",
                                  t.ccs, sb_ccs))
    if (t.deq_ptr !== sb_deq)
      `uvm_error("DEQ", $sformatf("deq_ptr=%0d, scoreboard=%0d",
                                  t.deq_ptr, sb_deq))
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("SB", $sformatf(
      "owned=%0d unowned=%0d transfers=%0d toggling-links=%0d plain-links=%0d",
      n_owned, n_unowned, n_transfers, n_tog_links, n_plain_links), UVM_LOW)

    // BOTH kinds of link must have been followed. A run with only one kind
    // cannot distinguish "invert on every link" from "invert on the right one".
    if (n_tog_links   == 0) `uvm_error("COVERAGE",
      "no closing link was ever followed -- the wrap is untested")
    if (n_plain_links == 0) `uvm_error("COVERAGE",
      "no plain segment join was ever followed -- 'invert on every link' would pass this run")
    if (n_unowned     == 0) `uvm_error("COVERAGE",
      "no unowned TRB was ever presented -- the ownership rule is untested")
  endfunction
endclass

The two report_phase guards on link kinds are the ones that matter. A regression containing only toggling links passes identically against mutation L2, and one containing only plain links passes identically against L3. Requiring both, by name, is the difference between exercising the toggle and testing it.

15.4 Functional coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup xhci_ring_cg with function sample(
    bit valid, bit cyc, bit ccs_now, bit is_link, bit tc, bit [2:0] deq,
    bit [2:0] target, trb_action_e action);

  cp_action : coverpoint action {
    bins idle     = {TRB_IDLE};
    bins transfer = {TRB_TRANSFER};
    bins link     = {TRB_LINK};       // a plain segment join
    bins link_tog = {TRB_LINK_TOG};   // the closing link
    bins unowned  = {TRB_UNOWNED};
  }

  // Ownership expressed as the RELATIONSHIP, not as the two bits. A coverage
  // model with separate coverpoints on trb_cycle and ccs closes happily
  // while never recording whether they ever DIFFERED -- which is the only
  // thing this block is about.
  cp_match : coverpoint (cyc == ccs_now) {
    bins owned   = {1};
    bins unowned = {0};
  }

  // The cycle state in both polarities. A ring that never wraps only ever
  // runs at CCS = 1, and half the design is then untested.
  cp_ccs : coverpoint ccs_now { bins one = {1}; bins zero = {0}; }

  // THE cross. Every action at both cycle-state polarities: the wrap is what
  // takes the design into CCS = 0, so closing this proves laps happened.
  x_action_ccs : cross cp_action, cp_ccs;

  // Both link kinds, crossed with ownership -- an UNOWNED link is a distinct
  // case from an owned one and is the L4 population.
  cp_link_kind : coverpoint {is_link, tc} {
    bins not_a_link  = {2'b00, 2'b01};
    bins plain_link  = {2'b10};
    bins closing_link = {2'b11};
  }
  x_link_owned : cross cp_link_kind, cp_match;

  // Every dequeue position, and every link target from every position.
  cp_deq    : coverpoint deq    { bins pos[8] = {[0:7]}; }
  cp_target : coverpoint target { bins tgt[8] = {[0:7]}; }
  x_jump : cross cp_deq, cp_target;
endgroup

cp_match deserves the same note as Chapter 21.1 §15.4's toggle coverpoint, because it is the same mistake in a different protocol: a coverage model built from the port list gets trb_cycle and ccs as two independent coverpoints, each dutifully hitting both values, and reports full coverage — while never recording whether the two ever differed. The interesting event is the relationship.

16. SystemVerilog Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module xhci_trb_ring_sva
  import xhci_ring_pkg::*;
#(
  parameter int RING_N = 8,
  parameter int PTRW   = 3
) (
  input logic            clk,
  input logic            rst_n,
  input logic            run,
  input logic            trb_valid,
  input logic            trb_cycle,
  input logic            trb_is_link,
  input logic            trb_toggle_cycle,
  input logic [PTRW-1:0] trb_link_target,
  input logic            doorbell,
  input logic            owned,
  input logic            consume,
  input logic            is_transfer,
  input logic            follow_link,
  input logic            toggle,
  input trb_action_e     action,
  input logic [PTRW-1:0] deq_ptr,
  input logic            ccs,
  input logic            ring_empty,
  input logic            stopped
);
  default clocking cb @(posedge clk); endclocking
  default disable iff (!rst_n);

  // ---- 1. THE property. An unowned TRB is never consumed. ----
  property p_unowned_never_consumed;
    (trb_valid && (trb_cycle != ccs)) |-> !consume;
  endproperty
  a_unowned_never_consumed : assert property (p_unowned_never_consumed)
    else $error("an unowned TRB was consumed -- that entry holds LAST LAP's descriptor");

  // ---- 2. ...and an unowned Link TRB is never followed. ----
  property p_unowned_link_never_followed;
    (trb_valid && (trb_cycle != ccs)) |-> (!follow_link && !toggle);
  endproperty
  a_unowned_link_never_followed :
    assert property (p_unowned_link_never_followed);

  // ---- 3. THE toggle rule, stated exactly. ----
  property p_toggle_iff_closing_link;
    toggle == (owned && trb_is_link && trb_toggle_cycle);
  endproperty
  a_toggle_iff_closing_link : assert property (p_toggle_iff_closing_link)
    else $error("the cycle state inverted on the wrong thing -- a desynchronised ring never recovers");

  // ---- 4. The cycle state moves ONLY on a toggle. ----
  property p_ccs_moves_only_on_toggle;
    (!$stable(ccs)) |-> $past(toggle || !run);
  endproperty
  a_ccs_moves_only_on_toggle :
    assert property (p_ccs_moves_only_on_toggle)
    else $error("CCS changed with no toggling link -- software and hardware now disagree for ever");

  // ---- 5. A Link TRB is structural, never a transfer. ----
  property p_link_is_not_a_transfer;
    follow_link |-> !is_transfer;
  endproperty
  a_link_is_not_a_transfer : assert property (p_link_is_not_a_transfer);

  // ---- 6. A followed link lands on its target. ----
  property p_link_jumps_to_target;
    (follow_link && run) |=> (deq_ptr == $past(trb_link_target));
  endproperty
  a_link_jumps_to_target : assert property (p_link_jumps_to_target)
    else $error("a followed link did not land on its target");

  // ---- 7. An ordinary consumed TRB advances by exactly one. ----
  property p_transfer_advances_by_one;
    (is_transfer && run && ($past(deq_ptr) != PTRW'(RING_N-1)))
      |=> (deq_ptr == $past(deq_ptr) + 1'b1);
  endproperty
  a_transfer_advances_by_one :
    assert property (p_transfer_advances_by_one);

  // ---- 8. The pointer moves ONLY when a TRB is consumed. ----
  property p_pointer_moves_only_on_consume;
    (!$stable(deq_ptr)) |-> $past(consume || !run);
  endproperty
  a_pointer_moves_only_on_consume :
    assert property (p_pointer_moves_only_on_consume);

  // ---- 9. Owned and empty partition the fetched TRBs. ----
  property p_owned_xor_empty;
    (run && !stopped && trb_valid) |-> (owned ^ ring_empty);
  endproperty
  a_owned_xor_empty : assert property (p_owned_xor_empty);

  // ---- 10. The named action agrees with the booleans. ----
  property p_action_agrees;
    ((action == TRB_TRANSFER) == is_transfer)
    && ((action == TRB_LINK_TOG) == toggle)
    && ((action == TRB_UNOWNED) == ring_empty);
  endproperty
  a_action_agrees : assert property (p_action_agrees);

  // ---- 11. A stopped consumer consumes nothing, and only the doorbell
  // ----     (or stopping the endpoint) releases it.
  property p_stopped_consumes_nothing;
    stopped |-> !consume;
  endproperty
  a_stopped_consumes_nothing :
    assert property (p_stopped_consumes_nothing);

  property p_stop_is_sticky;
    (stopped && run && !doorbell) |=> stopped;
  endproperty
  a_stop_is_sticky : assert property (p_stop_is_sticky);

  // ---- Cover: BOTH link kinds, and both cycle-state polarities. ----
  c_closing_link : cover property ((follow_link && trb_toggle_cycle));
  c_plain_link   : cover property ((follow_link && !trb_toggle_cycle));
  c_unowned_link : cover property ((trb_valid && trb_is_link
                                    && (trb_cycle != ccs)));
  c_ccs_zero     : cover property ((ccs == 1'b0));
  c_wrap         : cover property ((toggle));
endmodule

bind xhci_trb_ring xhci_trb_ring_sva #(.RING_N(RING_N), .PTRW(PTRW)) u_sva (.*);

17. Common Misconceptions

"The consumer needs to know where the producer's tail is." It does not. It needs to know whether this one entry has been handed over, and one bit answers that.

"An unowned TRB is empty or invalid." It is the previous lap's TRB — a complete, well-formed descriptor. Nothing about its contents marks it as stale.

"The cycle bit is a valid bit." A valid bit would be set by the producer and cleared by the consumer, requiring a write from each side. The cycle bit is never cleared; its meaning inverts.

"CCS initialising to 1 is arbitrary." Zeroed memory reads Cycle = 0. Starting CCS at 1 makes a fresh ring empty by construction. Start it at 0 and the controller consumes an array of blanks (L7, 430 000 failures).

"Wrapping inverts the cycle state." Following a Link TRB with Toggle Cycle set inverts it. Links that merely join segments do not, and inverting on those desynchronises any multi-segment ring.

"A Link TRB is not really a TRB." It has a Cycle bit and obeys the ownership rule like everything else. An unowned link must not be followed.

"A Link TRB completed, so it counts." It moved no data. Counting it inflates the endpoint's completion count by one per lap.

"If the ring desynchronises, the controller will notice." There is nothing to notice with. The mechanism carries no redundancy — that is why it is one bit — so a desynchronised ring is indistinguishable from an idle one.

18. Exercises

1. Drop the ownership test (L1) and predict which of the eight safety properties fires first. Then explain why L7 — one character, the reset value of CCS — scores higher than L1.

2. L3 never inverts CCS. Show that SVA property 4 holds vacuously against it, and identify which property does catch it. What does that tell you about writing "X only happens for a reason" properties?

3. Add a second ring segment and a segment_id input. Which of the eleven SVA properties need changing, and which cover properties become reachable only with more than one segment?

4. The scoreboard requires both n_tog_links > 0 and n_plain_links > 0. Construct a regression that satisfies only the first, run L2 against it, and confirm it survives. Then argue whether the guard belongs in the scoreboard or in the coverage model.

5. deq_inc compares against LAST_IDX = RING_N-1 rather than RING_N. Set RING_N = 5 with PTRW = 3 and work out what each version does. Which is correct, and what does the answer tell you about testing only power-of-two sizes?

6. Software's PCS and hardware's CCS both start at 1 and both invert on a wrap. Write the invariant relating them, state where it can be violated, and explain why no hardware mechanism can restore it once it is.

19. Summary

IdeaWhy it matters
xHCI uses rings, not linked listsfixed arrays, no pointer surgery
No head/tail pointers are exchangedon a PCIe device that is a round trip per transfer
One Cycle bit per TRB, one CCS in hardwareownership is a single comparison
An unowned entry holds last lap's TRBcomplete, plausible, and not live work
CCS resets to 1; zeroed memory reads 0a fresh ring is empty by construction
The wrap is not implicitonly a Link TRB with Toggle Cycle inverts CCS
Plain links join segments and do not toggleinverting on those desynchronises the ring
A Link TRB is itself a TRBunowned means do not follow
...and is structural, not a transfercounting it inflates completions
A desynchronised ring never recoversthe mechanism carries no redundancy
1024 + 64 exhaustive pointsownership decision and pointer update, separately
7 mutations, all killed in 3 languagesfour of them break the ring permanently

Tooling

StepCommand
Verilog-2005iverilog -g2005 -o tr_v.out tr_v.v tr_v_tb.v && ./tr_v.out
SystemVerilogiverilog -g2012 -o tr_sv.out tr_sv.sv tr_sv_tb.sv && ./tr_sv.out
VHDL-2008 analysenvc --std=2008 -a tr_vhdl.vhd tr_vhdl_tb.vhd
VHDL-2008 elaboratenvc --std=2008 -e tb_tr_vhdl
VHDL-2008 runnvc --std=2008 -r tb_tr_vhdl
One mutationiverilog -g2005 -DMUT_L1 -o mm tr_v_mut.v tr_v_tb.v && ./mm

All three implementations pass with 0 errors: 1024 of 1024 ownership transitions, 64 of 64 link targets, 40 000 randomised cycles, every dequeue position reached and asserted reached.


Chapter 22.3 — Host-Side Scheduling is the other half of what a host controller does: deciding what runs this frame. Periodic traffic has a reserved budget and absolute priority; bulk and control get what is left. And the rule that shapes the hardware is one most schedulers do not have — you cannot start a transaction you cannot finish inside the frame, because a transaction that runs past the boundary is not slow, it is malformed.

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.