Skip to content

PCIe · Module 20

High-Speed Data Movement — Keeping the Link Busy Without Breaking Anything

Throughput is not bigger packets. It is having legal work available every cycle — which means Tags, credits, buffers and descriptors must all be productive at once, and a faster engine is still the same ownership machine.

Chapters 20.220.4 built a DMA engine that is correct. It owns descriptors properly, tracks Tags, matches Completions, and walks segment lists without losing or duplicating work.

It is also slow, deliberately. Chapter 20.3 used a small Tag pool; Chapter 20.4 fetched one descriptor at a time. Both were the right teaching choice, and both leave the Link idle most of the time.

The instinct is to reach for bigger packets. That helps, and §8 quantifies exactly how much — but it is not where the bandwidth is.

The bandwidth is in having legal work ready every single cycle, and that turns out to be a multi-resource problem: Tags, flow-control credits, buffer space, and descriptors must all be simultaneously non-empty.

How does a DMA engine expose enough independent work to keep a high-speed Link busy — without relaxing a single thing Module 20 has established?

1. Sources and What Is Derived

2. What Actually Limits Throughput

Four terms, and the smallest one wins:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
available Link bandwidth
  × transaction efficiency        payload / (payload + overhead)      §8
  × ability to keep work in flight   Tags, credits, descriptors       §§6-7
  × ability to absorb what returns   completion buffering, sink rate  §12

Most DMA engines that underperform are not limited by the first term. They are limited by the third or fourth — and §20's debugging ladder exists to identify which.

And the terms are multiplicative, which has a practical consequence: fixing a term that is not the bottleneck changes nothing measurable. Doubling the Tag pool on a credit-limited engine produces exactly zero improvement — which is §20's second scenario and one of the most common wasted efforts in DMA tuning.

3. Performance Is Not Permission to Relax Correctness

4. Bandwidth and Latency Are Different Problems

Bandwidth is useful bytes per second. Latency is time from request to useful response.

Writes are posted (Chapter 20.3 §3), so host-memory latency does not bound write bandwidth. The engine issues and moves on; only credits and transmit opportunity limit it.

Reads are non-posted, so every request waits a round trip. A read engine's bandwidth is bounded by how much it can have outstanding, which is §5.

The derived relationship, and the single most useful formula in the chapter:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bandwidth  ≈  bytes_in_flight / round_trip_latency

This is an upper-bound intuition, not a PCIe rule (§1). It says: to go faster, either put more bytes in flight or reduce latency — and the engine only controls the first.

5. Bandwidth-Delay Product

Rearranged, the formula tells you what to build:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
required bytes in flight  ≈  target_bandwidth × round_trip_latency
required requests         ≈  ceil(bytes_in_flight / request_size)

Computed (§17) — derived model, illustrative latencies:

TargetRound tripBytes in flightRequests @512 BRequests @4 KiB
1 GB/s0.5 µs50011
4 GB/s1.0 µs4,00081
8 GB/s2.0 µs16,000324
16 GB/s1.0 µs16,000324
16 GB/s4.0 µs64,00012516

Three things to read out of this table.

Requirements grow with latency, not only with bandwidth. The 16 GB/s rows differ only in round trip, and the request requirement differs by .

Request size trades directly against request count. At 4 KiB per request, 16 GB/s over a 4 µs round trip needs 16 outstanding — comfortably inside a 5-bit Tag field (Chapter 20.3 §1). At 512 B it needs 125, which exceeds 32 Tags and forces Extended Tags.

And the last row is why Extended Tag exists. A high-bandwidth, high-latency path with modest request sizes genuinely cannot be served by 32 Tags — the arithmetic, not the protocol, is what demands them.

6. Why One Outstanding Read Is Slow

Chapter 20.3 §15's single-outstanding tracker was correct and deliberately bounded. Here is its ceiling.

Derived simulation (§17): 512-byte requests, a 250-cycle round trip, 200,000 cycles:

OutstandingBytes/cycleRelative
12.051.0×
24.092.0×
48.184.0×
816.368.0×
1632.7316.0×
3265.4532.0×
64130.9164.0×

Perfectly linear across the whole range, because in this model nothing else binds. The engine is doing 1/64th of what the same hardware could do, purely from lack of concurrency.

And that is the point of the model, not the numbers. Real scaling stops being linear as soon as another resource becomes the limit — which is §7, and which is exactly what §19's waveform shows.

7. More Tags Is Not More Bandwidth

The Tag-limited ceiling, computed (§17) — derived:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ceiling  ≈  tags × request_size / round_trip_latency
TagsRequest sizeRound tripCeiling
8512 B1 µs4.10 GB/s
32512 B1 µs16.38 GB/s
324096 B1 µs131.07 GB/s
256512 B1 µs131.07 GB/s

Read rows 3 and 4. The same ceiling reached two ways — 32 Tags with large requests, or 256 Tags with small ones. Request size and Tag count are interchangeable in this model, which is why "add Tags" is not automatically the answer.

8. Payload Size and Collapsing Returns

Larger payloads amortize per-TLP overhead — the one effect that genuinely is about packet size.

Computed (§17) with a normalized, illustrative overhead of 24 bytes per TLP:

PayloadEfficiencyΔ from previous
64 B72.7%
128 B84.2%+11.5 pts
256 B91.4%+7.2 pts
512 B95.5%+4.1 pts
1024 B97.7%+2.2 pts
2048 B98.8%+1.1 pts
4096 B99.4%+0.6 pts

