PCIe · Module 17
Deserialization — Collecting Bits Is the Easy Part
A receiver cannot ask the far end to pause, and cannot tell from the bits alone where one unit starts. Timing recovery, boundary alignment, elastic storage and lane identity are four separate problems — and only three of them are RTL.
Chapter 17.1 ended on an asymmetry. A transmitter that runs out of words causes an underrun rather than a stall, because the lane does not stop.
On the receive side that asymmetry stops being a footnote and becomes the whole design constraint.
A receiver cannot ask the far end to wait. It cannot slow the incoming stream, pause it while something upstream is busy, or request a retransmission at this layer. Bits arrive at the lane rate, continuously, whether anything is ready for them or not.
And before any of that matters, there is a harder problem: the bits alone do not say where one unit of information begins.
How does a receiver turn a timed serial stream back into ordered parallel information, when it must first recover timing, then find boundaries, then absorb differences it cannot control, and still know which lane it is looking at?
1. The Verified Structure
2. The CDR Boundary
Before a single bit can be collected, the receiver must know when to sample.
The transmitter's clock is not sent alongside the data. Timing is recovered from the signal itself, by a clock-and-data-recovery unit that is part of the PMA — §1's source lists it beside the receiver buffer and the deserializer.
The practical consequence for a debugging engineer is worth stating early: a fault in this domain looks completely different from a fault in the digital domain. §13's last scenario is built around telling them apart, and it is the single most useful diagnostic distinction in the chapter.
3. Two Different Recoveries
Bit recovery and boundary recovery are separate problems, and confusing them is the most common conceptual error here.
Suppose a lane carries a repeating pattern, and the receiver has achieved perfect bit recovery — every bit sampled correctly, no errors at all:
serial: . . . a b c d e f g h a b c d e f g h a b c d . . .Collecting every 8 bits still gives the wrong answer unless the receiver knows where to start:
phase 0 : abcdefgh abcdefgh ← correct
phase 2 : cdefghab cdefghab ← every word rotated by 2
phase 5 : fghabcde fghabcde ← every word rotated by 5The mechanism §1's source names is bit slip: the alignment logic requests a one-bit shift of the parallel boundary and re-evaluates, until the framing evidence it is looking for lines up. §5 builds that search loop generically.
4. What Counts as Evidence
Alignment requires something recognisable in the stream. What that something is depends entirely on the generation.
| Generation | Encoding | Alignment evidence |
|---|---|---|
| Gen1 / Gen2 | 8b/10b (5.1, 5.2) | special symbols in the code space |
| Gen3 – Gen5 | 128b/130b (5.3) | block-level framing |
| Gen6 | changed again (5.6) | changed again |
5. RTL — Collector and Frame Alignment
Two blocks, in the order a receiver needs them: gather bits into a word, then work out whether the word starts in the right place.
// SYNTHESIZABLE. Low-rate serial-to-parallel collector.
// TEACHING MODEL ONLY -- a production deserializer is dedicated circuitry
// inside the PMA (section 2). The BIT ORDER is a declared parameter for
// the reason Chapter 17.1 section 5 gives.
//
// NOTE THE INPUT HAS NO `ready`. The bit stream arrives from a macro fed
// by a lane that does not stop (section 9). This block therefore needs
// somewhere to put a finished word while the consumer is busy -- and
// section 11's counterexample is what happens when it has nowhere.
module teach_collector #(
parameter int WORD_W = 8,
parameter bit LSB_FIRST = 1'b1
) (
input logic clk,
input logic rst_n,
// ---- Serial in. UNBACKPRESSURABLE. --------------------------------------
input logic serial_valid,
input logic serial_bit,
// ---- Parallel out, to the elastic buffer of section 6 -------------------
output logic word_valid,
input logic word_ready,
output logic [WORD_W-1:0] word_data,
output logic word_dropped // completed word overwritten
);
// GUARDED. $clog2(1) is zero; WORD_W = 1 is a legal configuration.
localparam int IDX_W = (WORD_W <= 1) ? 1 : $clog2(WORD_W);
generate
if (WORD_W < 1) $error("WORD_W must be at least 1");
endgenerate
logic [WORD_W-1:0] shift_q;
logic [IDX_W-1:0] idx_q;
logic [WORD_W-1:0] out_q;
logic out_v_q, drop_q;
assign word_valid = out_v_q;
assign word_data = out_q;
assign word_dropped = drop_q;
wire last_bit = (idx_q == IDX_W'(WORD_W-1));
wire complete = serial_valid && last_bit;
wire pop = out_v_q && word_ready;
// The bit is placed at the position the declared convention says, so the
// assembled word can be compared against an INDEPENDENT model (P18).
wire [IDX_W-1:0] place = LSB_FIRST ? idx_q : (IDX_W'(WORD_W-1) - idx_q);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
shift_q <= '0; idx_q <= '0; out_q <= '0; out_v_q <= 1'b0;
drop_q <= 1'b0;
end else begin
if (serial_valid) begin
shift_q[place] <= serial_bit;
idx_q <= last_bit ? '0 : (idx_q + IDX_W'(1));
end
// NO PARTIAL WORD IS EVER PUBLISHED. The output register is written
// only on completion, from the fully assembled value.
if (complete) begin
out_q <= shift_q;
out_q[place] <= serial_bit; // include the final bit
out_v_q <= 1'b1;
end else if (pop) begin
out_v_q <= 1'b0;
end
// ================================================================
// A COMPLETED WORD OVERWRITTEN BEFORE IT WAS TAKEN.
//
// With ONE output register this is unavoidable -- the serial input
// cannot be refused (section 9). Reporting it is the honest minimum;
// section 6's elastic buffer is the actual fix, and section 11
// explains why back-pressure is not an option.
// ================================================================
if (complete && out_v_q && !pop) drop_q <= 1'b1;
end
end
endmoduleClassification: synthesizable (teaching model).
Architecture. A shift register that assembles into a separate output register, published only on completion.
The word_dropped output is the point of the block, not an afterthought. With one output register, a word completed while the previous one is still waiting overwrites it — and there is no ready on the serial side to prevent that. §11's counterexample walks it through, and §6's elastic buffer is the real answer.
Failure — four. Publishing shift_q directly exposes partial words as if they were complete. Advancing the index on anything other than serial_valid loses bit positions. $clog2(WORD_W) used directly fails at WORD_W = 1. And omitting word_dropped turns unavoidable loss into invisible loss.
Deliberately simplified: one output register — deliberately, to make the problem visible; no alignment, which is the next block's job.
// SYNTHESIZABLE. Generic frame-alignment acquisition.
// THE SEARCH-BY-SLIP MECHANISM is vendor-verified (section 1: "The
// deserializer has a bit-slip feature for word alignment ... adjusts the
// alignment of the deserialized word by 1-bit"). THE EVIDENCE and the
// CONFIRMATION THRESHOLD are ILLUSTRATIVE LOCAL POLICY -- PCIe's actual
// alignment rules belong to Chapter 18.3 (section 4).
module frame_aligner #(
parameter int WORD_W = 40, // vendor PMA-PCS width (section 1)
// How many correctly-spaced observations before declaring alignment.
// AN ILLUSTRATIVE POLICY. Not a PCIe number.
parameter int CONFIRM_N = 4,
// How many words without evidence before alignment is abandoned.
parameter int LOSE_N = 8,
parameter int CNT_W = (CONFIRM_N > LOSE_N) ? $clog2(CONFIRM_N+1)
: $clog2(LOSE_N+1)
) (
input logic clk,
input logic rst_n,
// ---- From the SerDes macro ----------------------------------------------
input logic word_valid,
input logic [WORD_W-1:0] word_data,
// ABSTRACT classification supplied by generation-specific logic: this
// word carries the framing evidence being searched for. Chapter 18.3
// owns what that evidence actually is in PCIe (section 4).
input logic evidence_match,
// ---- Slip request, to the macro ------------------------------------------
// Section 1's handshake: request a 1-bit slip, wait for the macro to
// acknowledge, then release.
output logic slip_req,
input logic slip_ack,
// ---- Aligned output ------------------------------------------------------
output logic aligned,
output logic out_valid,
output logic [WORD_W-1:0] out_data,
output logic align_lost // was aligned, no longer
);
generate
if (WORD_W < 1) $error("WORD_W must be at least 1");
if (CONFIRM_N < 1) $error("CONFIRM_N must be at least 1");
if (LOSE_N < 1) $error("LOSE_N must be at least 1");
endgenerate
typedef enum logic [1:0] { S_SEARCH, S_SLIP, S_CONFIRM, S_ALIGNED } st_e;
st_e st_q;
logic [CNT_W-1:0] hit_q, miss_q;
logic lost_q;
assign aligned = (st_q == S_ALIGNED);
assign align_lost = lost_q;
assign slip_req = (st_q == S_SLIP);
// ==================================================================
// UNALIGNED DATA IS NEVER DELIVERED UPWARD.
//
// A receiver that published words before confirming alignment would
// hand the Link layer correctly-received bits in the wrong arrangement
// (section 3) -- which downstream logic cannot distinguish from
// corruption, and will happily try to parse.
// ==================================================================
assign out_valid = aligned && word_valid;
assign out_data = word_data;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
st_q <= S_SEARCH; hit_q <= '0; miss_q <= '0; lost_q <= 1'b0;
end else begin
unique case (st_q)
// Look for evidence at the current boundary. If a whole word goes
// by without it, try the next boundary.
S_SEARCH :
if (word_valid) begin
if (evidence_match) begin
st_q <= S_CONFIRM;
hit_q <= CNT_W'(1);
end else begin
st_q <= S_SLIP;
end
end
// SLIP HANDSHAKE (section 1): hold the request until the macro
// acknowledges, then release and search again. Pulsing it would
// race the macro's own handshake.
S_SLIP :
if (slip_ack) st_q <= S_SEARCH;
// CONFIRM BY REPETITION. One match could be data that happens to
// look like evidence; several correctly-spaced matches could not.
S_CONFIRM :
if (word_valid) begin
if (evidence_match) begin
if (hit_q == CNT_W'(CONFIRM_N-1)) begin
st_q <= S_ALIGNED;
miss_q <= '0;
end else begin
hit_q <= hit_q + CNT_W'(1);
end
end else begin
// A miss during confirmation means this boundary was a
// coincidence. Back to searching, not back to aligned.
st_q <= S_SLIP;
hit_q <= '0;
end
end
S_ALIGNED :
if (word_valid) begin
if (evidence_match) begin
miss_q <= '0;
end else if (miss_q == CNT_W'(LOSE_N-1)) begin
// Alignment lost. REPORTED, and data delivery stops
// immediately -- out_valid is gated on `aligned`.
st_q <= S_SEARCH;
hit_q <= '0;
miss_q <= '0;
lost_q <= 1'b1;
end else begin
miss_q <= miss_q + CNT_W'(1);
end
end
default : st_q <= S_SEARCH;
endcase
end
end
endmoduleClassification: synthesizable (generic pedagogy — §4).
Architecture. A four-state search: look, slip, confirm, hold. The slip request is a level held until acknowledged, matching the handshake §1 quotes.
Cycle behaviour.
| State | Waiting for | Leaves when |
|---|---|---|
S_SEARCH | a word | evidence → confirm; no evidence → slip |
S_SLIP | the macro | slip_ack |
S_CONFIRM | repeated evidence | CONFIRM_N hits → aligned; a miss → slip |
S_ALIGNED | continued evidence | LOSE_N consecutive misses → search, reported |
Failure — five. Declaring alignment on a single match locks onto data that coincidentally resembles the evidence. Delivering words while unaligned hands rotated data upward as if it were valid. Pulsing slip_req races the macro's handshake and may slip more or fewer times than intended. Returning to S_ALIGNED on a single hit after misses oscillates on a marginal channel. And losing alignment silently removes the one observable that distinguishes §11's first two scenarios.
Deliberately simplified: evidence_match is abstract; the thresholds are policy, not protocol; one word per cycle.
6. RTL — Elastic Receive Buffer
// SYNTHESIZABLE. Elastic receive buffer between a continuously-producing
// SerDes macro and a digital consumer that may stall.
// That an elastic buffer exists in the receive path, and that OVERFLOW and
// UNDERFLOW are real reportable conditions, is VERIFIED (section 1's PIPE
// RXSTATUS quotation). The depth and the reporting policy are
// ILLUSTRATIVE.
module rx_elastic #(
parameter int WORD_W = 40,
parameter int DEPTH = 4,
// GUARDED. $clog2(1) is zero and a zero-width pointer cannot index.
parameter int PTR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH)
) (
input logic clk,
input logic rst_n,
// ---- From the macro. NOTE: NO `ready`. --------------------------------
// This is the point of the whole block. The macro produces words on its
// own schedule and CANNOT BE BACK-PRESSURED (section 9). If there is no
// room, the word is lost and the loss is REPORTED -- it cannot be
// refused, because refusing requires a mechanism the wire does not have.
input logic in_valid,
input logic [WORD_W-1:0] in_word,
// ---- To the digital consumer, which MAY stall -------------------------
output logic out_valid,
input logic out_ready,
output logic [WORD_W-1:0] out_word,
output logic [PTR_W:0] occupancy, // one bit wider than PTR_W
output logic overflow, // word arrived, no room
output logic underflow // consumer took nothing: empty
);
generate
if (DEPTH < 1) $error("DEPTH must be at least 1");
if (PTR_W < 1) $error("PTR_W must be at least 1");
if ((DEPTH > 1) && ((1 << PTR_W) < DEPTH))
$error("PTR_W too narrow to index DEPTH");
endgenerate
logic [WORD_W-1:0] mem_q [DEPTH];
logic [PTR_W-1:0] rd_q, wr_q;
// OCCUPANCY IS A COUNTER, ONE BIT WIDER THAN THE POINTERS. Full and empty
// are never inferred from pointer equality (Chapter 14.4 section 10).
logic [PTR_W:0] cnt_q;
logic ovf_q, unf_q;
assign occupancy = cnt_q;
assign overflow = ovf_q;
assign underflow = unf_q;
assign out_valid = (cnt_q != '0);
assign out_word = mem_q[rd_q];
wire pop = out_valid && out_ready;
wire full = (cnt_q == (PTR_W+1)'(DEPTH));
// Effective capacity: a slot vacated this cycle is usable this cycle.
wire can_take = !full || pop;
wire push = in_valid && can_take;
// EXPLICIT WRAP at DEPTH-1. A natural pointer wrap works only at a
// power-of-two DEPTH, and DEPTH is a free parameter.
function automatic logic [PTR_W-1:0] next_idx(input logic [PTR_W-1:0] i);
return (i == PTR_W'(DEPTH-1)) ? '0 : (i + PTR_W'(1));
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_q <= '0; wr_q <= '0; cnt_q <= '0; ovf_q <= 1'b0; unf_q <= 1'b0;
end else begin
if (push) begin
mem_q[wr_q] <= in_word;
wr_q <= next_idx(wr_q);
end
if (pop) rd_q <= next_idx(rd_q);
unique case ({push, pop})
2'b10 : cnt_q <= cnt_q + (PTR_W+1)'(1);
2'b01 : cnt_q <= cnt_q - (PTR_W+1)'(1);
default : ; // both or neither: unchanged
endcase
// ================================================================
// BOTH CONDITIONS ARE REPORTED, AND NEITHER IS RECOVERABLE HERE.
//
// OVERFLOW: a word arrived with no room. It is gone. The wire did
// not stop, so there was nothing else to do -- but silence would
// hide real data loss (section 9).
//
// UNDERFLOW: the consumer asked and the buffer was empty. In a real
// receive path this is where clock-tolerance compensation lives
// (section 1's +/-300 ppm and SKP handling), which this model does
// not implement.
// ================================================================
if (in_valid && !can_take) ovf_q <= 1'b1;
if (out_ready && !out_valid) unf_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
Architecture. A ring buffer with an explicit wrap and a separate occupancy counter one bit wider than the pointers — full and empty are indistinguishable by pointer equality alone.
The input has no ready, and that is the design. Every other decoupled interface in this curriculum has one. This one cannot, because the thing on the other side is a macro fed by a wire that does not stop (§9).
Cycle behaviour.
in_valid | out_ready | Result |
|---|---|---|
| 1 | 0, not full | occupancy +1 |
| 0 | 1, not empty | occupancy −1 |
| 1 | 1 | both apply — occupancy unchanged |
| 1, full | 0 | word lost, overflow reported |
| 0 | 1, empty | underflow reported |
Failure — five. Adding a ready to the input models an interface that does not exist and hides the overflow case entirely. Inferring full from wr == rd confuses full with empty. A natural pointer wrap breaks at any non-power-of-two DEPTH. $clog2(DEPTH) in a width fails at DEPTH = 1. And treating overflow or underflow as internal detail — §1's source reports both to the MAC, because both mean the receive path lost synchronisation with the far end's rate.
Deliberately simplified: single clock — a real elastic buffer sits at a rate boundary and needs CDC-safe structure (Chapter 17.1 §16); no SKP insertion or removal, which is where the ±300 ppm compensation actually happens; depth is a teaching choice.
7. Lane Identity and Skew
On a multi-lane Link, per-lane recovery is not the end. The lanes must be put back together — and they do not arrive together.
Each lane has its own channel, its own length, its own CDR. Content that was distributed across lanes at the same moment does not arrive at the same moment.
transmitted at T: lane0 = A lane1 = B lane2 = C lane3 = D
received: lane0 = A at cycle N
lane1 = B at cycle N+1 <-- later
lane2 = C at cycle N
lane3 = D at cycle N+2 <-- later still8. RTL — Two-Lane Deskew
// SYNTHESIZABLE. Hold per-lane words until matching pieces are available
// on every lane, then release them as one aligned group.
// THAT LANE-TO-LANE DESKEW IS REQUIRED is vendor-stated (section 1's PHY
// function list). THE STRIPE-ID MECHANISM is a teaching abstraction --
// PCIe establishes lane identity through training (Chapter 17.3), and this
// block does not model that.
module lane_deskew #(
parameter int WORD_W = 40,
parameter int STRIPE_W = 4, // abstract "which group" identifier
parameter int DEPTH = 4,
parameter int PTR_W = (DEPTH <= 1) ? 1 : $clog2(DEPTH)
) (
input logic clk,
input logic rst_n,
// ---- Per-lane recovered words. Neither lane can be back-pressured. ----
input logic l0_valid,
input logic [WORD_W-1:0] l0_word,
input logic [STRIPE_W-1:0] l0_stripe,
input logic l1_valid,
input logic [WORD_W-1:0] l1_word,
input logic [STRIPE_W-1:0] l1_stripe,
// ---- Aligned group out --------------------------------------------------
output logic grp_valid,
input logic grp_ready,
output logic [WORD_W-1:0] grp_l0,
output logic [WORD_W-1:0] grp_l1,
output logic [STRIPE_W-1:0] grp_stripe,
output logic skew_overflow, // one lane ran too far ahead
output logic stripe_mismatch // heads disagree: a fault
);
generate
if (DEPTH < 1) $error("DEPTH must be at least 1");
if (PTR_W < 1) $error("PTR_W must be at least 1");
if ((DEPTH > 1) && ((1 << PTR_W) < DEPTH))
$error("PTR_W too narrow to index DEPTH");
endgenerate
// ONE ELASTIC BUFFER PER LANE. A shared buffer could not absorb skew --
// the whole point is that the lanes are at different points in time.
logic [WORD_W-1:0] m0 [DEPTH], m1 [DEPTH];
logic [STRIPE_W-1:0] s0 [DEPTH], s1 [DEPTH];
logic [PTR_W-1:0] r0_q, w0_q, r1_q, w1_q;
logic [PTR_W:0] c0_q, c1_q;
logic ovf_q, mis_q;
assign skew_overflow = ovf_q;
assign stripe_mismatch = mis_q;
function automatic logic [PTR_W-1:0] nxt(input logic [PTR_W-1:0] i);
return (i == PTR_W'(DEPTH-1)) ? '0 : (i + PTR_W'(1));
endfunction
wire have0 = (c0_q != '0);
wire have1 = (c1_q != '0);
// A GROUP IS RELEASED ONLY WHEN BOTH LANES HAVE A PIECE AND THE PIECES
// AGREE. Releasing on "both have something" would pair mismatched
// stripes -- section 7's bug, one step later.
wire heads_match = have0 && have1 && (s0[r0_q] == s1[r1_q]);
assign grp_valid = heads_match;
assign grp_l0 = m0[r0_q];
assign grp_l1 = m1[r1_q];
assign grp_stripe = s0[r0_q];
wire pop = grp_valid && grp_ready;
wire full0 = (c0_q == (PTR_W+1)'(DEPTH));
wire full1 = (c1_q == (PTR_W+1)'(DEPTH));
wire push0 = l0_valid && (!full0 || (pop && have0));
wire push1 = l1_valid && (!full1 || (pop && have1));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
r0_q <= '0; w0_q <= '0; c0_q <= '0;
r1_q <= '0; w1_q <= '0; c1_q <= '0;
ovf_q <= 1'b0; mis_q <= 1'b0;
end else begin
if (push0) begin
m0[w0_q] <= l0_word; s0[w0_q] <= l0_stripe; w0_q <= nxt(w0_q);
end
if (push1) begin
m1[w1_q] <= l1_word; s1[w1_q] <= l1_stripe; w1_q <= nxt(w1_q);
end
if (pop) begin r0_q <= nxt(r0_q); r1_q <= nxt(r1_q); end
unique case ({push0, pop && have0})
2'b10 : c0_q <= c0_q + (PTR_W+1)'(1);
2'b01 : c0_q <= c0_q - (PTR_W+1)'(1);
default : ;
endcase
unique case ({push1, pop && have1})
2'b10 : c1_q <= c1_q + (PTR_W+1)'(1);
2'b01 : c1_q <= c1_q - (PTR_W+1)'(1);
default : ;
endcase
// SKEW BEYOND WHAT THE BUFFER ABSORBS. Reported, because a design
// that silently discards here loses data with no diagnostic.
if ((l0_valid && full0 && !(pop && have0))
|| (l1_valid && full1 && !(pop && have1))) ovf_q <= 1'b1;
// BOTH LANES HAVE A HEAD AND THE STRIPES DISAGREE. That is not skew
// -- skew is absorbed by waiting. It means a lane lost or gained a
// piece, which waiting can never fix.
if (have0 && have1 && (s0[r0_q] != s1[r1_q])) mis_q <= 1'b1;
end
end
endmoduleClassification: synthesizable (teaching abstraction — §7).
Architecture. One elastic buffer per lane, and a release condition that requires matching stripe identifiers at both heads. A shared buffer could not do this, because the lanes are deliberately at different points in time.
The distinction between the two error outputs is the lesson. skew_overflow means one lane ran further ahead than the buffer can hold — a sizing or channel problem that more depth might fix. stripe_mismatch means the heads disagree, which waiting can never resolve: a lane lost or gained a piece, and the recombination is broken rather than late.
Cycle behaviour.
| Situation | Result |
|---|---|
| lane 0 arrives, lane 1 has not | buffered; no group released |
| both heads present, stripes match | group released on handshake |
| both heads present, stripes differ | stripe_mismatch — not a skew condition |
| a lane full and still arriving | skew_overflow — the word is lost |
| consumer stalls | both lanes keep buffering until full |
Failure — five. Releasing on "both non-empty" without comparing stripes pairs pieces from different transmission instants (§7). A shared buffer cannot absorb skew at all. Treating a stripe mismatch as skew waits forever for a match that will never come. Silently discarding on overflow loses data with no diagnostic. And advancing only one read pointer on release desynchronises the lanes permanently.
Deliberately simplified: two lanes — the structure generalises, the code stays readable; the stripe identifier is abstract; no recovery policy after a mismatch.
9. The Receiver Cannot Say Wait
10. Assertions
// SVA over frame_aligner, rx_elastic and lane_deskew. These assert LOCAL
// digital contracts. They assert nothing about CDR lock, jitter tolerance,
// equalization, or the channel -- none of which is modelled (section 2).
// No liveness is asserted: "alignment is eventually acquired" depends on
// the far end transmitting evidence, which is an environment property.
// ---- ENVIRONMENT ------------------------------------------------------
// A1: the macro's word is stable while in_valid -- it came from the PMA.
assume property (@(posedge clk) disable iff (!rst_n)
in_valid |-> !$isunknown(in_word));
// A2: the macro acknowledges a held slip request within a bounded time.
assume property (@(posedge clk) disable iff (!rst_n)
slip_req |-> ##[1:16] slip_ack);
// ---- ALIGNMENT --------------------------------------------------------
// P1: UNALIGNED DATA IS NEVER DELIVERED UPWARD. The chapter's central
// alignment property -- rotated words are indistinguishable from
// corruption downstream (section 3).
property p_no_unaligned_output;
@(posedge clk) disable iff (!rst_n)
!aligned |-> !out_valid;
endproperty
a_gate : assert property (p_no_unaligned_output);
// P2: alignment is declared only after CONFIRM_N observations -- never on
// a single coincidental match.
property p_confirm_before_align;
@(posedge clk) disable iff (!rst_n)
$rose(aligned) |-> ($past(st_q) == S_CONFIRM)
&& ($past(hit_q) == CNT_W'(CONFIRM_N-1));
endproperty
a_confirm : assert property (p_confirm_before_align);
// P3: the slip request is a LEVEL held until acknowledged (section 1's
// handshake). Pulsing it races the macro.
property p_slip_held;
@(posedge clk) disable iff (!rst_n)
(slip_req && !slip_ack) |=> slip_req;
endproperty
a_slip : assert property (p_slip_held);
// P4: alignment is not abandoned on a single miss.
property p_no_single_miss_loss;
@(posedge clk) disable iff (!rst_n)
(aligned && word_valid && !evidence_match && (miss_q < CNT_W'(LOSE_N-1)))
|=> aligned;
endproperty
a_hysteresis : assert property (p_no_single_miss_loss);
// P5: losing alignment is REPORTED and stops delivery in the same cycle.
property p_loss_reported;
@(posedge clk) disable iff (!rst_n)
$fell(aligned) |-> (align_lost && !out_valid);
endproperty
a_loss : assert property (p_loss_reported);
// P6: BOUNDED ACQUISITION. From any starting boundary, alignment is
// reached or another slip is requested within a bounded window -- a
// deterministic local walk, so this is a bound rather than a fairness
// assumption (Chapter 14.3 section 13's reasoning).
property p_search_progresses;
@(posedge clk) disable iff (!rst_n)
(st_q == S_SEARCH) && word_valid
|-> ##[1:2] ((st_q == S_CONFIRM) || slip_req);
endproperty
a_progress : assert property (p_search_progresses);
// ---- COLLECTOR --------------------------------------------------------
// P18: THE ASSEMBLED WORD matches an INDEPENDENT reconstruction from the
// observed bit sequence under the declared convention -- not the module's
// own placement expression, which would agree with a wrong one.
property p_word_matches_reference;
@(posedge clk) disable iff (!rst_n)
$rose(word_valid) |-> (word_data == ref_assemble(observed_bits, LSB_FIRST));
endproperty
a_assemble : assert property (p_word_matches_reference);
// P19: NO PARTIAL WORD IS PUBLISHED. word_valid rises only on completion.
property p_no_partial_word;
@(posedge clk) disable iff (!rst_n)
$rose(word_valid) |-> $past(complete);
endproperty
a_no_partial : assert property (p_no_partial_word);
// P20: a completed word overwritten before it was taken is REPORTED. With
// one output register the loss is unavoidable (section 9) -- silence is not.
property p_drop_reported;
@(posedge clk) disable iff (!rst_n)
(complete && word_valid && !word_ready) |=> word_dropped;
endproperty
a_drop : assert property (p_drop_reported);
// ---- ELASTIC BUFFER ---------------------------------------------------
// P7: occupancy is bounded and never underflows.
property p_occ_sane;
@(posedge clk) disable iff (!rst_n)
(occupancy <= (PTR_W+1)'(DEPTH)) && !(pop && (occupancy == '0));
endproperty
a_occ : assert property (p_occ_sane);
// P8: a queued word is NOT overwritten while the consumer stalls. This is
// the property section 11's counterexample fails.
property p_no_overwrite_under_stall;
@(posedge clk) disable iff (!rst_n)
(out_valid && !out_ready) |=> (out_valid && $stable(out_word));
endproperty
a_hold : assert property (p_no_overwrite_under_stall);
// P9: simultaneous push and pop leave occupancy unchanged.
property p_simul;
@(posedge clk) disable iff (!rst_n)
(push && pop) |=> (occupancy == $past(occupancy));
endproperty
a_simul : assert property (p_simul);
// P10: OVERFLOW AND UNDERFLOW ARE REPORTED. A receiver cannot refuse
// (section 9), so silence would hide real data loss.
property p_overflow_reported;
@(posedge clk) disable iff (!rst_n)
(in_valid && !can_take) |=> overflow;
endproperty
a_ovf : assert property (p_overflow_reported);
property p_underflow_reported;
@(posedge clk) disable iff (!rst_n)
(out_ready && !out_valid) |=> underflow;
endproperty
a_unf : assert property (p_underflow_reported);
// P11: FIFO ORDER. Words leave in the order they arrived. (exp_order is a
// testbench queue.)
property p_order_preserved;
@(posedge clk) disable iff (!rst_n)
(out_valid && out_ready) |-> (out_word == exp_order[0]);
endproperty
a_order : assert property (p_order_preserved);
// ---- DESKEW -----------------------------------------------------------
// P12: A GROUP IS RELEASED ONLY WHEN THE STRIPE IDENTIFIERS MATCH.
// Section 7's bug, forbidden directly.
property p_only_matching_stripes;
@(posedge clk) disable iff (!rst_n)
grp_valid |-> (s0[r0_q] == s1[r1_q]);
endproperty
a_match : assert property (p_only_matching_stripes);
// P13: ONE LANE'S DATA IS NEVER SUBSTITUTED FOR ANOTHER'S. The released
// group takes lane 0's word from lane 0's buffer and lane 1's from
// lane 1's -- asserted because a wiring swap is otherwise invisible.
property p_no_lane_substitution;
@(posedge clk) disable iff (!rst_n)
(grp_valid && grp_ready) |-> ((grp_l0 == m0[r0_q]) && (grp_l1 == m1[r1_q]));
endproperty
a_no_swap : assert property (p_no_lane_substitution);
// P14: both read pointers advance together on release -- advancing one
// desynchronises the lanes permanently.
property p_pointers_advance_together;
@(posedge clk) disable iff (!rst_n)
(grp_valid && grp_ready) |=> ((r0_q == $past(nxt(r0_q)))
&& (r1_q == $past(nxt(r1_q))));
endproperty
a_together : assert property (p_pointers_advance_together);
// P15: a stripe mismatch is REPORTED and is not treated as skew -- waiting
// can never resolve it.
property p_mismatch_reported;
@(posedge clk) disable iff (!rst_n)
(have0 && have1 && (s0[r0_q] != s1[r1_q])) |=> stripe_mismatch;
endproperty
a_mismatch : assert property (p_mismatch_reported);
// ---- LAYER ------------------------------------------------------------
// P16: nothing here parses packets. A recovered word is not a TLP
// (section 12's misconception).
property p_no_packet_semantics;
@(posedge clk) disable iff (!rst_n)
out_valid |-> !(dut_tl.mem_valid || dut_tl.cpl_valid);
endproperty
a_layer : assert property (p_no_packet_semantics);
// P17: reset discards partial state, clears alignment, and empties the
// buffers.
property p_reset;
@(posedge clk)
!rst_n |=> (!aligned && !out_valid && (occupancy == '0) && !grp_valid);
endproperty
a_reset : assert property (p_reset);P1 is the alignment property that matters most, and its consequence is worth restating: rotated data is indistinguishable from corruption to everything downstream. A receiver that publishes unaligned words hands the Link layer garbage it will earnestly try to parse.
P10's two halves are not symmetric in meaning. Overflow means data was lost because the consumer could not keep up. Underflow means the consumer got ahead — which in a real receive path is what clock-tolerance compensation exists to prevent (§1's ±300 ppm and SKP handling). Both are reported for the same reason: the receiver cannot refuse, so it must tell.
P12 and P15 are the deskew pair, and they distinguish two conditions a design must not conflate. P12 forbids releasing mismatched pieces; P15 requires reporting the mismatch rather than waiting on it. A design that treats every disagreement as skew waits forever for a match that will never arrive.
No liveness is asserted. "Alignment is eventually acquired" depends on the far end transmitting recognisable evidence — an environment property this design cannot guarantee. P6 gives the bounded local claim instead: the search always makes progress.
11. Verification and Fault Injection
The scoreboard serializes a known word stream itself, then reconstructs what a correct receiver would produce — without ever reading the aligner's state, the FIFO's pointers, or the deskew buffers.
Collector
WORD_W= 1 and 2 — the$clog2(1)corner. Required.- A known bit sequence, checked against an independent assembly model (P18), with
LSB_FIRSTboth ways. - Consumer stalled across a word boundary — verify
word_droppedis reported (P20), and that this is exactly what §6's buffer removes. - Reset mid-word — verify no partial word is published (P19).
Alignment
- A known word stream at every bit phase. For an 8-bit unit that is 8 starting phases; verify the aligner reaches the correct one from each. Required test.
- Evidence appearing once, then not again — verify alignment is not declared (P2).
- Evidence at the right spacing for exactly
CONFIRM_Nobservations — verify it is declared on the last one, not earlier. - A single miss while aligned — verify alignment holds (P4).
LOSE_Nconsecutive misses — verify loss,align_lost, and that output stops in the same cycle (P5).slip_ackdelayed — verify the request is held, not pulsed (P3).- Reset while aligned.
Elastic buffer
DEPTH= 1, 3, 4 — including the$clog2(1)corner and a non-power-of-two. Required.- Consumer stalls while words keep arriving — verify no overwrite (P8) and, once full, overflow reported (P10).
- Push and pop in the same cycle at empty, mid, and full.
- Consumer ready with an empty buffer — verify
underflow. - Continuous arrival with a consumer that takes every other cycle — the realistic rate-mismatch case.
- Reset with content.
Deskew
- No skew — both lanes in step. Verify groups release immediately.
- Lane 1 one cycle late, then lane 0 late — verify the group waits and then releases correctly paired.
- Skew of
DEPTHandDEPTH + 1— the second must reportskew_overflow. - A stripe missing on one lane — verify
stripe_mismatch, not an indefinite wait (P15). - Lane words swapped at the input — verify P13 catches it. This is the test for a wiring error that is otherwise invisible.
- Repeated identical stripe IDs, and IDs wrapping.
Mutations
| # | Mutation | Caught by | Silicon symptom |
|---|---|---|---|
| 1 | output published while unaligned | P1 | Link layer parses rotated words as corruption |
| 2 | alignment declared on one match | P2 | false lock on data resembling evidence; random loss |
| 3 | slip_req pulsed instead of held | P3 | slips over- or under-shoot; alignment never converges |
| 4 | alignment dropped on a single miss | P4 | thrashing in and out of lock on a marginal channel |
| 5 | elastic input given a ready | P10 never fires | overflow becomes invisible; silent data loss |
| 6 | held word overwritten under stall | P8 | corruption only when downstream is busy |
| 7 | full inferred from wr == rd | P7 | buffer treated as empty when full; wholesale loss |
| 8 | $clog2(DEPTH) used directly | elaboration fails at DEPTH = 1 | build break at a legal configuration |
| 9 | group released on "both non-empty" | P12 | pieces from different instants combined — undetectable downstream |
| 10 | one read pointer advanced on release | P14 | lanes desynchronise permanently after one group |
| 11 | lane buffers swapped | P13 | one lane's content consistently in the wrong position |
| 12 | stripe mismatch treated as skew | P15 | receiver hangs waiting for a match that cannot come |
| 13 | overflow discarded silently | P10 | data loss with no diagnostic anywhere |
| 14 | collector publishes shift_q directly | P19 | partial words delivered as complete |
| 15 | collector drop not reported | P20 | word loss invisible whenever the consumer stalls |
| 16 | collector bit placement reversed | P18 | every word bit-reversed — content right, order wrong |
12. A Trace
Internal teaching signals. WORD_W = 8, alignment already acquired, DEPTH = 2.
step 1 2 3 4 5 6 7
in_valid 1 1 1 1 1 0 0
in_word W0 W1 W2 W3 W4 - -
occupancy 0 1 2 2 2 1 0
out_valid 0 1 1 1 1 1 1
out_ready 0 0 0 1 0 1 1
out_word - W0 W0 W0 W1 W1 W2
overflow 0 0 0 1 1 1 1Read steps 1–3. Words arrive and are buffered; the consumer is stalled. Occupancy reaches DEPTH.
Read step 3 → 4. W3 arrives with the buffer full and the consumer taking W0 in the same cycle. can_take is true because a slot vacates, so W3 is accepted — occupancy stays at 2.
Read step 4 → 5 — the loss. W4 arrives, the buffer is full, and the consumer is not taking. W4 is gone, and overflow is set. Note it stays set: it is a sticky report, not a momentary condition.
Read steps 6–7. The consumer drains. W4 never appears — the stream continued past it and nothing can bring it back.
And note what a ready on the input would have done here: nothing. There is no producer to stall.
13. Debugging
Symptom → hypothesis → signals → distinguishing experiment.
Every received word contains correct bits in the wrong order
Boundary alignment, not bit errors (§3).
Inspect: aligned, align_lost, the slip request handshake, and whether output is being published while unaligned (P1).
The distinguishing experiment: send a known repeating pattern and check whether the rotation is constant. A fixed rotation on every word is an alignment phase; a varying one is not alignment at all.
Errors appear only when downstream stalls
Missing or undersized elastic storage (§11's counterexample).
Inspect: overflow, and whether a completed word is being overwritten while offered (P8).
The distinguishing experiment — and this one is decisive: hold the consumer ready permanently and re-run. If the errors vanish entirely, the channel is fine and the problem is buffering. A signal-integrity fault does not care whether your consumer is busy.
x1 works and x4 corrupts
Lane recombination or deskew (§7), not the per-lane path — which the x1 case has already proven correct.
Inspect: stripe_mismatch, skew_overflow, and whether groups are being released on "both non-empty" rather than on matching stripes (P12).
The distinguishing experiment: run x4 with all four lanes carrying identical content. If it still corrupts, the fault is in the recombination structure; if it works, the fault is skew or identity — because identical content makes a mis-pairing harmless.
One lane's content consistently appears in the wrong position
Lane identity or a wiring swap (P13), not skew — skew produces timing errors, not positional ones.
Inspect: the mapping from physical lane to logical lane, which Chapter 17.3 establishes.
The distinguishing experiment: send a per-lane distinct pattern — lane n carries the value n. A swap shows up immediately and unambiguously.
Burst errors correlated with temperature, cable, or a reseat
Leave this chapter. Alignment, buffering and deskew bugs are deterministic: same stimulus, same failure, every time.
Errors that vary with physical conditions belong to the CDR and electrical domain — Chapters 17.4 and 17.5.
The distinguishing experiment is the most valuable in both PHY chapters: run identical traffic twice. A digital bug reproduces exactly. A channel problem does not.
14. Common Misconceptions
- "Deserialization is just a reverse shift register." Timing recovery, boundary alignment, elastic absorption and lane identity are four separate problems (§1).
- "The receiver can back-pressure the wire with
ready." It cannot. There is no such mechanism at this layer — the only option is to absorb (§9). - "CDR is ordinary synchronous RTL." It is dedicated mixed-signal circuitry, and a behavioural PLL in SystemVerilog models none of what makes it hard (§2).
- "Once N bits are collected you have a TLP." You have a word. Alignment, decoding, descrambling, lane recombination and Link-layer processing all still lie ahead (§3, P16).
- "Gen1 comma alignment applies identically to Gen5." Different encodings, different framing, different mechanisms (§4).
- "Lanes can be concatenated by local cycle number." They do not arrive together; concatenating by cycle builds words that were never transmitted (§7).
- "Deskew and equalization are the same thing." Deskew is digital lane alignment; equalization is channel compensation — Chapter 17.4.
- "Lane identity stops mattering once training finishes." It is exactly what makes recombination possible afterwards (§7, P13).
- "An elastic buffer is a replay buffer." Replay storage holds packets for retransmission (Chapter 14.4); an elastic buffer absorbs a rate difference and can lose data (§9).
- "Stalling the output can safely stop sampling an active lane." The lane keeps running; stopping means discarding (§11).
- "A stripe mismatch is just more skew — wait longer." Skew is resolved by waiting; a mismatch never is (P15).
- "Overflow is an internal detail." §1's source reports it to the MAC, because it means data was lost (P10).
15. Understanding Check
16. What's Next
Collecting bits was the easy part. Timing comes from dedicated circuitry; boundaries must be searched for by slipping and confirmed by repetition; rate differences must be absorbed because they cannot be refused; and lanes must be identified and realigned before they can be recombined.
The receiver's defining constraint is that it cannot say wait. Every structure in this chapter follows from that — the elastic buffer exists to survive it, and both of its failure modes are reported because a receiver that cannot refuse must at least be able to tell.
Two of those four problems depend on something this chapter assumed and never explained. Alignment needs a partner that is transmitting recognisable evidence. Lane identity and grouping need both ends to agree on which lanes exist and what they are called.
Neither is true when a Link first powers up. Chapter 17.3 — Link Training is how two ports get from "traces are connected" to a usable negotiated Link — a mutually supported speed, a confirmed set of lanes, and a configuration the rest of the stack can rely on.
The idea to carry forward: a receiver's architecture is shaped entirely by what it is not allowed to ask for.