Skip to content

PCIe · Module 14

Sequence Numbers — Identity in a Space That Wraps

The identity every chapter in Module 14 has been abstracting. A Link-local number that wraps, compared by modular distance rather than by less-than — and a design that uses ordinary comparison works for millions of packets and then fails exactly once, at rollover.

Four chapters have used an abstraction and said so every time. Chapter 14.2 retired a prefix by searching for an identity. Chapter 14.3 built an entire FSM to walk a window one position per cycle. Chapter 14.4 gave its store a second read port so that search could happen.

All of that machinery exists because the comparison was not available. This chapter supplies it — and the searches disappear.

How does PCIe identify TLP delivery progress on one Link, distinguish the next expected packet from a duplicate or a gap, and compare wrapping sequence identities without confusing Link reliability with Transaction Layer Tags or ordering?

1. The Verified Mechanism

2. A Sequence Number Is Not a Tag

Both are identities attached to a packet. They belong to different layers, have different scopes, and answer different questions.

Sequence NumberTag
LayerData LinkTransaction
Scopeone Linkthe transaction, end to end
Identifiesthis packet's place in Link delivery orderwhich Request a Completion answers
Assigned bythe transmitting port of each Linkthe Requester, once
Survives a hop?no — reassignedyes (Chapter 11.3 §6)
Used byACK, NAK, replayCompletion correlation (Chapter 10.2 §6)
Wraps within4096its own field width

A sequence number is consumed when a packet enters Link reliability ownership — and only then.

EventConsumes a new sequence value?
a new TLP accepted from the Transaction Layeryes
that TLP transmitted the first timeno — it already has one
that TLP replayedno — it keeps the one it was assigned
a Switch forwarding it onto the next Linkyes — a new one, from that Link's counter

The third row is Chapter 14.3 §6's identity distinction, expressed in the numbering. One Transaction Layer packet, one sequence value per Link, any number of transmissions.

A design that assigns a fresh value on replay makes the retransmission look like a different packet: the receiver would not recognise it as a duplicate, the transmitter's retained entry would not match the acknowledgement, and the replay would achieve nothing except consuming the numbering space.

4. Generic Modular Arithmetic First

Before any PCIe constant, the mathematics — because it is the durable part and it is correct for any width.

For an N-bit identity space, the modulus is 2^N, and:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
delta = (candidate − reference)  mod  2^N

In fixed-width unsigned logic that subtraction wraps for free. a - b on SEQ_W-bit values is the modular difference; no explicit modulo operator is needed.

But the delta alone decides nothing. It is a distance around a ring, and a ring has no inherent "before" and "after" — going forward 4095 and going backward 1 are the same step.

5. The Wrap, With Small Numbers

A 3-bit space — modulus 8, window 4 — because 12 bits obscures the shape.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        7   0
      6       1
      5       2
        4   3          ← the ring; there is no "end"

Counting: … 5, 6, 7, 0, 1, 2 …. After 7 comes 0, and 0 is newer.

Now the classification, in the order it must be evaluated:

expectedrxdelta = (expected − rx) mod 8Verdict
000the expected packet — accept
071history — duplicate
062history — duplicate
017ahead — gap
677ahead — gap

Read row 2 and row 4 together. With expected = 0, the value 7 is a duplicate and the value 1 is a gap — and 7 is numerically larger than 1. Any comparison based on magnitude gets both backwards.

The evaluation order matters and is not optional:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1.  rx == expected                          →  accept, advance expected
2.  else (expected − rx) mod M  ≤  window   →  duplicate
3.  else                                    →  gap

Step 1 comes first because delta == 0 also satisfies step 2's inequality. A classifier that tested for duplicates before testing for equality would call every correct packet a duplicate.

6. The Counterexample

This is the failure the chapter exists to prevent, and it is one line of code.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG.
if (rx_seq < expected_seq)
  duplicate = 1'b1;

It is correct everywhere except at one boundary.

expectedrxnaive rx < expectedcorrect verdict
53duplicate ✓duplicate ✓
57not duplicate ✓gap ✓
40950duplicate ✗the expected packet — accept
04095not duplicate ✗duplicate

