Skip to content
VLSI Mentor

USB · Module 28

USB vs PCIe

USB holds one transaction outstanding per endpoint so its throughput is exactly 1/(latency+1) whatever the wire carries; PCIe tags many at once and needs exactly latency+1 tags to saturate — both measured as closed forms over 64 points.

The last of four comparisons, and the last chapter of this module. The first three were all about naming a peer. This one is not: PCIe usually has exactly one peer and it is soldered down. PCIe's problem is naming a transaction.

1. The Comparison That Is Not About Bandwidth

"PCIe is faster" is true and explains nothing, because it treats the gap as a matter of degree. It is not. USB and PCIe differ in how many things they allow to be happening at once, and that difference produces the bandwidth gap rather than resulting from it.

The headline result:

outstandingcycles for 24 transactions at latency 8measured as
USB (usb_xact_slot)1 per endpoint216exactly 24 × (8+1)
PCIe (pcie_tag_tracker)8 tags34approaching 24 + 8

Same fabric, same latency, same clock. 6.4×, and not one bit of it is because the wire is faster.

2. Two Identities, At Two Different Layers

PCIe's layering is worth one diagram, because it contains a trap that catches people who have read about it but not built it.

PCIe identifies a transaction twice, at two layers, for two different purposes

PCIe's layers and where transaction identity lives. The software layer issues a read and receives a completion callback. The transaction layer creates a TLP carrying a Requester ID and a Tag, which identifies the transaction end to end and must survive every hop. The data link layer adds a sequence number for acknowledgement and replay on a single hop only, which is discarded at each switch. The physical layer stripes the packet across lanes. USB has neither identifier because its transactions are identified by when they occur.PCIe: a per-hop identity and a per-transaction identity, which are notthe same thingSoftware / driverissues a read, is called back when it completes — never sees a tagissues a read, is called back when it completes — never sees a tagTransaction layer — the TAG lives hereTLP carries Requester ID + Tag — identifies a transaction END TO END, across every hopTLP carries Requester ID + Tag — identifies a transaction END TO END, across every hopData link layer — a DIFFERENT identitysequence number + ACK/NAK + replay, for ONE hop only — discarded at every switchsequence number + ACK/NAK + replay, for ONE hop only — discarded at every switchPhysical layerlanes, striping, 128b/130b encoding — no notion of a transactionlanes, striping, 128b/130b encoding — no notion of a transaction
Read bottom-up. The link layer's sequence number identifies a packet on ONE hop and is thrown away at the next switch. The transaction layer's tag identifies a transaction END TO END and must survive every hop. They are different mechanisms solving different problems, and the chapter's RTL builds only the second.

3. Two Designs, One Question

pcie_tag_trackerusb_xact_slot
identifiera tag per requestnone
outstandingup to N, settable at run timeexactly 1 per endpoint
completion orderanythe only one possible
split completionstracked by byte countcannot occur
what a request needsa free tagan idle endpoint
writesposted — no tag, never stalledno distinction
the bug it can havea tag reused too earlynone of this shape

tag_limit is an input, not a parameter. That is what makes section 5's surface a single exhaustive sweep instead of eight separate elaborations — and it is also how the USB case is reached inside the PCIe design: tag_limit = 1.

4. The Designs (Verilog-2005)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  OUTSTANDING TRANSACTIONS -- THE MECHANISM PCIe NEEDS AND USB DOES NOT.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  Two modules. Neither is a controller: there is no TLP encoder, no
//  link layer, no credits, no USB packet engine and no scheduler. Each
//  is the part that answers one question.
//
//      "This response just arrived. Which request was it for?"
//
//  USB answers it by CONSTRUCTION. The host issues one transaction to an
//  endpoint and waits for it. The response is the next thing on the wire,
//  so there is nothing to match -- and USB packets carry no transaction
//  identifier at all, because none is needed.
//
//  PCIe answers it with a TAG. A requester may have many non-posted
//  requests in flight at once; completions travel independently, come
//  back OUT OF ORDER, and may arrive split into several pieces. So every
//  request carries a tag, and the requester must track each one until the
//  last byte of its completion has arrived.
//
//  The difference is not bandwidth, it is CONCURRENCY:
//
//      USB:  one outstanding transaction, so throughput is bounded by
//            1 / latency, whatever the wire can carry.
//      PCIe: N outstanding transactions, so throughput is bounded by
//            min(1, N / latency) -- and with enough tags, not by latency
//            at all.
//
//  That is a formula, so the chapter measures it. Section 5's table is
//  throughput against tag count and latency, and the USB case is exactly
//  its first row.
//
//  What the tags cost is a class of bug that cannot exist on USB: a tag
//  reused before its completion arrives silently attributes one
//  transaction's data to another.
// =====================================================================

