Skip to content

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.

ResponsibilityOwned by
packet meaning, headers, creditsTransaction / Data Link (Module 3)
framing, encoding, scramblingdigital PCS
lane distributiondigital PCS
buffering and handoff to the macrodigital PCS — RTL you write
final parallel-to-serial conversiondedicated SerDes
pre/post-emphasis, driver, impedanceanalog
the wire itselfphysics

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

A digital packet stream from the link layer enters a framing and encoding stage, then a lane distributor which assigns ordered content to each active lane. Each lane has its own parallel holding register feeding a SerDes macro boundary. Beyond that boundary, dedicated circuitry performs the final parallel to serial conversion and drives the differential physical lane. The framing, distribution and holding stages are ordinary synchronous digital logic; the serializer and driver are not.From the Linklayersordered digitalstreamFraming andencodingModule 5 owns theschemeLane distributorordered content perlanePer-lane holdingstagestable handoff, onelane eachSerDes macrofinal conversion —not RTLDifferentiallaneanalog, and physicsper lanehandoff12
Figure 1 — the transmit chain from digital packet stream to physical lane, following the vendor decomposition quoted in §1. Everything to the left of the SerDes boundary is ordinary synchronous logic. Everything to the right is dedicated circuitry: the final conversion, the analog driver, and the channel. The chapter's RTL lives entirely in the left half.

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:

QuestionAnswered by
What representation should be transmitted?encoding / framing (Module 5)
In what time sequence do those bits leave the lane?serialization
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
packet bytes
  → framing / encoding        ← what to send
  → scrambling                 ← Module 5
  → serialization              ← in what order, over time
  → the lane

5. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
x1   → 1 serializer, 1 differential pair
x4   → 4 serializers, 4 differential pairs, running concurrently
x16  → 16 of each

Before 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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.

pushpopResult
10occupancy +1
01head leaves, w1 shifts down
11both apply — occupancy unchanged
1, full0refused via phy_ready — never dropped
macro ready, nothing heldunderrun 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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.

SituationResult
idle, word offeredloaded, busy set, index 0
bit offered, serial_ready lownothing moves — bit and index stable
bit transfers, not lastindex +1
last bit transfers with a new word offerednew word loaded the same cycle, index restarts
last bit transfers, nothing offeredbusy clears
WORD_W = 1one 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
 
endmodule

Classification: 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.

SituationResult
chunk offered, selected lane readydelivered to that lane only; selector advances
selected lane not readyin_ready low; nothing delivered
lane deactivated under the selectorselector moves to an active lane
no active laneno_active_lane; nothing accepted
LANES = 1every 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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     1

Read 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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_W guarded. 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_valid held 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_FIRST both 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 the LANES = 1 width 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_active reduced while running — verify the selector moves rather than stalling.
  • No active lane — verify no_active_lane and that nothing is accepted.
  • One lane not ready — verify the chunk waits rather than diverting.

Mutations

#MutationCaught bySilicon symptom
1index advances on serial_validP4, mid-word stallone bit lost per stall cycle; words shifted, not garbage
2new word accepted while busyP3first word truncated, second corrupt
3output bit read from in_wordP2, P6stream follows the producer, not the captured word
4bit order reversedP6every word bit-reversed — content right, arrangement wrong
5busy cleared one bit earlyP1, P7final bit of every word missing
6$clog2(WORD_W) used directlyelaboration fails at WORD_W = 1build break at a legal configuration
7handoff word overwritten before the macro takes itP10intermittent word loss under macro back-pressure
8underrun not reportedP12a real wire condition invisible to firmware
9inactive lane drivenP14traffic on a lane the Link did not negotiate
10distributor uses a natural counter wrapP15 at LANES = 3chunks delivered to a non-existent lane
11one chunk broadcast to all lanesP13x4 behaves as four copies of x1
12handoff drops instead of back-pressuringP9silent 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-onebusy 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 ready is 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.