7. A Trace Across the Wrap

Internal teaching signals, not PCIe wire signals. SEQ_W = 12, so the modulus is 4096.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
step            1      2      3      4      5      6      7      8
tl_accept       1      1      1      0      0      1      0      0
tx_seq       4094   4095      0      -      -      1      -      -
next_tx_seq  4095      0      1      1      1      2      2      2
 
rx_seq          -   4094   4095      0   4095      0      1      -
expected_seq 4094   4094   4095      0      0      1      1      2
rx_class        -    EXP    EXP    EXP    DUP    DUP    EXP      -
deliver_up      -      1      1      1      0      0      1      -

Read steps 2–4. The receiver accepts 4094, 4095, then 0 — and expected_seq goes 4094 → 4095 → 0 → 1. The wrap is unremarkable when the comparison is modular: 0 is simply the next value.

Read step 5. 4095 arrives again — a replay. expected is 0, so delta = (0 − 4095) mod 4096 = 1, which is inside the window: duplicate. deliver_up is 0 and expected_seq does not move. That is the property that stops a Link replay from becoming a duplicate Transaction Layer operation (Chapter 14.1 §5).

Read step 6. 0 arrives again, also a duplicate — delta = 0and this is exactly why the classifier tests equality against expected first, not against history. expected is now 1, so (1 − 0) mod 4096 = 1 → duplicate. Correct.

And read next_tx_seq at step 3. It goes 4095 → 0 with no special case: the counter is SEQ_W bits wide, so it wraps by construction. §9's assigner has no wrap logic at all, which is the point.

8. RTL — Modular Distance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE (function: compile-time elaborated).
// Modular distance in a SEQ_W-bit identity space.
// That fixed-width unsigned subtraction IS modular subtraction: arithmetic
// fact. The INTERPRETATION of the result is section 4's argument and is not
// contained in this function.
function automatic logic [SEQ_W-1:0] seq_delta
    (input logic [SEQ_W-1:0] a, input logic [SEQ_W-1:0] b);
  // No modulo operator, no conditional, no width extension. On SEQ_W-bit
  // unsigned values this subtraction wraps at 2^SEQ_W by construction —
  // which is exactly (a - b) mod 2^SEQ_W.
  seq_delta = a - b;
endfunction

Classification: synthesizable (a function, elaborated into its callers).

What it is. One subtractor. What it is not: a legality test. seq_delta answers how far around the ring, and nothing about direction or validity — that requires the window, which is §9's job.

The mistake it exists to prevent is reaching for % or for a wider intermediate. Widening the operands before subtracting destroys the wrap: {1'b0,a} - {1'b0,b} produces a signed-looking result that no longer represents the ring, and a design that then compares it against a threshold has silently reintroduced §6's bug in a more expensive form.

9. RTL — Sequence Classifier

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Classify a received sequence identity against the
// receiver's expected value, using modular distance within a bounded window.
// The modular comparison and the half-range window: NORMATIVE for the
// conventional non-Flit representation (section 1). The enum and the
// reported conditions: ILLUSTRATIVE normalized metadata.
package seq_pkg;
 
  typedef enum logic [1:0] {
    SEQ_EXPECTED  = 2'd0,   // exactly the next packet
    SEQ_DUPLICATE = 2'd1,   // within the history window — already seen
    SEQ_GAP       = 2'd2    // ahead of expected — something is missing
  } seq_class_e;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import seq_pkg::*;
 
