PCIe · Module 17
Serialization — Where Digital Logic Stops and the Lane Begins
A packet is a wide parallel structure; a lane carries one bit at a time. Serialization is that boundary — and most of it is not RTL you write. What the digital side owns, what the SerDes owns, and why the line between them is the whole lesson.
Sixteen modules have moved packets around as structures: fields, headers, payloads, sequence numbers, credits. Every one of them assumed a wide parallel representation that a design can index into and reason about.
A lane does not carry a structure. It carries one voltage transition after another, forever, at a rate measured in gigatransfers per second.
Serialization is the boundary between those two worlds — and the most useful thing to understand about it is not how a shift register works. It is which side of the boundary each responsibility falls on, because RTL you can write ends well before the wire begins.
What does it actually mean to turn a wide digital representation into a timed serial stream, and which parts of that operation belong to synthesizable logic rather than to dedicated SerDes hardware?
1. The Verified Structure
2. The Boundary Is the Lesson
Start here, not at a shift register.
| Responsibility | Owned by |
|---|---|
| packet meaning, headers, credits | Transaction / Data Link (Module 3) |
| framing, encoding, scrambling | digital PCS |
| lane distribution | digital PCS |
| buffering and handoff to the macro | digital PCS — RTL you write |
| final parallel-to-serial conversion | dedicated SerDes |
| pre/post-emphasis, driver, impedance | analog |
| the wire itself | physics |
Why this matters more than it sounds. A design engineer working on a PCIe controller will write a great deal of logic that feeds a serializer and essentially none that is one. The SerDes arrives as a hard macro with a datasheet. The engineering problem is the interface, not the conversion — and §8's handoff buffer is the shape that problem actually takes.
3. The Chain
Three things to read out of the figure.
The distributor comes before the holding stages, not after. Each lane gets its own ordered content and then its own path outward — which is why §6 exists and why a x16 Link is sixteen streams rather than one fast one.
The handoff is a real interface with a real contract, and §8 builds it. It is where a digital producer with its own timing meets a macro with its own.
And the last two blocks are drawn differently on purpose. They are not RTL. Depicting a serializer as a shift register in a block diagram of a production PHY is the diagram telling a small lie that §2 spends a section undoing.
4. Serialization Is Not Packetization, and Not Encoding
Two separations, both of which get collapsed.
The serializer decides nothing about meaning. It does not know whether the bits are a Memory Read, a Completion, an ACK or a credit update. Those decisions were made in Chapters 3.1 and 3.2 and are finished by the time anything reaches here.
And encoding is a different question from ordering:
| Question | Answered by |
|---|---|
| What representation should be transmitted? | encoding / framing (Module 5) |
| In what time sequence do those bits leave the lane? | serialization |
packet bytes
→ framing / encoding ← what to send
→ scrambling ← Module 5
→ serialization ← in what order, over time
→ the lane5. Bit Order — What Is Safe to Say
The question sounds trivial and is not.
What §1's source states at a concrete interface, and this is quotable: at that vendor's PMA boundary, pma_txdata[0] is the first bit out of the serializer. And for 8b/10b, "the 'a' bit of each code-group is the first bit transmitted … as per 8b10b protocol standards."
What this chapter will not say is "PCIe always transmits bit 0 first."
6. One Lane, One Stream
Reinforcing Chapter 6.1 with the PHY's view of it.
Each active lane has its own transmit serial path. A x4 Link is four simultaneous serial streams, not one stream going four times faster.
x1 → 1 serializer, 1 differential pair
x4 → 4 serializers, 4 differential pairs, running concurrently
x16 → 16 of eachBefore serialization, the ordered stream is distributed across the active lanes (Chapter 6.6), and each lane then serializes its own assigned content independently.
7. Width and Clocking
Why a production PHY is not one clock domain at one width — with real numbers rather than invented ones.
§1's source states it directly: the transmitter "allows two, four, or eight octets to be transferred from the fabric per clock beat", and the internal PCS domain is consequently at twice, the same as, or half the fabric clock frequency — "The domains are always synchronous even when they are at different frequencies."
Read the trade-off in that sentence. A wider fabric datapath moves the same data at a lower frequency, which is what makes multi-GT/s rates achievable in ordinary logic at all. Narrower and faster near the SerDes; wider and slower toward the packet logic.
8. RTL — Handoff to the SerDes Macro
// SYNTHESIZABLE. Elastic handoff between the digital PCS and a SerDes
// macro interface.
// That a serializer sits behind a fixed-width parallel interface is
// VERIFIED (section 1: the PMA-PCS data path, up to 40 bits wide in the
// quoted device). The two-entry depth and the ready/valid contract are
// ILLUSTRATIVE IMPLEMENTATION POLICY.
module serdes_handoff #(
parameter int WORD_W = 40 // vendor PMA-PCS width in section 1
) (
input logic clk,
input logic rst_n,
// ---- From the digital PCS ----------------------------------------------
input logic phy_valid,
output logic phy_ready,
input logic [WORD_W-1:0] phy_word,
// ---- To the SerDes macro ------------------------------------------------
output logic macro_valid,
input logic macro_ready,
output logic [WORD_W-1:0] macro_word,
output logic underrun // macro wanted a word, none held
);
generate
if (WORD_W < 1) $error("WORD_W must be at least 1");
endgenerate
// TWO ENTRIES. One is enough to prevent overwrite; the second decouples
// the producer from a macro that accepts every other cycle, which is the
// ordinary case when the two sides run at different widths (section 7).
logic [WORD_W-1:0] w0_q, w1_q;
logic [1:0] cnt_q;
logic un_q;
assign macro_valid = (cnt_q != 2'd0);
assign macro_word = w0_q;
assign underrun = un_q;
wire pop = macro_valid && macro_ready;
// EFFECTIVE capacity: a slot vacated this cycle is usable this cycle.
wire can_take = (cnt_q != 2'd2) || pop;
assign phy_ready = can_take;
wire push = phy_valid && phy_ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
w0_q <= '0; w1_q <= '0; cnt_q <= 2'd0; un_q <= 1'b0;
end else begin
unique case ({push, pop})
2'b10 : begin // push only
if (cnt_q == 2'd0) w0_q <= phy_word;
else w1_q <= phy_word;
cnt_q <= cnt_q + 2'd1;
end
2'b01 : begin // pop only
w0_q <= w1_q;
cnt_q <= cnt_q - 2'd1;
end
2'b11 : begin // both
if (cnt_q == 2'd1) w0_q <= phy_word;
else begin w0_q <= w1_q; w1_q <= phy_word; end
end
default : ;
endcase
// REPORTED, NOT HIDDEN. A macro that asked for a word when none was
// held is a real condition with real consequences on the wire, and
// section 11 explains why it cannot simply be back-pressured away.
if (macro_ready && !macro_valid) un_q <= 1'b1;
end
end
endmoduleClassification: synthesizable.
Architecture. A two-entry elastic stage. One entry would prevent overwrite; two decouple the producer from a macro that does not accept every cycle, which is the normal case when the two sides differ in width (§7).
Cycle behaviour.
push | pop | Result |
|---|---|---|
| 1 | 0 | occupancy +1 |
| 0 | 1 | head leaves, w1 shifts down |
| 1 | 1 | both apply — occupancy unchanged |
| 1, full | 0 | refused via phy_ready — never dropped |
| — | macro ready, nothing held | underrun reported |
Failure — four. A single register with no valid tracking lets a new word overwrite one the macro has not taken. Inferring full from a pointer comparison confuses full with empty (Chapter 14.4 §10). Dropping instead of back-pressuring phy_ready loses a word silently. And treating underrun as harmless hides the condition §11 says the wire cannot forgive.
Deliberately simplified: single clock — a real macro boundary may need a CDC-safe structure (§16); no gearbox — the width conversion of §7 is not modelled; macro_ready is an abstraction.
9. RTL — Teaching Serializer
// SYNTHESIZABLE. Low-rate digital parallel-to-serial converter.
// TEACHING MODEL ONLY -- a production PCIe serializer is dedicated
// circuitry (section 2). The BIT ORDER is a declared PARAMETER, not an
// assumption: section 5 explains why the general claim is unsafe even
// though the specific vendor and 8b/10b statements are quotable.
module teach_serializer #(
parameter int WORD_W = 8,
// Declared, not assumed (section 5). Default follows the convention
// quoted in section 1 at that vendor's PMA interface.
parameter bit LSB_FIRST = 1'b1
) (
input logic clk,
input logic rst_n,
// ---- Parallel in --------------------------------------------------------
input logic in_valid,
output logic in_ready,
input logic [WORD_W-1:0] in_word,
// ---- Serial out ---------------------------------------------------------
// A DECOUPLED interface, deliberately -- section 11 explains that a real
// lane has no such thing, and why teaching it this way is still right.
output logic serial_valid,
input logic serial_ready,
output logic serial_bit,
output logic busy
);
// GUARDED INDEX WIDTH. $clog2(1) is zero and a zero-width counter cannot
// index anything -- 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] word_q;
logic [IDX_W-1:0] idx_q;
logic busy_q;
assign busy = busy_q;
assign serial_valid = busy_q;
// The word is IMMUTABLE while it is being emitted, so the bit selection
// reads captured state and never the live input.
assign serial_bit = LSB_FIRST ? word_q[idx_q]
: word_q[IDX_W'(WORD_W-1) - idx_q];
// A new word is accepted only when nothing is in flight, or on the exact
// cycle the last bit transfers. Anything looser overwrites a word that
// is still being sent.
wire last_bit = (idx_q == IDX_W'(WORD_W-1));
wire bit_fire = serial_valid && serial_ready;
wire finishing = bit_fire && last_bit;
assign in_ready = !busy_q || finishing;
wire load = in_valid && in_ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
word_q <= '0; idx_q <= '0; busy_q <= 1'b0;
end else begin
// LOAD HAS PRIORITY over finishing, so a back-to-back word starts
// immediately rather than costing an idle cycle -- and the index
// restarts, which is what makes the two cases distinguishable.
if (load) begin
word_q <= in_word;
idx_q <= '0;
busy_q <= 1'b1;
end else if (finishing) begin
busy_q <= 1'b0;
idx_q <= '0;
end else if (bit_fire) begin
// ===========================================================
// THE INDEX ADVANCES ONLY ON A TRANSFER.
//
// Advancing on serial_valid alone skips a bit whenever the
// consumer stalls -- section 12's counterexample, and a one-line
// mistake that is invisible until something downstream is busy.
// ===========================================================
idx_q <= idx_q + IDX_W'(1);
end
end
end
endmoduleClassification: synthesizable (teaching model).
Architecture. A captured word, a bit index, and a busy flag. The index advances only on serial_valid && serial_ready — the single most important line in the module.
Cycle behaviour.
| Situation | Result |
|---|---|
| idle, word offered | loaded, busy set, index 0 |
bit offered, serial_ready low | nothing moves — bit and index stable |
| bit transfers, not last | index +1 |
| last bit transfers with a new word offered | new word loaded the same cycle, index restarts |
| last bit transfers, nothing offered | busy clears |
WORD_W = 1 | one bit per word; IDX_W guarded to 1 |
Failure — five. Advancing the index on serial_valid skips a bit under stall (§12). Accepting a new word while busy overwrites one still being sent. $clog2(WORD_W) used directly fails to elaborate at WORD_W = 1. Reading in_word instead of word_q for the output bit couples the emitted stream to whatever the producer now holds. And assuming the bit order from vector indexing rather than declaring it (§5).
Deliberately simplified: low rate; no encoding; a decoupled serial interface that a real lane does not have (§11).
10. RTL — Lane Distributor
// SYNTHESIZABLE. Distribute an ordered chunk stream across active lanes.
// THE DISTRIBUTION POLICY IS GENERIC ROUND-ROBIN TEACHING PEDAGOGY
// (section 6). The per-lane independence, the active-lane mask and the
// range safety are the architectural lessons and those DO generalise.
module lane_distributor #(
parameter int LANES = 4,
parameter int CHUNK_W = 8,
// GUARDED. $clog2(1) is zero; LANES = 1 is a legal configuration.
parameter int LANE_W = (LANES <= 1) ? 1 : $clog2(LANES)
) (
input logic clk,
input logic rst_n,
// ---- Ordered chunk in ---------------------------------------------------
input logic in_valid,
output logic in_ready,
input logic [CHUNK_W-1:0] in_chunk,
// ---- Active lane configuration, from Chapter 17.3's committed result ---
// A NORMALIZED mask: bit p set means logical lane p carries traffic.
input logic [LANES-1:0] lane_active,
// ---- Per-lane output ----------------------------------------------------
output logic [LANES-1:0] lane_valid,
input logic [LANES-1:0] lane_ready,
output logic [CHUNK_W-1:0] lane_chunk [LANES],
output logic no_active_lane
);
generate
if (LANES < 1) $error("LANES must be at least 1");
if (LANE_W < 1) $error("LANE_W must be at least 1");
if ((LANES > 1) && ((1 << LANE_W) < LANES))
$error("LANE_W too narrow to index LANES");
endgenerate
logic [LANE_W-1:0] sel_q;
assign no_active_lane = (lane_active == '0);
// NEXT ACTIVE LANE, with an explicit wrap at LANES-1. Never rely on a
// counter wrapping naturally -- it only does at a power-of-two LANES,
// and LANES is a free parameter.
function automatic logic [LANE_W-1:0] next_active(input logic [LANE_W-1:0] cur);
logic [LANE_W-1:0] c;
c = cur;
for (int i = 0; i < LANES; i++) begin
c = (c == LANE_W'(LANES-1)) ? '0 : (c + LANE_W'(1));
if (lane_active[c]) return c;
end
return cur; // no active lane: hold
endfunction
// RANGE SAFETY. sel_q is internally generated here, but the pattern is
// written out because an externally-supplied lane id must ALWAYS be
// checked before an array access -- never rely on "the protocol will not
// send that".
localparam int CHK = LANE_W + 1;
wire sel_legal = (CHK'(sel_q) < CHK'(LANES));
wire selected_ok = sel_legal && lane_active[sel_q] && lane_ready[sel_q];
assign in_ready = selected_ok;
wire fire = in_valid && in_ready;
always_comb begin
for (int p = 0; p < LANES; p++) begin
// AN INACTIVE LANE NEVER RECEIVES NORMAL TRAFFIC.
lane_valid[p] = in_valid && sel_legal && lane_active[p]
&& (LANE_W'(p) == sel_q);
lane_chunk[p] = in_chunk;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sel_q <= '0;
end else if (fire) begin
sel_q <= next_active(sel_q);
end else if (!no_active_lane && !lane_active[sel_q]) begin
// The committed lane set changed under us and the current selection
// is no longer active. Move to one that is, rather than stalling on
// a lane that will never be ready.
sel_q <= next_active(sel_q);
end
end
endmoduleClassification: synthesizable (explicitly-scoped teaching policy).
Architecture. A rotating selector over the active lanes, with the wrap written out and the index range-checked.
Cycle behaviour.
| Situation | Result |
|---|---|
| chunk offered, selected lane ready | delivered to that lane only; selector advances |
| selected lane not ready | in_ready low; nothing delivered |
| lane deactivated under the selector | selector moves to an active lane |
| no active lane | no_active_lane; nothing accepted |
LANES = 1 | every chunk to lane 0; LANE_W guarded |
Failure — five. A natural counter wrap breaks at any non-power-of-two LANES. $clog2(LANES) in a width fails at LANES = 1. Indexing lane_ready[sel] before a range check reads out of bounds when the selector is externally influenced. Driving lane_valid for an inactive lane sends traffic onto a lane the Link did not negotiate (Chapter 17.3). And stalling forever on a deactivated lane when the committed width shrinks.
Deliberately simplified: the round-robin policy is pedagogy (§6); one chunk per cycle; no framing awareness.
11. The Wire Cannot Be Back-Pressured
12. A Trace, and the One-Line Bug
Internal teaching signals. WORD_W = 8, LSB_FIRST.
step 1 2 3 4 5 6 7
in_valid 1 0 0 0 0 0 0
in_word A5 - - - - - -
busy 0 1 1 1 1 1 1
idx - 0 1 1 1 2 3
serial_valid 0 1 1 1 1 1 1
serial_ready - 1 1 0 0 1 1
serial_bit - 1 0 1 1 0 1Read steps 3–5. The consumer stalls for two cycles. idx holds at 1 and serial_bit holds its value. The bit is presented until it is taken.
13. Assertions
// SVA over teach_serializer, serdes_handoff and lane_distributor. These
// assert LOCAL digital contracts. They assert nothing about the analog
// serializer, PLL, driver, equalization, or the channel -- none of which
// is modelled (section 2).
// ---- ENVIRONMENT ------------------------------------------------------
// A1: the input word is stable while offered and not accepted.
assume property (@(posedge clk) disable iff (!rst_n)
(in_valid && !in_ready) |=> ($stable(in_word) && in_valid));
// A2: lane_active is Chapter 17.3's committed, normalized mask and does
// not change mid-word.
assume property (@(posedge clk) disable iff (!rst_n)
busy |-> $stable(lane_active));
// ---- SERIALIZER -------------------------------------------------------
// P1: an accepted word is emitted EXACTLY WORD_W times. (bits_sent is a
// testbench counter reset on load.)
property p_exact_bit_count;
@(posedge clk) disable iff (!rst_n)
$fell(busy) |-> (bits_sent == WORD_W);
endproperty
a_count : assert property (p_exact_bit_count);
// P2: THE WORD IS IMMUTABLE while it is being emitted. Catches an output
// wired to the live input rather than to captured state.
property p_word_immutable;
@(posedge clk) disable iff (!rst_n)
(busy && !load) |=> $stable(word_q);
endproperty
a_immutable : assert property (p_word_immutable);
// P3: no new word is accepted while one is in flight, except on the exact
// cycle the last bit transfers.
property p_no_overwrite;
@(posedge clk) disable iff (!rst_n)
(busy && !finishing) |-> !in_ready;
endproperty
a_no_overwrite : assert property (p_no_overwrite);
// P4: THE INDEX ADVANCES ONLY ON A TRANSFER. Section 12's counterexample.
property p_index_only_on_fire;
@(posedge clk) disable iff (!rst_n)
(busy && !bit_fire && !load) |=> $stable(idx_q);
endproperty
a_index : assert property (p_index_only_on_fire);
// P5: the presented bit is stable while stalled.
property p_bit_stable_under_stall;
@(posedge clk) disable iff (!rst_n)
(serial_valid && !serial_ready) |=> (serial_valid && $stable(serial_bit));
endproperty
a_bit_stable : assert property (p_bit_stable_under_stall);
// P6: THE ORDER PROPERTY. The emitted bit matches an INDEPENDENT
// restatement of the declared convention -- not the module's own
// expression, which would agree with a wrong one (section 5).
property p_bit_order_matches_declared;
@(posedge clk) disable iff (!rst_n)
serial_valid |-> (serial_bit == (LSB_FIRST ? word_q[idx_q]
: word_q[WORD_W-1-idx_q]));
endproperty
a_order : assert property (p_bit_order_matches_declared);
// P7: busy clears only after the FINAL bit transfers.
property p_busy_clears_on_last;
@(posedge clk) disable iff (!rst_n)
$fell(busy) |-> $past(finishing);
endproperty
a_busy : assert property (p_busy_clears_on_last);
// P8: the index is never out of range -- the WORD_W = 1 corner included.
property p_index_in_range;
@(posedge clk) disable iff (!rst_n)
busy |-> (idx_q <= IDX_W'(WORD_W-1));
endproperty
a_range : assert property (p_index_in_range);
// ---- HANDOFF ----------------------------------------------------------
// P9: no word is dropped -- a refused word is back-pressured, not lost.
property p_no_word_dropped;
@(posedge clk) disable iff (!rst_n)
(phy_valid && !phy_ready) |=> (phy_valid && $stable(phy_word));
endproperty
a_no_drop : assert property (p_no_word_dropped);
// P10: the held word is not overwritten before the macro takes it.
property p_no_macro_overwrite;
@(posedge clk) disable iff (!rst_n)
(macro_valid && !macro_ready) |=> (macro_valid && $stable(macro_word));
endproperty
a_hold : assert property (p_no_macro_overwrite);
// P11: simultaneous push and pop leave occupancy unchanged.
property p_simul;
@(posedge clk) disable iff (!rst_n)
(push && pop) |=> (cnt_q == $past(cnt_q));
endproperty
a_simul : assert property (p_simul);
// P12: an underrun is REPORTED. The wire cannot be stalled (section 11),
// so this condition must never be silent.
property p_underrun_reported;
@(posedge clk) disable iff (!rst_n)
(macro_ready && !macro_valid) |=> underrun;
endproperty
a_underrun : assert property (p_underrun_reported);
// ---- DISTRIBUTOR ------------------------------------------------------
// P13: a chunk goes to EXACTLY ONE lane.
property p_one_lane_only;
@(posedge clk) disable iff (!rst_n)
in_valid |-> ($countones(lane_valid) <= 1);
endproperty
a_one_lane : assert property (p_one_lane_only);
// P14: AN INACTIVE LANE NEVER RECEIVES NORMAL TRAFFIC. A lane the Link did
// not negotiate must carry nothing (Chapter 17.3).
generate for (genvar p = 0; p < LANES; p++) begin : g_inactive
property p_inactive_silent;
@(posedge clk) disable iff (!rst_n)
!lane_active[p] |-> !lane_valid[p];
endproperty
a_inactive : assert property (p_inactive_silent);
end endgenerate
// P15: the selector index is always in range, at every LANES value.
property p_sel_in_range;
@(posedge clk) disable iff (!rst_n)
(LANES == 1) || (sel_q < LANE_W'(LANES));
endproperty
a_sel : assert property (p_sel_in_range);
// P16: THE LAYER PROPERTY. Nothing in this chapter interprets packet
// meaning -- the serializer has no notion of TLP type (section 4).
property p_no_packet_semantics;
@(posedge clk) disable iff (!rst_n)
serial_valid |-> !(dut_tl.cpl_valid || dut_tl.mem_valid);
endproperty
a_layer : assert property (p_no_packet_semantics);
// P17: reset discards a partial word and clears output validity.
property p_reset;
@(posedge clk)
!rst_n |=> (!busy && !serial_valid && !macro_valid);
endproperty
a_reset : assert property (p_reset);P6 is written as an independent restatement of the declared convention, deliberately. A property that compared serial_bit against the module's own expression would verify only that the wire equals itself — it would agree with a reversed implementation. Restating the indexing means a convention mistake produces a mismatch.
P4 and P5 are a pair. P4 says the index does not move under stall; P5 says the presented bit does not change. A design can hold the index and still recompute the bit from a mutating source, which is why both exist.
P12 exists because §11's asymmetry is real. An underrun is not a stall — the macro will emit something on the lane regardless — so a design that treats "no word available" as a benign back-pressure case has misunderstood the interface.
14. Verification and Fault Injection
The scoreboard reconstructs the original word from the observed bit stream, using its own independently-written ordering model. It never inspects word_q or idx_q — reading the shift register would verify only that the register contains what the register contains.
Serializer
WORD_W = 1— one bit per word,IDX_Wguarded. Required parameter corner.WORD_W = 2, and a non-power-of-two width if the design claims one.- All-zero, all-one, walking one, alternating,
0xA5, random. The walking-one pattern localises an ordering error to a specific bit position. - Stall on the first bit, in the middle, and on the final bit. The middle case is §12's counterexample; the final-bit case catches a terminal off-by-one.
- Back-to-back words with
in_validheld high — verify the new word loads on the cycle the last bit transfers and the index restarts (P4, P7). - Reset mid-word — verify the partial word is discarded and no fragment is emitted (P17).
LSB_FIRSTboth ways, each checked against the independent model (P6).
Handoff
- Macro stalled across several words — verify back-pressure, no drop (P9, P10).
- Push and pop in the same cycle at occupancy 1 and 2 (P11).
- Macro ready with nothing held — verify
underrun(P12). - Reset with words held.
Distributor
LANES= 1, 2, 4 — and theLANES = 1width corner.- All lanes active — verify round-robin coverage and that each chunk lands on exactly one lane (P13).
- A subset active — verify inactive lanes carry nothing (P14) and the selector skips them.
lane_activereduced while running — verify the selector moves rather than stalling.- No active lane — verify
no_active_laneand that nothing is accepted. - One lane not ready — verify the chunk waits rather than diverting.
Mutations
| # | Mutation | Caught by | Silicon symptom |
|---|---|---|---|
| 1 | index advances on serial_valid | P4, mid-word stall | one bit lost per stall cycle; words shifted, not garbage |
| 2 | new word accepted while busy | P3 | first word truncated, second corrupt |
| 3 | output bit read from in_word | P2, P6 | stream follows the producer, not the captured word |
| 4 | bit order reversed | P6 | every word bit-reversed — content right, arrangement wrong |
| 5 | busy cleared one bit early | P1, P7 | final bit of every word missing |
| 6 | $clog2(WORD_W) used directly | elaboration fails at WORD_W = 1 | build break at a legal configuration |
| 7 | handoff word overwritten before the macro takes it | P10 | intermittent word loss under macro back-pressure |
| 8 | underrun not reported | P12 | a real wire condition invisible to firmware |
| 9 | inactive lane driven | P14 | traffic on a lane the Link did not negotiate |
| 10 | distributor uses a natural counter wrap | P15 at LANES = 3 | chunks delivered to a non-existent lane |
| 11 | one chunk broadcast to all lanes | P13 | x4 behaves as four copies of x1 |
| 12 | handoff drops instead of back-pressuring | P9 | silent word loss under load |
15. Debugging
Symptom → hypothesis → signals → distinguishing experiment.
Every received word is rotated by one bit position
Content is correct and arrangement is wrong, which immediately rules out the electrical domain — bit errors corrupt values, they do not permute them.
Inspect: the serializer's index advance condition, the load-versus-send priority on the final bit, and the declared bit-order convention at both ends.
The distinguishing experiment: send a walking one. A convention mismatch shows every word off by the same amount; a lost bit shows a cumulative drift that grows with the number of stalls. One test separates §12's counterexample from an ordering disagreement.
Only the last bit of every word is missing
A terminal off-by-one — busy clearing before the final transfer, or the index comparison using WORD_W where it should use WORD_W - 1.
Inspect: busy, idx_q and bit_fire on the final cycle of a word.
The first word after a stall is corrupt
A word was overwritten before it finished (P3), or the handoff stage let a new word in on top of a held one (P10).
The distinguishing experiment: stall for exactly one cycle, then for many. If corruption scales with stall length it is the index; if it happens on the first stalled cycle regardless of length it is the overwrite.
x4 behaves like four copies of the same x1 stream
The distributor is broadcasting rather than distributing (P13, mutation 11).
Inspect: lane_valid — $countones should never exceed one.
Random burst errors, correlated with temperature or cable
Stop looking at this chapter. Bit rotation, truncation and distribution bugs are deterministic and reproducible. Random bursts that vary with physical conditions belong to the electrical and CDR domain — Chapters 17.4 and 17.5.
The distinguishing experiment is the most valuable one in the chapter: run the same traffic twice. A digital ordering bug reproduces exactly. A channel problem does not.
16. Clock Domains — What This Chapter Does Not Model
Every RTL block here is single-clock, deliberately.
§1's quoted vendor arrangement has PCS domains at half, equal, or twice the fabric frequency and states they are "always synchronous even when they are at different frequencies" — so that particular width conversion is a gearbox, not a clock-domain crossing.
A genuinely asynchronous boundary is a different problem. It needs Gray-coded pointers, multi-flop synchronizers, and full/empty logic that is safe under metastability. Presenting a casually-written two-clock FIFO as production RTL would be worse than omitting it, so this chapter omits it and names the requirement instead.
What a real design owns at that boundary: CDC-safe elastic storage, clock-tolerance compensation (§1's source quotes ±300 ppm), and the SKP handling that goes with it. Chapter 17.2 §5 returns to this from the receive side, where the need is unavoidable rather than architectural.
17. Common Misconceptions
- "The serializer creates or understands TLPs." It has no notion of packet meaning; that was settled layers earlier (§4, P16).
- "Serialization and encoding are the same operation." Encoding decides what to send; serialization decides in what time order (§4).
- "A production PCIe serializer is a shift register." The final conversion is dedicated circuitry (§2, §9).
- "x16 means one serial line running sixteen times faster." Sixteen concurrent streams, each at the lane rate (§6).
- "RTL can model the analog output stage." It cannot, and a diagram that draws it as a block is misleading (§2).
- "PCIe defines the internal parallel datapath width." It defines the lane rate and the wire representation. Internal width is an implementation choice (§7).
- "Lane striping is byte round-robin, universally." The exact rule is generation-specific and not claimed here (§6).
- "The serial lane can pause because an RTL
readyis low." It cannot. A shortage becomes an underrun, not a stall (§11). - "Bit order can be assumed from vector indexing." It is an interface convention that must be declared and agreed (§5).
- "Equalization happens inside the digital serializer." Different mechanism, different domain — Chapter 17.4.
- "A word can be loaded whenever the input is valid." Not while one is in flight, or the first is truncated (P3).
- "An underrun is just back-pressure." Back-pressure stops a producer. An underrun happens on a wire that did not stop (§11, P12).
18. Understanding Check
19. What's Next
The boundary is the lesson. The digital side owns framing, distribution, buffering and a stable handoff; a dedicated SerDes owns the final conversion; the analog domain owns the wire. RTL that pretends otherwise is teaching a comfortable fiction.
Three ownership rules carried the chapter: a word is accepted once, emitted completely, and never disturbed in flight — and the index advances only on a real transfer, which is a one-line mistake away from losing a bit per stall.
And the wire does not stop. valid/ready is an internal abstraction that makes ownership visible; an active lane has no such mechanism, so a shortage becomes an underrun rather than a pause.
Chapter 17.2 — Deserialization takes the receive side, where that asymmetry stops being a teaching note and becomes the central constraint. A receiver cannot ask the far end to wait, so it must absorb every timing difference internally — and before it can do even that, it has to work out where one unit of information starts, which is a problem the transmitter never had.
The idea to carry forward: the interesting engineering is at the boundary, not in the conversion.