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 Number | Tag | |
|---|---|---|
| Layer | Data Link | Transaction |
| Scope | one Link | the transaction, end to end |
| Identifies | this packet's place in Link delivery order | which Request a Completion answers |
| Assigned by | the transmitting port of each Link | the Requester, once |
| Survives a hop? | no — reassigned | yes (Chapter 11.3 §6) |
| Used by | ACK, NAK, replay | Completion correlation (Chapter 10.2 §6) |
| Wraps within | 4096 | its own field width |
3. Assignment Is Per Link, Not Per Transaction
A sequence number is consumed when a packet enters Link reliability ownership — and only then.
| Event | Consumes a new sequence value? |
|---|---|
| a new TLP accepted from the Transaction Layer | yes |
| that TLP transmitted the first time | no — it already has one |
| that TLP replayed | no — it keeps the one it was assigned |
| a Switch forwarding it onto the next Link | yes — 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:
delta = (candidate − reference) mod 2^NIn 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.
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:
expected | rx | delta = (expected − rx) mod 8 | Verdict |
|---|---|---|---|
| 0 | 0 | 0 | the expected packet — accept |
| 0 | 7 | 1 | history — duplicate |
| 0 | 6 | 2 | history — duplicate |
| 0 | 1 | 7 | ahead — gap |
| 6 | 7 | 7 | ahead — 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:
1. rx == expected → accept, advance expected
2. else (expected − rx) mod M ≤ window → duplicate
3. else → gapStep 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.
// WRONG.
if (rx_seq < expected_seq)
duplicate = 1'b1;It is correct everywhere except at one boundary.
expected | rx | naive rx < expected | correct verdict |
|---|---|---|---|
| 5 | 3 | duplicate ✓ | duplicate ✓ |
| 5 | 7 | not duplicate ✓ | gap ✓ |
| 4095 | 0 | duplicate ✗ | the expected packet — accept |
| 0 | 4095 | not duplicate ✗ | duplicate |
7. A Trace Across the Wrap
Internal teaching signals, not PCIe wire signals. SEQ_W = 12, so the modulus is 4096.
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 = 0… and 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
// 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;
endfunctionClassification: 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
// 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;
endpackageimport 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
endmoduleClassification: 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
// 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// 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
endmoduleClassification: 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:
| Situation | Resolution |
|---|---|
| accept + acknowledgement update | both apply; seq_separation recomputes next cycle |
| a duplicate arriving | deliver_up low, exp_q unchanged |
| a gap arriving | deliver_up low, exp_q unchanged, gap_seen set |
| a corrupted packet | not classified at all — usable is low |
throttled + tl_valid | tl_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.
| Quantity | What it is | Where it lives |
|---|---|---|
| sequence separation | (NEXT_TRANSMIT_SEQ − ACKD_SEQ) mod M — a distance in the numbering space | §10's assigner |
| retained occupancy | how many entries the replay buffer actually holds | Chapter 14.4 §6 |
| packets sent | a cumulative total | telemetry (Chapter 12.5 §14) |
| packets acknowledged | another cumulative total | telemetry |
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
// 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:
// 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;
endfunctionBoundary 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, thenMAX. Approaching the boundary.MAX→0. The wrap itself: verifynext_qandexp_qboth roll with no special case (P2).- A duplicate arriving just before the wrap —
expected = 4095,rx = 4094. - A duplicate arriving just after the wrap —
expected = 0,rx = 4095. The mutant's first failure (§11). - A new packet just after the wrap —
expected = 4095,rx = 0in the transmit direction. The mutant's second failure. - A forward gap across the wrap —
expected = 4094,rx = 1. - Several sequential packets straight through the wrap — 4090 to 5, checking
expectedat every step. - A replay across the wrap — a packet assigned 4095, replayed after
expectedhas 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 − 1unacknowledged. Verifytl_readyis still high. - Reach
WINDOW. Verifytl_readydrops (P3) and no further identity is consumed. - Acknowledge, then verify acceptance resumes.
- The throttle across the wrap —
next_qpast 4095 withackd_qbelow it. Verifyseq_separationcomputes modularly (next=10, ackd=4090 → separation=16, not a negative number). - The simple case, explicitly: send one packet, acknowledge it, and verify
retained_countis zero whileseq_separationreads 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
expectedat 0, 1,MAX−1,MAXagainstrxat each of the same, plusexpected ± WINDOW. - A duplicate, then the expected packet. Verify
expectedmoved only once. - A gap, then the missing packet. Verify
expecteddid not advance on the gap. - A corrupted packet at each classification boundary. Verify none is classified and
expectednever 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
| # | Mutation | Caught by |
|---|---|---|
| 1 | identity advances on send attempt, not acceptance | P1, on the first replay |
| 2 | replay consumes a new identity | P1, and the retained entry stops matching acknowledgements |
| 3 | wrap omitted — counter saturates | P2 |
| 4 | signed comparison | P5, at half the space |
| 5 | ordinary < across the wrap | P5, P7 — §11's counterexample |
| 6 | receiver advances expected on a duplicate | P9, and every later packet misclassifies |
| 7 | receiver advances expected on a corrupted packet | P9, P12 |
| 8 | a gap delivered upward | P11 |
| 9 | a duplicate delivered upward | P10 — and the Transaction Layer performs the operation twice |
| 10 | two live packets assigned the same identity | P3 — only reachable if the throttle is removed |
| 11 | ACK matched against a Tag instead of a Sequence Number | Chapter 14.2's retirement finds nothing; event_unknown |
| 12 | a Switch forwards the incoming identity instead of assigning its own | §13's fourth scenario — fails only through a Switch |
13. Debugging
The Link works for millions of packets, then breaks exactly at rollover
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
expectedpast 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.1 | the contract — what the layer promises, and what it does not |
| 14.2 | cumulative acknowledgement, and the retirement frontier |
| 14.3 | one event, two effects — retire the prefix, replay the suffix |
| 14.4 | the ordered store where reading is not dequeuing |
| 14.5 | the 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.