Skip to content
VLSI Mentor

Ethernet · Module 2

The PHY Layer

Coding, serialisation, clock recovery and line drive all exist because a real channel attenuates, disperses and carries no clock. The PCS, PMA and PMD split follows the same logic — each owns one consequence of physics, and each changes on its own schedule.

Chapter 2.5 organised the MAC's six responsibilities under one principle: each exists because the medium is shared, unreliable, or both. The PHY has its own principle and it is different.

The MAC's problem is that bits have no structure. The PHY's problem is that bits do not exist.

A conductor carries a voltage. A fibre carries light. Neither carries a one or a zero, and neither carries a clock. Everything the PHY does is a consequence of converting between a digital abstraction the MAC believes in and a continuous physical quantity that is all the medium actually offers.

What does the PHY do, why is it internally divided into three sublayers, and what does each sublayer own?

1. The Gap the PHY Closes

The same construction Chapter 2.5 §1 used, from the other side.

What the medium offers: a continuous physical quantity that changes over time, degrades with distance, and arrives with no annotation. There is no clock, no framing, no notion of a symbol boundary, and no guarantee the received waveform resembles the transmitted one.

What the MAC needs: a stream of bits, in order, at a known rate, with an indication of whether the stream is trustworthy.

The gapThe responsibilityBecause the channel
a receiver has no clockline coding guarantees transitionscarries no clock
a channel with DC bias distortsline coding balances the signalis AC-coupled or optical
the medium carries one signal, the datapath is parallelserialisationis one channel
symbol timing must be extracted from the dataclock recoverycarries no clock
a signal degrades with distanceline drive and equalisationattenuates and disperses
a receiver must know where blocks beginblock synchronisationdelivers no boundaries
two ends run on independent clockselastic bufferingconnects two independent oscillators

Two entries name line coding, and that is the observation worth carrying. Coding is not one mechanism serving one purpose — it simultaneously guarantees the transition density clock recovery needs and controls the DC content the channel requires, and those are independent physical constraints that happen to be solvable by the same choice of code.

2. Why the PHY Is Divided Into Three

The MAC is one block. The PHY is three sublayers, and the division is not arbitrary.

SublayerOwnsChanges when
PCSline coding, block boundaries, lane striping and alignment, idle insertion and deletionthe coding changes — a new generation, a new rate
PMAserialisation, deserialisation, clock recovery, per-lane timingthe rate or lane count changes
PMDdrivers, receivers, electrical or optical levels, the connectorthe medium changes — copper to fibre, one reach to another

The right-hand column is the reason for the split, and it is Chapter 2.3's test applied inside the PHY: put a boundary where change is predicted, and these three change independently.

The clearest evidence is that they are mixed and matched in practice. The same PCS serves several PMDs — one coding scheme, several media and reaches. A PMD change for a longer reach leaves the PCS untouched. Collapsing all three into "the PHY" hides which one a given change affects, which is why Chapter 2.1 §6 insisted on naming them separately.

3. Line Coding — Two Problems, One Mechanism

Coding is the PCS's central job and it solves two independent problems at once.

Transition density, for clock recovery. A receiver extracts timing from the signal's transitions. A long run of identical bits produces no transitions, the receiver's timing drifts, and it eventually samples in the wrong place. Coding guarantees a transition within a bounded number of symbols.

DC balance, for the channel. Many channels are AC-coupled — transformer or capacitor isolated — and cannot pass a sustained DC offset. A code that emitted long runs of one polarity would charge the coupling and shift the receiver's decision threshold. Coding keeps the running average near zero.

The cost is overhead, and it is the direct trade. A code that maps 8 bits to 10 symbols spends 20% of the channel guaranteeing those two properties. A code that maps 64 bits to 66 spends about 3% and works harder to achieve the same guarantees. Chapter 3.5 covers the specific codes; the structural point is that overhead buys transition density and DC balance, and nothing else.

A crucial distinction, and it is the one most often blurred. Line coding maps bits to symbols. Modulation maps symbols to physical levels — how many distinguishable voltage levels a symbol occupies. They are separate choices at separate sublayers, and Chapter 3.6 owns modulation. A code is not a modulation and PAM4 is not a block code.

4. RTL 1 — A Block Coder

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. A small block coder illustrating the two things every line
// code does: guarantee transitions, and control running disparity.
//
// NOT 4B/5B, 8B/10B or 64B/66B — the table is illustrative. Chapter 3.5
// owns the real codes and their normative mappings.
module block_coder #(
  parameter int unsigned IN_W  = 4,
  parameter int unsigned OUT_W = 5
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic             in_valid,
  input  logic [IN_W-1:0]  in_data,
  input  logic             in_control,   // a control code, not data
  output logic             in_ready,
 
  output logic             out_valid,
  output logic [OUT_W-1:0] out_code,
  input  logic             out_ready,
 
  // Running disparity: the cumulative excess of ones over zeros. The
  // channel cannot pass a sustained offset, so this must stay bounded.
  output logic signed [5:0] disparity,
  output logic              disparity_alarm
);
 
  // ILLUSTRATIVE table. Every entry has at least two transitions, which is
  // the transition-density guarantee — a receiver's timing recovery has
  // something to lock onto no matter what the data is.
  function automatic logic [OUT_W-1:0] encode(input logic [IN_W-1:0] d);
    case (d)
      4'h0: encode = 5'b11110; 4'h1: encode = 5'b01001;
      4'h2: encode = 5'b10100; 4'h3: encode = 5'b10101;
      4'h4: encode = 5'b01010; 4'h5: encode = 5'b01011;
      4'h6: encode = 5'b01110; 4'h7: encode = 5'b01111;
      4'h8: encode = 5'b10010; 4'h9: encode = 5'b10011;
      4'hA: encode = 5'b10110; 4'hB: encode = 5'b10111;
      4'hC: encode = 5'b11010; 4'hD: encode = 5'b11011;
      4'hE: encode = 5'b11100; default: encode = 5'b11101;
    endcase
  endfunction
 
  // A control code is a symbol that CANNOT appear in the data mapping. That
  // is how a receiver distinguishes "this is an idle marker" from "this is
  // the data value that happens to look like one" — and it is why a code
  // must have spare symbols. A code with no spare capacity cannot signal
  // anything out of band.
  localparam logic [OUT_W-1:0] CTRL_IDLE = 5'b00111;
 
  logic signed [5:0] disp_q;
  logic [OUT_W-1:0]  code_q;
  logic              v_q;
 
  assign in_ready  = !v_q || out_ready;
  assign out_valid = v_q;
  assign out_code  = code_q;
 
  wire [OUT_W-1:0] chosen = in_control ? CTRL_IDLE : encode(in_data);
  // Ones minus zeros for this symbol. Summed over time this is the DC
  // content the channel sees.
  wire signed [5:0] delta = 6'sd0 + 6'($countones(chosen)) - 6'(OUT_W - $countones(chosen));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_q <= 1'b0; code_q <= '0; disp_q <= '0;
    end else begin
      if (v_q && out_ready) v_q <= 1'b0;
      if (in_valid && in_ready) begin
        code_q <= chosen;
        v_q    <= 1'b1;
        disp_q <= disp_q + delta;
      end
    end
  end
 
  assign disparity = disp_q;
  // A real code SELECTS between two representations of each value to hold
  // disparity bounded. This model only MEASURES it, and alarms when the
  // bound is exceeded — which makes the mechanism's absence visible rather
  // than pretending the simplification does not matter.
  assign disparity_alarm = (disp_q > 6'sd6) || (disp_q < -6'sd6);
 