The shape is the lesson. Each doubling gains roughly half what the previous one did. 128→256 is worth 7.2 points; 2048→4096 is worth 0.6 — and the second costs 16× the buffering, 16× the latency to assemble a payload, and 16× the granularity penalty on short transfers.

Three qualifications that keep this honest.

The overhead value is illustrative (§1). A different overhead shifts the numbers; it does not change the shape, which is what the section is for.

MPS and MRRS are different fields (Chapter 20.3 §8) bounding different things. Raising one does not raise the other, and mutation 22 is the design that conflates them.

And a large MRRS does not imply a single large Completion (Chapter 20.3 §10). One request may still return several — so §12's buffering must be sized for the request, not for an assumed single response.

9. The Issue Window

Every cycle, the engine asks one question:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
can_issue =  tag_available
          && credit_available
          && buffer_space_reserved
          && link_request_ready
          && work_available

All five, every time. §17 measured what happens when two are dropped: 18.6% of states issue illegally.

And each term must be a grant, not an observation — which is §11, and the most subtle failure in this chapter.

10. Reservation, Not Observation

11. Descriptor Prefetch, and the Stale-Descriptor Trap

Chapter 20.4 §5 established that descriptor fetch is DMA and sits on the critical path. Prefetching hides that latency: fetch descriptor N+1 while segment N executes.

And it introduces a failure that fetch-one-at-a-time cannot have.

12. Sizing the Sink

Request-side optimization without sink-side capacity produces Completions with nowhere to go.