module seq_classifier #(
  // 12 for the conventional PCIe representation (section 1). Parameterised
  // because the mechanism is architectural and the width is not.
  parameter int SEQ_W  = 12,
  // Half the space. This is not a tuned constant — it is the consequence of
  // the transmit throttle bounding the live population to half the ring
  // (section 4). Changing one without the other breaks the classification.
  parameter int WINDOW = (1 << (SEQ_W - 1))
) (
  input  logic [SEQ_W-1:0] rx_seq,
  input  logic [SEQ_W-1:0] expected_seq,
 
  output seq_class_e       rx_class,
  output logic [SEQ_W-1:0] distance     // telemetry: how far into history
);
 
  generate
    if (SEQ_W < 2)                     $error("SEQ_W must be at least 2");
    if (WINDOW > (1 << (SEQ_W - 1)))   $error("WINDOW must not exceed half the space");
  endgenerate
 
  // How far BACK from expected the received value sits, around the ring.
  wire [SEQ_W-1:0] back = expected_seq - rx_seq;
 
  assign distance = back;
 
  always_comb begin
    // ORDER MATTERS (section 5). Equality is tested FIRST, because a delta
    // of zero also satisfies the duplicate inequality — a classifier that
    // checked duplicates first would reject every correct packet.
    if (rx_seq == expected_seq)
      rx_class = SEQ_EXPECTED;
    else if (back <= SEQ_W'(WINDOW))
      // Inside the history window: already delivered.
      rx_class = SEQ_DUPLICATE;
    else
      // Beyond it: this is ahead of expected, so something is missing.
      rx_class = SEQ_GAP;
  end
 
endmodule

Classification: synthesizable (package: compile-time).

Architecture. One subtractor, one comparator, one equality test. There is no %, no wrap detection, and no special case for the boundary — because with modular subtraction the boundary is not special.

Contract. The caller guarantees the transmit throttle is enforced, so the live population never exceeds WINDOW. Without that, the classification is not merely approximate — it is undefined, because history and future become indistinguishable (§4).

Failure — four. rx_seq < expected_seq is §6's bug. Testing duplicates before equality rejects every correct packet. Widening the operands before subtracting destroys the wrap. And setting WINDOW larger than half the space makes some values classify as both, which the elaboration check refuses.

Deliberately simplified: three outcomes only; no integrity interaction; no response generation (Chapter 15.4).

10. RTL — Transmit Assigner and Receive Tracker

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Assign a sequence identity to each TLP entering Link
// reliability ownership, and throttle when too many are unacknowledged.
// The 12-bit modulo-4096 counter and the half-range throttle: NORMATIVE for
// the conventional representation (section 1). Port names: ILLUSTRATIVE.
module seq_tx_assign #(
  parameter int SEQ_W  = 12,
  parameter int WINDOW = (1 << (SEQ_W - 1))
) (
  input  logic clk,
  input  logic rst_n,
 
  // ---- New TLP entering reliability ownership --------------------------
  input  logic              tl_valid,
  output logic              tl_ready,
  output logic [SEQ_W-1:0]  assigned_seq,
 
  // ---- Acknowledgement progress (Chapter 14.2's frontier) --------------
  input  logic              ackd_valid,
  input  logic [SEQ_W-1:0]  ackd_seq,
 
  output logic [SEQ_W-1:0]  next_transmit_seq,
  output logic [SEQ_W-1:0]  ackd_seq_state,
  // SEQUENCE-SPACE SEPARATION between the two counters. This is NOT the
  // number of retained packets — see section 10a. Named accordingly.
  output logic [SEQ_W-1:0]  seq_separation,
  output logic              throttled
);
 
  logic [SEQ_W-1:0] next_q, ackd_q;
 
  // NORMATIVE THROTTLE (section 1). Note what this expression is and is not:
  // it is the modular DISTANCE from the most recently acknowledged sequence
  // number to the next one to be assigned. With ACKD_SEQ initialised to all
  // ones and NEXT_TRANSMIT_SEQ to zero, it reads 1 when nothing is
  // outstanding — so it is the retained count PLUS ONE, always (section 10a).
  wire [SEQ_W-1:0] separation = next_q - ackd_q;    // modular by construction
 
  assign seq_separation     = separation;
  assign throttled          = (separation >= SEQ_W'(WINDOW));
  assign tl_ready           = !throttled;
  assign assigned_seq       = next_q;
  assign next_transmit_seq  = next_q;
  assign ackd_seq_state     = ackd_q;
 
  wire accept = tl_valid && tl_ready;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      // NORMATIVE INITIALISATION (section 1): NEXT_TRANSMIT_SEQ to zero,
      // ACKD_SEQ to ALL ONES. Initialising ACKD_SEQ to zero instead would
      // make the separation read 0 with nothing sent — and the very first
      // packet would then be indistinguishable from a full window.
      next_q <= '0; ackd_q <= {SEQ_W{1'b1}};
    end else begin
      // Advances on ACCEPTANCE of a NEW packet — not on transmission, and
      // NOT on replay (section 3). A replay reuses the identity the packet
      // already owns, which is why this module never sees replay events.
      // No wrap logic: a SEQ_W-bit counter wraps at 2^SEQ_W by construction.
      if (accept) next_q <= next_q + SEQ_W'(1);
      if (ackd_valid) ackd_q <= ackd_seq;
    end
  end
 
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Track the next expected sequence identity on receive.
// That expected advances only on a correctly received next packet:
// NORMATIVE consequence of the duplicate/gap classification (section 1).
// The integrity input and the reported conditions: ILLUSTRATIVE — this
// module does not compute LCRC.
import seq_pkg::*;
 