endmodule

Classification: synthesizable.

What it teaches: that a line code does three jobs, not one. It guarantees transitions so timing can be recovered, it bounds running disparity so an AC-coupled channel is not charged, and — the one most often forgotten — it provides spare symbols that cannot occur in data, which is the only way to signal anything out of band.

The control-code point is the subtle one. Idle, start and error indications have to be distinguishable from every possible data value. A code with no spare capacity cannot express them, so the coding overhead is not purely a tax: part of it buys an out-of-band signalling alphabet the layers above depend on.

disparity_alarm is honest about the simplification. A real code selects between two representations of each value to actively hold disparity near zero. This model only measures it and reports when the bound is exceeded, which makes the missing mechanism visible instead of leaving a reader to assume it is there.

Deliberately simplified: an illustrative table rather than a normative one; no disparity-driven selection; one symbol at a time where a real coder is wide; no decoder.

Production implication: a real coder implements the selection rule that keeps disparity bounded, decodes with an invalid-symbol indication for codes that cannot occur, and reports invalid symbols as a distinct error — because an invalid code group means the channel corrupted a symbol, which is different from a frame that failed its check value.

5. Serialisation — Because the Medium Is One Channel

A datapath is parallel because parallel logic is cheap. A medium is serial because conductors and fibres are expensive and because parallel media suffer skew between their lanes.

So the PMA converts, and the conversion has consequences worth naming.

The serial rate is the parallel rate times the width. A 32-bit datapath at some clock rate becomes a serial stream at 32 times that rate, and that multiplication is why the serial side is the hard part of a high-rate design.

The bit order is a specification decision. Which end of the parallel word goes first must be agreed by both sides, and getting it wrong produces a link that comes up, reports health, and garbles every frame — Chapter 2.1 §14's failure, at a different boundary.

And multi-lane links reintroduce the problem serialisation solved. When one serial stream is not fast enough, the PCS stripes across several lanes and the lanes arrive skewed, so alignment markers are inserted and the receiver deskews them. Chapter 9.5 develops this; the structural point is that lane striping is the PCS's job and per-lane timing is the PMA's, which is why they are separate sublayers.