// ---------------------------------------------------------------------
//  pcie_tag_tracker -- many in flight, matched by tag.
//
//  `tag_limit` is an INPUT rather than a parameter so the testbench can
//  sweep how many tags the requester is allowed to use without
//  re-elaborating. That is what makes the throughput curve in section 5
//  a single exhaustive sweep instead of eight separate runs -- and it is
//  also how the USB case is reached: tag_limit = 1.
// ---------------------------------------------------------------------
module pcie_tag_tracker #(
  parameter integer N_TAG   = 8,
  // Bytes are tracked so a SPLIT completion can be modelled: a single
  // read may be answered by several completions, and the tag is not free
  // until the last byte arrives.
  parameter integer MAX_BYTES = 256
) (
  input  wire       clk,
  input  wire       rst_n,

  // How many tags the requester may use, 1..N_TAG. Zero is treated as
  // one: a requester that can have nothing outstanding cannot make
  // progress, and silently deadlocking is worse than clamping.
  input  wire [$clog2(N_TAG+1)-1:0] tag_limit,

  // ---- a request ----
  input  wire        req_valid,
  // POSTED requests (writes) expect no completion and consume no tag.
  // That is why a PCIe write is fast and a PCIe read is not: the write is
  // finished when it is sent, and the read is not finished until it comes
  // back.
  input  wire        req_posted,
  input  wire [15:0] req_bytes,

  output wire        req_ready,   // a tag was available
  output wire [$clog2(N_TAG)-1:0] req_tag,
  output wire        req_stall,   // no tag available: this is back-pressure

  // ---- a completion, arriving whenever the fabric feels like it ----
  input  wire        cpl_valid,
  input  wire [$clog2(N_TAG)-1:0] cpl_tag,
  input  wire [15:0] cpl_bytes,

  output wire        cpl_retire,  // this completion finished its request

  // ---- observability ----
  output wire [31:0] n_alloc,
  output wire [31:0] n_posted,
  output wire [31:0] n_stall,
  output wire [31:0] n_retire,
  output wire [31:0] n_partial,     // a completion that did not finish a tag
  output wire [31:0] n_bad_tag,     // a completion for a tag nobody owns
  output wire [31:0] n_over,        // more bytes returned than requested
  output wire [31:0] n_outstanding  // how many tags are in flight NOW
);

  localparam integer TW = $clog2(N_TAG);
  localparam integer LW = $clog2(N_TAG+1);

  reg         t_busy [0:N_TAG-1];
  reg [15:0]  t_rem  [0:N_TAG-1];

  integer i;

  // Clamp to at least one usable tag. A requester allowed zero
  // outstanding transactions can never make progress, and a design that
  // deadlocks silently is harder to debug than one that refuses to.
  wire [LW-1:0] lim = (tag_limit == 0) ? {{(LW-1){1'b0}}, 1'b1} : tag_limit;

  // -------------------------------------------------------------------
  //  ALLOCATE -- the lowest free tag strictly below the limit.
  //
  //  Walking downwards so the lowest index wins. Real requesters often
  //  allocate round-robin to spread wear on completion buffers; lowest-
  //  free is chosen here because it is deterministic, which is what makes
  //  the tag-reuse property checkable at all.
  // -------------------------------------------------------------------
  reg          free_any;
  reg [TW-1:0] free_tag;
  always @(*) begin
    free_any = 1'b0;
    free_tag = {TW{1'b0}};
    for (i = N_TAG - 1; i >= 0; i = i - 1) begin
      if (!t_busy[i] && (i < lim)) begin
        free_any = 1'b1;
        free_tag = i[TW-1:0];
      end
    end
  end

  // A posted request needs no tag, so it is never stalled by tag
  // exhaustion. This asymmetry is the whole reason PCIe separates the two
  // classes.
  assign req_ready = req_valid && (req_posted || free_any);
  assign req_tag   = free_tag;
  assign req_stall = req_valid && !req_posted && !free_any;

  // -------------------------------------------------------------------
  //  COMPLETE -- match by tag, subtract bytes, free on the last one.
  // -------------------------------------------------------------------
  wire          cpl_known = cpl_valid && t_busy[cpl_tag];
  wire [15:0]   rem_now   = t_rem[cpl_tag];
  // More bytes than were asked for. On a real link this is a malformed
  // completion; here it is flagged rather than allowed to wrap the
  // counter, because a wrapped counter would keep the tag outstanding
  // forever and turn a protocol error into a hang.
  wire          cpl_overrun = cpl_known && (cpl_bytes > rem_now);
  wire          cpl_last    = cpl_known && (cpl_bytes >= rem_now);

  assign cpl_retire = cpl_last;

  reg [31:0] alloc_c, posted_c, stall_c, retire_c, partial_c, bad_c, over_c;
  assign n_alloc   = alloc_c;
  assign n_posted  = posted_c;
  assign n_stall   = stall_c;
  assign n_retire  = retire_c;
  assign n_partial = partial_c;
  assign n_bad_tag = bad_c;
  assign n_over    = over_c;

  // Combinational, so it reports the tracker as it stands rather than as
  // it stood a cycle ago.
  reg [31:0] out_now;
  always @(*) begin
    out_now = 32'd0;
    for (i = 0; i < N_TAG; i = i + 1) if (t_busy[i]) out_now = out_now + 32'd1;
  end
  assign n_outstanding = out_now;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_TAG; i = i + 1) begin
        t_busy[i] <= 1'b0;
        t_rem[i]  <= 16'd0;
      end
      alloc_c   <= 32'd0;
      posted_c  <= 32'd0;
      stall_c   <= 32'd0;
      retire_c  <= 32'd0;
      partial_c <= 32'd0;
      bad_c     <= 32'd0;
      over_c    <= 32'd0;
    end else begin
      // ---- completions, then requests ----
      //
      // The order of these two blocks is immaterial to correctness, and it
      // is worth saying why rather than implying otherwise. `free_any` and
      // `free_tag` are combinational over the REGISTERED t_busy, so they
      // describe the table as it stood at the START of this cycle. A tag
      // retired by a completion in this cycle therefore becomes
      // allocatable in the NEXT one -- not this one.
      //
      // That is ONE CYCLE OF TURNAROUND per tag, and it is a real cost with
      // a visible consequence: saturating a fabric of latency L needs L+1
      // tags, not L. Section 5's table shows exactly that boundary.
      //
      // The two blocks cannot collide, because an index being retired is
      // busy and is therefore never the index free_any selects.
      if (cpl_valid) begin
        if (!t_busy[cpl_tag]) begin
          // A completion for a tag nobody owns. Either the fabric
          // invented it or this requester retired the tag early -- and
          // the second is exactly what a tag-reuse bug looks like from
          // here.
          bad_c <= bad_c + 32'd1;
        end else begin
          if (cpl_bytes >= rem_now) begin
            t_busy[cpl_tag] <= 1'b0;
            t_rem[cpl_tag]  <= 16'd0;
            retire_c <= retire_c + 32'd1;
            if (cpl_bytes > rem_now) over_c <= over_c + 32'd1;
          end else begin
            // A split completion: the request is not finished, so the tag
            // stays outstanding. Freeing it here would allow the tag to be
            // reused while the rest of the data was still in flight.
            t_rem[cpl_tag] <= rem_now - cpl_bytes;
            partial_c <= partial_c + 32'd1;
          end
        end
      end

      // ---- then the request ----
      if (req_valid) begin
        if (req_posted) begin
          posted_c <= posted_c + 32'd1;
        end else if (free_any) begin
          // free_tag was chosen from the table as it stood at the start of
          // the cycle, so it is not an index any completion is retiring
          // right now.
          t_busy[free_tag] <= 1'b1;
          t_rem[free_tag]  <= req_bytes;
          alloc_c <= alloc_c + 32'd1;
        end else begin
          stall_c <= stall_c + 32'd1;
        end
      end
    end
  end

endmodule


// ---------------------------------------------------------------------
//  usb_xact_slot -- one in flight, matched by nothing.
//
//  The same question, answered by construction. A USB host issues one
//  transaction to an endpoint and waits for the response; the response is
//  the next thing on the wire. So:
//
//    * There is no tag. There is no field in any USB packet that
//      identifies which transaction a response belongs to, because the
//      question never arises.
//
//    * There is no reordering. One outstanding transaction cannot be
//      overtaken.
//
//    * There is no split-completion reassembly. A transaction's data
//      arrives in one transaction.
//
//    * And there is no tag-reuse bug to have.
//
//  What it costs is in the port list too, by omission: there is no way to
//  have a second transaction outstanding, so throughput is 1 / latency
//  and no amount of link bandwidth changes that. Section 5 measures it.
// ---------------------------------------------------------------------
module usb_xact_slot #(
  parameter integer N_EP = 4
) (
  input  wire       clk,
  input  wire       rst_n,

  // ---- issue a transaction to an endpoint ----
  input  wire       iss_valid,
  input  wire [$clog2(N_EP)-1:0] iss_ep,

  output wire       iss_ready,   // that endpoint was idle
  output wire       iss_busy,    // that endpoint already has one in flight

  // ---- the response, which can only belong to that endpoint's ----
  input  wire       rsp_valid,
  input  wire [$clog2(N_EP)-1:0] rsp_ep,

  output wire       rsp_match,   // there was a transaction to answer
  output wire       rsp_spurious,// there was not

  output wire [31:0] n_issued,
  output wire [31:0] n_refused,
  output wire [31:0] n_answered,
  output wire [31:0] n_spurious,
  output wire [31:0] n_outstanding
);

  localparam integer EW = $clog2(N_EP);

  // One bit per endpoint. That is the entire mechanism -- compare it with
  // the tag array above, which needs a byte counter per entry because a
  // completion can be partial.
  reg  ep_busy [0:N_EP-1];

  integer i;

  assign iss_ready = iss_valid && !ep_busy[iss_ep];
  assign iss_busy  = iss_valid &&  ep_busy[iss_ep];

  // A response with nothing outstanding on that endpoint. On a real bus
  // this is a device talking when it was not asked, which USB treats as a
  // protocol error -- and which is detectable precisely because there is
  // only ever one thing it could have been answering.
  assign rsp_match    = rsp_valid &&  ep_busy[rsp_ep];
  assign rsp_spurious = rsp_valid && !ep_busy[rsp_ep];

  reg [31:0] iss_c, ref_c, ans_c, spur_c;
  assign n_issued   = iss_c;
  assign n_refused  = ref_c;
  assign n_answered = ans_c;
  assign n_spurious = spur_c;

  reg [31:0] out_now;
  always @(*) begin
    out_now = 32'd0;
    for (i = 0; i < N_EP; i = i + 1) if (ep_busy[i]) out_now = out_now + 32'd1;
  end
  assign n_outstanding = out_now;

  always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (i = 0; i < N_EP; i = i + 1) ep_busy[i] <= 1'b0;
      iss_c  <= 32'd0;
      ref_c  <= 32'd0;
      ans_c  <= 32'd0;
      spur_c <= 32'd0;
    end else begin
      // Responses first, for the same reason as the tracker: an endpoint
      // answered this cycle can be reissued in this cycle.
      if (rsp_valid) begin
        if (ep_busy[rsp_ep]) begin
          ep_busy[rsp_ep] <= 1'b0;
          ans_c <= ans_c + 32'd1;
        end else begin
          spur_c <= spur_c + 32'd1;
        end
      end

      if (iss_valid) begin
        if (!ep_busy[iss_ep]) begin
          ep_busy[iss_ep] <= 1'b1;
          iss_c <= iss_c + 32'd1;
        end else begin
          ref_c <= ref_c + 32'd1;
        end
      end
    end
  end

endmodule

5. The Measurement

A fabric model with a settable latency, requests offered as fast as the tracker accepts them, and a count of cycles to retire 24 transactions. Sweeping tag limit 1..8 against latency 1..8 gives 64 points, every one reachable, both dimensions independent inputs.

Cycles to move 24 transactions. Identical in Verilog, SystemVerilog and VHDL:

tagsL=1L=2L=3L=4L=5L=6L=7L=8
1487296120144168192216
225374961738597109
32526344250586674
42526273339455157
52526272833384348
62526272829333741
72526272829303438
82526272829303134

Row 1 is the USB case. The bold diagonal is where the link first saturates.

Two closed forms, asserted as equalities

The table is not an empirical curiosity. Both edges of it are exact, and the bench asserts them with == rather than >= — an inequality would also pass for a design that was slower still, and the point of a closed form is to pin the number from both sides.

One tag costs the full latency, every time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    cycles(tags = 1, L)  ==  K * (L + 1)          K = 24

24×2 = 48, 24×9 = 216. Every cell in row 1 is exactly that. Nothing overlaps, so each transaction costs its latency plus the cycle that issued it, and no amount of link bandwidth changes this number.

Latency+1 tags saturate the link, exactly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    cycles(tags >= L + 1, L)  ==  K + L

Row 2 at L=1 is 25 = 24+1. Row 3 at L=2 is 26 = 24+2. Row 8 at L=7 is 31 = 24+7. One issue per cycle, and the only cost left is draining the last transaction.

And fewer tags than that provably cannot saturate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    cycles(tags <= L, L)  >   K + L

That negative half matters. Without it, the saturation property would be satisfied by a design that was always saturated regardless of tag count — and the chapter would have no result at all.

What that means for the two protocols

USB's 125 µs frame and PCIe's ~2 µs round trip are usually quoted as a latency comparison. Row 1 of the table says something stronger: USB's throughput is its latency, because its concurrency is fixed at one. Halving USB's latency doubles its throughput; widening its wire does nothing.

PCIe decoupled the two. With enough tags its throughput stops depending on latency at all — which is why PCIe could keep scaling bandwidth by adding lanes while its round-trip latency barely moved, and why USB could not.

Four tags in flight, completing in the wrong order

Four non-posted requests are issued on consecutive cycles taking tags 0 through 3, and their completions arrive in the order 3, 1, 2, 0. Each completion retires its own request, and the outstanding count rises to four and falls back to zero.issue fourissue fouranswered out of orderanswered out of orderdraineddrainedfour in flight at oncefour in flight at oncet3 answers firstt3 answers firstt0 answers lastt0 answers lastclkreq_validreq_tagt0t1t2t3idleidleidleidleidleidlecpl_validcpl_tagidleidleidleidlet3t1t2t0idleidlecpl_retireoutstanding1234321000bad_tagt0t1t2t3t4t5t6t7t8t9
Four requests are issued on consecutive cycles, then answered 3, 1, 2, 0. Every one retires correctly, and the outstanding count is the only state that makes that possible. USB cannot produce this picture: with one transaction per endpoint there is no second thing to be out of order with.

Out-of-order is not one case, it is every permutation — so the bench sweeps all 3! = 6 orderings of three tags rather than demonstrating reverse order once. A tracker that happened to work for reverse order is not thereby correct for the rest.

6. Posted Versus Non-Posted

The other half of why PCIe is fast, and the half people skip.

A posted request — a write — expects no completion. It is finished when it is sent. So it consumes no tag, and tag exhaustion cannot block it.

A non-posted request — a read — is not finished until its data comes back. It consumes a tag for the whole round trip.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    a PCIe write is fast because it is over when it leaves
    a PCIe read is slow because it is not over until it returns

That asymmetry is why mixing reads and writes changes a PCIe link's behaviour so much, and why a design that made writes wait for read tags would serialise a workload PCIe exists to overlap. It is mutation N3, and the property is only observable when no tag is free — so the bench fills the table to capacity at each of the eight limits and checks it there.

7. Split Completions And The Bug USB Cannot Have

One PCIe read may be answered by several completions. The tag must stay outstanding until the last byte arrives.

Free it earlier and the tag is reused while data is still in flight — and the next transaction collects the remainder as though it were its own. That is the tag-reuse bug, it corrupts data silently, and it is mutation N1.

The bench sweeps every split of 256 bytes into equal pieces of 256, 128, 64, 32 and 16 — every shape a power-of-two payload can take — and after each non-final piece asserts both that the tag is still busy and that the design did not claim to retire.

8. The Testbench (Verilog)

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for pcie_tag_tracker and usb_xact_slot.
//
//  THE HEADLINE IS A THROUGHPUT SURFACE, NOT A PASS/FAIL.
//
//  Both designs answer "which request was this response for?". The
//  interesting question is what each one's answer COSTS, and the cost is
//  concurrency: how many transactions can be in flight at once.
//
//  So the bench contains a FABRIC MODEL with a settable latency, issues
//  requests as fast as the tracker will take them, and measures how many
//  cycles it takes to retire a fixed number. Sweeping (tag_limit,
//  latency) gives a surface, and the tag_limit = 1 row of that surface IS
//  the USB behaviour -- because a USB host has exactly one transaction
//  outstanding per endpoint, by construction.
//
//  THE SHADOW MODEL IS FORMULATED IN THE OPPOSITE DIRECTION. The tracker
//  allocates by walking its tags DOWNWARDS so the lowest free index wins;
//  the model walks UPWARDS and stops at the first free one. Same answer,
//  different derivation.
// =====================================================================
`timescale 1ns/1ps
module tb_tg_v;

  localparam integer N_TAG = 8;
  localparam integer N_EP  = 4;
  localparam integer MAXF  = 64;   // fabric depth: in-flight completions

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

  // ---- PCIe side ----
  reg  [3:0]  tag_limit = 4'd8;
  reg         req_valid = 1'b0, req_posted = 1'b0;
  reg  [15:0] req_bytes = 16'd0;
  wire        req_ready, req_stall;
  wire [2:0]  req_tag;
  reg         cpl_valid = 1'b0;
  reg  [2:0]  cpl_tag = 3'd0;
  reg  [15:0] cpl_bytes = 16'd0;
  wire        cpl_retire;
  wire [31:0] n_alloc, n_posted, n_stall, n_retire, n_partial,
              n_bad_tag, n_over, n_outstanding;

  pcie_tag_tracker #(.N_TAG(N_TAG)) dut (
    .clk(clk), .rst_n(rst_n), .tag_limit(tag_limit),
    .req_valid(req_valid), .req_posted(req_posted), .req_bytes(req_bytes),
    .req_ready(req_ready), .req_tag(req_tag), .req_stall(req_stall),
    .cpl_valid(cpl_valid), .cpl_tag(cpl_tag), .cpl_bytes(cpl_bytes),
    .cpl_retire(cpl_retire),
    .n_alloc(n_alloc), .n_posted(n_posted), .n_stall(n_stall),
    .n_retire(n_retire), .n_partial(n_partial), .n_bad_tag(n_bad_tag),
    .n_over(n_over), .n_outstanding(n_outstanding)
  );

  // ---- USB side ----
  reg        iss_valid = 1'b0;
  reg  [1:0] iss_ep = 2'd0;
  wire       iss_ready, iss_busy;
  reg        rsp_valid = 1'b0;
  reg  [1:0] rsp_ep = 2'd0;
  wire       rsp_match, rsp_spurious;
  wire [31:0] u_n_issued, u_n_refused, u_n_answered, u_n_spurious, u_n_out;

  usb_xact_slot #(.N_EP(N_EP)) udut (
    .clk(clk), .rst_n(rst_n),
    .iss_valid(iss_valid), .iss_ep(iss_ep),
    .iss_ready(iss_ready), .iss_busy(iss_busy),
    .rsp_valid(rsp_valid), .rsp_ep(rsp_ep),
    .rsp_match(rsp_match), .rsp_spurious(rsp_spurious),
    .n_issued(u_n_issued), .n_refused(u_n_refused),
    .n_answered(u_n_answered), .n_spurious(u_n_spurious),
    .n_outstanding(u_n_out)
  );

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

  // ---- cumulative across resets ----
  //
  // The DUTs' own counters are zeroed by every reset_all, so reading them in
  // the final summary would report only whatever happened after the last
  // one. Per-step checks still use the DUT counters directly; these are for
  // the totals.
  integer c_alloc = 0, c_posted = 0, c_stall = 0, c_retire = 0,
          c_partial = 0, c_bad = 0, c_over = 0;
  integer c_iss = 0, c_ref = 0, c_ans = 0, c_spur = 0;

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

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

  // ---- what the design said, sampled at a DEFINED instant ----
  //
  // cpl_retire is only meaningful while cpl_valid is asserted. A check
  // placed after do_cpl returns reads it after the deassert, where the
  // answer is a delta-cycle artefact -- which produced 4 failures against a
  // design that was entirely correct. Capture once, assert on the capture.
  reg obs_retire;

  // ---- the shadow tracker, maintained by the bench ----
  reg        m_busy [0:N_TAG-1];
  reg [15:0] m_rem  [0:N_TAG-1];
  reg        um_busy [0:N_EP-1];

  // Upward-and-stop, the opposite of the design's downward walk.
  function m_free_any(input [3:0] lim);
    integer j; reg f;
    begin
      f = 1'b0;
      for (j = 0; j < N_TAG; j = j + 1)
        if (!m_busy[j] && (j < lim) && !f) f = 1'b1;
      m_free_any = f;
    end
  endfunction

  function [2:0] m_free_tag(input [3:0] lim);
    integer j; reg f; reg [2:0] t;
    begin
      f = 1'b0; t = 3'd0;
      for (j = 0; j < N_TAG; j = j + 1)
        if (!m_busy[j] && (j < lim) && !f) begin t = j[2:0]; f = 1'b1; end
      m_free_tag = t;
    end
  endfunction

  function [31:0] m_out;
    input dummy;
    integer j; reg [31:0] c;
    begin
      c = 32'd0;
      for (j = 0; j < N_TAG; j = j + 1) if (m_busy[j]) c = c + 32'd1;
      m_out = c;
    end
  endfunction

  function [31:0] um_out;
    input dummy;
    integer j; reg [31:0] c;
    begin
      c = 32'd0;
      for (j = 0; j < N_EP; j = j + 1) if (um_busy[j]) c = c + 32'd1;
      um_out = c;
    end
  endfunction

  // ---- the fabric: completions in flight, each with a due cycle ----
  //
  // A model of the thing PCIe has and USB does not: a transport that holds
  // several requests at once and returns them WHENEVER, not in order.
  reg        f_act   [0:MAXF-1];
  reg [2:0]  f_tag   [0:MAXF-1];
  reg [15:0] f_bytes [0:MAXF-1];
  integer    f_due   [0:MAXF-1];
  integer    now_c;

  task fabric_clear;
    integer j;
    begin
      for (j = 0; j < MAXF; j = j + 1) f_act[j] = 1'b0;
      now_c = 0;
    end
  endtask

  task fabric_push(input [2:0] t, input [15:0] b, input integer due);
    integer j; reg placed;
    begin
      placed = 1'b0;
      for (j = 0; j < MAXF; j = j + 1)
        if (!f_act[j] && !placed) begin
          f_act[j] = 1'b1; f_tag[j] = t; f_bytes[j] = b; f_due[j] = due;
          placed = 1'b1;
        end
      // A full fabric would silently drop a completion and the tag would
      // stay outstanding forever, which reads as a design hang. Bound it.
      ck(placed, "TEST BUG: the fabric model overflowed");
    end
  endtask

  // Pick the earliest-due active completion at or before `now`. Only ONE
  // per cycle, because completions are serialised on a real link.
  task fabric_pop(input integer now, output found, output [2:0] t,
                  output [15:0] b);
    integer j, best, bestdue;
    begin
      best = -1; bestdue = 0; found = 1'b0; t = 3'd0; b = 16'd0;
      for (j = 0; j < MAXF; j = j + 1)
        if (f_act[j] && (f_due[j] <= now))
          if ((best < 0) || (f_due[j] < bestdue)) begin
            best = j; bestdue = f_due[j];
          end
      if (best >= 0) begin
        found = 1'b1; t = f_tag[best]; b = f_bytes[best];
        f_act[best] = 1'b0;
      end
    end
  endtask

  // ---------------------------------------------------------------
  //  THE MEASUREMENT.
  //
  //  Issue K non-posted requests as fast as the tracker accepts them,
  //  against a fabric of fixed latency, and count the cycles until the
  //  last one retires. Everything about the result is decided by how many
  //  tags the requester is allowed to hold.
  // ---------------------------------------------------------------
  integer K_XACT = 24;

  task measure(input integer lim, input integer lat, output integer cycles);
    integer issued, retired, guard;
    reg      pop_found;
    reg [2:0] pop_tag;
    reg [15:0] pop_bytes;
    reg [2:0]  got_tag;
    reg        pre_free;
    reg [2:0]  pre_tag;
    integer j;
    begin
      // reset both the DUT and the model
      rst_n = 1'b0;
      req_valid = 1'b0; cpl_valid = 1'b0; req_posted = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (j = 0; j < N_TAG; j = j + 1) begin m_busy[j] = 1'b0; m_rem[j] = 16'd0; end
      fabric_clear;

      tag_limit = lim[3:0];
      issued = 0; retired = 0; guard = 0;

      // Every wait loop is bounded. An unbounded one turns a design hang
      // into a test that never finishes, which is strictly worse.
      while ((retired < K_XACT) && (guard < 20000)) begin
        // present at most one due completion
        fabric_pop(now_c, pop_found, pop_tag, pop_bytes);
        cpl_valid = pop_found;
        cpl_tag   = pop_tag;
        cpl_bytes = pop_bytes;

        // offer a request whenever there are any left
        req_valid  = (issued < K_XACT);
        req_posted = 1'b0;
        req_bytes  = 16'd64;

        #1;
        got_tag = req_tag;

        // The allocation decision is taken from the table as it stood at the
        // START of this cycle, because the design's free_any is
        // combinational over the REGISTERED tag array. A tag retired by the
        // completion being presented right now is not allocatable until next
        // cycle -- one cycle of turnaround per tag, which is why saturating
        // a fabric of latency L needs L+1 tags.
        //
        // Applying the completion to the model first, and only then judging
        // the allocation, made the model one cycle ahead of the design. The
        // sweep then hung at every point where latency was less than the tag
        // limit, and reported 20000 -- the guard value -- for 54 of its 64
        // cells. Ordering, not arithmetic.
        pre_free = m_free_any(lim[3:0]);
        pre_tag  = m_free_tag(lim[3:0]);

        // ---- PROPERTY 1: the tracker and the model pick the same tag ----
        if (req_valid && pre_free) begin
          ck(req_ready === 1'b1, "a request was refused while a tag was free");
          ck(got_tag === pre_tag, "the tracker allocated a different tag than the model");
        end else if (req_valid) begin
          // ---- PROPERTY 2: no free tag means a stall, not a silent drop ----
          ck(req_stall === 1'b1, "the tracker neither accepted nor stalled a request");
          ck(req_ready === 1'b0, "the tracker accepted a request with no tag free");
        end

        // ---- now advance the model, completions first, exactly as the
        //      design's clocked process does ----
        if (cpl_valid && m_busy[pop_tag]) begin
          if (pop_bytes >= m_rem[pop_tag]) begin
            m_busy[pop_tag] = 1'b0; m_rem[pop_tag] = 16'd0;
            retired = retired + 1;
          end else begin
            m_rem[pop_tag] = m_rem[pop_tag] - pop_bytes;
          end
        end
        if (req_valid && pre_free) begin
          m_busy[pre_tag] = 1'b1;
          m_rem[pre_tag]  = 16'd64;
          fabric_push(pre_tag, 16'd64, now_c + lat);
          issued = issued + 1;
        end

        @(posedge clk); #1;

        // ---- PROPERTY 3: outstanding count is exact, every cycle ----
        //
        // Checked AFTER the edge. Before it, the design's combinational
        // count still describes the previous cycle while the model has
        // already advanced -- comparing across that boundary produced 1122
        // failures against a design and a model that agreed perfectly.
        ck(n_outstanding === m_out(0),
           "the tracker and the model disagree about how many tags are in flight");

        now_c = now_c + 1;
        guard = guard + 1;
        steps = steps + 1;
      end
      req_valid = 1'b0; cpl_valid = 1'b0;
      ck(retired == K_XACT, "the measurement did not retire every transaction");
      cycles = now_c;
    end
  endtask

  // ---------------------------------------------------------------
  //  Single-step helpers for the directed phases.
  // ---------------------------------------------------------------
  task do_req(input posted, input [15:0] bytes, output [2:0] tg,
              output accepted);
    begin
      req_valid = 1'b1; req_posted = posted; req_bytes = bytes;
      #1;
      tg = req_tag;
      accepted = req_ready;
      if (!posted) begin
        if (m_free_any(tag_limit)) begin
          ck(req_ready === 1'b1, "a request was refused while a tag was free");
          ck(req_tag === m_free_tag(tag_limit), "wrong tag allocated");
        end else begin
          ck(req_stall === 1'b1, "no stall was raised with every tag busy");
        end
      end else begin
        // ---- PROPERTY 4: a posted request never consumes a tag ----
        //
        // This is why a PCIe write is fast and a PCIe read is not: the
        // write is finished when it is sent.
        ck(req_ready === 1'b1, "a posted request was refused");
        ck(req_stall === 1'b0, "a posted request was stalled by tag exhaustion");
      end
      @(posedge clk); #1;
      req_valid = 1'b0;
      if (!posted && accepted) begin
        m_busy[tg] = 1'b1; m_rem[tg] = bytes;
        c_alloc = c_alloc + 1;
      end
      if (posted)                 c_posted = c_posted + 1;
      if (!posted && !accepted)   c_stall  = c_stall + 1;
      ck(n_outstanding === m_out(0), "outstanding count wrong after a request");
      steps = steps + 1;
    end
  endtask

  task do_cpl(input [2:0] t, input [15:0] bytes);
    reg e_known, e_last, e_over;
    reg [31:0] r0, p0, b0, o0;
    begin
      e_known = m_busy[t];
      e_last  = e_known && (bytes >= m_rem[t]);
      e_over  = e_known && (bytes >  m_rem[t]);
      r0 = n_retire; p0 = n_partial; b0 = n_bad_tag; o0 = n_over;

      cpl_valid = 1'b1; cpl_tag = t; cpl_bytes = bytes;
      #1;
      obs_retire = cpl_retire;
      // ---- PROPERTY 5: retire means the LAST byte arrived ----
      //
      // A tag freed on a partial completion could be reused while the rest
      // of the data was still in flight, and the next transaction would
      // then collect it.
      ck(cpl_retire === e_last,
         "retire does not mean the request was completely satisfied");
      @(posedge clk); #1;
      cpl_valid = 1'b0;

      if (!e_known)      c_bad     = c_bad + 1;
      else if (e_last)   c_retire  = c_retire + 1;
      else               c_partial = c_partial + 1;
      if (e_over)        c_over    = c_over + 1;

      if (!e_known) begin
        // ---- PROPERTY 6: a completion for a free tag is reported ----
        ck(n_bad_tag == b0 + 32'd1, "a completion for an unowned tag was not reported");
        ck(n_retire == r0, "an unowned completion retired something");
      end else if (e_last) begin
        m_busy[t] = 1'b0; m_rem[t] = 16'd0;
        ck(n_retire == r0 + 32'd1, "a finishing completion did not retire");
        ck(n_over == o0 + (e_over ? 32'd1 : 32'd0), "overrun miscounted");
      end else begin
        m_rem[t] = m_rem[t] - bytes;
        // ---- PROPERTY 7: a partial completion keeps the tag ----
        ck(n_partial == p0 + 32'd1, "a partial completion was not counted");
        ck(n_retire == r0, "a partial completion retired the tag");
      end
      ck(n_outstanding === m_out(0), "outstanding count wrong after a completion");
      steps = steps + 1;
    end
  endtask

  task reset_all;
    integer j;
    begin
      rst_n = 1'b0;
      req_valid = 1'b0; cpl_valid = 1'b0; req_posted = 1'b0;
      iss_valid = 1'b0; rsp_valid = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (j = 0; j < N_TAG; j = j + 1) begin m_busy[j] = 1'b0; m_rem[j] = 16'd0; end
      for (j = 0; j < N_EP;  j = j + 1) um_busy[j] = 1'b0;
      tag_limit = 4'd8;
    end
  endtask

  // ---- the USB side ----
  task do_iss(input [1:0] ep);
    reg e_busy;
    reg [31:0] i0, f0;
    begin
      e_busy = um_busy[ep];
      i0 = u_n_issued; f0 = u_n_refused;
      iss_valid = 1'b1; iss_ep = ep;
      #1;
      // ---- PROPERTY 8: one outstanding transaction per endpoint ----
      //
      // The entire mechanism. There is no second slot to have, which is
      // why no USB packet carries a transaction identifier.
      ck(iss_ready === !e_busy, "the slot accepted a second transaction on one endpoint");
      ck(iss_busy  ===  e_busy, "busy was not reported for an occupied endpoint");
      ck(!(iss_ready && iss_busy), "ready and busy were both asserted");
      @(posedge clk); #1;
      iss_valid = 1'b0;
      if (!e_busy) begin
        um_busy[ep] = 1'b1;
        c_iss = c_iss + 1;
        ck(u_n_issued == i0 + 32'd1, "an accepted transaction was not counted");
      end else begin
        c_ref = c_ref + 1;
        ck(u_n_refused == f0 + 32'd1, "a refused transaction was not counted");
      end
      ck(u_n_out === um_out(0), "usb outstanding count wrong after an issue");
      steps = steps + 1;
    end
  endtask

  task do_rsp(input [1:0] ep);
    reg e_busy;
    reg [31:0] a0, s0;
    begin
      e_busy = um_busy[ep];
      a0 = u_n_answered; s0 = u_n_spurious;
      rsp_valid = 1'b1; rsp_ep = ep;
      #1;
      // ---- PROPERTY 9: a response with nothing outstanding is spurious ----
      //
      // Detectable precisely BECAUSE there is only one thing it could have
      // been answering. With N tags in flight the same question needs a
      // tag field to answer at all.
      ck(rsp_match    ===  e_busy, "a response was not matched to its transaction");
      ck(rsp_spurious === !e_busy, "an unsolicited response was not flagged");
      @(posedge clk); #1;
      rsp_valid = 1'b0;
      if (e_busy) begin
        um_busy[ep] = 1'b0;
        c_ans = c_ans + 1;
        ck(u_n_answered == a0 + 32'd1, "an answered transaction was not counted");
      end else begin
        c_spur = c_spur + 1;
        ck(u_n_spurious == s0 + 32'd1, "a spurious response was not counted");
      end
      ck(u_n_out === um_out(0), "usb outstanding count wrong after a response");
      steps = steps + 1;
    end
  endtask

  // ---- results ----
  integer thr_cyc [0:8][0:8];   // cycles for (limit, latency)
  integer lim, lat, k, j, c;
  reg [2:0] tg;
  reg acc;
  reg [31:0] snap;

  // exhaustive reach over (tag_limit 1..8) x (latency 1..8) = 64
  reg reach [0:63];
  integer nr, ri;
  // and over the directed tracker state space, below
  reg reach_t [0:255];
  integer nrt;

  initial begin
    for (ri = 0; ri < 64;  ri = ri + 1) reach[ri]   = 1'b0;
    for (ri = 0; ri < 256; ri = ri + 1) reach_t[ri] = 1'b0;
    seed = 32'd28004;

    reset_all;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- THE THROUGHPUT SURFACE.
    //
    //  Every tag limit from 1 to 8 against every fabric latency from 1 to
    //  8: 64 points, all reachable, both dimensions independent inputs.
    //
    //  The tag_limit = 1 row is the USB case. A USB host has exactly one
    //  transaction outstanding per endpoint, so whatever that row says
    //  about throughput is what USB can do regardless of link speed.
    // =============================================================
    for (lim = 1; lim <= 8; lim = lim + 1)
    for (lat = 1; lat <= 8; lat = lat + 1) begin
      measure(lim, lat, c);
      thr_cyc[lim][lat] = c;
      reach[(lim - 1) * 8 + (lat - 1)] = 1'b1;
    end

    // ---- PROPERTY 10: more tags never make it slower ----
    //
    // Monotonicity in the tag count. Stated as a property rather than
    // eyeballed off the table, because a tracker that leaked tags would
    // produce a table that still looked plausible.
    for (lat = 1; lat <= 8; lat = lat + 1)
      for (lim = 2; lim <= 8; lim = lim + 1)
        ck(thr_cyc[lim][lat] <= thr_cyc[lim-1][lat],
           "adding a tag made the transfer slower");

    // ---- PROPERTY 11: more latency never makes it faster ----
    for (lim = 1; lim <= 8; lim = lim + 1)
      for (lat = 2; lat <= 8; lat = lat + 1)
        ck(thr_cyc[lim][lat] >= thr_cyc[lim][lat-1],
           "adding latency made the transfer faster");

    // ---- PROPERTY 12: one tag costs the full latency, EXACTLY ----
    //
    // THE USB RESULT, as a closed form rather than an observation. With one
    // outstanding transaction nothing overlaps, so each takes (latency + 1)
    // cycles -- latency to come back, one to issue the next -- and the total
    // is exactly K*(latency+1). No amount of link bandwidth changes it.
    //
    // Asserted with == rather than >=. An inequality would also pass for a
    // design that was slower still, and the point of a closed form is that
    // it pins the number from both sides.
    for (lat = 1; lat <= 8; lat = lat + 1)
      ck(thr_cyc[1][lat] == K_XACT * (lat + 1),
         "one outstanding transaction did not cost exactly latency+1 cycles each");

    // ---- PROPERTY 13: latency+1 tags saturate the link, EXACTLY ----
    //
    // The other closed form, and the one that answers "how many tags do I
    // need?". With L+1 tags the requester issues one per cycle and the only
    // cost left is draining the last one, so the total is exactly K + L.
    //
    // It is latency+1 and not latency because of the one-cycle tag
    // turnaround: free_any reads the REGISTERED tag array, so a tag retired
    // this cycle is allocatable next cycle. That single cycle is the
    // difference between needing L tags and needing L+1.
    for (lat = 1; lat <= 8; lat = lat + 1)
      for (lim = lat + 1; lim <= 8; lim = lim + 1)
        ck(thr_cyc[lim][lat] == K_XACT + lat,
           "latency+1 tags did not saturate the link");

    // ---- PROPERTY 14: fewer than latency+1 tags CANNOT saturate ----
    //
    // The negative half, without which the property above would be
    // satisfied by a design that was always saturated regardless of tags --
    // and the whole chapter would have no result.
    for (lat = 2; lat <= 8; lat = lat + 1)
      for (lim = 1; lim <= lat; lim = lim + 1)
        ck(thr_cyc[lim][lat] > K_XACT + lat,
           "fewer tags than latency+1 saturated the link anyway");

    // =============================================================
    //  PHASE 2 (DIRECTED) -- COMPLETIONS OUT OF ORDER.
    //
    //  The case USB cannot produce. Four tags are allocated in order and
    //  completed in REVERSE, and every one must retire correctly.
    // =============================================================
    reset_all;
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd0, "first tag should be 0");
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd1, "second tag should be 1");
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd2, "third tag should be 2");
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd3, "fourth tag should be 3");
    ck(n_outstanding === 32'd4, "four requests should leave four tags in flight");
    do_cpl(3'd3, 16'd64);
    do_cpl(3'd1, 16'd64);
    do_cpl(3'd2, 16'd64);
    do_cpl(3'd0, 16'd64);
    ck(n_outstanding === 32'd0, "completing every tag should empty the tracker");
    ck(n_retire === 32'd4, "four completions should retire four requests");
    ck(n_bad_tag === 32'd0, "out-of-order completion reported a bad tag");

    // =============================================================
    //  PHASE 3 (DIRECTED, EXHAUSTIVE over every completion order)
    //
    //  Three tags, all 3! = 6 completion orders. Out-of-order is not one
    //  case, it is every permutation, and a tracker that happened to work
    //  for reverse order is not thereby correct for the rest.
    // =============================================================
    for (k = 0; k < 6; k = k + 1) begin : perm
      integer a, b2, c2, t0;
      reset_all;
      do_req(1'b0, 16'd64, tg, acc);
      do_req(1'b0, 16'd64, tg, acc);
      do_req(1'b0, 16'd64, tg, acc);
      // the six orderings of {0,1,2}
      case (k)
        0: begin a = 0; b2 = 1; c2 = 2; end
        1: begin a = 0; b2 = 2; c2 = 1; end
        2: begin a = 1; b2 = 0; c2 = 2; end
        3: begin a = 1; b2 = 2; c2 = 0; end
        4: begin a = 2; b2 = 0; c2 = 1; end
        default: begin a = 2; b2 = 1; c2 = 0; end
      endcase
      do_cpl(a[2:0],  16'd64);
      do_cpl(b2[2:0], 16'd64);
      do_cpl(c2[2:0], 16'd64);
      ck(n_outstanding === 32'd0, "some completion order left a tag outstanding");
      ck(n_retire === 32'd3, "some completion order lost a retirement");
      ck(n_bad_tag === 32'd0, "some completion order was mistaken for a bad tag");
    end

    // =============================================================
    //  PHASE 4 (DIRECTED, EXHAUSTIVE over split shapes) -- SPLIT
    //  COMPLETIONS.
    //
    //  One request may be answered by several completions. The tag must
    //  stay outstanding until the LAST byte arrives -- freeing it early is
    //  the tag-reuse bug, and it would attribute the remaining data to
    //  whatever transaction took the tag next.
    //
    //  Every split of 256 bytes into equal pieces of 256, 128, 64, 32 and
    //  16 is swept, which is every shape a power-of-two payload can take.
    // =============================================================
    for (k = 0; k < 5; k = k + 1) begin : splits
      integer piece, pieces, n;
      reset_all;
      piece  = 256 >> k;
      pieces = 256 / piece;
      do_req(1'b0, 16'd256, tg, acc);
      for (n = 0; n < pieces; n = n + 1) begin
        do_cpl(tg, piece[15:0]);
        if (n < pieces - 1) begin
          // ---- PROPERTY 13: a partially completed tag stays busy ----
          ck(n_outstanding === 32'd1,
             "a tag was freed before its last completion arrived");
          ck(obs_retire === 1'b0, "a partial completion claimed to retire");
        end
      end
      ck(n_outstanding === 32'd0, "the tag was not freed by its last completion");
      ck(n_partial === (pieces - 1),
         "the number of partial completions does not match the split");
    end

    // =============================================================
    //  PHASE 5 (DIRECTED) -- TAG EXHAUSTION IS BACK-PRESSURE.
    //
    //  With two tags, a third request must STALL rather than be dropped or
    //  accepted. That stall is why PCIe read throughput depends on tag
    //  count, and it is the mechanism phase 1 measures.
    // =============================================================
    reset_all;
    tag_limit = 4'd2;
    do_req(1'b0, 16'd64, tg, acc); ck(acc === 1'b1, "first of two tags refused");
    do_req(1'b0, 16'd64, tg, acc); ck(acc === 1'b1, "second of two tags refused");
    snap = n_stall;
    do_req(1'b0, 16'd64, tg, acc);
    ck(acc === 1'b0, "a third request was accepted with only two tags");
    ck(n_stall == snap + 32'd1, "tag exhaustion did not raise a stall");
    ck(n_outstanding === 32'd2, "a stalled request consumed a tag anyway");
    // ---- PROPERTY 14: a POSTED request is never stalled ----
    //
    // Even with every tag busy. A write needs no completion, so tag
    // exhaustion cannot block it -- which is exactly why mixing posted and
    // non-posted traffic changes a PCIe link's behaviour so much.
    snap = n_posted;
    do_req(1'b1, 16'd64, tg, acc);
    ck(acc === 1'b1, "a posted request was blocked by tag exhaustion");
    ck(n_posted == snap + 32'd1, "a posted request was not counted");
    ck(n_outstanding === 32'd2, "a posted request consumed a tag");
    // and freeing one tag lets the next non-posted request through
    do_cpl(3'd0, 16'd64);
    do_req(1'b0, 16'd64, tg, acc);
    ck(acc === 1'b1, "freeing a tag did not admit the next request");
    ck(tg === 3'd0, "the freed tag was not the one reused");

    // =============================================================
    //  PHASE 5b (DIRECTED, EXHAUSTIVE over every tag limit)
    //
    //  The posted-request guarantee is only OBSERVABLE when no tag is free,
    //  because with a tag free a posted request would be accepted either way.
    //  So the table is filled to capacity at each of the eight limits and the
    //  guarantee is checked there.
    //
    //  That is the difference between a property being stated and being
    //  tested: phase 5 demonstrated it once, which gave the mutation that
    //  breaks it a domain of one.
    // =============================================================
    for (k = 1; k <= 8; k = k + 1) begin : postedsweep
      integer b;
      reg [31:0] psnap, osnap2;
      reset_all;
      tag_limit = k[3:0];
      for (b = 0; b < k; b = b + 1) begin
        do_req(1'b0, 16'd64, tg, acc);
        ck(acc === 1'b1, "a request was refused below the tag limit");
      end
      ck(n_outstanding === k, "filling to the limit did not use every tag");

      // a non-posted request must now stall
      do_req(1'b0, 16'd64, tg, acc);
      ck(acc === 1'b0, "a request was accepted above the tag limit");

      // ---- and a POSTED request must NOT ----
      //
      // This is why a PCIe write never blocks on read credit: it needs no
      // completion, so tag exhaustion cannot apply to it. A design that made
      // writes wait for tags would serialise a workload PCIe is specifically
      // built to overlap.
      psnap = n_posted; osnap2 = n_outstanding;
      do_req(1'b1, 16'd64, tg, acc);
      ck(acc === 1'b1, "a posted request was blocked by tag exhaustion");
      ck(n_posted == psnap + 32'd1, "a posted request was not counted");
      ck(n_outstanding === osnap2, "a posted request consumed a tag");
    end

    // =============================================================
    //  PHASE 6 (DIRECTED) -- MALFORMED COMPLETIONS.
    //
    //  A completion for a tag nobody owns, and a completion carrying more
    //  bytes than were asked for. Both are protocol errors and both must
    //  be REPORTED rather than absorbed -- an absorbed overrun would wrap
    //  the byte counter and leave the tag outstanding forever, turning a
    //  protocol error into a hang.
    // =============================================================
    reset_all;
    snap = n_bad_tag;
    do_cpl(3'd5, 16'd64);        // nobody owns tag 5
    ck(n_bad_tag == snap + 32'd1, "a completion for a free tag was accepted");
    ck(n_outstanding === 32'd0, "an unowned completion changed the tracker state");

    do_req(1'b0, 16'd64, tg, acc);
    snap = n_over;
    do_cpl(tg, 16'd128);         // twice what was asked for
    ck(n_over == snap + 32'd1, "an over-long completion was not reported");
    ck(n_outstanding === 32'd0, "an over-long completion left the tag outstanding");

    // =============================================================
    //  PHASE 6b (DIRECTED, EXHAUSTIVE over overrun shapes)
    //
    //  A completion carrying more bytes than were requested, for every
    //  request size and both interesting overrun amounts: one byte too many,
    //  and twice as many as asked for. Eight cases rather than the single
    //  one phase 6 had, because a mutation that stops reporting overruns
    //  should not be able to score 2.
    //
    //  In every case the tag must still RETIRE. Absorbing the overrun by
    //  wrapping the byte counter would leave the tag outstanding forever and
    //  turn a protocol error into a hang, which is strictly worse.
    // =============================================================
    for (k = 0; k < 8; k = k + 1) begin : overruns
      integer want, extra;
      reg [31:0] osnap;
      reset_all;
      want  = 16 << (k % 4);
      extra = (k < 4) ? 1 : want;      // one byte too many, or double
      do_req(1'b0, want[15:0], tg, acc);
      osnap = n_over;
      do_cpl(tg, (want + extra));
      ck(n_over == osnap + 32'd1, "an over-long completion was not reported");
      ck(n_outstanding === 32'd0,
         "an over-long completion left the tag outstanding");
      ck(n_retire > 32'd0, "an over-long completion did not retire the tag");
    end

    // =============================================================
    //  PHASE 7 (DIRECTED, EXHAUSTIVE over the tracker's busy-mask)
    //
    //  Every one of the 2**8 = 256 combinations of which tags are busy,
    //  reached by allocating and completing rather than by forcing state.
    //  For each, the allocation decision is checked against the model.
    // =============================================================
    for (k = 0; k < 256; k = k + 1) begin : masks
      integer b;
      reset_all;
      // allocate all eight, then complete the ones the mask says are free
      for (b = 0; b < 8; b = b + 1) do_req(1'b0, 16'd64, tg, acc);
      ck(n_outstanding === 32'd8, "eight requests did not fill eight tags");
      for (b = 0; b < 8; b = b + 1)
        if (!k[b]) do_cpl(b[2:0], 16'd64);
      // ---- a POSTED request, against every table state ----
      //
      // It must be accepted and must consume no tag, whatever else is in
      // flight -- including when every tag is busy. Swept here rather than
      // demonstrated once, because a property exercised a single time has a
      // mutation domain of one and tells you almost nothing.
      snap = n_outstanding;
      do_req(1'b1, 16'd64, tg, acc);
      ck(acc === 1'b1, "a posted request was refused");
      ck(n_outstanding === snap, "a posted request consumed a tag");

      // ---- a completion for a tag NOBODY OWNS, against every state ----
      //
      // Whichever tag is free, a completion for it is a protocol error and
      // must be reported rather than absorbed. With a full table this case
      // does not exist, so it is skipped rather than faked.
      if (m_free_any(4'd8)) begin : badcpl
        reg [2:0] ft;
        reg [31:0] bsnap, osnap;
        ft = m_free_tag(4'd8);
        bsnap = n_bad_tag; osnap = n_outstanding;
        do_cpl(ft, 16'd64);
        ck(n_bad_tag == bsnap + 32'd1,
           "a completion for an unowned tag was not reported");
        ck(n_outstanding === osnap,
           "a completion for an unowned tag changed the tracker state");
      end

      // Now exactly the tags set in k are busy. The next allocation must
      // pick the lowest free one, which the model computes independently.
      if (m_free_any(4'd8)) begin
        do_req(1'b0, 16'd64, tg, acc);
        ck(acc === 1'b1, "a request was refused with a free tag");
      end else begin
        do_req(1'b0, 16'd64, tg, acc);
        ck(acc === 1'b0, "a request was accepted with every tag busy");
      end
      reach_t[k] = 1'b1;
    end

    // =============================================================
    //  PHASE 8 (DIRECTED) -- THE USB SLOT.
    //
    //  One outstanding transaction per endpoint. The endpoints are
    //  independent, a second issue to a busy endpoint is refused, and a
    //  response with nothing outstanding is flagged.
    // =============================================================
    reset_all;
    for (k = 0; k < 4; k = k + 1) begin
      do_iss(k[1:0]);
      ck(u_n_out === (k + 1), "each endpoint should hold one transaction");
    end
    // every endpoint busy: a second issue to each must be refused
    for (k = 0; k < 4; k = k + 1) do_iss(k[1:0]);
    ck(u_n_refused === 32'd4, "four second-issues should all be refused");
    ck(u_n_out === 32'd4, "a refused issue changed the outstanding count");
    // answer them all
    for (k = 0; k < 4; k = k + 1) do_rsp(k[1:0]);
    ck(u_n_out === 32'd0, "answering every endpoint should empty the slots");
    // an unsolicited response
    snap = u_n_spurious;
    do_rsp(2'd2);
    ck(u_n_spurious == snap + 32'd1, "an unsolicited response was not flagged");

    // =============================================================
    //  PHASE 9 (DIRECTED, EXHAUSTIVE over the endpoint busy-mask)
    //
    //  All 2**4 = 16 combinations of which endpoints are busy, crossed
    //  with an issue and a response to each of the 4 endpoints. 16 x 4 x 2
    //  decisions, every one checked against the model.
    // =============================================================
    for (k = 0; k < 16; k = k + 1) begin : epmask
      integer b;
      reset_all;
      for (b = 0; b < 4; b = b + 1) if (k[b]) do_iss(b[1:0]);
      for (b = 0; b < 4; b = b + 1) do_iss(b[1:0]);
      reset_all;
      for (b = 0; b < 4; b = b + 1) if (k[b]) do_iss(b[1:0]);
      for (b = 0; b < 4; b = b + 1) do_rsp(b[1:0]);
    end

    // =============================================================
    //  PHASE 10 (RANDOM) -- mixed traffic with a reordering fabric.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_all;
    fabric_clear;
    for (k = 0; k < 800; k = k + 1) begin : rnd
      reg pf; reg [2:0] pt; reg [15:0] pb;
      // a due completion, if any
      fabric_pop(now_c, pf, pt, pb);
      if (pf) do_cpl(pt, pb);
      // a request, sometimes posted
      if ((urand(0) % 3) != 0) begin
        if ((urand(0) % 5) == 0) begin
          do_req(1'b1, 16'd64, tg, acc);
        end else begin
          do_req(1'b0, 16'd64, tg, acc);
          // A random latency, so completions come back out of order --
          // which is the property the whole tracker exists for.
          if (acc) fabric_push(tg, 16'd64, now_c + 1 + (urand(0) % 9));
        end
      end
      // and some USB traffic on the side
      if ((urand(0) % 2) == 0) do_iss((urand(0) % 4));
      else                     do_rsp((urand(0) % 4));
      now_c = now_c + 1;
    end
    // drain whatever is still in flight, bounded
    for (k = 0; k < 400; k = k + 1) begin : drain
      reg pf2; reg [2:0] pt2; reg [15:0] pb2;
      fabric_pop(now_c + 100, pf2, pt2, pb2);
      if (pf2) do_cpl(pt2, pb2);
      now_c = now_c + 1;
    end
`endif

    nr = 0;  for (ri = 0; ri < 64;  ri = ri + 1) if (reach[ri])   nr  = nr + 1;
    nrt = 0; for (ri = 0; ri < 256; ri = ri + 1) if (reach_t[ri]) nrt = nrt + 1;

    $display("steps=%0d checks=%0d reach_thr=%0d/64 reach_tag=%0d/256 errors=%0d",
             steps, checks, nr, nrt, errors);
    $display("[pcie] alloc=%0d posted=%0d stalls=%0d retire=%0d partial=%0d bad_tag=%0d over=%0d",
             c_alloc, c_posted, c_stall, c_retire, c_partial, c_bad, c_over);
    $display("[usb]  issued=%0d refused=%0d answered=%0d spurious=%0d",
             c_iss, c_ref, c_ans, c_spur);
    $display("--- cycles to move %0d transactions, by tags outstanding x latency ---", K_XACT);
    $display("    tags      L=1     L=2     L=3     L=4     L=5     L=6     L=7     L=8");
    for (lim = 1; lim <= 8; lim = lim + 1)
      $display("    %4d %8d%8d%8d%8d%8d%8d%8d%8d", lim,
               thr_cyc[lim][1], thr_cyc[lim][2], thr_cyc[lim][3], thr_cyc[lim][4],
               thr_cyc[lim][5], thr_cyc[lim][6], thr_cyc[lim][7], thr_cyc[lim][8]);
    $display("    (row tags=1 IS the USB case: one transaction outstanding)");
    if (nr != 64 || nrt != 256) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

9. SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  OUTSTANDING TRANSACTIONS -- SystemVerilog.
//
//  Same hardware contract as the Verilog file: same ports, same widths,
//  same reset values, same cycle-by-cycle behaviour. What changes is
//  `always_comb` / `always_ff`, loop variables declared in the loop, and
//  every continuous assignment written as `logic` + `assign` on separate
//  lines rather than as an initialised declaration.
//
//  THAT LAST POINT IS NOT COSMETIC. `logic x = expr;` is a one-shot
//  VARIABLE INITIALISER in SystemVerilog -- evaluated once at time zero and
//  never again -- while the Verilog `wire x = expr;` it came from is a
//  continuous assignment. Translating the five such lines in the previous
//  chapter mechanically produced 29,580 phantom failures against a design
//  that was entirely correct, and the symptom was structural: nothing ever
//  matched, anywhere.
//
//  OUTSTANDING TRANSACTIONS -- THE MECHANISM PCIe NEEDS AND USB DOES NOT.
//
//  CLASSIFICATION: simplified synthesisable teaching RTL.
//  Two modules. Neither is a controller: there is no TLP encoder, no
//  link layer, no credits, no USB packet engine and no scheduler. Each
//  is the part that answers one question.
//
//      "This response just arrived. Which request was it for?"
//
//  USB answers it by CONSTRUCTION. The host issues one transaction to an
//  endpoint and waits for it. The response is the next thing on the wire,
//  so there is nothing to match -- and USB packets carry no transaction
//  identifier at all, because none is needed.
//
//  PCIe answers it with a TAG. A requester may have many non-posted
//  requests in flight at once; completions travel independently, come
//  back OUT OF ORDER, and may arrive split into several pieces. So every
//  request carries a tag, and the requester must track each one until the
//  last byte of its completion has arrived.
//
//  The difference is not bandwidth, it is CONCURRENCY:
//
//      USB:  one outstanding transaction, so throughput is bounded by
//            1 / latency, whatever the wire can carry.
//      PCIe: N outstanding transactions, so throughput is bounded by
//            min(1, N / latency) -- and with enough tags, not by latency
//            at all.
//
//  That is a formula, so the chapter measures it. Section 5's table is
//  throughput against tag count and latency, and the USB case is exactly
//  its first row.
//
//  What the tags cost is a class of bug that cannot exist on USB: a tag
//  reused before its completion arrives silently attributes one
//  transaction's data to another.
// =====================================================================

// ---------------------------------------------------------------------
//  pcie_tag_tracker -- many in flight, matched by tag.
//
//  `tag_limit` is an INPUT rather than a parameter so the testbench can
//  sweep how many tags the requester is allowed to use without
//  re-elaborating. That is what makes the throughput curve in section 5
//  a single exhaustive sweep instead of eight separate runs -- and it is
//  also how the USB case is reached: tag_limit = 1.
// ---------------------------------------------------------------------
module pcie_tag_tracker #(
  parameter int N_TAG   = 8,
  // Bytes are tracked so a SPLIT completion can be modelled: a single
  // read may be answered by several completions, and the tag is not free
  // until the last byte arrives.
  parameter int MAX_BYTES = 256
) (
  input  logic       clk,
  input  logic       rst_n,

  // How many tags the requester may use, 1..N_TAG. Zero is treated as
  // one: a requester that can have nothing outstanding cannot make
  // progress, and silently deadlocking is worse than clamping.
  input  logic [$clog2(N_TAG+1)-1:0] tag_limit,

  // ---- a request ----
  input  logic        req_valid,
  // POSTED requests (writes) expect no completion and consume no tag.
  // That is why a PCIe write is fast and a PCIe read is not: the write is
  // finished when it is sent, and the read is not finished until it comes
  // back.
  input  logic        req_posted,
  input  logic [15:0] req_bytes,

  output logic        req_ready,   // a tag was available
  output logic [$clog2(N_TAG)-1:0] req_tag,
  output logic        req_stall,   // no tag available: this is back-pressure

  // ---- a completion, arriving whenever the fabric feels like it ----
  input  logic        cpl_valid,
  input  logic [$clog2(N_TAG)-1:0] cpl_tag,
  input  logic [15:0] cpl_bytes,

  output logic        cpl_retire,  // this completion finished its request

  // ---- observability ----
  output logic [31:0] n_alloc,
  output logic [31:0] n_posted,
  output logic [31:0] n_stall,
  output logic [31:0] n_retire,
  output logic [31:0] n_partial,     // a completion that did not finish a tag
  output logic [31:0] n_bad_tag,     // a completion for a tag nobody owns
  output logic [31:0] n_over,        // more bytes returned than requested
  output logic [31:0] n_outstanding  // how many tags are in flight NOW
);

  localparam int TW = $clog2(N_TAG);
  localparam int LW = $clog2(N_TAG+1);

  logic         t_busy [N_TAG];
  logic [15:0]  t_rem  [N_TAG];


  // Clamp to at least one usable tag. A requester allowed zero
  // outstanding transactions can never make progress, and a design that
  // deadlocks silently is harder to debug than one that refuses to.
  logic [LW-1:0] lim;
  assign lim = (tag_limit == 0) ? {{(LW-1){1'b0}}, 1'b1} : tag_limit;

  // -------------------------------------------------------------------
  //  ALLOCATE -- the lowest free tag strictly below the limit.
  //
  //  Walking downwards so the lowest index wins. Real requesters often
  //  allocate round-robin to spread wear on completion buffers; lowest-
  //  free is chosen here because it is deterministic, which is what makes
  //  the tag-reuse property checkable at all.
  // -------------------------------------------------------------------
  logic          free_any;
  logic [TW-1:0] free_tag;
  always_comb begin
    free_any = 1'b0;
    free_tag = '0;
    for (int i = N_TAG - 1; i >= 0; i--) begin
      if (!t_busy[i] && (i < lim)) begin
        free_any = 1'b1;
        free_tag = TW'(i);
      end
    end
  end

  // A posted request needs no tag, so it is never stalled by tag
  // exhaustion. This asymmetry is the whole reason PCIe separates the two
  // classes.
  assign req_ready = req_valid && (req_posted || free_any);
  assign req_tag   = free_tag;
  assign req_stall = req_valid && !req_posted && !free_any;

  // -------------------------------------------------------------------
  //  COMPLETE -- match by tag, subtract bytes, free on the last one.
  // -------------------------------------------------------------------
  logic cpl_known;
  assign cpl_known = cpl_valid && t_busy[cpl_tag];
  logic [15:0] rem_now;
  assign rem_now = t_rem[cpl_tag];
  // More bytes than were asked for. On a real link this is a malformed
  // completion; here it is flagged rather than allowed to wrap the
  // counter, because a wrapped counter would keep the tag outstanding
  // forever and turn a protocol error into a hang.
  logic cpl_overrun;
  assign cpl_overrun = cpl_known && (cpl_bytes > rem_now);
  logic cpl_last;
  assign cpl_last = cpl_known && (cpl_bytes >= rem_now);

  assign cpl_retire = cpl_last;

  logic [31:0] alloc_c, posted_c, stall_c, retire_c, partial_c, bad_c, over_c;
  assign n_alloc   = alloc_c;
  assign n_posted  = posted_c;
  assign n_stall   = stall_c;
  assign n_retire  = retire_c;
  assign n_partial = partial_c;
  assign n_bad_tag = bad_c;
  assign n_over    = over_c;

  // Combinational, so it reports the tracker as it stands rather than as
  // it stood a cycle ago.
  logic [31:0] out_now;
  always_comb begin
    out_now = 32'd0;
    for (int i = 0; i < N_TAG; i++) if (t_busy[i]) out_now = out_now + 32'd1;
  end
  assign n_outstanding = out_now;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < N_TAG; i++) begin
        t_busy[i] <= 1'b0;
        t_rem[i]  <= 16'd0;
      end
      alloc_c   <= 32'd0;
      posted_c  <= 32'd0;
      stall_c   <= 32'd0;
      retire_c  <= 32'd0;
      partial_c <= 32'd0;
      bad_c     <= 32'd0;
      over_c    <= 32'd0;
    end else begin
      // ---- completions, then requests ----
      //
      // The order of these two blocks is immaterial to correctness, and it
      // is worth saying why rather than implying otherwise. `free_any` and
      // `free_tag` are combinational over the REGISTERED t_busy, so they
      // describe the table as it stood at the START of this cycle. A tag
      // retired by a completion in this cycle therefore becomes
      // allocatable in the NEXT one -- not this one.
      //
      // That is ONE CYCLE OF TURNAROUND per tag, and it is a real cost with
      // a visible consequence: saturating a fabric of latency L needs L+1
      // tags, not L. Section 5's table shows exactly that boundary.
      //
      // The two blocks cannot collide, because an index being retired is
      // busy and is therefore never the index free_any selects.
      if (cpl_valid) begin
        if (!t_busy[cpl_tag]) begin
          // A completion for a tag nobody owns. Either the fabric
          // invented it or this requester retired the tag early -- and
          // the second is exactly what a tag-reuse bug looks like from
          // here.
          bad_c <= bad_c + 32'd1;
        end else begin
          if (cpl_bytes >= rem_now) begin
            t_busy[cpl_tag] <= 1'b0;
            t_rem[cpl_tag]  <= 16'd0;
            retire_c <= retire_c + 32'd1;
            if (cpl_bytes > rem_now) over_c <= over_c + 32'd1;
          end else begin
            // A split completion: the request is not finished, so the tag
            // stays outstanding. Freeing it here would allow the tag to be
            // reused while the rest of the data was still in flight.
            t_rem[cpl_tag] <= rem_now - cpl_bytes;
            partial_c <= partial_c + 32'd1;
          end
        end
      end

      // ---- then the request ----
      if (req_valid) begin
        if (req_posted) begin
          posted_c <= posted_c + 32'd1;
        end else if (free_any) begin
          // free_tag was chosen from the table as it stood at the start of
          // the cycle, so it is not an index any completion is retiring
          // right now.
          t_busy[free_tag] <= 1'b1;
          t_rem[free_tag]  <= req_bytes;
          alloc_c <= alloc_c + 32'd1;
        end else begin
          stall_c <= stall_c + 32'd1;
        end
      end
    end
  end

endmodule


// ---------------------------------------------------------------------
//  usb_xact_slot -- one in flight, matched by nothing.
//
//  The same question, answered by construction. A USB host issues one
//  transaction to an endpoint and waits for the response; the response is
//  the next thing on the wire. So:
//
//    * There is no tag. There is no field in any USB packet that
//      identifies which transaction a response belongs to, because the
//      question never arises.
//
//    * There is no reordering. One outstanding transaction cannot be
//      overtaken.
//
//    * There is no split-completion reassembly. A transaction's data
//      arrives in one transaction.
//
//    * And there is no tag-reuse bug to have.
//
//  What it costs is in the port list too, by omission: there is no way to
//  have a second transaction outstanding, so throughput is 1 / latency
//  and no amount of link bandwidth changes that. Section 5 measures it.
// ---------------------------------------------------------------------
module usb_xact_slot #(
  parameter int N_EP = 4
) (
  input  logic       clk,
  input  logic       rst_n,

  // ---- issue a transaction to an endpoint ----
  input  logic       iss_valid,
  input  logic [$clog2(N_EP)-1:0] iss_ep,

  output logic       iss_ready,   // that endpoint was idle
  output logic       iss_busy,    // that endpoint already has one in flight

  // ---- the response, which can only belong to that endpoint's ----
  input  logic       rsp_valid,
  input  logic [$clog2(N_EP)-1:0] rsp_ep,

  output logic       rsp_match,   // there was a transaction to answer
  output logic       rsp_spurious,// there was not

  output logic [31:0] n_issued,
  output logic [31:0] n_refused,
  output logic [31:0] n_answered,
  output logic [31:0] n_spurious,
  output logic [31:0] n_outstanding
);

  localparam int EW = $clog2(N_EP);

  // One bit per endpoint. That is the entire mechanism -- compare it with
  // the tag array above, which needs a byte counter per entry because a
  // completion can be partial.
  logic  ep_busy [N_EP];


  assign iss_ready = iss_valid && !ep_busy[iss_ep];
  assign iss_busy  = iss_valid &&  ep_busy[iss_ep];

  // A response with nothing outstanding on that endpoint. On a real bus
  // this is a device talking when it was not asked, which USB treats as a
  // protocol error -- and which is detectable precisely because there is
  // only ever one thing it could have been answering.
  assign rsp_match    = rsp_valid &&  ep_busy[rsp_ep];
  assign rsp_spurious = rsp_valid && !ep_busy[rsp_ep];

  logic [31:0] iss_c, ref_c, ans_c, spur_c;
  assign n_issued   = iss_c;
  assign n_refused  = ref_c;
  assign n_answered = ans_c;
  assign n_spurious = spur_c;

  logic [31:0] out_now;
  always_comb begin
    out_now = 32'd0;
    for (int i = 0; i < N_EP; i++) if (ep_busy[i]) out_now = out_now + 32'd1;
  end
  assign n_outstanding = out_now;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < N_EP; i++) ep_busy[i] <= 1'b0;
      iss_c  <= 32'd0;
      ref_c  <= 32'd0;
      ans_c  <= 32'd0;
      spur_c <= 32'd0;
    end else begin
      // Responses first, for the same reason as the tracker: an endpoint
      // answered this cycle can be reissued in this cycle.
      if (rsp_valid) begin
        if (ep_busy[rsp_ep]) begin
          ep_busy[rsp_ep] <= 1'b0;
          ans_c <= ans_c + 32'd1;
        end else begin
          spur_c <= spur_c + 32'd1;
        end
      end

      if (iss_valid) begin
        if (!ep_busy[iss_ep]) begin
          ep_busy[iss_ep] <= 1'b1;
          iss_c <= iss_c + 32'd1;
        end else begin
          ref_c <= ref_c + 32'd1;
        end
      end
    end
  end