module seq_rx_track #(
  parameter int SEQ_W  = 12,
  parameter int WINDOW = (1 << (SEQ_W - 1))
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic              rx_valid,
  input  logic [SEQ_W-1:0]  rx_seq,
  // ABSTRACT integrity verdict from machinery this module does not
  // implement (Chapter 14.1 section 9).
  input  logic              rx_integrity_ok,
 
  output logic              deliver_up,      // pass to the Transaction Layer
  output logic              duplicate_seen,
  output logic              gap_seen,
  output logic [SEQ_W-1:0]  expected_seq
);
 
  logic [SEQ_W-1:0] exp_q;
  seq_class_e       cls;
 
  seq_classifier #(.SEQ_W(SEQ_W), .WINDOW(WINDOW)) u_cls (
    .rx_seq(rx_seq), .expected_seq(exp_q), .rx_class(cls), .distance()
  );
 
  // INTEGRITY FIRST. A corrupted packet's sequence field is exactly as
  // suspect as the rest of it, so classifying it is meaningless — and
  // advancing `expected` on it would desynchronise the receiver
  // (Chapter 14.1 section 9's ordering, applied here).
  wire usable = rx_valid && rx_integrity_ok;
 
  assign expected_seq   = exp_q;
  assign deliver_up     = usable && (cls == SEQ_EXPECTED);
  assign duplicate_seen = usable && (cls == SEQ_DUPLICATE);
  assign gap_seen       = usable && (cls == SEQ_GAP);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      exp_q <= '0;
    end else begin
      // Advances ONLY on the expected packet. Not on a duplicate — it was
      // already counted. Not on a gap — the missing packets have not
      // arrived, and skipping them would deliver a hole. Not on a corrupted
      // packet — its identity is unreliable. Each exclusion is a distinct
      // bug if omitted (section 13's mutation table).
      if (deliver_up) exp_q <= exp_q + SEQ_W'(1);
    end
  end
 
endmodule

Classification: both synthesizable.

Architecture. Two counters and a classifier. Neither module contains wrap logic — the counters are SEQ_W bits and wrap by construction, and the comparison is modular.

Same-cycle contract:

SituationResolution
accept + acknowledgement updateboth apply; seq_separation recomputes next cycle
a duplicate arrivingdeliver_up low, exp_q unchanged
a gap arrivingdeliver_up low, exp_q unchanged, gap_seen set
a corrupted packetnot classified at allusable is low
throttled + tl_validtl_ready low; the packet waits

Failure — five, and §13 maps each. Advancing next_q on transmission rather than acceptance consumes an identity per attempt, so a replay takes a new one (§3). Advancing exp_q on a duplicate double-counts and permanently offsets the receiver. Advancing it on a gap delivers a hole. Advancing it on a corrupted packet trusts a field that is not trustworthy. And omitting the throttle lets the live population exceed half the ring, after which the classifier is not wrong so much as meaningless.

Deliberately simplified: no LCRC; no response generation (Chapter 15.4); no initialisation beyond reset-to-zero.

10a. Separation Is Not Occupancy

Four quantities live near each other in a transmitter, and three of them are routinely called "outstanding". They are different numbers.

QuantityWhat it isWhere it lives
sequence separation(NEXT_TRANSMIT_SEQ − ACKD_SEQ) mod M — a distance in the numbering space§10's assigner
retained occupancyhow many entries the replay buffer actually holdsChapter 14.4 §6
packets senta cumulative totaltelemetry (Chapter 12.5 §14)
packets acknowledgedanother cumulative totaltelemetry