6. RTL 2 — Serialiser and Deserialiser

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Parallel to serial and back, in one clock domain.
//
// NOT a real PMA: a real serial side runs at WIDTH times the parallel rate
// and uses circuit techniques outside RTL. This is the digital shape.
module pma_serdes #(
  parameter int unsigned WIDTH = 10,
  // Which end goes first. A SPECIFICATION decision that both ends must
  // share — and the classic bring-up failure when they do not, because the
  // link comes up and every frame is garbled.
  parameter bit MSB_FIRST = 1'b1
) (
  input  logic clk,
  input  logic rst_n,
 
  // Transmit: parallel in, serial out.
  input  logic             tx_valid,
  input  logic [WIDTH-1:0] tx_word,
  output logic             tx_ready,
  output logic             serial_out,
  output logic             serial_active,
 
  // Receive: serial in, parallel out. Alignment is NOT solved here — this
  // block emits a word every WIDTH bits from wherever it started counting,
  // which is almost certainly the wrong boundary. Section 8 fixes that.
  input  logic             serial_in,
  input  logic             serial_valid,
  output logic             rx_valid,
  output logic [WIDTH-1:0] rx_word
);
 
  localparam int unsigned CNT_W = $clog2(WIDTH);
 
  // ── Transmit ───────────────────────────────────────────────────────────
  logic [WIDTH-1:0] tx_sr_q;
  logic [CNT_W-1:0] tx_cnt_q;
  logic             tx_busy_q;
 
  assign tx_ready = !tx_busy_q || (tx_cnt_q == CNT_W'(WIDTH - 1));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      tx_sr_q <= '0; tx_cnt_q <= '0; tx_busy_q <= 1'b0;
    end else if (tx_valid && tx_ready) begin
      tx_sr_q  <= tx_word;
      tx_cnt_q <= '0;
      tx_busy_q <= 1'b1;
    end else if (tx_busy_q) begin
      // Shift toward whichever end goes first. The two cases are mirror
      // images and choosing wrongly is undetectable locally — both ends
      // must agree, and neither can tell alone.
      tx_sr_q  <= MSB_FIRST ? {tx_sr_q[WIDTH-2:0], 1'b0}
                            : {1'b0, tx_sr_q[WIDTH-1:1]};
      if (tx_cnt_q == CNT_W'(WIDTH - 1)) tx_busy_q <= 1'b0;
      else                               tx_cnt_q  <= tx_cnt_q + 1'b1;
    end
  end
 
  assign serial_out    = MSB_FIRST ? tx_sr_q[WIDTH-1] : tx_sr_q[0];
  assign serial_active = tx_busy_q;
 
  // ── Receive ────────────────────────────────────────────────────────────
  logic [WIDTH-1:0] rx_sr_q;
  logic [CNT_W-1:0] rx_cnt_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rx_sr_q <= '0; rx_cnt_q <= '0;
    end else if (serial_valid) begin
      rx_sr_q  <= MSB_FIRST ? {rx_sr_q[WIDTH-2:0], serial_in}
                            : {serial_in, rx_sr_q[WIDTH-1:1]};
      rx_cnt_q <= (rx_cnt_q == CNT_W'(WIDTH - 1)) ? '0 : rx_cnt_q + 1'b1;
    end
  end
 
  // THE HONEST LIMITATION. A word is emitted every WIDTH bits from an
  // arbitrary starting point. Nothing here knows where a code group
  // actually begins — that is block synchronisation, and it is a SEARCH
  // (Chapter 2.2's discovery problem) rather than a conversion.
  assign rx_valid = serial_valid && (rx_cnt_q == CNT_W'(WIDTH - 1));
  assign rx_word  = rx_sr_q;
 
endmodule

Classification: synthesizable.

What it teaches: that deserialisation and alignment are different problems, and that a deserialiser alone produces words from an arbitrary boundary. It converts; it does not discover. Section 8's block synchroniser is what finds the real boundary, and separating the two is why the PCS and PMA are different sublayers.

MSB_FIRST is a parameter and not a preference. Both ends must agree, and neither can determine the other's choice from its own observations. A mismatch produces a link that trains, locks, reports every status bit healthy, and delivers garbage — the same shape as Chapter 2.1 §14's width mismatch, and equally invisible to every per-layer status term.

Deliberately simplified: one clock domain, where a real SerDes has at least two; no analog behaviour; serial_valid stands in for a recovered bit clock; no loopback.

Production implication: a real serialiser's output rate is a multiple of its input clock and is generated by circuitry outside the RTL abstraction; its verification is partly measurement rather than simulation; and the bit-order choice is fixed by the standard rather than parameterised, precisely because a parameter invites two implementations to differ.

7. RTL 3 — Clock Recovery, With Its Abstraction Stated

The PHY responsibility that RTL models least honestly, so the model states its boundary explicitly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// BEHAVIOURAL MODEL — not synthesisable as a real recovery loop.
//
// ABSTRACTION BOUNDARY, stated: a real CDR is a phase detector, a loop
// filter and a controlled oscillator, operating in continuous time. This
// model captures only the digital consequences a MAC-side designer must
// account for: acquisition takes time, lock is lost when transitions stop,
// and re-acquisition takes time again.
module cdr_behavioural #(
  // ILLUSTRATIVE. Real acquisition time depends on loop bandwidth and the
  // incoming signal, and is specified in the PHY's datasheet.
  parameter int unsigned LOCK_SYMBOLS   = 64,
  // The bounded run length a code guarantees. Exceeding it starves the
  // recovery loop — which is WHY line codes guarantee transition density.
  parameter int unsigned MAX_RUN_LENGTH = 5
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic signal_present,   // from the PMD: something is arriving
  input  logic rx_bit,
  input  logic rx_bit_valid,
 
  output logic clk_locked,
  output logic [7:0] lock_acquisitions,
  output logic [7:0] lock_losses,
  output logic [3:0] longest_run
);
 
  logic [7:0] lock_cnt_q;
  logic [3:0] run_q, longest_q;
  logic       last_bit_q, locked_q;
  logic [7:0] acq_q, loss_q;
 
  // Run length: consecutive identical bits. This is the quantity a line
  // code exists to bound, and tracking it here is what connects Section 3's
  // coding argument to a measurable consequence.
  wire same_as_last = (rx_bit == last_bit_q);
  wire run_too_long = (run_q >= 4'(MAX_RUN_LENGTH));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      lock_cnt_q <= '0; run_q <= '0; longest_q <= '0;
      last_bit_q <= 1'b0; locked_q <= 1'b0; acq_q <= '0; loss_q <= '0;
    end else begin
      if (rx_bit_valid) begin
        last_bit_q <= rx_bit;
        run_q      <= same_as_last ? run_q + 1'b1 : '0;
        if (run_q > longest_q) longest_q <= run_q;
      end
 
      if (!signal_present) begin
        // No signal, no lock. Immediate, because there is nothing to track.
        if (locked_q && loss_q != 8'hFF) loss_q <= loss_q + 1'b1;
        locked_q   <= 1'b0;
        lock_cnt_q <= '0;
      end else if (run_too_long && locked_q) begin
        // Transitions stopped for longer than the code should ever allow.
        // A real loop drifts and eventually samples wrongly; this model
        // declares the loss, which is the digital consequence.
        if (loss_q != 8'hFF) loss_q <= loss_q + 1'b1;
        locked_q   <= 1'b0;
        lock_cnt_q <= '0;
      end else if (!locked_q && rx_bit_valid) begin
        // Acquisition takes time. That delay is why a link does not come up
        // instantly and why Chapter 2.1's link_up has a term for it.
        if (lock_cnt_q == 8'(LOCK_SYMBOLS - 1)) begin
          locked_q <= 1'b1;
          if (acq_q != 8'hFF) acq_q <= acq_q + 1'b1;
        end else lock_cnt_q <= lock_cnt_q + 1'b1;
      end
    end
  end
 
  assign clk_locked        = locked_q;
  assign lock_acquisitions = acq_q;
  assign lock_losses       = loss_q;
  assign longest_run       = longest_q;
 
endmodule

Classification: behavioural model; not synthesisable as a real recovery loop, and the header says so.

What it teaches: three digital consequences a designer above the PHY must account for. Acquisition takes time, so a link does not come up instantly and link_up needs a term for it. Lock can be lost, so the term is dynamic rather than a one-time event. Transition density determines whether lock holds, which is the direct connection between Section 3's coding argument and something measurable.

longest_run is the diagnostic that connects the layers. If the observed run length approaches what the code should guarantee, either the coder is wrong or the received symbols are corrupted — and in both cases the symptom appears as lock loss, two sublayers away from the cause.

Stating the abstraction boundary is the point of the module. A model that pretended to be a real CDR would teach a wrong mental picture, and an engineer who believed it would be surprised by every measurement. Naming what is abstracted is what makes the model useful rather than misleading.

Deliberately simplified: everything continuous-time; no jitter, no loop bandwidth, no frequency offset — Chapter 4.4's elastic buffer handles the offset's consequences and Section 9 models it; lock declared by a counter rather than by a phase relationship.

Production implication: real acquisition time comes from the PHY's datasheet and feeds the system's link-up budget; lock loss is counted and is a leading indicator of a marginal channel; and the recovery loop's behaviour is characterised by measurement, which is where RTL verification stops being sufficient.

Three PHY sublayers. The PCS owns coding, block boundaries and lane alignment and is fully digital. The PMA owns serialisation and clock recovery and is partly analog. The PMD owns drivers, receivers and the connector and is characterised by measurement rather than simulation.PCScoding, block lock, lanealignmentPMAserialise, recover the clockPMDdrive, receive, the connectorfully digitalproven by simulationpartly analogabstraction must be statedmeasuredeye diagrams, jitter, returnloss12
Figure 1 — three sublayers, and the boundary where simulation stops being enough.

Section 6's deserialiser emits words from an arbitrary boundary. Finding the real one is a separate problem and a different kind of problem.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Finds the code-group boundary by trying an offset,
// measuring how often it yields valid symbols, and shifting when it does
// not.
//
// NOT a standard's mechanism — Chapter 3.5 owns those. The structure is the
// content: hypothesis, evidence, commit, and re-verify.
module block_sync #(
  parameter int unsigned WIDTH      = 10,
  // How many consecutive valid symbols before declaring lock. Larger is
  // slower to lock and less likely to lock falsely.
  parameter int unsigned GOOD_LIMIT = 16,
  // How many invalid symbols before giving up the hypothesis.
  parameter int unsigned BAD_LIMIT  = 4
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic             sym_valid,
  input  logic [WIDTH-1:0] sym_data,
  input  logic             sym_legal,     // from the decoder: is this a
                                          // symbol the code can produce?
  output logic             shift_request, // try the next offset
  output logic             block_lock,
  output logic [$clog2(WIDTH)-1:0] offset,
  output logic [7:0]       lock_losses
);
 
  typedef enum logic [1:0] { S_HUNT, S_TEST, S_LOCKED } s_state_e;
  s_state_e state_q, state_d;
 
  logic [$clog2(GOOD_LIMIT+1)-1:0] good_q;
  logic [$clog2(BAD_LIMIT+1)-1:0]  bad_q;
  logic [$clog2(WIDTH)-1:0]        off_q;
  logic [7:0]                      loss_q;
 
  // THE EVIDENCE. A wrong offset slices code groups across their
  // boundaries, producing bit patterns the code cannot generate. Illegal
  // symbols are therefore the signal that the hypothesis is wrong — and
  // this is why a code needs INVALID symbols as well as spare valid ones.
  // A code in which every bit pattern were legal could not be synchronised
  // this way at all.
  always_comb begin
    state_d = state_q;
    case (state_q)
      S_HUNT: if (sym_valid) state_d = S_TEST;
      S_TEST: if (sym_valid && !sym_legal)      state_d = S_HUNT;
              else if (good_q == GOOD_LIMIT-1)  state_d = S_LOCKED;
      // Lock is not permanent. A locked receiver keeps checking, because a
      // channel that degrades will start producing illegal symbols and the
      // offset may need re-establishing.
      S_LOCKED: if (bad_q == BAD_LIMIT-1) state_d = S_HUNT;
      default: state_d = S_HUNT;
    endcase
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state_q <= S_HUNT; good_q <= '0; bad_q <= '0; off_q <= '0; loss_q <= '0;
    end else begin
      state_q <= state_d;
 
      if (state_q == S_HUNT && state_d == S_TEST) good_q <= '0;
      else if (state_q == S_TEST && sym_valid)
        good_q <= sym_legal ? good_q + 1'b1 : '0;
 
      if (state_q != S_LOCKED)                    bad_q <= '0;
      else if (sym_valid)                         bad_q <= sym_legal ? '0 : bad_q + 1'b1;
 
      // Each failed hypothesis advances to the next offset. WIDTH offsets
      // exist and exactly one is right, so this terminates — a search that
      // could not exhaust its space would be a receiver that hangs.
      if (state_q == S_TEST && state_d == S_HUNT)
        off_q <= (off_q == $clog2(WIDTH)'(WIDTH-1)) ? '0 : off_q + 1'b1;
 
      if (state_q == S_LOCKED && state_d == S_HUNT && loss_q != 8'hFF)
        loss_q <= loss_q + 1'b1;
    end
  end
 
  assign shift_request = (state_q == S_TEST) && (state_d == S_HUNT);
  assign block_lock    = (state_q == S_LOCKED);
  assign offset        = off_q;
  assign lock_losses   = loss_q;
 
endmodule
A three-state synchroniser. HUNT proposes an offset and moves to TEST. TEST returns to HUNT on an illegal symbol, advancing to the next offset, or reaches LOCKED after enough consecutive legal symbols. LOCKED returns to HUNT when too many illegal symbols accumulate.HUNTTESTLOCKEDtry an offsettry an offsetillegal symbolillegalsymbolenough legal symbolsenough legal symbolstoo many illegaltoo many illegal
Figure 2 — a hypothesis, evidence, a commitment, and continuous re-verification.

Classification: synthesizable.

What it teaches: that block synchronisation is Chapter 2.2's discovery problem in its purest form — a hypothesis, evidence gathered over time, a commitment, and continuous re-verification. It is not a conversion and it cannot be made one.

Illegal symbols are the evidence, and that is a design consequence. A wrong offset slices code groups across their boundaries and produces patterns the code cannot generate. A code in which every bit pattern were legal could not be synchronised this way at all — so the coding overhead in Section 4 buys a third thing beyond transitions and DC balance: a set of invalid patterns that makes alignment detectable.

GOOD_LIMIT and BAD_LIMIT are the two halves of a confidence trade. A large GOOD_LIMIT locks slowly and rarely locks falsely; a small one locks fast and can commit to a wrong offset that happens to produce a run of legal-looking symbols. A large BAD_LIMIT tolerates a noisy channel; a small one abandons a good lock on a single corrupted symbol.

Deliberately simplified: an abstract sym_legal input rather than a real decoder; one offset tried at a time, where a real design may test several in parallel; no distinction between a lock lost to noise and one lost to a genuine realignment.

Production implication: a real synchroniser's thresholds come from a target false-lock probability and a target lock time, both derived from the channel's expected error rate; and lock losses are counted because a rising count is a leading indicator of channel degradation, visible before frames start failing their check value.

9. RTL 5 — The Elastic Buffer

Two ends of a link run on independent oscillators that are nominally the same frequency and are not exactly equal. The receiver takes bits at the far end's rate and must forward them at its own.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Absorbs a bounded frequency difference between the
// recovered receive clock and the local clock by deleting idle when the
// buffer fills and inserting it when the buffer empties.
//
// Chapter 4.4 owns this mechanism in depth; this shows its place in the PHY.
module elastic_buffer #(
  parameter int unsigned WIDTH = 10,
  parameter int unsigned DEPTH = 16,
  localparam int unsigned PTR_W = $clog2(DEPTH),
  localparam int unsigned CNT_W = $clog2(DEPTH + 1)
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic             wr_valid,
  input  logic [WIDTH-1:0] wr_data,
  input  logic             wr_is_idle,   // this symbol is deletable
 
  input  logic             rd_ready,
  output logic             rd_valid,
  output logic [WIDTH-1:0] rd_data,
 
  output logic [CNT_W-1:0] occupancy,
  output logic [15:0]      idles_deleted,
  output logic [15:0]      idles_inserted,
  output logic             overflow,
  output logic             underflow
);
 
  localparam logic [CNT_W-1:0] HIGH_MARK = CNT_W'((DEPTH * 3) / 4);
  localparam logic [CNT_W-1:0] LOW_MARK  = CNT_W'(DEPTH / 4);
  localparam logic [WIDTH-1:0] IDLE_SYM  = {WIDTH{1'b0}};  // ILLUSTRATIVE
 
  logic [WIDTH-1:0] mem_q [DEPTH];
  logic [PTR_W-1:0] wr_q, rd_q;
  logic [CNT_W-1:0] cnt_q;
  logic [15:0]      del_q, ins_q;
 
  wire full  = (cnt_q == CNT_W'(DEPTH));
  wire empty = (cnt_q == '0);
 
  // THE ADJUSTMENT, and both directions are needed because the offset can
  // have either sign. Idle is the ONLY symbol that may be added or removed,
  // because it carries no information — which is why the interframe gap is
  // a MINIMUM rather than a fixed value (Chapter 2.5 section 8).
  wire delete_now = wr_valid && wr_is_idle && (cnt_q >= HIGH_MARK);
  wire insert_now = rd_ready && empty;
 
  wire do_wr = wr_valid && !full && !delete_now;
  wire do_rd = rd_ready && !empty;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      wr_q <= '0; rd_q <= '0; cnt_q <= '0; del_q <= '0; ins_q <= '0;
    end else begin
      if (do_wr) begin
        mem_q[wr_q] <= wr_data;
        wr_q <= (wr_q == PTR_W'(DEPTH-1)) ? '0 : wr_q + 1'b1;
      end
      if (do_rd) rd_q <= (rd_q == PTR_W'(DEPTH-1)) ? '0 : rd_q + 1'b1;
 
      case ({do_wr, do_rd})
        2'b10:   cnt_q <= cnt_q + 1'b1;
        2'b01:   cnt_q <= cnt_q - 1'b1;
        default: cnt_q <= cnt_q;
      endcase
 
      if (delete_now && del_q != 16'hFFFF) del_q <= del_q + 1'b1;
      if (insert_now && ins_q != 16'hFFFF) ins_q <= ins_q + 1'b1;
    end
  end
 
  assign rd_valid  = !empty || insert_now;
  assign rd_data   = empty ? IDLE_SYM : mem_q[rd_q];
  assign occupancy = cnt_q;
 
  // These two must NEVER assert. If they do, the frequency offset exceeded
  // what the buffer can absorb, or the idle supply was insufficient — and
  // the consequence is corrupted data rather than merely degraded timing.
  assign overflow      = wr_valid && full && !wr_is_idle;
  assign underflow     = rd_ready && empty && !insert_now;
  assign idles_deleted = del_q;
  assign idles_inserted = ins_q;
 
endmodule

Classification: synthesizable.

What it teaches: that idle is the only adjustable quantity in the stream, because it is the only part that carries no information. That is why the interframe gap is a minimum rather than a fixed value — the PCS may consume some of it absorbing a clock offset, and a receiver that demanded exactly the minimum would fail on any link with a real frequency difference.

Both directions are needed and the reason is that the offset has a sign. If the far end is faster the buffer fills and idle must be deleted; if slower it empties and idle must be inserted. A design implementing only one direction works against half the population of peers.

overflow and underflow must never assert, and counting them is what turns a silent corruption into a diagnosis. If either fires, the offset exceeded the buffer's capacity or the idle supply was insufficient — and the consequence is not degraded timing but lost or duplicated symbols, which appear far upstream as frames failing their check value with no apparent cause.

Deliberately simplified: one clock domain, where a real elastic buffer spans two and needs proper CDC; a single idle symbol rather than a sequence; fixed marks rather than ones derived from a specified frequency tolerance and maximum frame length.

Production implication: the depth is derived from the worst-case frequency offset multiplied by the longest interval without an idle opportunity — which is the maximum frame size — so a design supporting jumbo frames needs a deeper elastic buffer than one that does not. That is a direct dependency between Chapter 5.7's frame size and this block's area, and it is easy to miss.

Chapter 2.1 defined link_up as a conjunction of four terms from three layers. Three of them come from here.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Aggregates the PHY's own conditions into the terms
// Chapter 2.1's link_up consumes, and qualifies them so a MARGINAL link is
// distinguishable from a healthy one.
module phy_status #(
  // How long every condition must hold before the link is declared up.
  // Prevents a link that is oscillating from appearing usable.
  parameter int unsigned STABLE_SYMBOLS = 256,
  // Lock losses within the observation window that make a link marginal
  // even though it is currently up.
  parameter int unsigned MARGINAL_LOSSES = 4
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic signal_detect,   // PMD
  input  logic clk_locked,      // PMA
  input  logic block_lock,      // PCS
  input  logic lane_aligned,    // PCS
 
  input  logic [7:0] cdr_losses,
  input  logic [7:0] block_losses,
 
  output logic phy_ready,
  output logic phy_marginal,    // up, and should not be trusted
  output logic [1:0] lowest_failing   // which term fails first
);
 
  logic [$clog2(STABLE_SYMBOLS+1)-1:0] stable_q;
  logic ready_q;
 
  // ALL FOUR, and the order matters for diagnosis rather than for logic:
  // each depends on the ones below it, so the LOWEST failing term is the
  // cause and everything above it is a consequence. Chapter 2.1 section 15
  // builds the descent method on exactly this.
  wire all_ok = signal_detect && clk_locked && block_lock && lane_aligned;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      stable_q <= '0; ready_q <= 1'b0;
    end else if (!all_ok) begin
      // Any term dropping clears the timer immediately. A link that
      // oscillates never accumulates enough stability to be declared up,
      // which is the intended behaviour — an intermittent link is worse
      // than a down one, because software will try to use it.
      stable_q <= '0;
      ready_q  <= 1'b0;
    end else if (stable_q != $clog2(STABLE_SYMBOLS+1)'(STABLE_SYMBOLS)) begin
      stable_q <= stable_q + 1'b1;
    end else begin
      ready_q <= 1'b1;
    end
  end
 
  assign phy_ready = ready_q;
 
  // MARGINAL is the output that earns its area. A link can satisfy every
  // term right now and have lost lock repeatedly, which predicts failure
  // under load. Without this, "up" and "reliably up" are the same reading.
  assign phy_marginal = ready_q
                        && ((cdr_losses   >= 8'(MARGINAL_LOSSES))
                         || (block_losses >= 8'(MARGINAL_LOSSES)));
 
  // The lowest failing term, for the descent in Chapter 2.1 section 15.
  always_comb begin
    if      (!signal_detect) lowest_failing = 2'd0;   // PMD
    else if (!clk_locked)    lowest_failing = 2'd1;   // PMA
    else if (!block_lock)    lowest_failing = 2'd2;   // PCS
    else if (!lane_aligned)  lowest_failing = 2'd3;   // PCS
    else                     lowest_failing = 2'd3;
  end
 
endmodule

Classification: synthesizable.

What it teaches: that a link status built from a conjunction alone is insufficient. Four terms all true is "up now"; four terms true and stable for a declared interval is "usable"; and four terms true with a history of losses is marginal — a link that will fail under load and currently reports healthy.

phy_marginal is the output most often missing from real designs, and its absence is why a degrading link presents as an intermittent application problem rather than as a physical one. The information exists — the losses were counted — and without this output nobody looks at it until the link fails entirely.

lowest_failing encodes the dependency order that Chapter 2.1 §15's debugging method walks. The terms are not independent: block lock cannot hold without clock lock, which cannot hold without signal. Reporting the lowest failing term reports the cause; reporting all failing terms reports the cause and its consequences together, which is more data and less information.

Deliberately simplified: symbol-counted stability rather than a real timer; no hysteresis on the marginal threshold; losses as free-running counters rather than within a sliding window, which is what a real marginality measure needs.

Production implication: a real design measures losses within a sliding window so an old fault does not mark a link marginal forever, exposes every term individually through the management interface — Chapter 2.1 §7 — and defines a de-assertion threshold separate from the assertion one, so a marginal link does not oscillate between states.

Signal, clock, block, lanes, then ready

10 cycles
Ten clock cycles. Signal detect asserts at cycle 1. Clock lock follows at cycle 3 after acquisition. Block lock is achieved at cycle 5 after the synchroniser tries offsets. Lane alignment completes at cycle 6. The link is declared ready at cycle 8 after a stability interval.signal presentsignal presentclock recoveredclock recoveredoffset rejected, shiftoffset rejected, shiftstable: readystable: readyclksig_detectclk_lockedshift_reqblock_locklane_alignstable_cnt0000001233phy_readyelastic_occ0000047888t0t1t2t3t4t5t6t7t8t9
Figure 3 — the terms assert bottom-up, and each depends on the one below.

The terms assert strictly bottom-up and never out of order. Clock lock cannot precede signal detect, because there is nothing to lock to; block lock cannot precede clock lock, because symbol boundaries cannot be found without symbol timing. That dependency is why the debugging method descends, and P4 asserts it.

shift_req at cycle 5 is a rejected hypothesis. The synchroniser tried an offset, saw illegal symbols, and advanced. That is the search from Section 8 visible as a single pulse — and a link that never locks shows this pulse repeating forever, which is a far more useful symptom than "no block lock".

phy_ready lags lane_align by the stability interval. Everything is true at cycle 6 and the link is not declared until cycle 8, deliberately. A link that oscillates never accumulates the interval and is never declared up — which is correct, because an intermittent link is worse than a down one.

12. Assertions

Invariants of these models. None is an IEEE requirement, and the behavioural CDR's properties describe the model's declared abstraction rather than real recovery.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the modules in this chapter.
 
// SAFETY — P1: running disparity stays bounded. The DC-balance guarantee;
// exceeding it means an AC-coupled channel accumulates an offset and the
// receiver's threshold shifts.
property p_disparity_bounded;
  @(posedge clk) disable iff (!rst_n)
  !disparity_alarm;
endproperty
a_disparity_bounded : assert property (p_disparity_bounded);
 
// SAFETY — P2: a control code is never a data code. If they overlapped, a
// receiver could not distinguish an idle marker from a data value, and the
// out-of-band alphabet would not exist.
property p_control_distinct;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && in_control) |-> (out_code == CTRL_IDLE);
endproperty
a_control_distinct : assert property (p_control_distinct);
 
// CONSERVATION — P3: every parallel word produces exactly WIDTH serial
// bits. Catches a shift counter that skips or repeats, which shifts every
// subsequent word and garbles the link from that point on.
property p_serialiser_conserves;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && tx_ready) |=> (serial_active [*WIDTH]);
endproperty
a_serialiser_conserves : assert property (p_serialiser_conserves);
 
// SAFETY — P4: status terms assert bottom-up. Block lock without clock lock
// is impossible physically, so asserting it catches a status path that
// latches or a term computed from the wrong source.
property p_status_ordered;
  @(posedge clk) disable iff (!rst_n)
  block_lock |-> (clk_locked && signal_detect);
endproperty
a_status_ordered : assert property (p_status_ordered);
 
// SAFETY — P5: the block synchroniser's search terminates. WIDTH offsets
// exist and exactly one is correct; a search that could not exhaust its
// space is a receiver that hangs with no error.
property p_search_terminates;
  @(posedge clk) disable iff (!rst_n)
  (state_q == S_HUNT) |-> s_eventually (offset != $past(offset) || block_lock);
endproperty
a_search_terminates : assert property (p_search_terminates);
 
// SAFETY — P6: lock is re-verified while held. A locked receiver that
// stopped checking would hold a stale lock through a channel degradation
// and deliver garbage with every status bit green.
property p_lock_reverified;
  @(posedge clk) disable iff (!rst_n)
  (block_lock && sym_valid && !sym_legal) |=> (bad_q > 0 || !block_lock);
endproperty
a_lock_reverified : assert property (p_lock_reverified);
 
// SAFETY — P7: the elastic buffer never overflows or underflows. Both mean
// the frequency offset exceeded what the buffer absorbs, and the result is
// corrupted data rather than degraded timing.
property p_elastic_never_spills;
  @(posedge clk) disable iff (!rst_n)
  !overflow && !underflow;
endproperty
a_elastic_safe : assert property (p_elastic_never_spills);
 
// SAFETY — P8: only idle symbols are deleted. Deleting a data symbol
// silently corrupts a frame, and the check value two layers up is the only
// thing that would ever notice.
property p_only_idle_deleted;
  @(posedge clk) disable iff (!rst_n)
  delete_now |-> wr_is_idle;
endproperty
a_only_idle_deleted : assert property (p_only_idle_deleted);
 
// SAFETY — P9: the link is not declared ready until every term has been
// stable for the declared interval. Catches a design that reports up on the
// instant all four are true, which makes an oscillating link look usable.
property p_ready_needs_stability;
  @(posedge clk) disable iff (!rst_n)
  $rose(phy_ready) |-> (stable_q == STABLE_SYMBOLS);
endproperty
a_ready_stable : assert property (p_ready_needs_stability);
 
// LIVENESS — P10: a link with a good signal eventually becomes ready.
// ASSUMPTIONS, stated: the signal persists, and the symbols are legal at
// some offset. Without both this is false for a correct design — which is
// the whole point of a receiver that can fail to lock.
assume property (@(posedge clk) s_eventually (signal_detect));
assume property (@(posedge clk) s_eventually (sym_valid && sym_legal));
property p_link_eventually_ready;
  @(posedge clk) disable iff (!rst_n)
  signal_detect |-> s_eventually (phy_ready);
endproperty
a_link_ready : assert property (p_link_eventually_ready);

The property that must not be written

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// FALSE for a correct design. Included as a warning, not as a check.
// property p_lock_once_acquired_is_held;
//   @(posedge clk) disable iff (!rst_n)
//   block_lock |=> block_lock;
// endproperty

It reads like a stability requirement on a working receiver, and it forbids the mechanism that makes a receiver safe.

Lock must be losable. A channel that degrades starts producing illegal symbols, and a receiver that could not abandon a stale lock would keep delivering garbage with every status bit reporting healthy — which is strictly worse than reporting the link down. Section 8's BAD_LIMIT exists precisely to lose lock, and P6 asserts that it does.

The correct properties are P4 and P6: lock depends on the terms below it, and lock is continuously re-verified while held. Both are true of a correct design, and together they catch the two real bugs — a status term computed from the wrong source, and a lock that is never rechecked.

Writing the wrong version fires on the first channel-degradation test, gets waived as "the test is too aggressive", and removes attention from P6 — the property that catches a receiver holding a stale lock, which is the failure mode that produces corrupted data with a green link.

13. Verification

Monitors observe: the coder's disparity and control codes; the serialiser's bit count per word; the synchroniser's state, offset and both counters; the elastic buffer's occupancy, insert and delete counts; and every status term against the aggregate.

The scoreboard independently predicts the encoded symbol for each input from its own copy of the table, the expected serial bit sequence from the parallel word and the declared bit order, and the expected occupancy from the write and read events. It must not read the design's disparity accumulator — a checker sharing the design's accumulator agrees with it about every accumulation bug.

Scenarios

  1. Every data value encoded. Verify each maps to a legal symbol and that every symbol has at least the guaranteed transition count.
  2. A long run of one value. Verify disparity stays bounded (P1) — the case that would charge an AC-coupled channel.
  3. Control codes interleaved with data. Verify a control code is never producible from data (P2).
  4. Serialisation, both bit orders. Verify the serial sequence matches the declared order and that exactly WIDTH bits leave per word (P3).
  5. Bit-order mismatch. Serialise MSB-first and deserialise LSB-first. Verify the words differ — the failure that reports every status bit healthy.
  6. Clock recovery, acquisition. Verify lock after the declared interval and not before.
  7. Clock recovery, run-length violation. Feed a run longer than the code should permit. Verify lock is lost and counted.
  8. Block sync, correct offset first. Verify lock without any shift request.
  9. Block sync, every wrong offset. Start at each of the WIDTH offsets in turn and verify the search terminates at the right one (P5).
  10. Block sync, degradation while locked. Inject illegal symbols. Verify lock is lost after BAD_LIMIT (P6) and re-acquired when the channel recovers.
  11. Elastic buffer, far end faster. Verify idle deletion, that only idle is deleted (P8), and that occupancy stays centred.
  12. Elastic buffer, far end slower. Verify idle insertion and no underflow.
  13. Elastic buffer, offset beyond capacity. Verify overflow is detected — this scenario must fail P7 deliberately, to prove the detection works.
  14. Status, each term dropping alone. Four scenarios. Verify phy_ready falls and lowest_failing names the right term.
  15. Status, oscillating link. Toggle a term repeatedly. Verify phy_ready never asserts (P9) — the case that makes an intermittent link look usable.
  16. Reset in each state of each module. Verify no stale lock, no stale offset, no stale occupancy.

Coverage

Cross every data value against running disparity sign. Cover run lengths from one to beyond the code's guarantee. Cover all WIDTH starting offsets in the synchroniser. Cover elastic occupancy at empty, low mark, centre, high mark and full, with the offset in both directions. Cover every combination of the four status terms.

A directed stimulus for the bit-order mismatch

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// NON-SYNTHESIZABLE — directed stimulus. Instantiates a serialiser and a
// deserialiser with OPPOSITE bit orders and shows that every status bit
// reports healthy while every word is wrong.
task automatic bit_order_mismatch_reports_healthy();
  logic [WIDTH-1:0] sent, got;
 
  sent = 10'b1100100011;
  send_word(sent);
  wait (rx_valid);
  got = rx_word;
 
  // Everything that could report a problem does not.
  assert (dut_phy.phy_ready)
    else $error("precondition: the link should report ready");
  assert (dut_sync.block_lock)
    else $error("precondition: block lock should hold");
  assert (dut_eb.overflow == 1'b0 && dut_eb.underflow == 1'b0)
    else $error("precondition: the elastic buffer should be quiet");
 
  // AND THE DATA IS REVERSED. This is the only evidence, and it requires
  // comparing across the boundary — no single block can detect it.
  assert (got == {<<{sent}})
    else $error("expected a bit-reversed word; the mismatch model is wrong");
  assert (got != sent)
    else $error("bit orders did not actually differ — the test proves nothing");
 
  $display("bit-order mismatch: sent %b, received %b, every status term healthy",
           sent, got);
endtask

The final $display is the finding, not debug output. It records that a link can report entirely healthy and deliver reversed data, which is the argument for an end-to-end identity check — Chapter 2.2 §7 — existing at all.

14. Debugging — Descending the PHY

Chapter 2.1 §15 gave the descent; this fills in what each PHY term actually means when it fails.

Failing termSublayerLikely cause
No signal detectPMDCable, connector, transceiver, far end unpowered
Signal, no clock lockPMARate mismatch, or a channel too degraded to recover timing
Clock locked, no block lockPCSCoding mismatch, or a rate close enough to lock but wrong
Block lock, no lane alignmentPCSSkew beyond the deskew range, a swapped lane, a missing lane
All terms up, frames garbledboundaryBit order or width mismatch — no term reports it
All terms up, marginal flag setPMA or PCSRepeated lock loss; a degrading channel before it fails

The fifth row has no status term and needs Section 13's scenario 5. Every per-layer condition is satisfied and the data is wrong, because the fault is in a relationship between two blocks and no single block can observe a relationship.

The sixth row is the one worth building for. phy_marginal reports a link that is currently up and has lost lock repeatedly — which predicts failure under load and is visible before anything breaks. Most designs have the information and do not expose it, so the first symptom is an outage rather than a warning.

15. Common Misconceptions

"The PHY is one block."

The wrong model: everything below the MAC is a single unit called the PHY.

What it costs: it becomes impossible to say which part a change affects, so a PMD change for a longer reach is treated as a whole-PHY revision, and a coding change is assumed to require new analog work. It also hides where verification stops being sufficient.

The corrected model: three sublayers that change independently. The PCS changes with the coding, the PMA with the rate or lane count, the PMD with the medium. The same PCS serves several PMDs in practice, which is only expressible if the three are named separately.

"Line coding is just overhead."

The wrong model: a tax on the channel, imposed by the standard, that a cleverer design would avoid.

What it costs: proposals to reduce or remove it that would break clock recovery, DC balance and block synchronisation simultaneously — and the failures would appear at three different places with no obvious common cause.

The corrected model: coding buys three things. Transition density, so timing can be recovered from the data. DC balance, so an AC-coupled channel is not charged. And invalid symbols, without which block synchronisation has no evidence to work from and alignment cannot be found at all. The overhead is the price of all three, which is why it fell from 25% to about 3% rather than to zero.

"PAM4 is a line code."

The wrong model: coding and modulation are the same kind of choice.

What it costs: the two are made at different sublayers for different reasons, and conflating them makes the generation history incoherent — it becomes unclear why a rate increase sometimes changed the coding and sometimes the modulation.

The corrected model: line coding maps bits to symbols; modulation maps symbols to physical levels. A block code and a modulation scheme are independent choices, and a link has both. Chapter 3.5 owns codes, Chapter 3.6 owns modulation, and Section 3 keeps them apart deliberately.

"If the link reports up, the physical layer is working."

The wrong model: link_up is a health indicator for everything below the MAC.

What it costs: the bit-order and width mismatches in Section 13's scenario 5 are undiagnosable, and a marginal link that has lost lock repeatedly is treated as healthy until it fails outright.

The corrected model: link_up is a conjunction of per-layer conditions, and every term is about a layer meeting its own requirement. None is about two layers agreeing with each other, and none is about the link's stability. A correct status design adds two things the conjunction lacks: a stability interval before declaring up, and a marginality indication from the loss counters.

16. Interview Reasoning

Everything that exists because a real channel is analog — it attenuates, disperses, and carries no clock.

The responsibilities:

  • Line coding guarantees transitions so the receiver can recover timing from the data itself, keeps the signal DC-balanced for an AC-coupled channel, and provides invalid symbols that make block synchronisation possible.
  • Serialisation converts a parallel datapath onto a medium that carries one signal.
  • Clock recovery extracts symbol timing, because no clock was transmitted alongside the data.
  • Line drive and equalisation put a signal on the medium that survives the channel.
  • Block synchronisation and elastic buffering find code-group boundaries and absorb the frequency difference between two independent oscillators.

Why three sublayers: they change independently. The PCS changes with the coding, the PMA with the rate or lane count, the PMD with the medium. One PCS serves several PMDs in practice, and a PMD change for a longer reach leaves the PCS untouched — which is only expressible if they are named separately.

What separates a good answer from a complete one: naming where the evidence changes. The PCS is fully digital and provable in simulation. The PMD is characterised by measurement — eye diagrams, jitter, return loss — and the PMA straddles the two. A project that plans only RTL verification for "the PHY" has budgeted for a fraction of the work.

The follow-up to be ready for: why is line coding not just overhead? Because it buys three independent things at once, and the third is the one people forget — a set of invalid bit patterns. Without invalid symbols, a receiver has no evidence for which offset is the right code-group boundary, and block synchronisation is impossible.

17. Understanding Check

18. What's Next

The PHY exists because a channel is analog: it carries no clock, it attenuates, it disperses, and it offers a continuous quantity rather than bits. Coding recovers timing and balance and supplies the invalid symbols alignment needs. Serialisation matches a parallel datapath to a single channel. Clock recovery extracts timing that was never sent. Elastic buffering absorbs the difference between two independent oscillators.

The three sublayers exist because those responsibilities change on different schedules — and because the evidence changes too, from simulation at the PCS to measurement at the PMD.

Chapter 2.7 — End Systems, Switches and Routers closes Module 2 by placing these layers into the devices built from them, and by fixing the forwarding boundary between layer two and layer three that Chapter 2.4 identified.

Module 3 then takes the PHY properly: copper and fibre channels, the PCS, PMA and PMD in depth, block coding against line modulation, forward error correction, and how a link physically comes up.

The full path is on the Ethernet curriculum index.

Continue learning

Standards & specifications

Governing standard
IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)

Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Ethernet curriculum.