endmodule

The SystemVerilog testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// =====================================================================
//  Testbench for pcie_tag_tracker and usb_xact_slot -- SystemVerilog.
//
//  SAME SEED AND SAME PHASE ORDER AS THE VERILOG BENCH, deliberately.
//  Icarus seeds $random identically, so both drive identical stimulus and
//  any difference between the two mutation columns is a real difference
//  between the two DESIGNS. The independent-stimulus role is VHDL's.
//
//  THE HEADLINE IS A THROUGHPUT SURFACE, NOT A PASS/FAIL.
//
//  Both designs answer "which request was this response for?". The
//  interesting question is what each one's answer COSTS, and the cost is
//  concurrency: how many transactions can be in flight at once.
//
//  So the bench contains a FABRIC MODEL with a settable latency, issues
//  requests as fast as the tracker will take them, and measures how many
//  cycles it takes to retire a fixed number. Sweeping (tag_limit,
//  latency) gives a surface, and the tag_limit = 1 row of that surface IS
//  the USB behaviour -- because a USB host has exactly one transaction
//  outstanding per endpoint, by construction.
//
//  THE SHADOW MODEL IS FORMULATED IN THE OPPOSITE DIRECTION. The tracker
//  allocates by walking its tags DOWNWARDS so the lowest free index wins;
//  the model walks UPWARDS and stops at the first free one. Same answer,
//  different derivation.
// =====================================================================
`timescale 1ns/1ps
module tb_tg_sv;

  localparam int N_TAG = 8;
  localparam int N_EP  = 4;
  localparam int MAXF  = 64;   // fabric depth: in-flight completions

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

  // ---- PCIe side ----
  logic [3:0]  tag_limit = 4'd8;
  logic         req_valid = 1'b0, req_posted = 1'b0;
  logic [15:0] req_bytes = 16'd0;
  logic        req_ready, req_stall;
  logic [2:0]  req_tag;
  logic         cpl_valid = 1'b0;
  logic [2:0]  cpl_tag = 3'd0;
  logic [15:0] cpl_bytes = 16'd0;
  logic        cpl_retire;
  logic [31:0] n_alloc, n_posted, n_stall, n_retire, n_partial,
              n_bad_tag, n_over, n_outstanding;

  pcie_tag_tracker #(.N_TAG(N_TAG)) dut (
    .clk(clk), .rst_n(rst_n), .tag_limit(tag_limit),
    .req_valid(req_valid), .req_posted(req_posted), .req_bytes(req_bytes),
    .req_ready(req_ready), .req_tag(req_tag), .req_stall(req_stall),
    .cpl_valid(cpl_valid), .cpl_tag(cpl_tag), .cpl_bytes(cpl_bytes),
    .cpl_retire(cpl_retire),
    .n_alloc(n_alloc), .n_posted(n_posted), .n_stall(n_stall),
    .n_retire(n_retire), .n_partial(n_partial), .n_bad_tag(n_bad_tag),
    .n_over(n_over), .n_outstanding(n_outstanding)
  );

  // ---- USB side ----
  logic        iss_valid = 1'b0;
  logic [1:0] iss_ep = 2'd0;
  logic       iss_ready, iss_busy;
  logic        rsp_valid = 1'b0;
  logic [1:0] rsp_ep = 2'd0;
  logic       rsp_match, rsp_spurious;
  logic [31:0] u_n_issued, u_n_refused, u_n_answered, u_n_spurious, u_n_out;

  usb_xact_slot #(.N_EP(N_EP)) udut (
    .clk(clk), .rst_n(rst_n),
    .iss_valid(iss_valid), .iss_ep(iss_ep),
    .iss_ready(iss_ready), .iss_busy(iss_busy),
    .rsp_valid(rsp_valid), .rsp_ep(rsp_ep),
    .rsp_match(rsp_match), .rsp_spurious(rsp_spurious),
    .n_issued(u_n_issued), .n_refused(u_n_refused),
    .n_answered(u_n_answered), .n_spurious(u_n_spurious),
    .n_outstanding(u_n_out)
  );

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

  // ---- cumulative across resets ----
  //
  // The DUTs' own counters are zeroed by every reset_all, so reading them in
  // the final summary would report only whatever happened after the last
  // one. Per-step checks still use the DUT counters directly; these are for
  // the totals.
  int c_alloc = 0, c_posted = 0, c_stall = 0, c_retire = 0,
          c_partial = 0, c_bad = 0, c_over = 0;
  int c_iss = 0, c_ref = 0, c_ans = 0, c_spur = 0;

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

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

  // ---- what the design said, sampled at a DEFINED instant ----
  //
  // cpl_retire is only meaningful while cpl_valid is asserted. A check
  // placed after do_cpl returns reads it after the deassert, where the
  // answer is a delta-cycle artefact -- which produced 4 failures against a
  // design that was entirely correct. Capture once, assert on the capture.
  logic obs_retire;

  // ---- the shadow tracker, maintained by the bench ----
  logic        m_busy [0:N_TAG-1];
  logic [15:0] m_rem  [0:N_TAG-1];
  logic        um_busy [0:N_EP-1];

  // Upward-and-stop, the opposite of the design's downward walk.
  function automatic logic m_free_any(input [3:0] lim);
    int j; logic f;
    begin
      f = 1'b0;
      for (j = 0; j < N_TAG; j = j + 1)
        if (!m_busy[j] && (j < lim) && !f) f = 1'b1;
      m_free_any = f;
    end
  endfunction

  function automatic logic [2:0] m_free_tag(input [3:0] lim);
    int j; logic f; logic [2:0] t;
    begin
      f = 1'b0; t = 3'd0;
      for (j = 0; j < N_TAG; j = j + 1)
        if (!m_busy[j] && (j < lim) && !f) begin t = j[2:0]; f = 1'b1; end
      m_free_tag = t;
    end
  endfunction

  function automatic logic [31:0] m_out(input logic dummy);
    int j; logic [31:0] c;
    begin
      c = 32'd0;
      for (j = 0; j < N_TAG; j = j + 1) if (m_busy[j]) c = c + 32'd1;
      m_out = c;
    end
  endfunction

  function automatic logic [31:0] um_out(input logic dummy);
    int j; logic [31:0] c;
    begin
      c = 32'd0;
      for (j = 0; j < N_EP; j = j + 1) if (um_busy[j]) c = c + 32'd1;
      um_out = c;
    end
  endfunction

  // ---- the fabric: completions in flight, each with a due cycle ----
  //
  // A model of the thing PCIe has and USB does not: a transport that holds
  // several requests at once and returns them WHENEVER, not in order.
  logic        f_act   [0:MAXF-1];
  logic [2:0]  f_tag   [0:MAXF-1];
  logic [15:0] f_bytes [0:MAXF-1];
  integer    f_due   [0:MAXF-1];
  int    now_c;

  task automatic fabric_clear;
    int j;
    begin
      for (j = 0; j < MAXF; j = j + 1) f_act[j] = 1'b0;
      now_c = 0;
    end
  endtask

  task automatic fabric_push(input [2:0] t, input [15:0] b, input integer due);
    int j; logic placed;
    begin
      placed = 1'b0;
      for (j = 0; j < MAXF; j = j + 1)
        if (!f_act[j] && !placed) begin
          f_act[j] = 1'b1; f_tag[j] = t; f_bytes[j] = b; f_due[j] = due;
          placed = 1'b1;
        end
      // A full fabric would silently drop a completion and the tag would
      // stay outstanding forever, which reads as a design hang. Bound it.
      ck(placed, "TEST BUG: the fabric model overflowed");
    end
  endtask

  // Pick the earliest-due active completion at or before `now`. Only ONE
  // per cycle, because completions are serialised on a real link.
  task automatic fabric_pop(input integer now, output found, output [2:0] t,
                  output [15:0] b);
    int j, best, bestdue;
    begin
      best = -1; bestdue = 0; found = 1'b0; t = 3'd0; b = 16'd0;
      for (j = 0; j < MAXF; j = j + 1)
        if (f_act[j] && (f_due[j] <= now))
          if ((best < 0) || (f_due[j] < bestdue)) begin
            best = j; bestdue = f_due[j];
          end
      if (best >= 0) begin
        found = 1'b1; t = f_tag[best]; b = f_bytes[best];
        f_act[best] = 1'b0;
      end
    end
  endtask

  // ---------------------------------------------------------------
  //  THE MEASUREMENT.
  //
  //  Issue K non-posted requests as fast as the tracker accepts them,
  //  against a fabric of fixed latency, and count the cycles until the
  //  last one retires. Everything about the result is decided by how many
  //  tags the requester is allowed to hold.
  // ---------------------------------------------------------------
  int K_XACT = 24;

  task automatic measure(input integer lim, input integer lat, output integer cycles);
    int issued, retired, guard;
    logic      pop_found;
    logic [2:0] pop_tag;
    logic [15:0] pop_bytes;
    logic [2:0]  got_tag;
    logic        pre_free;
    logic [2:0]  pre_tag;
    int j;
    begin
      // reset both the DUT and the model
      rst_n = 1'b0;
      req_valid = 1'b0; cpl_valid = 1'b0; req_posted = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (j = 0; j < N_TAG; j = j + 1) begin m_busy[j] = 1'b0; m_rem[j] = 16'd0; end
      fabric_clear;

      tag_limit = lim[3:0];
      issued = 0; retired = 0; guard = 0;

      // Every wait loop is bounded. An unbounded one turns a design hang
      // into a test that never finishes, which is strictly worse.
      while ((retired < K_XACT) && (guard < 20000)) begin
        // present at most one due completion
        fabric_pop(now_c, pop_found, pop_tag, pop_bytes);
        cpl_valid = pop_found;
        cpl_tag   = pop_tag;
        cpl_bytes = pop_bytes;

        // offer a request whenever there are any left
        req_valid  = (issued < K_XACT);
        req_posted = 1'b0;
        req_bytes  = 16'd64;

        #1;
        got_tag = req_tag;

        // The allocation decision is taken from the table as it stood at the
        // START of this cycle, because the design's free_any is
        // combinational over the REGISTERED tag array. A tag retired by the
        // completion being presented right now is not allocatable until next
        // cycle -- one cycle of turnaround per tag, which is why saturating
        // a fabric of latency L needs L+1 tags.
        //
        // Applying the completion to the model first, and only then judging
        // the allocation, made the model one cycle ahead of the design. The
        // sweep then hung at every point where latency was less than the tag
        // limit, and reported 20000 -- the guard value -- for 54 of its 64
        // cells. Ordering, not arithmetic.
        pre_free = m_free_any(lim[3:0]);
        pre_tag  = m_free_tag(lim[3:0]);

        // ---- PROPERTY 1: the tracker and the model pick the same tag ----
        if (req_valid && pre_free) begin
          ck(req_ready === 1'b1, "a request was refused while a tag was free");
          ck(got_tag === pre_tag, "the tracker allocated a different tag than the model");
        end else if (req_valid) begin
          // ---- PROPERTY 2: no free tag means a stall, not a silent drop ----
          ck(req_stall === 1'b1, "the tracker neither accepted nor stalled a request");
          ck(req_ready === 1'b0, "the tracker accepted a request with no tag free");
        end

        // ---- now advance the model, completions first, exactly as the
        //      design's clocked process does ----
        if (cpl_valid && m_busy[pop_tag]) begin
          if (pop_bytes >= m_rem[pop_tag]) begin
            m_busy[pop_tag] = 1'b0; m_rem[pop_tag] = 16'd0;
            retired = retired + 1;
          end else begin
            m_rem[pop_tag] = m_rem[pop_tag] - pop_bytes;
          end
        end
        if (req_valid && pre_free) begin
          m_busy[pre_tag] = 1'b1;
          m_rem[pre_tag]  = 16'd64;
          fabric_push(pre_tag, 16'd64, now_c + lat);
          issued = issued + 1;
        end

        @(posedge clk); #1;

        // ---- PROPERTY 3: outstanding count is exact, every cycle ----
        //
        // Checked AFTER the edge. Before it, the design's combinational
        // count still describes the previous cycle while the model has
        // already advanced -- comparing across that boundary produced 1122
        // failures against a design and a model that agreed perfectly.
        ck(n_outstanding === m_out(0),
           "the tracker and the model disagree about how many tags are in flight");

        now_c = now_c + 1;
        guard = guard + 1;
        steps = steps + 1;
      end
      req_valid = 1'b0; cpl_valid = 1'b0;
      ck(retired == K_XACT, "the measurement did not retire every transaction");
      cycles = now_c;
    end
  endtask

  // ---------------------------------------------------------------
  //  Single-step helpers for the directed phases.
  // ---------------------------------------------------------------
  task automatic do_req(input posted, input [15:0] bytes, output [2:0] tg,
              output accepted);
    begin
      req_valid = 1'b1; req_posted = posted; req_bytes = bytes;
      #1;
      tg = req_tag;
      accepted = req_ready;
      if (!posted) begin
        if (m_free_any(tag_limit)) begin
          ck(req_ready === 1'b1, "a request was refused while a tag was free");
          ck(req_tag === m_free_tag(tag_limit), "wrong tag allocated");
        end else begin
          ck(req_stall === 1'b1, "no stall was raised with every tag busy");
        end
      end else begin
        // ---- PROPERTY 4: a posted request never consumes a tag ----
        //
        // This is why a PCIe write is fast and a PCIe read is not: the
        // write is finished when it is sent.
        ck(req_ready === 1'b1, "a posted request was refused");
        ck(req_stall === 1'b0, "a posted request was stalled by tag exhaustion");
      end
      @(posedge clk); #1;
      req_valid = 1'b0;
      if (!posted && accepted) begin
        m_busy[tg] = 1'b1; m_rem[tg] = bytes;
        c_alloc = c_alloc + 1;
      end
      if (posted)                 c_posted = c_posted + 1;
      if (!posted && !accepted)   c_stall  = c_stall + 1;
      ck(n_outstanding === m_out(0), "outstanding count wrong after a request");
      steps = steps + 1;
    end
  endtask

  task automatic do_cpl(input [2:0] t, input [15:0] bytes);
    logic e_known, e_last, e_over;
    logic [31:0] r0, p0, b0, o0;
    begin
      e_known = m_busy[t];
      e_last  = e_known && (bytes >= m_rem[t]);
      e_over  = e_known && (bytes >  m_rem[t]);
      r0 = n_retire; p0 = n_partial; b0 = n_bad_tag; o0 = n_over;

      cpl_valid = 1'b1; cpl_tag = t; cpl_bytes = bytes;
      #1;
      obs_retire = cpl_retire;
      // ---- PROPERTY 5: retire means the LAST byte arrived ----
      //
      // A tag freed on a partial completion could be reused while the rest
      // of the data was still in flight, and the next transaction would
      // then collect it.
      ck(cpl_retire === e_last,
         "retire does not mean the request was completely satisfied");
      @(posedge clk); #1;
      cpl_valid = 1'b0;

      if (!e_known)      c_bad     = c_bad + 1;
      else if (e_last)   c_retire  = c_retire + 1;
      else               c_partial = c_partial + 1;
      if (e_over)        c_over    = c_over + 1;

      if (!e_known) begin
        // ---- PROPERTY 6: a completion for a free tag is reported ----
        ck(n_bad_tag == b0 + 32'd1, "a completion for an unowned tag was not reported");
        ck(n_retire == r0, "an unowned completion retired something");
      end else if (e_last) begin
        m_busy[t] = 1'b0; m_rem[t] = 16'd0;
        ck(n_retire == r0 + 32'd1, "a finishing completion did not retire");
        ck(n_over == o0 + (e_over ? 32'd1 : 32'd0), "overrun miscounted");
      end else begin
        m_rem[t] = m_rem[t] - bytes;
        // ---- PROPERTY 7: a partial completion keeps the tag ----
        ck(n_partial == p0 + 32'd1, "a partial completion was not counted");
        ck(n_retire == r0, "a partial completion retired the tag");
      end
      ck(n_outstanding === m_out(0), "outstanding count wrong after a completion");
      steps = steps + 1;
    end
  endtask

  task automatic reset_all;
    int j;
    begin
      rst_n = 1'b0;
      req_valid = 1'b0; cpl_valid = 1'b0; req_posted = 1'b0;
      iss_valid = 1'b0; rsp_valid = 1'b0;
      @(posedge clk); @(posedge clk);
      rst_n = 1'b1;
      @(posedge clk); #1;
      for (j = 0; j < N_TAG; j = j + 1) begin m_busy[j] = 1'b0; m_rem[j] = 16'd0; end
      for (j = 0; j < N_EP;  j = j + 1) um_busy[j] = 1'b0;
      tag_limit = 4'd8;
    end
  endtask

  // ---- the USB side ----
  task automatic do_iss(input [1:0] ep);
    logic e_busy;
    logic [31:0] i0, f0;
    begin
      e_busy = um_busy[ep];
      i0 = u_n_issued; f0 = u_n_refused;
      iss_valid = 1'b1; iss_ep = ep;
      #1;
      // ---- PROPERTY 8: one outstanding transaction per endpoint ----
      //
      // The entire mechanism. There is no second slot to have, which is
      // why no USB packet carries a transaction identifier.
      ck(iss_ready === !e_busy, "the slot accepted a second transaction on one endpoint");
      ck(iss_busy  ===  e_busy, "busy was not reported for an occupied endpoint");
      ck(!(iss_ready && iss_busy), "ready and busy were both asserted");
      @(posedge clk); #1;
      iss_valid = 1'b0;
      if (!e_busy) begin
        um_busy[ep] = 1'b1;
        c_iss = c_iss + 1;
        ck(u_n_issued == i0 + 32'd1, "an accepted transaction was not counted");
      end else begin
        c_ref = c_ref + 1;
        ck(u_n_refused == f0 + 32'd1, "a refused transaction was not counted");
      end
      ck(u_n_out === um_out(0), "usb outstanding count wrong after an issue");
      steps = steps + 1;
    end
  endtask

  task automatic do_rsp(input [1:0] ep);
    logic e_busy;
    logic [31:0] a0, s0;
    begin
      e_busy = um_busy[ep];
      a0 = u_n_answered; s0 = u_n_spurious;
      rsp_valid = 1'b1; rsp_ep = ep;
      #1;
      // ---- PROPERTY 9: a response with nothing outstanding is spurious ----
      //
      // Detectable precisely BECAUSE there is only one thing it could have
      // been answering. With N tags in flight the same question needs a
      // tag field to answer at all.
      ck(rsp_match    ===  e_busy, "a response was not matched to its transaction");
      ck(rsp_spurious === !e_busy, "an unsolicited response was not flagged");
      @(posedge clk); #1;
      rsp_valid = 1'b0;
      if (e_busy) begin
        um_busy[ep] = 1'b0;
        c_ans = c_ans + 1;
        ck(u_n_answered == a0 + 32'd1, "an answered transaction was not counted");
      end else begin
        c_spur = c_spur + 1;
        ck(u_n_spurious == s0 + 32'd1, "a spurious response was not counted");
      end
      ck(u_n_out === um_out(0), "usb outstanding count wrong after a response");
      steps = steps + 1;
    end
  endtask

  // ---- results ----
  integer thr_cyc [0:8][0:8];   // cycles for (limit, latency)
  int lim, lat, k, j, c;
  logic [2:0] tg;
  logic acc;
  logic [31:0] snap;

  // exhaustive reach over (tag_limit 1..8) x (latency 1..8) = 64
  logic reach [0:63];
  int nr, ri;
  // and over the directed tracker state space, below
  logic reach_t [0:255];
  int nrt;

  initial begin
    for (ri = 0; ri < 64;  ri = ri + 1) reach[ri]   = 1'b0;
    for (ri = 0; ri < 256; ri = ri + 1) reach_t[ri] = 1'b0;
    seed = 32'd28004;

    reset_all;

    // =============================================================
    //  PHASE 1 (DIRECTED, EXHAUSTIVE) -- THE THROUGHPUT SURFACE.
    //
    //  Every tag limit from 1 to 8 against every fabric latency from 1 to
    //  8: 64 points, all reachable, both dimensions independent inputs.
    //
    //  The tag_limit = 1 row is the USB case. A USB host has exactly one
    //  transaction outstanding per endpoint, so whatever that row says
    //  about throughput is what USB can do regardless of link speed.
    // =============================================================
    for (lim = 1; lim <= 8; lim = lim + 1)
    for (lat = 1; lat <= 8; lat = lat + 1) begin
      measure(lim, lat, c);
      thr_cyc[lim][lat] = c;
      reach[(lim - 1) * 8 + (lat - 1)] = 1'b1;
    end

    // ---- PROPERTY 10: more tags never make it slower ----
    //
    // Monotonicity in the tag count. Stated as a property rather than
    // eyeballed off the table, because a tracker that leaked tags would
    // produce a table that still looked plausible.
    for (lat = 1; lat <= 8; lat = lat + 1)
      for (lim = 2; lim <= 8; lim = lim + 1)
        ck(thr_cyc[lim][lat] <= thr_cyc[lim-1][lat],
           "adding a tag made the transfer slower");

    // ---- PROPERTY 11: more latency never makes it faster ----
    for (lim = 1; lim <= 8; lim = lim + 1)
      for (lat = 2; lat <= 8; lat = lat + 1)
        ck(thr_cyc[lim][lat] >= thr_cyc[lim][lat-1],
           "adding latency made the transfer faster");

    // ---- PROPERTY 12: one tag costs the full latency, EXACTLY ----
    //
    // THE USB RESULT, as a closed form rather than an observation. With one
    // outstanding transaction nothing overlaps, so each takes (latency + 1)
    // cycles -- latency to come back, one to issue the next -- and the total
    // is exactly K*(latency+1). No amount of link bandwidth changes it.
    //
    // Asserted with == rather than >=. An inequality would also pass for a
    // design that was slower still, and the point of a closed form is that
    // it pins the number from both sides.
    for (lat = 1; lat <= 8; lat = lat + 1)
      ck(thr_cyc[1][lat] == K_XACT * (lat + 1),
         "one outstanding transaction did not cost exactly latency+1 cycles each");

    // ---- PROPERTY 13: latency+1 tags saturate the link, EXACTLY ----
    //
    // The other closed form, and the one that answers "how many tags do I
    // need?". With L+1 tags the requester issues one per cycle and the only
    // cost left is draining the last one, so the total is exactly K + L.
    //
    // It is latency+1 and not latency because of the one-cycle tag
    // turnaround: free_any reads the REGISTERED tag array, so a tag retired
    // this cycle is allocatable next cycle. That single cycle is the
    // difference between needing L tags and needing L+1.
    for (lat = 1; lat <= 8; lat = lat + 1)
      for (lim = lat + 1; lim <= 8; lim = lim + 1)
        ck(thr_cyc[lim][lat] == K_XACT + lat,
           "latency+1 tags did not saturate the link");

    // ---- PROPERTY 14: fewer than latency+1 tags CANNOT saturate ----
    //
    // The negative half, without which the property above would be
    // satisfied by a design that was always saturated regardless of tags --
    // and the whole chapter would have no result.
    for (lat = 2; lat <= 8; lat = lat + 1)
      for (lim = 1; lim <= lat; lim = lim + 1)
        ck(thr_cyc[lim][lat] > K_XACT + lat,
           "fewer tags than latency+1 saturated the link anyway");

    // =============================================================
    //  PHASE 2 (DIRECTED) -- COMPLETIONS OUT OF ORDER.
    //
    //  The case USB cannot produce. Four tags are allocated in order and
    //  completed in REVERSE, and every one must retire correctly.
    // =============================================================
    reset_all;
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd0, "first tag should be 0");
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd1, "second tag should be 1");
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd2, "third tag should be 2");
    do_req(1'b0, 16'd64, tg, acc); ck(tg === 3'd3, "fourth tag should be 3");
    ck(n_outstanding === 32'd4, "four requests should leave four tags in flight");
    do_cpl(3'd3, 16'd64);
    do_cpl(3'd1, 16'd64);
    do_cpl(3'd2, 16'd64);
    do_cpl(3'd0, 16'd64);
    ck(n_outstanding === 32'd0, "completing every tag should empty the tracker");
    ck(n_retire === 32'd4, "four completions should retire four requests");
    ck(n_bad_tag === 32'd0, "out-of-order completion reported a bad tag");

    // =============================================================
    //  PHASE 3 (DIRECTED, EXHAUSTIVE over every completion order)
    //
    //  Three tags, all 3! = 6 completion orders. Out-of-order is not one
    //  case, it is every permutation, and a tracker that happened to work
    //  for reverse order is not thereby correct for the rest.
    // =============================================================
    for (k = 0; k < 6; k = k + 1) begin : perm
      int a, b2, c2, t0;
      reset_all;
      do_req(1'b0, 16'd64, tg, acc);
      do_req(1'b0, 16'd64, tg, acc);
      do_req(1'b0, 16'd64, tg, acc);
      // the six orderings of {0,1,2}
      case (k)
        0: begin a = 0; b2 = 1; c2 = 2; end
        1: begin a = 0; b2 = 2; c2 = 1; end
        2: begin a = 1; b2 = 0; c2 = 2; end
        3: begin a = 1; b2 = 2; c2 = 0; end
        4: begin a = 2; b2 = 0; c2 = 1; end
        default: begin a = 2; b2 = 1; c2 = 0; end
      endcase
      do_cpl(a[2:0],  16'd64);
      do_cpl(b2[2:0], 16'd64);
      do_cpl(c2[2:0], 16'd64);
      ck(n_outstanding === 32'd0, "some completion order left a tag outstanding");
      ck(n_retire === 32'd3, "some completion order lost a retirement");
      ck(n_bad_tag === 32'd0, "some completion order was mistaken for a bad tag");
    end

    // =============================================================
    //  PHASE 4 (DIRECTED, EXHAUSTIVE over split shapes) -- SPLIT
    //  COMPLETIONS.
    //
    //  One request may be answered by several completions. The tag must
    //  stay outstanding until the LAST byte arrives -- freeing it early is
    //  the tag-reuse bug, and it would attribute the remaining data to
    //  whatever transaction took the tag next.
    //
    //  Every split of 256 bytes into equal pieces of 256, 128, 64, 32 and
    //  16 is swept, which is every shape a power-of-two payload can take.
    // =============================================================
    for (k = 0; k < 5; k = k + 1) begin : splits
      int piece, pieces, n;
      reset_all;
      piece  = 256 >> k;
      pieces = 256 / piece;
      do_req(1'b0, 16'd256, tg, acc);
      for (n = 0; n < pieces; n = n + 1) begin
        do_cpl(tg, piece[15:0]);
        if (n < pieces - 1) begin
          // ---- PROPERTY 13: a partially completed tag stays busy ----
          ck(n_outstanding === 32'd1,
             "a tag was freed before its last completion arrived");
          ck(obs_retire === 1'b0, "a partial completion claimed to retire");
        end
      end
      ck(n_outstanding === 32'd0, "the tag was not freed by its last completion");
      ck(n_partial === (pieces - 1),
         "the number of partial completions does not match the split");
    end

    // =============================================================
    //  PHASE 5 (DIRECTED) -- TAG EXHAUSTION IS BACK-PRESSURE.
    //
    //  With two tags, a third request must STALL rather than be dropped or
    //  accepted. That stall is why PCIe read throughput depends on tag
    //  count, and it is the mechanism phase 1 measures.
    // =============================================================
    reset_all;
    tag_limit = 4'd2;
    do_req(1'b0, 16'd64, tg, acc); ck(acc === 1'b1, "first of two tags refused");
    do_req(1'b0, 16'd64, tg, acc); ck(acc === 1'b1, "second of two tags refused");
    snap = n_stall;
    do_req(1'b0, 16'd64, tg, acc);
    ck(acc === 1'b0, "a third request was accepted with only two tags");
    ck(n_stall == snap + 32'd1, "tag exhaustion did not raise a stall");
    ck(n_outstanding === 32'd2, "a stalled request consumed a tag anyway");
    // ---- PROPERTY 14: a POSTED request is never stalled ----
    //
    // Even with every tag busy. A write needs no completion, so tag
    // exhaustion cannot block it -- which is exactly why mixing posted and
    // non-posted traffic changes a PCIe link's behaviour so much.
    snap = n_posted;
    do_req(1'b1, 16'd64, tg, acc);
    ck(acc === 1'b1, "a posted request was blocked by tag exhaustion");
    ck(n_posted == snap + 32'd1, "a posted request was not counted");
    ck(n_outstanding === 32'd2, "a posted request consumed a tag");
    // and freeing one tag lets the next non-posted request through
    do_cpl(3'd0, 16'd64);
    do_req(1'b0, 16'd64, tg, acc);
    ck(acc === 1'b1, "freeing a tag did not admit the next request");
    ck(tg === 3'd0, "the freed tag was not the one reused");

    // =============================================================
    //  PHASE 5b (DIRECTED, EXHAUSTIVE over every tag limit)
    //
    //  The posted-request guarantee is only OBSERVABLE when no tag is free,
    //  because with a tag free a posted request would be accepted either way.
    //  So the table is filled to capacity at each of the eight limits and the
    //  guarantee is checked there.
    //
    //  That is the difference between a property being stated and being
    //  tested: phase 5 demonstrated it once, which gave the mutation that
    //  breaks it a domain of one.
    // =============================================================
    for (k = 1; k <= 8; k = k + 1) begin : postedsweep
      int b;
      logic [31:0] psnap, osnap2;
      reset_all;
      tag_limit = k[3:0];
      for (b = 0; b < k; b = b + 1) begin
        do_req(1'b0, 16'd64, tg, acc);
        ck(acc === 1'b1, "a request was refused below the tag limit");
      end
      ck(n_outstanding === k, "filling to the limit did not use every tag");

      // a non-posted request must now stall
      do_req(1'b0, 16'd64, tg, acc);
      ck(acc === 1'b0, "a request was accepted above the tag limit");

      // ---- and a POSTED request must NOT ----
      //
      // This is why a PCIe write never blocks on read credit: it needs no
      // completion, so tag exhaustion cannot apply to it. A design that made
      // writes wait for tags would serialise a workload PCIe is specifically
      // built to overlap.
      psnap = n_posted; osnap2 = n_outstanding;
      do_req(1'b1, 16'd64, tg, acc);
      ck(acc === 1'b1, "a posted request was blocked by tag exhaustion");
      ck(n_posted == psnap + 32'd1, "a posted request was not counted");
      ck(n_outstanding === osnap2, "a posted request consumed a tag");
    end

    // =============================================================
    //  PHASE 6 (DIRECTED) -- MALFORMED COMPLETIONS.
    //
    //  A completion for a tag nobody owns, and a completion carrying more
    //  bytes than were asked for. Both are protocol errors and both must
    //  be REPORTED rather than absorbed -- an absorbed overrun would wrap
    //  the byte counter and leave the tag outstanding forever, turning a
    //  protocol error into a hang.
    // =============================================================
    reset_all;
    snap = n_bad_tag;
    do_cpl(3'd5, 16'd64);        // nobody owns tag 5
    ck(n_bad_tag == snap + 32'd1, "a completion for a free tag was accepted");
    ck(n_outstanding === 32'd0, "an unowned completion changed the tracker state");

    do_req(1'b0, 16'd64, tg, acc);
    snap = n_over;
    do_cpl(tg, 16'd128);         // twice what was asked for
    ck(n_over == snap + 32'd1, "an over-long completion was not reported");
    ck(n_outstanding === 32'd0, "an over-long completion left the tag outstanding");

    // =============================================================
    //  PHASE 6b (DIRECTED, EXHAUSTIVE over overrun shapes)
    //
    //  A completion carrying more bytes than were requested, for every
    //  request size and both interesting overrun amounts: one byte too many,
    //  and twice as many as asked for. Eight cases rather than the single
    //  one phase 6 had, because a mutation that stops reporting overruns
    //  should not be able to score 2.
    //
    //  In every case the tag must still RETIRE. Absorbing the overrun by
    //  wrapping the byte counter would leave the tag outstanding forever and
    //  turn a protocol error into a hang, which is strictly worse.
    // =============================================================
    for (k = 0; k < 8; k = k + 1) begin : overruns
      int want, extra;
      logic [31:0] osnap;
      reset_all;
      want  = 16 << (k % 4);
      extra = (k < 4) ? 1 : want;      // one byte too many, or double
      do_req(1'b0, want[15:0], tg, acc);
      osnap = n_over;
      do_cpl(tg, (want + extra));
      ck(n_over == osnap + 32'd1, "an over-long completion was not reported");
      ck(n_outstanding === 32'd0,
         "an over-long completion left the tag outstanding");
      ck(n_retire > 32'd0, "an over-long completion did not retire the tag");
    end

    // =============================================================
    //  PHASE 7 (DIRECTED, EXHAUSTIVE over the tracker's busy-mask)
    //
    //  Every one of the 2**8 = 256 combinations of which tags are busy,
    //  reached by allocating and completing rather than by forcing state.
    //  For each, the allocation decision is checked against the model.
    // =============================================================
    for (k = 0; k < 256; k = k + 1) begin : masks
      int b;
      reset_all;
      // allocate all eight, then complete the ones the mask says are free
      for (b = 0; b < 8; b = b + 1) do_req(1'b0, 16'd64, tg, acc);
      ck(n_outstanding === 32'd8, "eight requests did not fill eight tags");
      for (b = 0; b < 8; b = b + 1)
        if (!k[b]) do_cpl(b[2:0], 16'd64);
      // ---- a POSTED request, against every table state ----
      //
      // It must be accepted and must consume no tag, whatever else is in
      // flight -- including when every tag is busy. Swept here rather than
      // demonstrated once, because a property exercised a single time has a
      // mutation domain of one and tells you almost nothing.
      snap = n_outstanding;
      do_req(1'b1, 16'd64, tg, acc);
      ck(acc === 1'b1, "a posted request was refused");
      ck(n_outstanding === snap, "a posted request consumed a tag");

      // ---- a completion for a tag NOBODY OWNS, against every state ----
      //
      // Whichever tag is free, a completion for it is a protocol error and
      // must be reported rather than absorbed. With a full table this case
      // does not exist, so it is skipped rather than faked.
      if (m_free_any(4'd8)) begin : badcpl
        logic [2:0] ft;
        logic [31:0] bsnap, osnap;
        ft = m_free_tag(4'd8);
        bsnap = n_bad_tag; osnap = n_outstanding;
        do_cpl(ft, 16'd64);
        ck(n_bad_tag == bsnap + 32'd1,
           "a completion for an unowned tag was not reported");
        ck(n_outstanding === osnap,
           "a completion for an unowned tag changed the tracker state");
      end

      // Now exactly the tags set in k are busy. The next allocation must
      // pick the lowest free one, which the model computes independently.
      if (m_free_any(4'd8)) begin
        do_req(1'b0, 16'd64, tg, acc);
        ck(acc === 1'b1, "a request was refused with a free tag");
      end else begin
        do_req(1'b0, 16'd64, tg, acc);
        ck(acc === 1'b0, "a request was accepted with every tag busy");
      end
      reach_t[k] = 1'b1;
    end

    // =============================================================
    //  PHASE 8 (DIRECTED) -- THE USB SLOT.
    //
    //  One outstanding transaction per endpoint. The endpoints are
    //  independent, a second issue to a busy endpoint is refused, and a
    //  response with nothing outstanding is flagged.
    // =============================================================
    reset_all;
    for (k = 0; k < 4; k = k + 1) begin
      do_iss(k[1:0]);
      ck(u_n_out === (k + 1), "each endpoint should hold one transaction");
    end
    // every endpoint busy: a second issue to each must be refused
    for (k = 0; k < 4; k = k + 1) do_iss(k[1:0]);
    ck(u_n_refused === 32'd4, "four second-issues should all be refused");
    ck(u_n_out === 32'd4, "a refused issue changed the outstanding count");
    // answer them all
    for (k = 0; k < 4; k = k + 1) do_rsp(k[1:0]);
    ck(u_n_out === 32'd0, "answering every endpoint should empty the slots");
    // an unsolicited response
    snap = u_n_spurious;
    do_rsp(2'd2);
    ck(u_n_spurious == snap + 32'd1, "an unsolicited response was not flagged");

    // =============================================================
    //  PHASE 9 (DIRECTED, EXHAUSTIVE over the endpoint busy-mask)
    //
    //  All 2**4 = 16 combinations of which endpoints are busy, crossed
    //  with an issue and a response to each of the 4 endpoints. 16 x 4 x 2
    //  decisions, every one checked against the model.
    // =============================================================
    for (k = 0; k < 16; k = k + 1) begin : epmask
      int b;
      reset_all;
      for (b = 0; b < 4; b = b + 1) if (k[b]) do_iss(b[1:0]);
      for (b = 0; b < 4; b = b + 1) do_iss(b[1:0]);
      reset_all;
      for (b = 0; b < 4; b = b + 1) if (k[b]) do_iss(b[1:0]);
      for (b = 0; b < 4; b = b + 1) do_rsp(b[1:0]);
    end

    // =============================================================
    //  PHASE 10 (RANDOM) -- mixed traffic with a reordering fabric.
    // =============================================================
`ifndef DIRECTED_ONLY
    reset_all;
    fabric_clear;
    for (k = 0; k < 800; k = k + 1) begin : rnd
      logic pf; reg [2:0] pt; reg [15:0] pb;
      // a due completion, if any
      fabric_pop(now_c, pf, pt, pb);
      if (pf) do_cpl(pt, pb);
      // a request, sometimes posted
      if ((urand() % 3) != 0) begin
        if ((urand() % 5) == 0) begin
          do_req(1'b1, 16'd64, tg, acc);
        end else begin
          do_req(1'b0, 16'd64, tg, acc);
          // A random latency, so completions come back out of order --
          // which is the property the whole tracker exists for.
          if (acc) fabric_push(tg, 16'd64, now_c + 1 + (urand() % 9));
        end
      end
      // and some USB traffic on the side
      if ((urand() % 2) == 0) do_iss((urand() % 4));
      else                     do_rsp((urand() % 4));
      now_c = now_c + 1;
    end
    // drain whatever is still in flight, bounded
    for (k = 0; k < 400; k = k + 1) begin : drain
      logic pf2; reg [2:0] pt2; reg [15:0] pb2;
      fabric_pop(now_c + 100, pf2, pt2, pb2);
      if (pf2) do_cpl(pt2, pb2);
      now_c = now_c + 1;
    end
`endif

    nr = 0;  for (ri = 0; ri < 64;  ri = ri + 1) if (reach[ri])   nr  = nr + 1;
    nrt = 0; for (ri = 0; ri < 256; ri = ri + 1) if (reach_t[ri]) nrt = nrt + 1;

    $display("steps=%0d checks=%0d reach_thr=%0d/64 reach_tag=%0d/256 errors=%0d",
             steps, checks, nr, nrt, errors);
    $display("[pcie] alloc=%0d posted=%0d stalls=%0d retire=%0d partial=%0d bad_tag=%0d over=%0d",
             c_alloc, c_posted, c_stall, c_retire, c_partial, c_bad, c_over);
    $display("[usb]  issued=%0d refused=%0d answered=%0d spurious=%0d",
             c_iss, c_ref, c_ans, c_spur);
    $display("--- cycles to move %0d transactions, by tags outstanding x latency ---", K_XACT);
    $display("    tags      L=1     L=2     L=3     L=4     L=5     L=6     L=7     L=8");
    for (lim = 1; lim <= 8; lim = lim + 1)
      $display("    %4d %8d%8d%8d%8d%8d%8d%8d%8d", lim,
               thr_cyc[lim][1], thr_cyc[lim][2], thr_cyc[lim][3], thr_cyc[lim][4],
               thr_cyc[lim][5], thr_cyc[lim][6], thr_cyc[lim][7], thr_cyc[lim][8]);
    $display("    (row tags=1 IS the USB case: one transaction outstanding)");
    if (nr != 64 || nrt != 256) begin
      $display("FAIL: exhaustive sweep incomplete"); errors = errors + 1;
    end
    if (errors == 0) $display("PASS: 0 errors in %0d checks", checks);
    else             $display("FAIL: %0d errors in %0d checks", errors, checks);
    $finish;
  end

endmodule

Same seed and phase order as the Verilog bench, so any difference between those two mutation columns is a real difference between the designs. Their outputs are byte-identical, including the whole throughput surface.

10. VHDL-2008

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  OUTSTANDING TRANSACTIONS -- VHDL-2008.
--
--  CLASSIFICATION: simplified synthesisable teaching RTL.
--  Two entities, same hardware contract as the Verilog and SystemVerilog
--  files: same ports, same widths, same reset values, same cycle-by-cycle
--  behaviour.
--
--      "This response just arrived. Which request was it for?"
--
--  USB answers it by CONSTRUCTION: one transaction per endpoint, so the
--  response is the next thing on the wire and no USB packet carries a
--  transaction identifier at all.
--
--  PCIe answers it with a TAG. Many non-posted requests may be in flight,
--  completions come back OUT OF ORDER and may arrive SPLIT, so every
--  request carries a tag the requester must track until the last byte.
--
--  The difference is CONCURRENCY, not bandwidth:
--
--      USB:  one outstanding transaction  -> throughput <= 1 / latency
--      PCIe: N outstanding transactions   -> throughput <= min(1, N/latency)
--
--  Both combinational processes use `process (all)`. In a file that gets
--  mutated nine times that is not a convenience: a mutation adding a
--  branch that reads a new signal would otherwise need the sensitivity
--  list extended by hand, and forgetting produces a mutant that fails for
--  the wrong reason.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

package tg_pkg is
  -- Ceiling log2, for index widths. Prefixed so it cannot collide with a
  -- port name: a VHDL port shadows a same-named package object, and VHDL
  -- is case-insensitive, so the collision would be silent.
  function tg_clog2 (n : natural) return natural;
end package;

package body tg_pkg is
  function tg_clog2 (n : natural) return natural is
    variable r : natural := 0;
    variable v : natural := 1;
  begin
    while v < n loop
      v := v * 2;
      r := r + 1;
    end loop;
    if r = 0 then
      return 1;
    else
      return r;
    end if;
  end function;
end package body;


-- ---------------------------------------------------------------------
--  pcie_tag_tracker -- many in flight, matched by tag.
--
--  `tag_limit` is an INPUT rather than a generic so the testbench can
--  sweep how many tags the requester may use without re-elaborating. That
--  is what makes the throughput surface a single exhaustive sweep, and it
--  is also how the USB case is reached: tag_limit = 1.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.tg_pkg.all;

entity pcie_tag_tracker is
  generic (
    N_TAG     : natural := 8;
    MAX_BYTES : natural := 256
  );
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    -- How many tags the requester may use, 1..N_TAG. Zero is clamped to
    -- one: a requester allowed nothing outstanding can never make
    -- progress, and deadlocking silently is worse than refusing to.
    tag_limit : in std_logic_vector(tg_clog2(N_TAG + 1) - 1 downto 0);

    req_valid  : in std_logic;
    -- POSTED requests (writes) expect no completion and consume no tag.
    -- That is why a PCIe write is fast and a PCIe read is not.
    req_posted : in std_logic;
    req_bytes  : in std_logic_vector(15 downto 0);

    req_ready : out std_logic;
    req_tag   : out std_logic_vector(tg_clog2(N_TAG) - 1 downto 0);
    req_stall : out std_logic;

    cpl_valid : in std_logic;
    cpl_tag   : in std_logic_vector(tg_clog2(N_TAG) - 1 downto 0);
    cpl_bytes : in std_logic_vector(15 downto 0);

    cpl_retire : out std_logic;

    n_alloc       : out std_logic_vector(31 downto 0);
    n_posted      : out std_logic_vector(31 downto 0);
    n_stall       : out std_logic_vector(31 downto 0);
    n_retire      : out std_logic_vector(31 downto 0);
    n_partial     : out std_logic_vector(31 downto 0);
    n_bad_tag     : out std_logic_vector(31 downto 0);
    n_over        : out std_logic_vector(31 downto 0);
    n_outstanding : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of pcie_tag_tracker is
  constant TW : natural := tg_clog2(N_TAG);
  constant LW : natural := tg_clog2(N_TAG + 1);

  type busy_arr_t is array (0 to N_TAG - 1) of std_logic;
  type rem_arr_t  is array (0 to N_TAG - 1) of unsigned(15 downto 0);

  signal t_busy : busy_arr_t := (others => '0');
  signal t_rem  : rem_arr_t  := (others => (others => '0'));

  signal lim : unsigned(LW - 1 downto 0) := (others => '0');

  signal free_any : std_logic := '0';
  signal free_tag : unsigned(TW - 1 downto 0) := (others => '0');

  signal cpl_known   : std_logic := '0';
  signal rem_now     : unsigned(15 downto 0) := (others => '0');
  signal cpl_overrun : std_logic := '0';
  signal cpl_last    : std_logic := '0';

  signal alloc_c   : unsigned(31 downto 0) := (others => '0');
  signal posted_c  : unsigned(31 downto 0) := (others => '0');
  signal stall_c   : unsigned(31 downto 0) := (others => '0');
  signal retire_c  : unsigned(31 downto 0) := (others => '0');
  signal partial_c : unsigned(31 downto 0) := (others => '0');
  signal bad_c     : unsigned(31 downto 0) := (others => '0');
  signal over_c    : unsigned(31 downto 0) := (others => '0');

  signal out_now : unsigned(31 downto 0) := (others => '0');
begin

  lim <= to_unsigned(1, LW) when unsigned(tag_limit) = 0
         else unsigned(tag_limit);

  -- -------------------------------------------------------------------
  --  ALLOCATE -- the lowest free tag strictly below the limit.
  --
  --  Walking downwards so the lowest index wins. Real requesters often
  --  allocate round-robin; lowest-free is chosen here because it is
  --  deterministic, which is what makes the tag-reuse property checkable.
  -- -------------------------------------------------------------------
  alloc : process (all)
    variable fa : std_logic;
    variable ft : unsigned(TW - 1 downto 0);
  begin
    fa := '0';
    ft := (others => '0');
    for i in N_TAG - 1 downto 0 loop
      if t_busy(i) = '0' and to_unsigned(i, LW) < lim then
        fa := '1';
        ft := to_unsigned(i, TW);
      end if;
    end loop;
    free_any <= fa;
    free_tag <= ft;
  end process;

  -- A posted request needs no tag, so tag exhaustion never stalls it. This
  -- asymmetry is the whole reason PCIe separates the two classes.
  req_ready <= '1' when (req_valid = '1' and (req_posted = '1' or free_any = '1'))
               else '0';
  req_tag   <= std_logic_vector(free_tag);
  req_stall <= '1' when (req_valid = '1' and req_posted = '0' and free_any = '0')
               else '0';

  -- -------------------------------------------------------------------
  --  COMPLETE -- match by tag, subtract bytes, free on the last one.
  -- -------------------------------------------------------------------
  cpl_known <= '1' when (cpl_valid = '1' and t_busy(to_integer(unsigned(cpl_tag))) = '1')
               else '0';
  rem_now   <= t_rem(to_integer(unsigned(cpl_tag)));
  -- More bytes than were asked for. On a real link a malformed completion;
  -- flagged rather than absorbed, because absorbing it would wrap the byte
  -- counter, keep the tag outstanding forever, and turn a protocol error
  -- into a hang.
  cpl_overrun <= '1' when (cpl_known = '1' and unsigned(cpl_bytes) > rem_now) else '0';
  cpl_last    <= '1' when (cpl_known = '1' and unsigned(cpl_bytes) >= rem_now) else '0';

  cpl_retire <= cpl_last;

  n_alloc   <= std_logic_vector(alloc_c);
  n_posted  <= std_logic_vector(posted_c);
  n_stall   <= std_logic_vector(stall_c);
  n_retire  <= std_logic_vector(retire_c);
  n_partial <= std_logic_vector(partial_c);
  n_bad_tag <= std_logic_vector(bad_c);
  n_over    <= std_logic_vector(over_c);

  -- Combinational, so it reports the tracker as it stands rather than as it
  -- stood a cycle ago.
  occupancy : process (all)
    variable c : unsigned(31 downto 0);
  begin
    c := (others => '0');
    for i in 0 to N_TAG - 1 loop
      if t_busy(i) = '1' then c := c + 1; end if;
    end loop;
    out_now <= c;
  end process;
  n_outstanding <= std_logic_vector(out_now);

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      t_busy    <= (others => '0');
      t_rem     <= (others => (others => '0'));
      alloc_c   <= (others => '0');
      posted_c  <= (others => '0');
      stall_c   <= (others => '0');
      retire_c  <= (others => '0');
      partial_c <= (others => '0');
      bad_c     <= (others => '0');
      over_c    <= (others => '0');
    elsif rising_edge(clk) then

      -- ---- completions, then requests ----
      --
      -- The order of these two blocks is immaterial to correctness, and it
      -- is worth saying why. free_any and free_tag are combinational over
      -- the REGISTERED t_busy, so they describe the table as it stood at the
      -- START of this cycle. A tag retired by a completion in this cycle
      -- becomes allocatable in the NEXT one, not this one.
      --
      -- That is ONE CYCLE OF TURNAROUND per tag, with a visible consequence:
      -- saturating a fabric of latency L needs L+1 tags, not L.
      --
      -- The blocks cannot collide, because an index being retired is busy
      -- and is therefore never the index free_any selects.
      if cpl_valid = '1' then
        if t_busy(to_integer(unsigned(cpl_tag))) = '0' then
          -- A completion for a tag nobody owns. Either the fabric invented
          -- it or this requester retired the tag early -- and the second is
          -- exactly what a tag-reuse bug looks like from here.
          bad_c <= bad_c + 1;
        else
          if unsigned(cpl_bytes) >= rem_now then
            t_busy(to_integer(unsigned(cpl_tag))) <= '0';
            t_rem(to_integer(unsigned(cpl_tag)))  <= (others => '0');
            retire_c <= retire_c + 1;
            if unsigned(cpl_bytes) > rem_now then
              over_c <= over_c + 1;
            end if;
          else
            -- A split completion: the request is not finished, so the tag
            -- stays outstanding. Freeing it here would let the tag be reused
            -- while the rest of the data was still in flight.
            t_rem(to_integer(unsigned(cpl_tag))) <= rem_now - unsigned(cpl_bytes);
            partial_c <= partial_c + 1;
          end if;
        end if;
      end if;

      if req_valid = '1' then
        if req_posted = '1' then
          posted_c <= posted_c + 1;
        elsif free_any = '1' then
          -- free_tag was chosen from the table as it stood at the start of
          -- the cycle, so it is not an index any completion is retiring now.
          t_busy(to_integer(free_tag)) <= '1';
          t_rem(to_integer(free_tag))  <= unsigned(req_bytes);
          alloc_c <= alloc_c + 1;
        else
          stall_c <= stall_c + 1;
        end if;
      end if;
    end if;
  end process;

end architecture;


-- ---------------------------------------------------------------------
--  usb_xact_slot -- one in flight, matched by nothing.
--
--  The same question, answered by construction. A USB host issues one
--  transaction to an endpoint and waits; the response is the next thing on
--  the wire. So there is no tag, no reordering, no split-completion
--  reassembly -- and no tag-reuse bug to have.
--
--  What it costs is visible in the port list by OMISSION: there is no way
--  to have a second transaction outstanding, so throughput is 1 / latency
--  and no amount of link bandwidth changes that.
-- ---------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.tg_pkg.all;

entity usb_xact_slot is
  generic (
    N_EP : natural := 4
  );
  port (
    clk   : in std_logic;
    rst_n : in std_logic;

    iss_valid : in std_logic;
    iss_ep    : in std_logic_vector(tg_clog2(N_EP) - 1 downto 0);

    iss_ready : out std_logic;
    iss_busy  : out std_logic;

    rsp_valid : in std_logic;
    rsp_ep    : in std_logic_vector(tg_clog2(N_EP) - 1 downto 0);

    rsp_match    : out std_logic;
    rsp_spurious : out std_logic;

    n_issued      : out std_logic_vector(31 downto 0);
    n_refused     : out std_logic_vector(31 downto 0);
    n_answered    : out std_logic_vector(31 downto 0);
    n_spurious    : out std_logic_vector(31 downto 0);
    n_outstanding : out std_logic_vector(31 downto 0)
  );
end entity;

architecture rtl of usb_xact_slot is
  type ep_arr_t is array (0 to N_EP - 1) of std_logic;

  -- One bit per endpoint. That is the entire mechanism -- compare it with
  -- the tag array above, which needs a byte counter per entry because a
  -- completion can be partial.
  signal ep_busy : ep_arr_t := (others => '0');

  signal iss_c  : unsigned(31 downto 0) := (others => '0');
  signal ref_c  : unsigned(31 downto 0) := (others => '0');
  signal ans_c  : unsigned(31 downto 0) := (others => '0');
  signal spur_c : unsigned(31 downto 0) := (others => '0');

  signal out_now : unsigned(31 downto 0) := (others => '0');
begin

  iss_ready <= '1' when (iss_valid = '1' and ep_busy(to_integer(unsigned(iss_ep))) = '0')
               else '0';
  iss_busy  <= '1' when (iss_valid = '1' and ep_busy(to_integer(unsigned(iss_ep))) = '1')
               else '0';

  -- A response with nothing outstanding on that endpoint. On a real bus this
  -- is a device talking when it was not asked, and it is detectable
  -- precisely because there is only one thing it could have been answering.
  rsp_match    <= '1' when (rsp_valid = '1' and ep_busy(to_integer(unsigned(rsp_ep))) = '1')
                  else '0';
  rsp_spurious <= '1' when (rsp_valid = '1' and ep_busy(to_integer(unsigned(rsp_ep))) = '0')
                  else '0';

  n_issued   <= std_logic_vector(iss_c);
  n_refused  <= std_logic_vector(ref_c);
  n_answered <= std_logic_vector(ans_c);
  n_spurious <= std_logic_vector(spur_c);

  occupancy : process (all)
    variable c : unsigned(31 downto 0);
  begin
    c := (others => '0');
    for i in 0 to N_EP - 1 loop
      if ep_busy(i) = '1' then c := c + 1; end if;
    end loop;
    out_now <= c;
  end process;
  n_outstanding <= std_logic_vector(out_now);

  process (clk, rst_n)
  begin
    if rst_n = '0' then
      ep_busy <= (others => '0');
      iss_c   <= (others => '0');
      ref_c   <= (others => '0');
      ans_c   <= (others => '0');
      spur_c  <= (others => '0');
    elsif rising_edge(clk) then
      -- Responses first, for the same reason as the tracker: the decision
      -- signals are combinational over the REGISTERED busy bits, so an
      -- endpoint answered this cycle is reissuable next cycle.
      if rsp_valid = '1' then
        if ep_busy(to_integer(unsigned(rsp_ep))) = '1' then
          ep_busy(to_integer(unsigned(rsp_ep))) <= '0';
          ans_c <= ans_c + 1;
        else
          spur_c <= spur_c + 1;
        end if;
      end if;

      if iss_valid = '1' then
        if ep_busy(to_integer(unsigned(iss_ep))) = '0' then
          ep_busy(to_integer(unsigned(iss_ep))) <= '1';
          iss_c <= iss_c + 1;
        else
          ref_c <= ref_c + 1;
        end if;
      end if;
    end if;
  end process;

end architecture;

The VHDL testbench

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- =====================================================================
--  Testbench for pcie_tag_tracker and usb_xact_slot -- VHDL-2008.
--
--  THE HEADLINE IS A THROUGHPUT SURFACE, NOT A PASS/FAIL.
--
--  Both designs answer "which response belongs to which request?". The
--  interesting question is what each answer COSTS, and the cost is
--  concurrency: how many transactions can be in flight at once.
--
--  So the bench contains a FABRIC MODEL with a settable latency, issues
--  requests as fast as the tracker will take them, and measures how many
--  cycles it takes to retire a fixed number. Sweeping (tag_limit, latency)
--  gives a surface, and the tag_limit = 1 row of that surface IS the USB
--  behaviour, because a USB host has exactly one transaction outstanding
--  per endpoint by construction.
--
--  THE SHADOW MODEL IS FORMULATED IN THE OPPOSITE DIRECTION. The tracker
--  allocates by walking its tags DOWNWARDS so the lowest free index wins;
--  the model walks UPWARDS and stops at the first free one.
--
--  THIS IS THE INDEPENDENT BENCH. The directed phases are structurally
--  identical to the Verilog and SystemVerilog benches, so the DIRECTED
--  mutation columns must agree EXACTLY and any disagreement is a real
--  finding. The random phase uses a VHDL-native generator.
-- =====================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.textio.all;

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

architecture sim of tb_tg_vhdl is

  constant N_TAG : natural := 8;
  constant N_EP  : natural := 4;
  constant MAXF  : natural := 64;

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

  signal tag_limit  : std_logic_vector(3 downto 0) := "1000";
  signal req_valid  : std_logic := '0';
  signal req_posted : std_logic := '0';
  signal req_bytes  : std_logic_vector(15 downto 0) := (others => '0');
  signal req_ready  : std_logic;
  signal req_tag    : std_logic_vector(2 downto 0);
  signal req_stall  : std_logic;
  signal cpl_valid  : std_logic := '0';
  signal cpl_tag    : std_logic_vector(2 downto 0) := "000";
  signal cpl_bytes  : std_logic_vector(15 downto 0) := (others => '0');
  signal cpl_retire : std_logic;
  signal n_alloc, n_posted, n_stall, n_retire, n_partial,
         n_bad_tag, n_over, n_outstanding : std_logic_vector(31 downto 0);

  signal iss_valid : std_logic := '0';
  signal iss_ep    : std_logic_vector(1 downto 0) := "00";
  signal iss_ready : std_logic;
  signal iss_busy  : std_logic;
  signal rsp_valid : std_logic := '0';
  signal rsp_ep    : std_logic_vector(1 downto 0) := "00";
  signal rsp_match : std_logic;
  signal rsp_spurious : std_logic;
  signal u_n_issued, u_n_refused, u_n_answered, u_n_spurious, u_n_out
         : std_logic_vector(31 downto 0);

begin

  dut : entity work.pcie_tag_tracker
    generic map (N_TAG => N_TAG)
    port map (
      clk => clk, rst_n => rst_n, tag_limit => tag_limit,
      req_valid => req_valid, req_posted => req_posted, req_bytes => req_bytes,
      req_ready => req_ready, req_tag => req_tag, req_stall => req_stall,
      cpl_valid => cpl_valid, cpl_tag => cpl_tag, cpl_bytes => cpl_bytes,
      cpl_retire => cpl_retire,
      n_alloc => n_alloc, n_posted => n_posted, n_stall => n_stall,
      n_retire => n_retire, n_partial => n_partial, n_bad_tag => n_bad_tag,
      n_over => n_over, n_outstanding => n_outstanding
    );

  udut : entity work.usb_xact_slot
    generic map (N_EP => N_EP)
    port map (
      clk => clk, rst_n => rst_n,
      iss_valid => iss_valid, iss_ep => iss_ep,
      iss_ready => iss_ready, iss_busy => iss_busy,
      rsp_valid => rsp_valid, rsp_ep => rsp_ep,
      rsp_match => rsp_match, rsp_spurious => rsp_spurious,
      n_issued => u_n_issued, n_refused => u_n_refused,
      n_answered => u_n_answered, n_spurious => u_n_spurious,
      n_outstanding => u_n_out
    );

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

  main : process

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

    constant K_XACT : integer := 24;

    -- ---- what the design said, sampled at a DEFINED instant ----
    --
    -- cpl_retire is only meaningful while cpl_valid is asserted. A check
    -- placed after the procedure returns reads it where the answer is a
    -- delta-cycle artefact.
    variable obs_retire : std_logic := '0';

    -- ---- the shadow tracker, maintained by the bench ----
    type mbusy_t is array (0 to N_TAG - 1) of std_logic;
    type mrem_t  is array (0 to N_TAG - 1) of integer;
    variable m_busy : mbusy_t := (others => '0');
    variable m_rem  : mrem_t  := (others => 0);
    type umbusy_t is array (0 to N_EP - 1) of std_logic;
    variable um_busy : umbusy_t := (others => '0');

    -- ---- cumulative across resets ----
    --
    -- The DUTs' own counters are zeroed by every reset_all, so reading them
    -- in the final summary would report only the last phase.
    variable c_alloc, c_posted, c_stall, c_retire, c_partial, c_bad, c_over
             : integer := 0;
    variable c_iss, c_ref, c_ans, c_spur : integer := 0;

    -- ---- the fabric: completions in flight, each with a due cycle ----
    type fact_t  is array (0 to MAXF - 1) of boolean;
    type ftag_t  is array (0 to MAXF - 1) of integer;
    variable f_act   : fact_t := (others => false);
    variable f_tag   : ftag_t := (others => 0);
    variable f_bytes : ftag_t := (others => 0);
    variable f_due   : ftag_t := (others => 0);
    variable now_c   : integer := 0;

    type thr_t is array (0 to 8, 0 to 8) of integer;
    variable thr_cyc : thr_t := (others => (others => 0));

    type reach_t  is array (0 to 63) of boolean;
    type reacht_t is array (0 to 255) of boolean;
    variable reach   : reach_t  := (others => false);
    variable reach_tag : reacht_t := (others => false);
    variable nr, nrt : integer := 0;

    variable rnd_state : unsigned(31 downto 0) := x"000C06D4";
    impure function urand return integer is
    begin
      rnd_state := resize(rnd_state * to_unsigned(1103515245, 32), 32)
                   + to_unsigned(12345, 32);
      -- The HIGH bits. In an LCG with a power-of-two modulus bit i has
      -- period 2**(i+1), so `urand mod 4` off the low bits cycles
      -- 3,0,1,2,... in lockstep while its histogram stays perfectly uniform.
      return to_integer(rnd_state(30 downto 15));
    end function;

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

    -- Upward-and-stop, the opposite of the design's downward walk.
    impure function m_free_any (lim : integer) return boolean is
      variable f : boolean := false;
    begin
      for j in 0 to N_TAG - 1 loop
        if m_busy(j) = '0' and j < lim and not f then f := true; end if;
      end loop;
      return f;
    end function;

    impure function m_free_tag (lim : integer) return integer is
      variable f : boolean := false;
      variable t : integer := 0;
    begin
      for j in 0 to N_TAG - 1 loop
        if m_busy(j) = '0' and j < lim and not f then t := j; f := true; end if;
      end loop;
      return t;
    end function;

    impure function m_out return integer is
      variable c : integer := 0;
    begin
      for j in 0 to N_TAG - 1 loop
        if m_busy(j) = '1' then c := c + 1; end if;
      end loop;
      return c;
    end function;

    impure function um_out return integer is
      variable c : integer := 0;
    begin
      for j in 0 to N_EP - 1 loop
        if um_busy(j) = '1' then c := c + 1; end if;
      end loop;
      return c;
    end function;

    procedure fabric_clear is
    begin
      f_act := (others => false);
      now_c := 0;
    end procedure;

    procedure fabric_push (t, b, due : integer) is
      variable placed : boolean := false;
    begin
      for j in 0 to MAXF - 1 loop
        if not f_act(j) and not placed then
          f_act(j) := true; f_tag(j) := t; f_bytes(j) := b; f_due(j) := due;
          placed := true;
        end if;
      end loop;
      -- A full fabric would silently drop a completion and the tag would stay
      -- outstanding forever, which reads as a design hang. Bound it.
      ck(placed, "TEST BUG: the fabric model overflowed");
    end procedure;

    -- Pick the earliest-due active completion at or before `nw`. Only ONE per
    -- cycle, because completions are serialised on a real link.
    procedure fabric_pop (nw : integer; found : out boolean;
                          t : out integer; b : out integer) is
      variable best, bestdue : integer;
    begin
      best := -1; bestdue := 0; found := false; t := 0; b := 0;
      for j in 0 to MAXF - 1 loop
        if f_act(j) and f_due(j) <= nw then
          if best < 0 or f_due(j) < bestdue then
            best := j; bestdue := f_due(j);
          end if;
        end if;
      end loop;
      if best >= 0 then
        found := true; t := f_tag(best); b := f_bytes(best);
        f_act(best) := false;
      end if;
    end procedure;

    procedure reset_all is
    begin
      rst_n <= '0';
      req_valid <= '0'; cpl_valid <= '0'; req_posted <= '0';
      iss_valid <= '0'; rsp_valid <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      wait until rising_edge(clk);
      wait for 1 ns;
      m_busy  := (others => '0');
      m_rem   := (others => 0);
      um_busy := (others => '0');
      tag_limit <= "1000";
    end procedure;

    -- ---------------------------------------------------------------
    --  THE MEASUREMENT.
    -- ---------------------------------------------------------------
    procedure measure (lim, lat : integer; cycles : out integer) is
      variable issued, retired, guard : integer;
      variable pop_found : boolean;
      variable pop_tag, pop_bytes : integer;
      variable got_tag : integer;
      variable pre_free : boolean;
      variable pre_tag  : integer;
    begin
      rst_n <= '0';
      req_valid <= '0'; cpl_valid <= '0'; req_posted <= '0';
      wait until rising_edge(clk);
      wait until rising_edge(clk);
      rst_n <= '1';
      wait until rising_edge(clk);
      wait for 1 ns;
      m_busy := (others => '0');
      m_rem  := (others => 0);
      fabric_clear;

      tag_limit <= std_logic_vector(to_unsigned(lim, 4));
      issued := 0; retired := 0; guard := 0;

      -- Every wait loop is bounded. An unbounded one turns a design hang
      -- into a test that never finishes, which is strictly worse.
      while retired < K_XACT and guard < 20000 loop
        fabric_pop(now_c, pop_found, pop_tag, pop_bytes);
        if pop_found then cpl_valid <= '1'; else cpl_valid <= '0'; end if;
        cpl_tag   <= std_logic_vector(to_unsigned(pop_tag, 3));
        cpl_bytes <= std_logic_vector(to_unsigned(pop_bytes, 16));

        if issued < K_XACT then req_valid <= '1'; else req_valid <= '0'; end if;
        req_posted <= '0';
        req_bytes  <= std_logic_vector(to_unsigned(64, 16));

        wait for 1 ns;
        got_tag := to_integer(unsigned(req_tag));

        -- The allocation decision is taken from the table as it stood at the
        -- START of this cycle, because the design's free_any is combinational
        -- over the REGISTERED tag array. A tag retired by the completion
        -- being presented right now is not allocatable until next cycle --
        -- one cycle of turnaround per tag, which is why saturating a fabric
        -- of latency L needs L+1 tags.
        pre_free := m_free_any(lim);
        pre_tag  := m_free_tag(lim);

        -- ---- PROPERTY 1: the tracker and the model pick the same tag ----
        if issued < K_XACT and pre_free then
          ck(req_ready = '1', "a request was refused while a tag was free");
          ck(got_tag = pre_tag, "the tracker allocated a different tag than the model");
        elsif issued < K_XACT then
          -- ---- PROPERTY 2: no free tag means a stall, not a silent drop ----
          ck(req_stall = '1', "the tracker neither accepted nor stalled a request");
          ck(req_ready = '0', "the tracker accepted a request with no tag free");
        end if;

        -- ---- advance the model, completions first, as the design does ----
        if pop_found and m_busy(pop_tag) = '1' then
          if pop_bytes >= m_rem(pop_tag) then
            m_busy(pop_tag) := '0'; m_rem(pop_tag) := 0;
            retired := retired + 1;
          else
            m_rem(pop_tag) := m_rem(pop_tag) - pop_bytes;
          end if;
        end if;
        if issued < K_XACT and pre_free then
          m_busy(pre_tag) := '1';
          m_rem(pre_tag)  := 64;
          fabric_push(pre_tag, 64, now_c + lat);
          issued := issued + 1;
        end if;

        wait until rising_edge(clk);
        wait for 1 ns;

        -- ---- PROPERTY 3: outstanding count is exact, every cycle ----
        --
        -- Checked AFTER the edge. Before it, the design's combinational count
        -- still describes the previous cycle while the model has already
        -- advanced, and comparing across that boundary compares two
        -- different instants.
        ck(to_integer(unsigned(n_outstanding)) = m_out,
           "the tracker and the model disagree about how many tags are in flight");

        now_c := now_c + 1;
        guard := guard + 1;
        steps := steps + 1;
      end loop;
      req_valid <= '0'; cpl_valid <= '0';
      ck(retired = K_XACT, "the measurement did not retire every transaction");
      cycles := now_c;
    end procedure;

    procedure do_req (posted : boolean; bytes : integer;
                      tg : out integer; accepted : out boolean) is
    begin
      req_valid <= '1';
      if posted then req_posted <= '1'; else req_posted <= '0'; end if;
      req_bytes <= std_logic_vector(to_unsigned(bytes, 16));
      wait for 1 ns;
      tg := to_integer(unsigned(req_tag));
      accepted := (req_ready = '1');
      if not posted then
        if m_free_any(to_integer(unsigned(tag_limit))) then
          ck(req_ready = '1', "a request was refused while a tag was free");
          ck(to_integer(unsigned(req_tag)) = m_free_tag(to_integer(unsigned(tag_limit))),
             "wrong tag allocated");
        else
          ck(req_stall = '1', "no stall was raised with every tag busy");
        end if;
      else
        -- ---- PROPERTY 4: a posted request never consumes a tag ----
        ck(req_ready = '1', "a posted request was refused");
        ck(req_stall = '0', "a posted request was stalled by tag exhaustion");
      end if;
      wait until rising_edge(clk);
      wait for 1 ns;
      req_valid <= '0';
      if (not posted) and accepted then
        m_busy(tg) := '1'; m_rem(tg) := bytes;
        c_alloc := c_alloc + 1;
      end if;
      if posted then c_posted := c_posted + 1; end if;
      if (not posted) and (not accepted) then c_stall := c_stall + 1; end if;
      ck(to_integer(unsigned(n_outstanding)) = m_out,
         "outstanding count wrong after a request");
      steps := steps + 1;
    end procedure;

    procedure do_cpl (t : integer; bytes : integer) is
      variable e_known, e_last, e_over : boolean;
      variable r0, p0, b0, o0 : integer;
    begin
      e_known := (m_busy(t) = '1');
      e_last  := e_known and (bytes >= m_rem(t));
      e_over  := e_known and (bytes >  m_rem(t));
      r0 := to_integer(unsigned(n_retire));
      p0 := to_integer(unsigned(n_partial));
      b0 := to_integer(unsigned(n_bad_tag));
      o0 := to_integer(unsigned(n_over));

      cpl_valid <= '1';
      cpl_tag   <= std_logic_vector(to_unsigned(t, 3));
      cpl_bytes <= std_logic_vector(to_unsigned(bytes, 16));
      wait for 1 ns;
      obs_retire := cpl_retire;
      -- ---- PROPERTY 5: retire means the LAST byte arrived ----
      ck((cpl_retire = '1') = e_last,
         "retire does not mean the request was completely satisfied");
      wait until rising_edge(clk);
      wait for 1 ns;
      cpl_valid <= '0';

      if not e_known then c_bad := c_bad + 1;
      elsif e_last     then c_retire := c_retire + 1;
      else                  c_partial := c_partial + 1; end if;
      if e_over then c_over := c_over + 1; end if;

      if not e_known then
        -- ---- PROPERTY 6: a completion for a free tag is reported ----
        ck(to_integer(unsigned(n_bad_tag)) = b0 + 1,
           "a completion for an unowned tag was not reported");
        ck(to_integer(unsigned(n_retire)) = r0, "an unowned completion retired something");
      elsif e_last then
        m_busy(t) := '0'; m_rem(t) := 0;
        ck(to_integer(unsigned(n_retire)) = r0 + 1, "a finishing completion did not retire");
        if e_over then
          ck(to_integer(unsigned(n_over)) = o0 + 1, "overrun miscounted");
        else
          ck(to_integer(unsigned(n_over)) = o0, "overrun miscounted");
        end if;
      else
        m_rem(t) := m_rem(t) - bytes;
        -- ---- PROPERTY 7: a partial completion keeps the tag ----
        ck(to_integer(unsigned(n_partial)) = p0 + 1, "a partial completion was not counted");
        ck(to_integer(unsigned(n_retire)) = r0, "a partial completion retired the tag");
      end if;
      ck(to_integer(unsigned(n_outstanding)) = m_out,
         "outstanding count wrong after a completion");
      steps := steps + 1;
    end procedure;

    procedure do_iss (ep : integer) is
      variable e_busy : boolean;
      variable i0, f0 : integer;
    begin
      e_busy := (um_busy(ep) = '1');
      i0 := to_integer(unsigned(u_n_issued));
      f0 := to_integer(unsigned(u_n_refused));
      iss_valid <= '1';
      iss_ep    <= std_logic_vector(to_unsigned(ep, 2));
      wait for 1 ns;
      -- ---- PROPERTY 8: one outstanding transaction per endpoint ----
      ck((iss_ready = '1') = (not e_busy),
         "the slot accepted a second transaction on one endpoint");
      ck((iss_busy = '1') = e_busy, "busy was not reported for an occupied endpoint");
      ck(not (iss_ready = '1' and iss_busy = '1'), "ready and busy were both asserted");
      wait until rising_edge(clk);
      wait for 1 ns;
      iss_valid <= '0';
      if not e_busy then
        um_busy(ep) := '1';
        c_iss := c_iss + 1;
        ck(to_integer(unsigned(u_n_issued)) = i0 + 1,
           "an accepted transaction was not counted");
      else
        c_ref := c_ref + 1;
        ck(to_integer(unsigned(u_n_refused)) = f0 + 1,
           "a refused transaction was not counted");
      end if;
      ck(to_integer(unsigned(u_n_out)) = um_out,
         "usb outstanding count wrong after an issue");
      steps := steps + 1;
    end procedure;

    procedure do_rsp (ep : integer) is
      variable e_busy : boolean;
      variable a0, s0 : integer;
    begin
      e_busy := (um_busy(ep) = '1');
      a0 := to_integer(unsigned(u_n_answered));
      s0 := to_integer(unsigned(u_n_spurious));
      rsp_valid <= '1';
      rsp_ep    <= std_logic_vector(to_unsigned(ep, 2));
      wait for 1 ns;
      -- ---- PROPERTY 9: a response with nothing outstanding is spurious ----
      ck((rsp_match = '1') = e_busy, "a response was not matched to its transaction");
      ck((rsp_spurious = '1') = (not e_busy), "an unsolicited response was not flagged");
      wait until rising_edge(clk);
      wait for 1 ns;
      rsp_valid <= '0';
      if e_busy then
        um_busy(ep) := '0';
        c_ans := c_ans + 1;
        ck(to_integer(unsigned(u_n_answered)) = a0 + 1,
           "an answered transaction was not counted");
      else
        c_spur := c_spur + 1;
        ck(to_integer(unsigned(u_n_spurious)) = s0 + 1,
           "a spurious response was not counted");
      end if;
      ck(to_integer(unsigned(u_n_out)) = um_out,
         "usb outstanding count wrong after a response");
      steps := steps + 1;
    end procedure;

    variable c, tg : integer;
    variable acc : boolean;
    variable snap : integer;
    -- Hoisted to the process declarative region. VHDL has no inline
    -- `declare` block inside a process body, so per-iteration locals live
    -- here with distinct names -- distinct on purpose: two phases sharing one
    -- index variable is exactly the bug that made an earlier chapter in this
    -- track report 1 babble cutoff where there were 4.
    variable pa, pb2v, pc2 : integer;          -- phase 3, permutation
    variable piece, pieces : integer;          -- phase 4, split shapes
    variable psnap, osnap2 : integer;          -- phase 5b, posted sweep
    variable want, extra, osnap : integer;     -- phase 6b, overrun shapes
    variable ft, bsnap, osnap3 : integer;      -- phase 7, unowned completion
    variable rpf, rpf2 : boolean;              -- phase 10, fabric
    variable rpt, rpb, rpt2, rpb2 : integer;
    variable kmask : std_logic_vector(7 downto 0);
    variable emask : std_logic_vector(3 downto 0);

  begin
    reset_all;

    -- ===============================================================
    --  PHASE 1 (DIRECTED, EXHAUSTIVE) -- THE THROUGHPUT SURFACE.
    --
    --  Every tag limit 1..8 against every fabric latency 1..8: 64 points,
    --  all reachable, both dimensions independent inputs.
    --
    --  The tag_limit = 1 row is the USB case.
    -- ===============================================================
    for lim in 1 to 8 loop
      for lat in 1 to 8 loop
        measure(lim, lat, c);
        thr_cyc(lim, lat) := c;
        reach((lim - 1) * 8 + (lat - 1)) := true;
      end loop;
    end loop;

    -- ---- PROPERTY 10: more tags never make it slower ----
    for lat in 1 to 8 loop
      for lim in 2 to 8 loop
        ck(thr_cyc(lim, lat) <= thr_cyc(lim - 1, lat),
           "adding a tag made the transfer slower");
      end loop;
    end loop;

    -- ---- PROPERTY 11: more latency never makes it faster ----
    for lim in 1 to 8 loop
      for lat in 2 to 8 loop
        ck(thr_cyc(lim, lat) >= thr_cyc(lim, lat - 1),
           "adding latency made the transfer faster");
      end loop;
    end loop;

    -- ---- PROPERTY 12: one tag costs the full latency, EXACTLY ----
    --
    -- THE USB RESULT as a closed form. With one outstanding transaction
    -- nothing overlaps, so each costs (latency + 1) cycles and the total is
    -- exactly K*(latency+1). Asserted with = rather than >=, because an
    -- inequality would also pass for a design that was slower still.
    for lat in 1 to 8 loop
      ck(thr_cyc(1, lat) = K_XACT * (lat + 1),
         "one outstanding transaction did not cost exactly latency+1 cycles each");
    end loop;

    -- ---- PROPERTY 13: latency+1 tags saturate the link, EXACTLY ----
    --
    -- It is latency+1 and not latency because of the one-cycle tag
    -- turnaround: free_any reads the REGISTERED tag array, so a tag retired
    -- this cycle is allocatable next cycle.
    for lat in 1 to 8 loop
      for lim in lat + 1 to 8 loop
        ck(thr_cyc(lim, lat) = K_XACT + lat, "latency+1 tags did not saturate the link");
      end loop;
    end loop;

    -- ---- PROPERTY 14: fewer than latency+1 tags CANNOT saturate ----
    --
    -- The negative half. Without it, property 13 would be satisfied by a
    -- design that was always saturated regardless of tags, and the chapter
    -- would have no result.
    for lat in 2 to 8 loop
      for lim in 1 to lat loop
        ck(thr_cyc(lim, lat) > K_XACT + lat,
           "fewer tags than latency+1 saturated the link anyway");
      end loop;
    end loop;

    -- ===============================================================
    --  PHASE 2 (DIRECTED) -- COMPLETIONS OUT OF ORDER.
    -- ===============================================================
    reset_all;
    do_req(false, 64, tg, acc); ck(tg = 0, "first tag should be 0");
    do_req(false, 64, tg, acc); ck(tg = 1, "second tag should be 1");
    do_req(false, 64, tg, acc); ck(tg = 2, "third tag should be 2");
    do_req(false, 64, tg, acc); ck(tg = 3, "fourth tag should be 3");
    ck(to_integer(unsigned(n_outstanding)) = 4,
       "four requests should leave four tags in flight");
    do_cpl(3, 64);
    do_cpl(1, 64);
    do_cpl(2, 64);
    do_cpl(0, 64);
    ck(to_integer(unsigned(n_outstanding)) = 0,
       "completing every tag should empty the tracker");
    ck(to_integer(unsigned(n_retire)) = 4, "four completions should retire four requests");
    ck(to_integer(unsigned(n_bad_tag)) = 0, "out-of-order completion reported a bad tag");

    -- ===============================================================
    --  PHASE 3 (DIRECTED, EXHAUSTIVE over every completion order)
    --
    --  Three tags, all 3! = 6 orders. Out-of-order is not one case, it is
    --  every permutation.
    -- ===============================================================
    for k in 0 to 5 loop
      reset_all;
      do_req(false, 64, tg, acc);
      do_req(false, 64, tg, acc);
      do_req(false, 64, tg, acc);
      case k is
        when 0 => pa := 0; pb2v := 1; pc2 := 2;
        when 1 => pa := 0; pb2v := 2; pc2 := 1;
        when 2 => pa := 1; pb2v := 0; pc2 := 2;
        when 3 => pa := 1; pb2v := 2; pc2 := 0;
        when 4 => pa := 2; pb2v := 0; pc2 := 1;
        when others => pa := 2; pb2v := 1; pc2 := 0;
      end case;
      do_cpl(pa, 64);
      do_cpl(pb2v, 64);
      do_cpl(pc2, 64);
      ck(to_integer(unsigned(n_outstanding)) = 0,
         "some completion order left a tag outstanding");
      ck(to_integer(unsigned(n_retire)) = 3, "some completion order lost a retirement");
      ck(to_integer(unsigned(n_bad_tag)) = 0,
         "some completion order was mistaken for a bad tag");
    end loop;

    -- ===============================================================
    --  PHASE 4 (DIRECTED, EXHAUSTIVE over split shapes)
    --
    --  The tag must stay outstanding until the LAST byte arrives. Freeing it
    --  early is the tag-reuse bug, and it would attribute the remainder to
    --  whatever transaction took the tag next.
    -- ===============================================================
    for k in 0 to 4 loop
      reset_all;
      piece  := 256 / (2 ** k);
      pieces := 256 / piece;
      do_req(false, 256, tg, acc);
      for n in 0 to pieces - 1 loop
        do_cpl(tg, piece);
        if n < pieces - 1 then
          -- ---- PROPERTY 15: a partially completed tag stays busy ----
          ck(to_integer(unsigned(n_outstanding)) = 1,
             "a tag was freed before its last completion arrived");
          ck(obs_retire = '0', "a partial completion claimed to retire");
        end if;
      end loop;
      ck(to_integer(unsigned(n_outstanding)) = 0,
         "the tag was not freed by its last completion");
      ck(to_integer(unsigned(n_partial)) = pieces - 1,
         "the number of partial completions does not match the split");
    end loop;

    -- ===============================================================
    --  PHASE 5 (DIRECTED) -- TAG EXHAUSTION IS BACK-PRESSURE.
    -- ===============================================================
    reset_all;
    tag_limit <= "0010";
    wait for 1 ns;
    do_req(false, 64, tg, acc); ck(acc, "first of two tags refused");
    do_req(false, 64, tg, acc); ck(acc, "second of two tags refused");
    snap := to_integer(unsigned(n_stall));
    do_req(false, 64, tg, acc);
    ck(not acc, "a third request was accepted with only two tags");
    ck(to_integer(unsigned(n_stall)) = snap + 1, "tag exhaustion did not raise a stall");
    ck(to_integer(unsigned(n_outstanding)) = 2, "a stalled request consumed a tag anyway");
    snap := to_integer(unsigned(n_posted));
    do_req(true, 64, tg, acc);
    ck(acc, "a posted request was blocked by tag exhaustion");
    ck(to_integer(unsigned(n_posted)) = snap + 1, "a posted request was not counted");
    ck(to_integer(unsigned(n_outstanding)) = 2, "a posted request consumed a tag");
    do_cpl(0, 64);
    do_req(false, 64, tg, acc);
    ck(acc, "freeing a tag did not admit the next request");
    ck(tg = 0, "the freed tag was not the one reused");

    -- ===============================================================
    --  PHASE 5b (DIRECTED, EXHAUSTIVE over every tag limit)
    --
    --  The posted-request guarantee is only OBSERVABLE when no tag is free,
    --  so the table is filled to capacity at each of the eight limits.
    -- ===============================================================
    for k in 1 to 8 loop
      reset_all;
      tag_limit <= std_logic_vector(to_unsigned(k, 4));
        wait for 1 ns;
        for b in 0 to k - 1 loop
          do_req(false, 64, tg, acc);
          ck(acc, "a request was refused below the tag limit");
        end loop;
        ck(to_integer(unsigned(n_outstanding)) = k,
           "filling to the limit did not use every tag");
        do_req(false, 64, tg, acc);
        ck(not acc, "a request was accepted above the tag limit");
        psnap  := to_integer(unsigned(n_posted));
        osnap2 := to_integer(unsigned(n_outstanding));
        do_req(true, 64, tg, acc);
        ck(acc, "a posted request was blocked by tag exhaustion");
      ck(to_integer(unsigned(n_posted)) = psnap + 1, "a posted request was not counted");
      ck(to_integer(unsigned(n_outstanding)) = osnap2, "a posted request consumed a tag");
    end loop;

    -- ===============================================================
    --  PHASE 6 (DIRECTED) -- MALFORMED COMPLETIONS.
    -- ===============================================================
    reset_all;
    snap := to_integer(unsigned(n_bad_tag));
    do_cpl(5, 64);
    ck(to_integer(unsigned(n_bad_tag)) = snap + 1,
       "a completion for a free tag was accepted");
    ck(to_integer(unsigned(n_outstanding)) = 0,
       "an unowned completion changed the tracker state");
    do_req(false, 64, tg, acc);
    snap := to_integer(unsigned(n_over));
    do_cpl(tg, 128);
    ck(to_integer(unsigned(n_over)) = snap + 1, "an over-long completion was not reported");
    ck(to_integer(unsigned(n_outstanding)) = 0,
       "an over-long completion left the tag outstanding");

    -- ===============================================================
    --  PHASE 6b (DIRECTED, EXHAUSTIVE over overrun shapes)
    -- ===============================================================
    for k in 0 to 7 loop
      reset_all;
      want := 16 * (2 ** (k mod 4));
        if k < 4 then extra := 1; else extra := want; end if;
        do_req(false, want, tg, acc);
        osnap := to_integer(unsigned(n_over));
        do_cpl(tg, want + extra);
        ck(to_integer(unsigned(n_over)) = osnap + 1,
           "an over-long completion was not reported");
        ck(to_integer(unsigned(n_outstanding)) = 0,
           "an over-long completion left the tag outstanding");
      ck(to_integer(unsigned(n_retire)) > 0,
         "an over-long completion did not retire the tag");
    end loop;

    -- ===============================================================
    --  PHASE 7 (DIRECTED, EXHAUSTIVE over the tracker's busy-mask)
    --
    --  All 2**8 = 256 combinations of which tags are busy, reached by
    --  allocating and completing rather than by forcing state.
    -- ===============================================================
    for k in 0 to 255 loop
      kmask := std_logic_vector(to_unsigned(k, 8));
      reset_all;
      for b in 0 to 7 loop do_req(false, 64, tg, acc); end loop;
      ck(to_integer(unsigned(n_outstanding)) = 8,
         "eight requests did not fill eight tags");
      for b in 0 to 7 loop
        if kmask(b) = '0' then do_cpl(b, 64); end if;
      end loop;

      -- ---- a POSTED request, against every table state ----
      snap := to_integer(unsigned(n_outstanding));
      do_req(true, 64, tg, acc);
      ck(acc, "a posted request was refused");
      ck(to_integer(unsigned(n_outstanding)) = snap, "a posted request consumed a tag");

      -- ---- a completion for a tag NOBODY OWNS, against every state ----
      if m_free_any(8) then
        ft := m_free_tag(8);
        bsnap  := to_integer(unsigned(n_bad_tag));
        osnap3 := to_integer(unsigned(n_outstanding));
        do_cpl(ft, 64);
        ck(to_integer(unsigned(n_bad_tag)) = bsnap + 1,
           "a completion for an unowned tag was not reported");
        ck(to_integer(unsigned(n_outstanding)) = osnap3,
           "a completion for an unowned tag changed the tracker state");
      end if;

      if m_free_any(8) then
        do_req(false, 64, tg, acc);
        ck(acc, "a request was refused with a free tag");
      else
        do_req(false, 64, tg, acc);
        ck(not acc, "a request was accepted with every tag busy");
      end if;
      reach_tag(k) := true;
    end loop;

    -- ===============================================================
    --  PHASE 8 (DIRECTED) -- THE USB SLOT.
    -- ===============================================================
    reset_all;
    for k in 0 to 3 loop
      do_iss(k);
      ck(to_integer(unsigned(u_n_out)) = k + 1,
         "each endpoint should hold one transaction");
    end loop;
    for k in 0 to 3 loop do_iss(k); end loop;
    ck(to_integer(unsigned(u_n_refused)) = 4,
       "four second-issues should all be refused");
    ck(to_integer(unsigned(u_n_out)) = 4,
       "a refused issue changed the outstanding count");
    for k in 0 to 3 loop do_rsp(k); end loop;
    ck(to_integer(unsigned(u_n_out)) = 0,
       "answering every endpoint should empty the slots");
    snap := to_integer(unsigned(u_n_spurious));
    do_rsp(2);
    ck(to_integer(unsigned(u_n_spurious)) = snap + 1,
       "an unsolicited response was not flagged");

    -- ===============================================================
    --  PHASE 9 (DIRECTED, EXHAUSTIVE over the endpoint busy-mask)
    -- ===============================================================
    for k in 0 to 15 loop
      emask := std_logic_vector(to_unsigned(k, 4));
      reset_all;
      for b in 0 to 3 loop
        if emask(b) = '1' then do_iss(b); end if;
      end loop;
      for b in 0 to 3 loop do_iss(b); end loop;
      reset_all;
      for b in 0 to 3 loop
        if emask(b) = '1' then do_iss(b); end if;
      end loop;
      for b in 0 to 3 loop do_rsp(b); end loop;
    end loop;

    -- ===============================================================
    --  PHASE 10 (RANDOM) -- mixed traffic with a reordering fabric.
    -- ===============================================================
    if not DIRECTED_ONLY then
      reset_all;
      fabric_clear;
      for k in 0 to 799 loop
        fabric_pop(now_c, rpf, rpt, rpb);
        if rpf then do_cpl(rpt, rpb); end if;
        if (urand mod 3) /= 0 then
          if (urand mod 5) = 0 then
            do_req(true, 64, tg, acc);
          else
            do_req(false, 64, tg, acc);
            -- A random latency, so completions come back out of order -- the
            -- property the whole tracker exists for.
            if acc then fabric_push(tg, 64, now_c + 1 + (urand mod 9)); end if;
          end if;
        end if;
        if (urand mod 2) = 0 then do_iss(urand mod 4);
        else                      do_rsp(urand mod 4); end if;
        now_c := now_c + 1;
      end loop;
      for k in 0 to 399 loop
        fabric_pop(now_c + 100, rpf2, rpt2, rpb2);
        if rpf2 then do_cpl(rpt2, rpb2); end if;
        now_c := now_c + 1;
      end loop;
    end if;

    nr := 0;
    for i in 0 to 63 loop
      if reach(i) then nr := nr + 1; end if;
    end loop;
    nrt := 0;
    for i in 0 to 255 loop
      if reach_tag(i) then nrt := nrt + 1; end if;
    end loop;

    write(lo, string'("steps=") & integer'image(steps) &
              string'(" checks=") & integer'image(checks) &
              string'(" reach_thr=") & integer'image(nr) &
              string'("/64 reach_tag=") & integer'image(nrt) &
              string'("/256 errors=") & integer'image(errors));
    writeline(output, lo);
    write(lo, string'("[pcie] alloc=") & integer'image(c_alloc) &
              string'(" posted=") & integer'image(c_posted) &
              string'(" stalls=") & integer'image(c_stall) &
              string'(" retire=") & integer'image(c_retire) &
              string'(" partial=") & integer'image(c_partial) &
              string'(" bad_tag=") & integer'image(c_bad) &
              string'(" over=") & integer'image(c_over));
    writeline(output, lo);
    write(lo, string'("[usb]  issued=") & integer'image(c_iss) &
              string'(" refused=") & integer'image(c_ref) &
              string'(" answered=") & integer'image(c_ans) &
              string'(" spurious=") & integer'image(c_spur));
    writeline(output, lo);
    write(lo, string'("--- cycles to move ") & integer'image(K_XACT) &
              string'(" transactions, by tags outstanding x latency ---"));
    writeline(output, lo);
    write(lo, string'("    tags      L=1     L=2     L=3     L=4     L=5     L=6     L=7     L=8"));
    writeline(output, lo);
    for lim in 1 to 8 loop
      write(lo, string'("    "));
      write(lo, lim, right, 4);
      for lat in 1 to 8 loop
        write(lo, thr_cyc(lim, lat), right, 8);
      end loop;
      writeline(output, lo);
    end loop;
    write(lo, string'("    (row tags=1 IS the USB case: one transaction outstanding)"));
    writeline(output, lo);

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

    done <= true;
    wait;
  end process;

end architecture;

11. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  Properties for the tag tracker and the transaction slot.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus Verilog does not support
//  concurrent assertions, so every number published here comes from the
//  procedural checks in the testbenches. These are the same obligations in
//  the form a commercial simulator or a formal tool would take.
//
//  As in 28.2 and 28.3, read the SHAPE of the two groups. The tracker needs
//  properties about IDENTITY -- which tag, held how long, matched to what --
//  because it has many transactions to confuse. The slot needs none of
//  those, and the reason is not that it is simpler: it is that it has
//  nothing to confuse.
// ---------------------------------------------------------------------
module tg_sva #(parameter int N_TAG = 8, parameter int N_EP = 4) (
  input logic        clk,
  input logic        rst_n,
  // tracker
  input logic [3:0]  tag_limit,
  input logic        req_valid,
  input logic        req_posted,
  input logic        req_ready,
  input logic [2:0]  req_tag,
  input logic        req_stall,
  input logic        cpl_valid,
  input logic [2:0]  cpl_tag,
  input logic        cpl_retire,
  input logic [31:0] n_outstanding,
  input logic [31:0] n_alloc,
  input logic [31:0] n_retire,
  // slot
  input logic        iss_valid,
  input logic [1:0]  iss_ep,
  input logic        iss_ready,
  input logic        iss_busy,
  input logic        rsp_valid,
  input logic        rsp_match,
  input logic        rsp_spurious,
  input logic [31:0] u_n_outstanding
);

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

  // ---- 1. ready and stall are exact complements, for a read ----
  //
  // A request must be either accepted or refused. Neither means the
  // requester has no idea whether the transaction exists.
  a_req_decided : assert property
    ((req_valid && !req_posted) |-> (req_ready ^ req_stall));

  // ---- 2. a POSTED request is never stalled ----
  //
  // THE property that makes a PCIe write fast. It needs no completion, so
  // tag exhaustion cannot apply to it.
  a_posted_never_stalls : assert property
    ((req_valid && req_posted) |-> (req_ready && !req_stall));

  // ---- 3. the allocated tag is within the limit ----
  //
  // A tag above the limit would be one the fabric was never told to expect,
  // and the completion for it would come back addressed to nobody.
  a_tag_in_range : assert property
    ((req_ready && !req_posted) |-> (req_tag < tag_limit));

  // ---- 4. the outstanding count never exceeds the limit ----
  a_within_limit : assert property (n_outstanding <= tag_limit);

  // ---- 5. a retire always follows a completion ----
  a_retire_needs_cpl : assert property (cpl_retire |-> cpl_valid);

  // ---- 6. allocation and retirement balance ----
  //
  // The invariant that makes a tag leak visible. A tracker that allocated
  // more than it retired and more than it holds has lost a tag, and a lost
  // tag is a permanent loss of one unit of concurrency.
  a_balance : assert property (n_alloc == n_retire + n_outstanding);

  // ---- 7. THE TAG-REUSE PROPERTY ----
  //
  // A tag must not be allocated again until it has been retired. This is
  // the obligation N1 breaks, it is the one that cannot be checked at all
  // without tracking bytes, and it is the reason the tracker holds a byte
  // count per tag rather than a single busy bit.
  //
  // Written per-tag with a generate, because the obligation is about one
  // tag's history rather than about any cycle.
  generate
    for (genvar t = 0; t < N_TAG; t++) begin : g_reuse
      property p_no_reuse;
        (req_ready && !req_posted && (req_tag == t))
          |=> (!(req_ready && !req_posted && (req_tag == t)))
              until_with (cpl_retire && (cpl_tag == t));
      endproperty
      a_no_reuse : assert property (p_no_reuse);
    end
  endgenerate

  // ---- 8. the slot's two outputs are exact complements ----
  a_iss_decided : assert property (iss_valid |-> (iss_ready ^ iss_busy));

  // ---- 9. the slot holds at most one per endpoint ----
  //
  // Stated as a bound on the total, which for N_EP endpoints each holding at
  // most one is the strongest form available without naming endpoints.
  a_slot_bound : assert property (u_n_outstanding <= N_EP);

  // ---- 10. a response is matched or spurious, never both nor neither ----
  //
  // USB's whole matching guarantee in one line. It is this simple ONLY
  // because there is one outstanding transaction; property 7 is what the
  // same guarantee costs when there are eight.
  a_rsp_decided : assert property (rsp_valid |-> (rsp_match ^ rsp_spurious));

  // ---- COVER: the interesting states are reached ----
  // Assertions over stimulus that never fills the tracker, never splits a
  // completion and never reorders one prove nothing.
  c_full     : cover property (n_outstanding == tag_limit);
  c_stall    : cover property (req_stall);
  c_partial  : cover property (cpl_valid && !cpl_retire);
  c_reorder  : cover property ((cpl_tag != 0) ##[1:8] (cpl_tag == 0));
  c_posted   : cover property (req_valid && req_posted && (n_outstanding == tag_limit));
  c_spurious : cover property (rsp_spurious);

endmodule

12. Where UVM Fits

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
//  UVM structure for the tag tracker and the transaction slot.
//
//  NOT SIMULATED IN THIS CHAPTER. Icarus cannot compile UVM -- it breaks on
//  virtual method dispatch -- so every number comes from the procedural
//  benches. This is the structure a production environment would use.
//
//  THE DESIGN DECISION: the fabric is an AGENT, not a scoreboard helper.
//  It owns the reordering, the latency distribution and the splitting, and
//  it is the only component that knows when a completion is due. That
//  separation is what lets the scoreboard be a pure matcher -- and a pure
//  matcher is the only kind that can detect a tag-reuse bug, because it has
//  no opinion about what SHOULD have been in flight.
// ---------------------------------------------------------------------

// ---- a request, and the fabric's freedom to answer it however ----
class pcie_req extends uvm_sequence_item;
  `uvm_object_utils(pcie_req)

  rand bit        posted;
  rand int        bytes;
  // How the fabric will answer: after how long, and in how many pieces.
  // Randomised HERE rather than in the driver, so the reordering is
  // reproducible from the seed and a failing case can be replayed.
  rand int        latency;
  rand int        n_pieces;

  constraint c_sane {
    bytes    inside {64, 128, 256};
    latency  inside {[1:12]};
    n_pieces inside {[1:4]};
    // A posted request is never answered, so its answer shape is
    // meaningless -- pinned rather than left random, because a randomised
    // don't-care is a coverage hole that looks like coverage.
    posted -> n_pieces == 1;
  }
  // Reads dominate. A workload of pure writes never allocates a tag and
  // would measure nothing the chapter is about.
  constraint c_mix { posted dist { 0 := 7, 1 := 3 }; }

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

// ---- the fabric agent: latency, reordering and splitting live here ----
//
// It holds completions in a queue sorted by due time, which is what makes
// them come back out of order without the sequence having to describe the
// reordering explicitly. A sequence that had to specify the order would be
// testing the order it thought of.
class fabric_agent extends uvm_component;
  `uvm_component_utils(fabric_agent)

  typedef struct {
    int tag;
    int bytes_left;
    int piece;
    int due;
  } inflight_t;

  inflight_t q[$];
  int unsigned now;

  virtual pcie_if vif;

  task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      now++;
      deliver_one();
    end
  endtask

  // ONE completion per cycle. A real link serialises them, and a fabric that
  // delivered several at once would hide every ordering bug in the tracker.
  task deliver_one();
    int best = -1;
    foreach (q[i])
      if (q[i].due <= now && (best < 0 || q[i].due < q[best].due)) best = i;
    if (best < 0) begin
      vif.cpl_valid <= 1'b0;
      return;
    end
    vif.cpl_valid <= 1'b1;
    vif.cpl_tag   <= q[best].tag;
    vif.cpl_bytes <= q[best].piece;
    q[best].bytes_left -= q[best].piece;
    if (q[best].bytes_left <= 0) q.delete(best);
    else                         q[best].due = now + 1;
  endtask
endclass

// ---- the scoreboard is a pure MATCHER ----
//
// It holds what it believes is outstanding, and it holds it from observing
// the interface -- not from the sequence's intent. That is deliberate: a
// scoreboard fed the intended tag would agree with a tag-reuse bug, because
// the intent and the reuse are the same tag.
class tag_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(tag_scoreboard)

  int unsigned bytes_left [int];   // tag -> bytes still expected
  int unsigned n_reuse, n_orphan, n_matched;

  function void saw_alloc(int tag, int bytes);
    // ---- THE CHECK ----
    // A tag that is already outstanding has been reused. Nothing else in
    // the environment can see this, because the DUT's own busy bit is what
    // the bug corrupts.
    if (bytes_left.exists(tag) && bytes_left[tag] > 0) begin
      n_reuse++;
      `uvm_error("TAG", $sformatf("tag %0d reused with %0d bytes outstanding",
                                  tag, bytes_left[tag]))
    end
    bytes_left[tag] = bytes;
  endfunction

  function void saw_completion(int tag, int bytes);
    if (!bytes_left.exists(tag) || bytes_left[tag] == 0) begin
      // A completion nobody was waiting for. On a real link a fabric error;
      // in a simulation it is usually this environment's own book-keeping,
      // which is why it is counted rather than asserted immediately.
      n_orphan++;
      return;
    end
    if (bytes > bytes_left[tag]) begin
      `uvm_error("TAG", $sformatf("tag %0d over-completed by %0d bytes",
                                  tag, bytes - bytes_left[tag]))
      bytes_left[tag] = 0;
    end else begin
      bytes_left[tag] -= bytes;
    end
    if (bytes_left[tag] == 0) n_matched++;
  endfunction

  function void report_phase(uvm_phase phase);
    `uvm_info("TAG", $sformatf("matched %0d, reused %0d, orphaned %0d",
                               n_matched, n_reuse, n_orphan), UVM_LOW)
    // Every tag must be settled at the end. A tag left outstanding is a leak
    // and costs one unit of concurrency permanently.
    foreach (bytes_left[t])
      if (bytes_left[t] != 0)
        `uvm_error("TAG", $sformatf("tag %0d left outstanding with %0d bytes",
                                    t, bytes_left[t]))
  endfunction
endclass

// ---- coverage: the CONCURRENCY, which is the whole subject ----
class tag_coverage extends uvm_subscriber #(pcie_req);
  `uvm_component_utils(tag_coverage)

  int unsigned observed_outstanding;

  covergroup cg with function sample(pcie_req r, int outstanding);
    cp_posted : coverpoint r.posted;
    cp_bytes  : coverpoint r.bytes  { bins b[] = {64, 128, 256}; }
    cp_pieces : coverpoint r.n_pieces { bins p[] = {[1:4]}; }
    // How many were in flight when this request was issued. THE coverage
    // point of the chapter: a regression that never exceeded two outstanding
    // has not tested the mechanism at all, whatever its line coverage says.
    cp_conc   : coverpoint outstanding { bins c[] = {[0:8]}; }
    // Splitting crossed with concurrency, because a split completion while
    // several tags are in flight is where a reuse bug actually bites.
    x_split   : cross cp_pieces, cp_conc;
    // And a posted request while the tracker is FULL, which is the only
    // condition under which the posted guarantee is observable.
    x_posted_full : cross cp_posted, cp_conc {
      ignore_bins uninteresting = binsof(cp_conc) with (cp_conc < 8);
    }
  endgroup

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

  function void write(pcie_req t);
    cg.sample(t, observed_outstanding);
  endfunction
endclass

13. Mutation Testing

Nine defects, six in the tag tracker and three in the transaction slot, injected one at a time into all three languages. Every replacement asserted; each mutation generated as its own file.

#the injected defectV-allV-dirSV-allSV-dirVHDL-allVHDL-dir
BASEunmodified designs000000
N1a tag is freed by any completion, not the last949494949494
N2a completion is applied without checking ownership768768768768768768
N3a posted request consumes a tag202020202020
N4the highest free tag is allocated, not the lowest329728903297289033082890
N5tag_limit is ignored270427042704270427042704
N6an over-long completion is absorbed silently181818181818
N7a second transaction accepted on a busy endpoint554725547248272
N8an unsolicited response treated as a match490664906644466
N9a response frees endpoint 0 whatever it answered163143163143174443

Every mutation is killed, and every one by directed stimulus alone. The directed column is identical across all three languages at all nine rows — 94, 768, 20, 2890, 2704, 18, 72, 66, 43.

N5 is the mutation that would have invalidated the chapter

N5 makes the tracker ignore tag_limit and use every tag it has. The design still works: every transaction completes, every tag is matched, no data is corrupted. Nothing a functional test would look for is wrong.

What breaks is the measurement. With the limit ignored, every row of the throughput surface becomes the saturated row, and the closed forms in section 5 — the USB result and the saturation boundary — both evaporate.

It scores 2704, all directed, and almost all of that is properties 12 and 14: the exact K*(L+1) equality and the negative half that says fewer tags cannot saturate.

Run totals

stepschecksthroughput reachtag-state reacherrors
Verilog, full916634,15564 / 64256 / 2560
Verilog, directed only745226,98164 / 64256 / 2560
SystemVerilog, full916634,15564 / 64256 / 2560
SystemVerilog, directed only745226,98164 / 64256 / 2560
VHDL, full920234,27664 / 64256 / 2560
VHDL, directed only745226,98164 / 64256 / 2560

The directed-only rows are identical across all three languages in every column, and so is the entire 64-cell throughput surface. The full rows differ only in VHDL's check count, by 121, from its independent random stream.

14. What This Does Not Cover

No ordering rules between posted and completion traffic. Real PCIe has a table of which transaction classes may pass which, and it is a substantial subject. This chapter models the tag accounting only; a posted request here consumes no tag and is otherwise independent.

No credits. PCIe's link layer uses credit-based flow control, which is a second and separate back-pressure mechanism. The only back-pressure modelled here is tag exhaustion.

No link layer at all. No sequence numbers, no ACK/NAK, no replay buffer. Section 2 argues that the link-layer identity and the transaction-layer identity are different mechanisms; only the second is built.

Eight tags. Real PCIe allows 32 by default and 256 with extended tags. The sweep is exhaustive at 8, and the closed forms are stated in terms of L and are not specific to 8 — but the table is for 8.

Latency is a fixed integer per measurement. Real fabric latency has a distribution. Fixing it is what makes the closed forms exact rather than approximate; the random phase uses a variable latency precisely to check that the tracker does not depend on it being fixed.

One requester. Multiple requesters sharing a completion path is where real tag management gets hard, and it is out of scope.

USB's scheduling is not modelled. usb_xact_slot models one transaction per endpoint. The frame structure, the periodic budget and the transaction scheduling that sit above it are modules 14 through 18 of this track.

The 6.4× figure is cycles, not seconds. It compares the two mechanisms on one clock and one latency. Real USB and real PCIe also differ in clock rate and width, and those differences are additional rather than included.

15. The Interview Answer

Three sentences, and do not start with the bandwidth numbers.

1. Name the mechanism. "USB has one transaction outstanding per endpoint — the host issues it and waits — so no USB packet needs a transaction identifier. PCIe has many in flight at once, so every non-posted request carries a tag and the requester tracks it until the last byte of its completion arrives."

2. Name the consequence with the formula. "That makes USB's throughput exactly one transaction per latency-plus-one cycles, whatever the wire can carry. PCIe's is min(1, tags/(latency+1)) — so with enough tags its throughput stops depending on latency at all. That is why PCIe kept scaling bandwidth by adding lanes while its round-trip latency barely moved."

3. Name the cost, because the tags are not free. "The tags buy a bug USB cannot have: reuse a tag before its completion arrives and you attribute one transaction's data to another, silently. That is why a tracker has to hold a byte count per tag and not just a busy bit — a split completion must not free it."

If there is time, the best follow-up detail is the one from section 5: you need latency+1 tags to saturate, not latency, because a tag retired this cycle is not allocatable until the next. Knowing that is the difference between having read about outstanding transactions and having counted cycles on one.

16. What This Module Measured

Four comparisons, four mechanisms, four measured costs:

chapterthe mechanism the other protocol lacksthe measured cost
28.1, UARTsynchronisation from a single edgea tolerance budget shrinking as 1/N
28.2, SPIasking a device who it is0 of 11 failures detectable by any slave
28.3, Ethernetan authority to assign addresses294 of 1065 mis-delivered, vs 0 of 130
28.4, PCIenaming a transaction216 cycles vs 34, at one tag vs eight

Not one of those is a bandwidth number, and not one of them appears on a comparison table.

And the method, stated once, because it is reusable: find the one mechanism the two protocols do not share, build it in hardware, verify it exhaustively over a reachable domain, and mutate it to prove the verification would have noticed. Then the comparison is a number with a derivation, and the conversation is about engineering rather than about preference.

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.