This is a general hazard, not a PCIe quirk. Any protocol with a "last completed" pointer and a "next to issue" pointer has the same off-by-one, and calling the distance an occupancy is the most common way a correct design acquires a wrong-looking bug report — followed by a "fix" that breaks the comparison the distance was for.

11. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over seq_classifier, seq_tx_assign and seq_rx_track. These assert the
// NORMATIVE modular comparison and throttle of section 1 plus the LOCAL
// counter contracts. Nothing here asserts DLLP format (Module 15) or LCRC.
 
// ---- ENVIRONMENT -----------------------------------------------------
// A1: acknowledgement progress is itself within the window — the frontier
// never names something outside the live population.
assume property (@(posedge clk) disable iff (!rst_n)
  ackd_valid |-> ((next_q - ackd_seq) <= SEQ_W'(WINDOW)));
 
// ---- TRANSMIT --------------------------------------------------------
 
// P1: a new packet consumes EXACTLY ONE identity, and nothing else does.
property p_seq_advances_on_acceptance_only;
  @(posedge clk) disable iff (!rst_n)
  !$stable(next_q) |-> ($past(accept)
                     && (next_q == $past(next_q) + SEQ_W'(1)));
endproperty
a_tx_advance : assert property (p_seq_advances_on_acceptance_only);
 
// P2: THE WRAP PROPERTY. From the maximum, the next value is zero — with no
// special case anywhere in the design.
property p_wrap_max_to_zero;
  @(posedge clk) disable iff (!rst_n)
  (accept && (next_q == {SEQ_W{1'b1}})) |=> (next_q == '0);
endproperty
a_wrap : assert property (p_wrap_max_to_zero);
 
// P3: THE NORMATIVE THROTTLE. Never more than WINDOW unacknowledged.
property p_throttle_bounds_separation;
  @(posedge clk) disable iff (!rst_n)
  accept |-> (seq_separation < SEQ_W'(WINDOW));
endproperty
a_throttle : assert property (p_throttle_bounds_separation);
 
// P3a: THE NAMING PROPERTY (section 10a). The separation is the retained
// count PLUS ONE — asserted so the distinction cannot quietly decay back
// into "outstanding". `retained_count` is the replay buffer's occupancy
// (Chapter 14.4 section 6), supplied by the testbench.
property p_separation_is_retained_plus_one;
  @(posedge clk) disable iff (!rst_n)
  (seq_separation == SEQ_W'(retained_count) + SEQ_W'(1));
endproperty
a_separation_meaning : assert property (p_separation_is_retained_plus_one);
 
// P3b: the initialisation that makes P3a true at reset. With nothing sent
// and nothing retained, the separation reads 1 — not 0.
property p_reset_separation_is_one;
  @(posedge clk)
  !rst_n |=> (seq_separation == SEQ_W'(1));
endproperty
a_reset_sep : assert property (p_reset_separation_is_one);
 
// P4: the assigned identity is stable while the consumer stalls.
property p_assigned_stable;
  @(posedge clk) disable iff (!rst_n)
  (tl_valid && !tl_ready) |=> $stable(assigned_seq);
endproperty
a_assigned_stable : assert property (p_assigned_stable);
 
// ---- CLASSIFIER ------------------------------------------------------
 
// P5: THE CENTRAL PROPERTY. The classification matches an independent
// modular reference. `ref_class` is a testbench function computing the same
// thing with integer modulo arithmetic — NOT a call into the DUT.
property p_matches_modular_reference;
  @(posedge clk) disable iff (!rst_n)
  (rx_class == ref_class(rx_seq, expected_seq, WINDOW));
endproperty
a_ref_match : assert property (p_matches_modular_reference);
 
// P6: equality wins. The expected value is never classified as a duplicate.
property p_equal_is_expected;
  @(posedge clk) disable iff (!rst_n)
  (rx_seq == expected_seq) |-> (rx_class == SEQ_EXPECTED);
endproperty
a_equal : assert property (p_equal_is_expected);
 
// P7: THE COUNTEREXAMPLE PROPERTY (section 6). At the wrap boundary, the
// value zero following a maximum expected value is the NEXT packet, not a
// duplicate. This is the one a naive comparator fails.
property p_wrap_zero_is_not_duplicate;
  @(posedge clk) disable iff (!rst_n)
  ((expected_seq == '0) && (rx_seq == '0)) |-> (rx_class == SEQ_EXPECTED);
endproperty
a_wrap_class : assert property (p_wrap_zero_is_not_duplicate);
 
// P8: classification is total and exclusive — exactly one outcome always.
property p_classification_total;
  @(posedge clk) disable iff (!rst_n)
  (rx_class inside {SEQ_EXPECTED, SEQ_DUPLICATE, SEQ_GAP});
endproperty
a_total : assert property (p_classification_total);
 
// ---- RECEIVE ---------------------------------------------------------
 
// P9: expected advances ONLY on a delivered packet. Four exclusions in one
// property: not on duplicate, not on gap, not on corrupted, not on idle.
property p_expected_advances_only_on_deliver;
  @(posedge clk) disable iff (!rst_n)
  !$stable(exp_q) |-> ($past(deliver_up)
                    && (exp_q == $past(exp_q) + SEQ_W'(1)));
endproperty
a_rx_advance : assert property (p_expected_advances_only_on_deliver);
 
// P10: A DUPLICATE IS NEVER DELIVERED UPWARD. The property that stops a Link
// replay from becoming a duplicate Transaction Layer operation.
property p_duplicate_not_delivered;
  @(posedge clk) disable iff (!rst_n)
  duplicate_seen |-> !deliver_up;
endproperty
a_no_dup_up : assert property (p_duplicate_not_delivered);
 
// P11: a gap is never delivered upward — delivering it would hand the
// Transaction Layer a hole.
property p_gap_not_delivered;
  @(posedge clk) disable iff (!rst_n)
  gap_seen |-> !deliver_up;
endproperty
a_no_gap_up : assert property (p_gap_not_delivered);
 
// P12: a corrupted packet is not classified and does not move anything.
property p_corrupt_inert;
  @(posedge clk) disable iff (!rst_n)
  (rx_valid && !rx_integrity_ok)
    |-> (!deliver_up && !duplicate_seen && !gap_seen) ##1 $stable(exp_q);
endproperty
a_corrupt_inert : assert property (p_corrupt_inert);
 
// P13: reset initialises both counters to a defined value.
property p_reset_defined;
  @(posedge clk)
  !rst_n |=> ((next_q == '0) && (exp_q == '0));
endproperty
a_reset : assert property (p_reset_defined);

P5 is the property that subsumes the others and it is worth the reference model. An independent integer-modulo function classifying the same pair catches every comparison bug at once, including ones nobody anticipated. P6, P7 and P8 remain because they name specific failures — and a failure that names its cause is worth more than one that says "did not match reference."

P3a is the property that keeps §10a's distinction from decaying. A separation of 1 with nothing retained looks like an off-by-one to anyone who has not read §10a, and the natural "fix" is to subtract one somewhere — which then breaks the normative throttle comparison. Asserting the relationship explicitly means the next person to look at it finds the invariant rather than the apparent bug.

P9's four exclusions are deliberately one property. Duplicate, gap, corrupted and idle are four different ways expected could wrongly advance, and each produces the same catastrophic outcome: the receiver permanently out of step. Stating it as "advances only on delivery" covers all four and cannot be partially satisfied.

12. Verification

Monitors observe: the acceptance interface with the assigned identity, the acknowledgement input, the receive interface with its integrity verdict, and every classifier and tracker output.

The scoreboard uses an independent integer reference model:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// VERIFICATION-ONLY. Reference classification in integer arithmetic.
// Deliberately NOT the DUT's expression — a mirror of `expected - rx`
// would agree with a design that widened its operands and destroyed the
// wrap, because both would compute the same wrong thing.
function automatic seq_class_e ref_class(int rx, int exp, int window);
  int m, back;
  m    = 1 << SEQ_W;
  back = ((exp - rx) % m + m) % m;      // explicit, positive modulo
  if (rx == exp)          return SEQ_EXPECTED;
  else if (back <= window) return SEQ_DUPLICATE;
  else                     return SEQ_GAP;
endfunction

Boundary tests — mandatory

These are the chapter, and a test plan without them has verified nothing that matters.

  • The first value after reset.
  • A middle value — 2000, say. The easy case.
  • MAX − 1, then MAX. Approaching the boundary.
  • MAX0. The wrap itself: verify next_q and exp_q both roll with no special case (P2).
  • A duplicate arriving just before the wrapexpected = 4095, rx = 4094.
  • A duplicate arriving just after the wrapexpected = 0, rx = 4095. The mutant's first failure (§11).
  • A new packet just after the wrapexpected = 4095, rx = 0 in the transmit direction. The mutant's second failure.
  • A forward gap across the wrapexpected = 4094, rx = 1.
  • Several sequential packets straight through the wrap — 4090 to 5, checking expected at every step.
  • A replay across the wrap — a packet assigned 4095, replayed after expected has moved to 2.

And start from a randomised sequence value, not always zero. A bench that resets to 0 every run needs 4096 packets before it can reach the boundary at all — which is why this bug survives regression suites that run for hours.

Window and throttle

  • Fill to WINDOW − 1 unacknowledged. Verify tl_ready is still high.
  • Reach WINDOW. Verify tl_ready drops (P3) and no further identity is consumed.
  • Acknowledge, then verify acceptance resumes.
  • The throttle across the wrapnext_q past 4095 with ackd_q below it. Verify seq_separation computes modularly (next=10, ackd=4090 → separation=16, not a negative number).
  • The simple case, explicitly: send one packet, acknowledge it, and verify retained_count is zero while seq_separation reads 1 (P3a). This is the test that catches a design calling the separation "outstanding" — it would report one retained packet where there are none.

Receive classification

  • Every combination of expected at 0, 1, MAX−1, MAX against rx at each of the same, plus expected ± WINDOW.
  • A duplicate, then the expected packet. Verify expected moved only once.
  • A gap, then the missing packet. Verify expected did not advance on the gap.
  • A corrupted packet at each classification boundary. Verify none is classified and expected never moves (P12).
  • SEQ_W = 3. The teaching width from §5 — small enough to enumerate the entire space exhaustively, which is the strongest test available and costs nothing.

Mutations and what kills each

#MutationCaught by
1identity advances on send attempt, not acceptanceP1, on the first replay
2replay consumes a new identityP1, and the retained entry stops matching acknowledgements
3wrap omitted — counter saturatesP2
4signed comparisonP5, at half the space
5ordinary < across the wrapP5, P7 — §11's counterexample
6receiver advances expected on a duplicateP9, and every later packet misclassifies
7receiver advances expected on a corrupted packetP9, P12
8a gap delivered upwardP11
9a duplicate delivered upwardP10 — and the Transaction Layer performs the operation twice
10two live packets assigned the same identityP3 — only reachable if the throttle is removed
11ACK matched against a Tag instead of a Sequence NumberChapter 14.2's retirement finds nothing; event_unknown
12a Switch forwards the incoming identity instead of assigning its own§13's fourth scenario — fails only through a Switch

13. Debugging

The strongest signature in Module 14. Go straight to the comparison.

Grep for <, >, <= and >= applied to sequence values. Any magnitude comparison between two identities is wrong unless it is a comparison of a modular difference against a window bound.

Then check the operand widths. A design that widened before subtracting has destroyed the wrap even if it looks modular (§8).

And note what to rule out immediately. The physical layer did not change at packet 4096. The error rate did not change. The only thing that changed is that a counter reached its maximum — and if the design has been running correctly until then, everything except the comparison has already been proven.

The same TLP is delivered upward twice, but only after a replay

Duplicate suppression. The receiver classified the replayed packet as SEQ_EXPECTED rather than SEQ_DUPLICATE.

Two causes. The comparison is wrong — check §11's counterexample cases specifically. Or expected advanced when it should not have: if it advanced on the original packet and something else, it is now ahead, and the replay lands outside the history window and reads as a gap or as expected.

The observation: print expected_seq and rx_seq for both deliveries. If expected moved twice for one logical packet, the tracker is the bug; if it moved once and the classification differed, the classifier is.

The replay buffer reports an unknown acknowledgement, but only near the wrap

The retirement frontier and the sequence comparison disagree.

Chapter 14.2 retires by matching an identity; once that search is replaced by modular arithmetic, the frontier computation is a sequence comparison — and if it uses a different rule from the classifier, the two disagree exactly at the boundary.

Check that both use the same modulus and the same window. A classifier at 12 bits and a frontier computation that widened to 16 will agree for 4095 packets out of 4096.

The problem appears only through a Switch, never point to point

A Link-local identity is being forwarded instead of reassigned (§2).

The Switch must assign its own Link B sequence value from Link B's counter. If it forwards the value the packet arrived with, Link B's numbering is now driven by Link A's traffic — and the two counters diverge immediately, because they were never related.

The tell: capture the same logical packet on both Links and compare the sequence values. They should be unrelated. If they are equal, that is the bug — and it is invisible point to point, because with one Link there is nothing to forward to.

14. Common Misconceptions

  • "A Sequence Number is a Requester Tag." Different layers, different scopes. A Tag survives every hop; a Sequence Number is reassigned at each one (§2).
  • "One TLP keeps the same Sequence Number end to end." Each Link assigns its own from its own counter (§2, §3).
  • "A replay gets a new Sequence Number because it is a new send." It keeps the one it was assigned. A new one would make it unrecognisable as a duplicate (§3).
  • "Ordinary integer comparison is enough." It is correct everywhere except the boundary, and the boundary occurs once per wrap period (§6).
  • "Zero is always older than MAX." On a ring, 0 is the value after MAX. Order comes from modular distance within a bounded window (§4).
  • "A duplicate Link transmission should reach the Transaction Layer twice." It must be delivered once. That is the whole reason duplicate detection exists (§10, P10).
  • "An ACK's sequence and a Completion's Tag are interchangeable." Different layers, different mechanisms, different lifetimes (Chapter 14.2 §2).
  • "Sequence numbering enforces Transaction Layer ordering." It describes Link delivery progress. Transaction ordering is Chapter 13.4's and is a different mechanism at a different layer.
  • "Sequence numbering is global across all Links." Each Link has its own counters, advancing independently (§2).
  • "A gap means a software Request was lost." It means a Link-local delivery is missing. Recovery is hardware replay, and the Transaction Layer never learns of it.
  • "The receiver may advance expected past a gap." Then it delivers a hole and every subsequent packet is misclassified (P9, P11).
  • "Wrap is a rare corner that can be ignored." It is guaranteed to occur, on every Link, at a known rate — and it breaks a naive design deterministically (§6).

15. Understanding Check

16. Module 14 Complete

Five chapters have built one Link's reliability from the contract down to the arithmetic.

14.1the contract — what the layer promises, and what it does not
14.2cumulative acknowledgement, and the retirement frontier
14.3one event, two effects — retire the prefix, replay the suffix
14.4the ordered store where reading is not dequeuing
14.5the identity all four were abstracting, and the arithmetic that compares it

And the abstraction is now paid off. Chapter 14.2 §8 searched a window for an identity because no comparison rule was available; 14.3 §11 built a whole FSM to do that search one position per cycle; 14.4 §6a gave its store a second read port to serve it.

With modular comparison, all of it collapses. The retirement prefix length is (ack_seq − oldest_seq) mod M + 1, computed in one cycle from the identity alone. No search, no FSM, no second port — which is why §11a of Chapter 14.3 said the FSM is the price of not having this arithmetic yet.

Module 15 asks the next question. Every one of these mechanisms exchanges control information between neighbours — acknowledgement progress, retry requests, and things Module 14 has not touched at all. Chapter 15.1 — DLLP Types covers the packets that carry it: what a Data Link Layer Packet is, why it is not a TLP, and what categories of Link-local control PCIe exchanges.

The idea to carry forward: identity in a wrapping space is a distance, not a magnitude — and the window that makes the distance meaningful is a separate rule the comparison silently depends on.