The invariant (§13's RTL):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reserved_bytes  ≤  capacity        always

Reservation happens before the request is issued, not when data returns — because by the time it returns it is too late to decline.

And buffer sizing has an honest limit. The derived model is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
required buffering  ≥  arrival_rate × maximum_expected_stall

If no bounded maximum stall exists, no finite buffer is sufficient. That is not a sizing problem; it is an architecture problem, and the answer is backpressure rather than depth — which §18's decision table makes concrete.

13. RTL — Admission, Tags and Reservation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Multi-resource admission control (section 9).
// EVERY TERM IS REQUIRED. Section 16 measured an admission check that used
// only tags and work: it issues without credit or buffer in 18.6% of
// resource states -- which is a flow-control violation, not a stall.
//
// THIS BLOCK DOES NOT IMPLEMENT PCIe FLOW CONTROL. It consumes a
// normalized grant from the credit manager (Module 16).
module dma_admission (
  input  logic tag_available,        // section 14's allocator
  input  logic credit_granted,       // NORMALIZED grant, not a raw counter (§10)
  input  logic buffer_reserved,      // section 15
  input  logic tx_request_ready,
  input  logic work_available,
  output logic issue_allowed
);
  // A conjunction, deliberately flat and readable: any future edit that
  // drops a term is visible in review rather than buried in an FSM.
  assign issue_allowed = tag_available
                      && credit_granted
                      && buffer_reserved
                      && tx_request_ready
                      && work_available;
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Multi-Tag allocator with an optional rotating search.
// DEEPENS Chapter 20.3 section 15's allocator: same lease semantics, plus
// same-cycle free-and-allocate and a fairness option.
//
// Section 16: 600,000 random operations across TAGS in {1,2,3,8,32},
// including simultaneous free+alloc -- 0 disagreements with an
// independent set model.
module tag_pool #(
  parameter int TAGS       = 32,
  parameter bit ROTATE     = 1,           // 1 = round-robin search (§ fairness)
  parameter int TW = (TAGS <= 1) ? 1 : $clog2(TAGS)
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic          alloc_req,
  output logic          alloc_valid,
  output logic [TW-1:0] alloc_tag,
 
  input  logic          free_req,
  input  logic [TW-1:0] free_tag,
 
  output logic [TAGS-1:0] busy_map,
  output logic [TW:0]     free_count,     // one bit wider: 0..TAGS
  output logic            err_bad_free
);
  generate if (TAGS < 1) $error("TAGS must be at least 1"); endgenerate
 
  logic [TAGS-1:0] busy_q;
  logic [TW-1:0]   rr_q;
  logic            bad_q;
  assign busy_map     = busy_q;
  assign err_bad_free = bad_q;
 
  // Free count is a real count, not a popcount recomputed downstream --
  // section 15's reservation logic needs it every cycle.
  always_comb begin
    free_count = '0;
    for (int i = 0; i < TAGS; i++) if (!busy_q[i]) free_count = free_count + 1;
  end
 
  // ==================================================================
  // SEARCH ORDER IS A FAIRNESS POLICY, NOT A CORRECTNESS ONE (§ below).
  // A fixed low-index-first search is functionally correct and repeatedly
  // favours low Tag IDs -- which is harmless protocol-wise and can skew
  // lab observation and latency distribution. ROTATE makes it round-robin.
  // PCIe REQUIRES NO TAG FAIRNESS; this is implementation policy.
  // ==================================================================
  logic [TW-1:0] pick;
  logic          found;
  always_comb begin
    pick = '0; found = 1'b0;
    for (int k = TAGS-1; k >= 0; k--) begin
      int idx = ROTATE ? ((int'(rr_q) + k) % TAGS) : k;
      if (!busy_q[idx]) begin pick = TW'(idx); found = 1'b1; end
    end
  end
  assign alloc_valid = alloc_req && found;
  assign alloc_tag   = pick;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin busy_q <= '0; rr_q <= '0; bad_q <= 1'b0; end
    else begin
      // ==============================================================
      // SAME-CYCLE FREE AND ALLOCATE IS THE STEADY STATE of a busy
      // engine: a context retiring while a new request issues. Free is
      // applied first so the released Tag is immediately re-allocatable.
      // ==============================================================
      if (free_req) begin
        if (free_tag < TW'(TAGS) && busy_q[free_tag]) busy_q[free_tag] <= 1'b0;
        else bad_q <= 1'b1;                       // double or out-of-range free
      end
      if (alloc_valid) begin
        busy_q[alloc_tag] <= 1'b1;
        rr_q <= (alloc_tag == TW'(TAGS-1)) ? '0 : (alloc_tag + TW'(1));
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Reserve sink capacity BEFORE issuing (section 12).
// RESERVE-THEN-ISSUE, RELEASE-ON-COMMIT. Reserving after the request is
// issued is too late: the Completion is already coming.
module completion_reservation #(
  parameter int CAPACITY_BYTES = 16384,
  parameter int CW = $clog2(CAPACITY_BYTES + 1)
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic [CW-1:0] req_bytes,
  input  logic          reserve_req,
  output logic          reserve_ok,
 
  input  logic          release_valid,
  input  logic [CW-1:0] release_bytes,
 
  output logic [CW-1:0] reserved_bytes,
  output logic [CW-1:0] free_bytes,
  output logic          almost_full,
  output logic          err_over_release
);
  logic [CW-1:0] res_q;
  logic          over_q;
  assign reserved_bytes  = res_q;
  assign free_bytes      = CW'(CAPACITY_BYTES) - res_q;
  assign almost_full     = (free_bytes < CW'(CAPACITY_BYTES/8));
  assign err_over_release = over_q;
 
  // ADMISSION USES FREE CAPACITY, not a full bit -- a request needs a
  // specific number of bytes, and "not full" does not mean "enough".
  assign reserve_ok = reserve_req && (req_bytes <= free_bytes);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin res_q <= '0; over_q <= 1'b0; end
    else begin
      unique case ({reserve_ok, release_valid})
        2'b10: res_q <= res_q + req_bytes;
        2'b01: if (release_bytes <= res_q) res_q <= res_q - release_bytes;
               else over_q <= 1'b1;                   // released more than held
        2'b11: begin
                 // Same cycle: net the two so the invariant never breaks
                 // transiently (section 20's audit).
                 if (release_bytes <= res_q) res_q <= res_q - release_bytes + req_bytes;
                 else over_q <= 1'b1;
               end
        default: ;
      endcase
    end
  end
  // INVARIANT: reserved_bytes <= CAPACITY_BYTES, always (P8).
endmodule

Classification: all three synthesizable.

The allocator was verified (§17): 600,000 operations across TAGS = 1, 2, 3, 8, 32 including same-cycle free-and-allocate — 0 disagreements with an independent set model.

ROTATE is fairness policy, not correctness — PCIe requires no Tag fairness, and a fixed search is functionally fine. It skews latency distribution and lab observation, which is worth a parameter and not worth a claim.

And reserve_ok uses free_bytes, not almost_full (§12): a request needs a specific number of bytes.

Failure — five. Dropping a term from issue_allowed18.6% illegal issues. Observing a credit counter instead of taking a grant (§10). Reserving after issuing. Admission on a full bit rather than free capacity. And TAGS = 1, where the guarded width keeps the index one bit rather than zero.

14. RTL — Prefetch Epoch, Packer and Shared Arbiter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Job epoch -- the stale-prefetch defence (section 11).
// IMPLEMENTATION POLICY, and the epoch is LOCAL: it is never transmitted
// over PCIe and has no protocol meaning.
//
// Section 16: without this, 161,182 stale descriptors executed across
// 40,000 randomized abort sequences. With it, 0.
module job_epoch #(parameter int EW = 4) (
  input  logic clk,
  input  logic rst_n,
  input  logic job_start,
  input  logic job_abort,
 
  input  logic [EW-1:0] item_epoch,     // carried with each prefetched item
  input  logic          item_valid,
  output logic          item_is_stale,
 
  output logic [EW-1:0] current_epoch
);
  logic [EW-1:0] ep_q;
  assign current_epoch = ep_q;
  // Wrap is harmless: a stale item would need exactly 2^EW epoch changes
  // to alias, and the FIFO is far shallower than that.
  assign item_is_stale = item_valid && (item_epoch != ep_q);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)                    ep_q <= '0;
    else if (job_abort || job_start) ep_q <= ep_q + EW'(1);
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Assemble small source beats into efficient payloads (§8).
// BOUNDED THREE WAYS so it never waits forever: the target size, the end
// of a descriptor, and an explicit flush. Waiting only for the target
// would stall a transfer whose final bytes never reach it.
//
// Section 16: 60,000 random (beat, target, ready) cases -- byte
// conservation `accepted == emitted + buffered` held in every one.
module payload_packer #(
  parameter int TARGET_BYTES = 256,
  parameter int BUF_BYTES    = 1024,
  parameter int BW = $clog2(BUF_BYTES + 1)
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic          in_valid,
  output logic          in_ready,
  input  logic [7:0]    in_bytes,        // beat size, normalized
  input  logic          in_last,         // end of descriptor -> flush
 
  output logic          pkt_valid,
  input  logic          pkt_ready,
  output logic [BW-1:0] pkt_bytes,
  output logic          pkt_last,
 
  output logic [BW-1:0] buffered_bytes
);
  logic [BW-1:0] buf_q;
  logic          last_q;
  assign buffered_bytes = buf_q;
 
  // Accept while there is room for a maximum beat -- never overflow.
  assign in_ready = (buf_q + BW'(255) <= BW'(BUF_BYTES));
 
  wire have_target = (buf_q >= BW'(TARGET_BYTES));
  // EMIT ON TARGET **OR** ON FLUSH. The second condition is what makes a
  // final short payload emit at all (section 20, mutation 11).
  assign pkt_valid = have_target || (last_q && (buf_q != '0));
  assign pkt_bytes = have_target ? BW'(TARGET_BYTES) : buf_q;
  assign pkt_last  = last_q && !have_target;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin buf_q <= '0; last_q <= 1'b0; end
    else begin
      // Both directions on their own handshake. Same-cycle in and out is
      // the normal case at speed, and the arithmetic must net correctly.
      unique case ({in_valid && in_ready, pkt_valid && pkt_ready})
        2'b10: buf_q <= buf_q + BW'(in_bytes);
        2'b01: buf_q <= buf_q - pkt_bytes;
        2'b11: buf_q <= buf_q + BW'(in_bytes) - pkt_bytes;
        default: ;
      endcase
      if (in_valid && in_ready && in_last) last_q <= 1'b1;
      if (pkt_valid && pkt_ready && pkt_last) last_q <= 1'b0;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. One shared PCIe transmit opportunity, several producers.
// GRANT IS HELD UNTIL THE TRANSFER COMPLETES (section 20's counterexample).
// A combinational priority encoder re-evaluated every cycle changes the
// selected producer while `valid && !ready`, so the request that finally
// transfers is not the one that was offered.
module tx_arbiter #(
  parameter int N = 4,
  parameter int IW = (N <= 1) ? 1 : $clog2(N)
) (
  input  logic clk,
  input  logic rst_n,
  input  logic [N-1:0] req,
  input  logic         down_ready,
  output logic [N-1:0] grant,
  output logic         grant_valid,
  output logic [IW-1:0] grant_idx
);
  logic [IW-1:0] owner_q, rr_q;
  logic          held_q;
 
  assign grant_valid = held_q;
  assign grant_idx   = owner_q;
  always_comb begin
    grant = '0;
    if (held_q) grant[owner_q] = 1'b1;
  end
 
  // Round-robin from the last owner: no producer is starved while it keeps
  // requesting. FAIRNESS IS IMPLEMENTATION POLICY, not a PCIe requirement.
  logic [IW-1:0] pick; logic found;
  always_comb begin
    pick='0; found=1'b0;
    for (int k = N-1; k >= 0; k--) begin
      int idx = (int'(rr_q) + k) % N;
      if (req[idx]) begin pick = IW'(idx); found = 1'b1; end
    end
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin held_q <= 1'b0; owner_q <= '0; rr_q <= '0; end
    else begin
      if (!held_q) begin
        if (found) begin owner_q <= pick; held_q <= 1'b1; end
      end else if (down_ready) begin
        // ==========================================================
        // OWNERSHIP RELEASED ONLY ON AN ACTUAL TRANSFER. Not when the
        // requester deasserts, not on a timer. Section 16 verified
        // conservation across 60,000 runs: transfers never exceeded
        // offers.
        // ==========================================================
        held_q <= 1'b0;
        rr_q   <= (owner_q == IW'(N-1)) ? '0 : (owner_q + IW'(1));
      end
    end
  end
endmodule

Classification: all three synthesizable.

The packer's flush condition is what makes it terminate. Emitting only at TARGET_BYTES would strand a descriptor's final short payload forever — mutation 11, and §17's conservation equation is what catches it.

And the arbiter releases only on transfer, which is Chapter 18.2 §11's ownership rule applied to a shared resource.

Failure — five. Emitting only on the target size strands the tail. A combinational grant mutates the request under stall (§21). Releasing the grant when the requester deasserts hands the slot away mid-transfer. Accepting a beat without room overflows. And no epoch on prefetched descriptors161,182 stale executions.

15. Performance Counters and the Bottleneck Classifier

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Bottleneck attribution -- the chapter's debugging tool.
// EVERY COUNTER IS DIAGNOSTIC ONLY and drives no functional signal (P12).
// Priority is DECLARED so simultaneous causes are attributed consistently
// rather than double-counted.
module dma_bottleneck #(parameter int CW = 32) (
  input  logic clk,
  input  logic rst_n,
  input  logic tx_valid, tx_ready,
  input  logic work_available, tag_available, credit_granted, buffer_reserved,
  input  logic clear,
 
  output logic [CW-1:0] tx_busy_cycles,
  output logic [CW-1:0] tx_stall_cycles,
  output logic [CW-1:0] no_work_cycles,
  output logic [CW-1:0] no_tag_cycles,
  output logic [CW-1:0] no_credit_cycles,
  output logic [CW-1:0] no_buffer_cycles
);
  logic [CW-1:0] busy_q, stall_q, work_q, tag_q, cred_q, buf_q;
  assign {tx_busy_cycles, tx_stall_cycles} = {busy_q, stall_q};
  assign {no_work_cycles, no_tag_cycles}   = {work_q, tag_q};
  assign {no_credit_cycles, no_buffer_cycles} = {cred_q, buf_q};
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      busy_q<='0; stall_q<='0; work_q<='0; tag_q<='0; cred_q<='0; buf_q<='0;
    end else begin
      // ==============================================================
      // DECLARED PRIORITY (section 19's ladder reads these in this order):
      //   a transfer happened          -> busy
      //   offered but not accepted     -> downstream stall
      //   otherwise, the FIRST missing resource, in a fixed order
      //
      // Without a declared order, a cycle missing two resources is
      // counted twice and the percentages stop summing.
      // ==============================================================
      if (tx_valid && tx_ready)        begin if(!(&busy_q))  busy_q  <= busy_q  + CW'(1); end
      else if (tx_valid && !tx_ready)  begin if(!(&stall_q)) stall_q <= stall_q + CW'(1); end
      else if (!work_available)        begin if(!(&work_q))  work_q  <= work_q  + CW'(1); end
      else if (!tag_available)         begin if(!(&tag_q))   tag_q   <= tag_q   + CW'(1); end
      else if (!credit_granted)        begin if(!(&cred_q))  cred_q  <= cred_q  + CW'(1); end
      else if (!buffer_reserved)       begin if(!(&buf_q))   buf_q   <= buf_q   + CW'(1); end
    end
  end
endmodule

Classification: synthesizable (instrumentation).

The declared priority is what makes the counters sum. A cycle missing both a Tag and a credit is attributed once, to the first in the order — so the six counters partition every cycle and their proportions are meaningful.

Without an order they double-count, and §20's first question — "which resource is the bottleneck" — becomes unanswerable from the very numbers built to answer it.

16. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the high-speed blocks. LOCAL contract only. Nothing asserts
// that downstream becomes ready, that credits are returned, that
// Completions arrive, or that any bandwidth is achieved.
 
// ---- ADMISSION --------------------------------------------------------
 
// P1: ISSUE REQUIRES EVERY RESOURCE. Section 16 measured a two-term check:
// it issues without credit or buffer in 18.6% of resource states.
property p_admission_complete;
  @(posedge clk) disable iff (!rst_n)
  issue_allowed |-> (tag_available && credit_granted && buffer_reserved
                                   && tx_request_ready && work_available);
endproperty
a_adm : assert property (p_admission_complete);
 
// ---- TAG POOL ---------------------------------------------------------
 
// P2: AN ALLOCATED TAG WAS PREVIOUSLY FREE -- no double allocation.
property p_alloc_was_free;
  @(posedge clk) disable iff (!rst_n)
  alloc_valid |-> !busy_map[alloc_tag];
endproperty
a_alloc : assert property (p_alloc_was_free);
 
// P3: A TAG STAYS BUSY UNTIL EXPLICITLY FREED -- the lease from 20.3 §9.
property p_tag_lease;
  @(posedge clk) disable iff (!rst_n)
  (busy_map[t] && !(free_req && (free_tag == TW'(t)))) |=> busy_map[t];
endproperty
a_lease : assert property (p_tag_lease);
 
// P3b: freeing a non-busy or out-of-range Tag is REPORTED.
property p_bad_free;
  @(posedge clk) disable iff (!rst_n)
  (free_req && ((free_tag >= TW'(TAGS)) || !busy_map[free_tag])) |=> err_bad_free;
endproperty
a_badfree : assert property (p_bad_free);
 
// P4: THE OUTSTANDING COUNT EQUALS THE LIVE CONTEXT POPULATION. A drift
// here is a leak that stays invisible until Tags exhaust (mutation 18).
property p_outstanding_matches;
  @(posedge clk) disable iff (!rst_n)
  ($countones(busy_map) + free_count) == (TW+1)'(TAGS);
endproperty
a_pop : assert property (p_outstanding_matches);
 
// ---- SHARED RESOURCES -------------------------------------------------
 
// P5: A GRANT IS CONSUMED BY AT MOST ONE PRODUCER. The property section
// 16's first counterexample violates -- two engines spending one credit.
property p_single_consumer;
  @(posedge clk) disable iff (!rst_n)
  $onehot0(grant);
endproperty
a_one : assert property (p_single_consumer);
 
// ---- COMPLETION RESERVATION -------------------------------------------
 
// P8: RESERVED BYTES NEVER EXCEED CAPACITY. The core sink invariant.
property p_reservation_bounded;
  @(posedge clk) disable iff (!rst_n)
  reserved_bytes <= CW'(CAPACITY_BYTES);
endproperty
a_res : assert property (p_reservation_bounded);
 
// P8b: reservation happens BEFORE issue, never after (section 12).
property p_reserve_before_issue;
  @(posedge clk) disable iff (!rst_n)
  issue_allowed |-> buffer_reserved;
endproperty
a_pre : assert property (p_reserve_before_issue);
 
// P9: RELEASE IS BOUNDED AND EXACTLY ONCE -- releasing more than is held
// is reported, not silently wrapped (mutation 19).
property p_release_bounded;
  @(posedge clk) disable iff (!rst_n)
  (release_valid && (release_bytes > reserved_bytes)) |=> err_over_release;
endproperty
a_rel : assert property (p_release_bounded);
 
// ---- PAYLOAD PACKER ---------------------------------------------------
 
// P10: BYTE CONSERVATION. Section 16 verified this across 60,000 random
// (beat, target, ready) cases with zero violations.
property p_packer_conserves;
  @(posedge clk) disable iff (!rst_n)
  (buffered_bytes <= BW'(BUF_BYTES))
  && ((pkt_valid && pkt_ready) |-> (pkt_bytes <= buffered_bytes));
endproperty
a_pack : assert property (p_packer_conserves);
 
// P10b: THE FINAL PARTIAL PAYLOAD IS EMITTED. Without the flush condition
// a descriptor's tail is stranded forever (mutation 11).
property p_final_flush;
  @(posedge clk) disable iff (!rst_n)
  (last_q && (buffered_bytes != '0)) |-> pkt_valid;
endproperty
a_flush : assert property (p_final_flush);
 
// P11: THROUGHPUT ACCOUNTING COUNTS TRANSFERS, NOT OFFERS. The rule from
// 18.9 §15, 19.5 §15, 20.1 §19 -- fourth module, same law.
property p_count_on_fire;
  @(posedge clk) disable iff (!rst_n)
  (tx_busy_cycles > $past(tx_busy_cycles)) |-> ($past(tx_valid) && $past(tx_ready));
endproperty
a_cnt : assert property (p_count_on_fire);
 
// P12: DIAGNOSTICS DO NOT AFFECT FUNCTION. A counter that gates behaviour
// is a functional block wearing a diagnostic label (section 15).
property p_counters_inert;
  @(posedge clk) disable iff (!rst_n)
  clear |-> ($stable(issue_allowed) || $past(tx_ready) || $past(work_available));
endproperty
a_inert : assert property (p_counters_inert);
 
// ---- ARBITER ----------------------------------------------------------
 
// P13: THE GRANT IS STABLE UNDER STALL. Section 16's second counterexample
// is the combinational encoder that moves it mid-transaction.
property p_grant_held;
  @(posedge clk) disable iff (!rst_n)
  (grant_valid && !down_ready) |=> (grant_valid && $stable(grant_idx));
endproperty
a_hold : assert property (p_grant_held);
 
// P13b: and it is released ONLY on an actual transfer, not when the
// requester deasserts.
property p_release_on_transfer;
  @(posedge clk) disable iff (!rst_n)
  ($fell(grant_valid)) |-> $past(down_ready);
endproperty
a_reltx : assert property (p_release_on_transfer);
 
// ---- PREFETCH ---------------------------------------------------------
 
// P14: THE PREFETCH FIFO NEVER OVERFLOWS OR UNDERFLOWS.
property p_fifo_bounded;
  @(posedge clk) disable iff (!rst_n)
  (prefetch_count <= PF_W'(PREFETCH_DEPTH));
endproperty
a_fifo : assert property (p_fifo_bounded);
 
// P15: A STALE-EPOCH ITEM CANNOT EXECUTE. Section 16: 161,182 stale
// descriptors executed without this check, 0 with it.
property p_no_stale_execute;
  @(posedge clk) disable iff (!rst_n)
  item_is_stale |-> !descriptor_executes;
endproperty
a_stale : assert property (p_no_stale_execute);
 
// P15b: an abort or a new job changes the epoch, so everything already in
// the FIFO becomes stale by construction.
property p_epoch_advances;
  @(posedge clk) disable iff (!rst_n)
  (job_abort || job_start) |=> (current_epoch != $past(current_epoch));
endproperty
a_epoch : assert property (p_epoch_advances);
 
// P16: reset clears every ownership -- no Tag, reservation or grant
// survives.
property p_reset;
  @(posedge clk)
  !rst_n |=> ((busy_map == '0) && (reserved_bytes == '0) && !grant_valid);
endproperty
a_reset : assert property (p_reset);

P1 is the chapter's admission property, and it is deliberately a flat conjunction so a dropped term fails immediately rather than degrading quietly.

P5 and P13 are the shared-resource pair. P5 forbids two consumers of one grant (§16's first counterexample); P13 forbids the grant moving while a transaction is pending (§16's second).

P10 with P10b is the packer's conservation pair — bytes are never lost, and the tail is never stranded.

And P15 is the optimization-specific one. Prefetch is the only thing in this chapter that creates work which can outlive its reason, and P15 is what keeps §3's claim true: the faster engine is still the same ownership machine.

No liveness. "Downstream eventually ready", "credits are returned" and "Completions arrive" are environment properties. P13's held grant is the bounded form — ownership persists until the transfer, however long that takes.

17. Verification, Fault Injection, and Model Verification

Executed before publication. All performance numbers in this chapter are script-generated (§1).

Tag pool — 600,000 operations

Across TAGS = 1, 2, 3, 8, 32, including same-cycle free-and-allocate, compared against an independent set model: 0 disagreements. No double allocation, no invalid free accepted, exhaustion reported exactly when full.

Admission control — 200,000 resource vectors

Admission ruleIssues without a required resource
all five terms (§13)0
Tags and work only37,291 — 18.6%

Outstanding reads — derived scaling model

512-byte requests, 250-cycle round trip, 200,000 cycles: linear from 1 to 64 outstanding — 2.05 to 130.91 bytes/cycle, 64× (§6). Derived model, illustrative inputs (§1).

Payload packer — 60,000 cases

accepted == emitted + buffered across random beat patterns, target sizes and ready patterns: 0 violations, including the final short payload.

Prefetch epoch — 40,000 abort sequences

DesignStale descriptors executed
epoch-checked0
no epoch check161,182

Arbiter — 60,000 randomized runs

Three producers with random request and ready patterns: transfers never exceeded offers — no duplication, and the grant is structurally held because ownership clears only on transfer.

Directed tests

  • TAGS = 1, 2, 32 — allocation, exhaustion, same-cycle free+alloc. Required.
  • ROTATE = 0 and 1 — verify both are functionally correct and only distribution differs.
  • Each admission term removed in turn — verify issue_allowed falls (P1). Required.
  • Reservation exceeding capacity — verify refusal, not truncation (P8).
  • Release larger than reserved — verify err_over_release (P9).
  • Simultaneous reserve and release — verify the net is correct (§21).
  • Packer with beats smaller than target, larger, and exactly equal; final short payload (P10). Required.
  • in_last mid-buffer — verify flush emits the remainder exactly once.
  • Arbiter: all producers requesting, downstream stalled 50 cycles — verify the grant does not move (P13). Required.
  • A producer deasserting mid-grant — verify it keeps the grant until transfer.
  • Prefetch with abort — verify stale descriptors are discarded (P15). Required.
  • Counters — verify they change no functional signal (P12).

The scoreboard maintains independent Tag, reservation and byte models and never reads busy_map, reserved_bytes or the packer's buffer.

Mutations

#MutationCaught bySymptom
1same Tag allocated twiceP2Completions unattributable (20.3 §19)
2Tag freed before the final CompletionP3same, and it only appears with concurrency
3outstanding count decremented on the first fragmentP4Tag freed while data still returning
4admission ignores creditsP1flow-control violation — 18.6% of states
5admission ignores completion bufferP1Completion data with nowhere to go
6two engines spend the same creditP5over-subscription; one credit used twice (§10)
7arbiter grant changes under stallP13the request that transfers is not the one offered
8request counter advances on validP11throughput reported that did not happen
9prefetch FIFO overflowsP14descriptors silently dropped
10prefetched descriptor executes after abortP15161,182 stale executions (measured)
11packer never flushes the final partial payloadP10last bytes of every descriptor stranded
12packer duplicates a beat under stallP10byte conservation broken
13write progress advances before TX ownership20.1 P3b79.5% overshoot (measured there)
14larger MRRS without larger reservationP8Completion overrun
15fixed-priority arbiter starves descriptor fetchreview + P13throughput collapses when data is busy
16TAGS = 1 produces a zero-width indexelaborationbuild failure at the simplest configuration
17non-power-of-two FIFO pointer aliases20.2 P919.9% out of range (measured there)
18outstanding count disagrees with valid contextsP4leak invisible until Tags exhaust
19reservation released twiceP9reservation underflows; over-admission follows
20source FIFO assumes a bounded stall that does not existreview + §12overflow under sustained backpressure
21throughput counter counts offersP11measured bandwidth exceeds the Link's
22MPS and MRRS treated as one knobreview + 20.3 §8over-large writes or fragmented reads
23large MRRS assumed to give one Completion20.3 P12bretires on the first fragment
24Tags assumed the only limitP1adding Tags changes nothing; §20's scenario 2
25prefetch treated as ownership transferP15aborted job's descriptor executes in the next job
26credit observed rather than grantedP5§10's double-spend

Same-cycle audit

CaseDeclared resolution
Tag freed + allocatedfree first; the Tag is immediately re-allocatable (§13)
reserve + release same cyclenetted in one arithmetic step, so the invariant never breaks transiently
packer input beat + output payloadboth on their own handshake; the 2'b11 arm nets them
grant transfer + a higher-priority requestthe transfer completes; the new request wins the next arbitration
abort + a prefetched descriptor already poppedthe epoch check is on the consumer side, so a popped stale item is still discarded (P15)
all resources available + tx_ready lowcounted as a downstream stall, not a resource shortage (§15's priority)

18. Backpressure and the Limits of Buffering

A high-speed producer cannot assume the transmit path is always ready. What to do about it depends entirely on whether the source can be stalled.

SourceCan it stall?Required architecture
stream with readyyespropagate backpressure; a FIFO only smooths bursts
fixed-rate samplernosized elastic buffer and a defined overflow behaviour
memory reader under our controlcontrolledschedule reads against buffer occupancy

19. The Throughput Waveform

Three phases, showing that fixing one bottleneck reveals the next. Internal teaching signals.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                    PHASE A          PHASE B           PHASE C
                 1 outstanding   32 outstanding   32 outstanding
cycle           1   2   3   4  |  5   6   7   8  |  9  10  11  12
desc_ready      1   1   1   1  |  1   1   1   1  |  1   1   1   1
request_valid   1   0   0   0  |  1   1   1   1  |  1   1   0   0
request_ready   1   0   0   0  |  1   1   1   1  |  1   1   0   0
outstanding     1   1   1   1  | 12  20  28  32  | 32  32  32  32
tag_free_count 31  31  31  31  | 20  12   4   0  |  4   2   0   0
np_credit_ok    1   1   1   1  |  1   1   1   1  |  1   1   0   0
cpl_buf_free   16k 16k 16k 16k | 12k  8k  4k  2k |  1k  0   0   0
cpl_valid       0   0   0   1  |  0   1   1   1  |  1   1   1   1
tx_utilized     1   0   0   0  |  1   1   1   1  |  1   1   0   0
stall_reason    -  RTT RTT RTT |  -   -   -   -  |  -   -  CRD BUF

Phase A — one outstanding read. request_valid fires once, then three idle cycles waiting for the round trip. tag_free_count sits at 31 — 31 Tags unused while the Link idles. This is §6's 1× row.

Phase B — 32 outstanding. Requests issue every cycle, outstanding climbs to 32, and tx_utilized stays high. The latency is now hidden by concurrency, not removed.

Phase C — the next bottleneck appears. tag_free_count reaches 0 and, one cycle later, np_credit_ok drops and cpl_buf_free hits zero. stall_reason changes from CRD to BUF.

That transition is the whole lesson. Optimizing the request side moved the constraint to the return side. Adding more Tags in Phase C would do nothing — §20's second scenario, and §15's counters are what tell you so.

20. Debugging

Symptom → which resource → counter → experiment.

Do not start at the PHY. Read §15's counters and compute proportions — the declared priority makes them partition every cycle.

Dominant counterBottleneckFix
tx_stall_cyclesdownstream — the PCIe path is not acceptingarbitration, credits at the core, Link state
no_work_cyclesdescriptor starvationprefetch (§11), ring refill, software
no_tag_cyclesTag-limitedExtended Tag, or larger requests (§7)
no_credit_cyclescredit-limitedthe partner is not returning credits (16.6)
no_buffer_cyclessink-limitedcompletion buffering, downstream drain rate

One reading identifies the class, which is the point of building them.

Throughput stops improving when Tags are added

Expected (§7), and §19's Phase C is the picture. Another resource became the constraint.

The distinguishing experiment: add Tags and re-read the counters. If no_tag_cycles fell to near zero and no_credit_cycles or no_buffer_cycles rose to take its place, the Tag limit is gone and the next one has bound. Adding still more Tags is now provably useless.

And check request size instead (§7's table): 32 Tags at 4 KiB reaches the same modelled ceiling as 256 Tags at 512 B.

It works at small transfers and fails under sustained load

A leak, and the counters localize it. Watch tag_free_count and reserved_bytes over a long run: a monotonic drift toward zero that never recovers is a leak, not congestion.

Three candidates: a Tag not freed on an error path (Chapter 20.6 §14 measures this at 99.9%), a reservation released on success but not on error, or a grant never released because the arbiter waits for a producer that withdrew.

The distinguishing experiment: inject errors deliberately and watch whether the free counts return to their initial values. They must.

Throughput oscillates

Look at occupancy over time rather than at averages. Periodic collapse usually means a supply-side refill cycle: the prefetch FIFO drains, descriptors run out, the engine idles while the next batch is fetched, then bursts.

no_work_cycles rising in bursts confirms it, and the fix is prefetch depth (Chapter 20.5 §11) or ring management — not more Tags.

21. Common Misconceptions

  • "Maximum Link bandwidth is what the application gets." Four multiplicative terms, smallest wins (§2).
  • "Larger MPS always improves throughput." Collapsing returns: +7.2 points at 128→256, +0.6 at 2048→4096 (§8).
  • "Larger MRRS always improves throughput." It trades against request count and buffering (§7, §12).
  • "More Tags always improves throughput." Only if Tags are the binding resource — §19's Phase C.
  • "One outstanding read can saturate a fast Link." 64× below the same hardware's capability in the derived model (§6).
  • "Credits are a correctness concern, not a performance one." They are a first-class throughput resource (§9).
  • "Descriptor prefetch transfers ownership early." Fetched ≠ executing (§11); 161,182 stale executions without an epoch.
  • "A deeper FIFO fixes backpressure." Not if the stall is unbounded (§18).
  • "An arbiter may re-evaluate every cycle." The grant must be held (§17's counterexample).
  • "Writes are unlimited because they are posted." They consume posted credits and transmit opportunity (§4).
  • "Completion ordering makes Tag context unnecessary." Chapter 20.3 §19's third counterexample.
  • "Performance work means wider datapaths." It means occupancy — every stage productive at once (§2).

22. Understanding Check

23. What's Next

Throughput is occupancy, not packet size.

Four multiplicative terms (§2), and the smallest wins — so optimizing a term that is not binding produces exactly zero improvement, which §19's waveform shows and §20's counters diagnose.

Reads need concurrency (§6): 64× between one outstanding request and 64 in the derived model. And Tags are one resource of four (§9) — an admission check that forgets credits or buffering issues illegally in 18.6% of states.

Every optimization here added concurrency and relaxed nothing (§3). Prefetch needed an epoch (161,182 stale executions without one); shared resources needed grants rather than observations; the arbiter needed to hold its choice.

Chapter 20.6 — FPGA Examples closes Module 20 by composing everything: the descriptor engine, the read and write engines, the shared transmit scheduler, the Completion path with its context RAM, the buffers, the error cleanup, and the interrupt hand-off — with the clock-domain, memory-latency and reset boundaries that only appear when the blocks become one design.

The idea to carry forward: availability is not permission.