Ethernet · Module 9
200G to 800G — PAM4, FEC and the Modern Data-Centre Link
PAM4 halves the baud rate and costs 9.54 dB, so RS(544,514) becomes mandatory — and the FEC's waterfall means a link with full margin and one about to fail report identical post-correction statistics.
Chapter 9.5 made a link faster by making it wider — twenty PCS lanes, striped, marked and deskewed — while every lane still signalled the way 10GBASE-R did.
Above 100 Gigabit that stops working, because the lanes themselves have to get faster and NRZ runs out of channel.
PAM4 is the answer, and it is a trade rather than an improvement. Four amplitude levels instead of two carry two bits per symbol, so a given bit rate needs half the baud — and half the baud is half the bandwidth the channel has to support, which is the entire point.
What it costs is margin. Four levels in the same voltage swing means three eyes where there was one, each a third of the height. Which is a signal-to-noise penalty of
20 × log₁₀(3) = 9.54 dB
And at that penalty the raw link no longer meets Ethernet's error expectation on its own. So forward error correction stops being an option a medium may specify and becomes mandatory, in the base specification, for every PAM4 interface.
1. Scope — What This Chapter Owns
This chapter owns PAM4 and the error correction it makes compulsory.
It covers the SNR penalty and where it comes from, the transcoding that pays for the FEC's parity, the rate build-up for 200, 400 and 800 Gigabit, the RS(544,514) code and why it rather than the weaker one, what the FEC adds to a latency budget, and — most of all — what a design must measure to know its own margin, given that the FEC is built to hide exactly that.
It does not re-derive what other chapters own. Chapter 3.6 owns multi-level signalling as a mechanism; Chapter 9.5 owns lane striping and deskew, which this chapter's lanes still use; Chapter 8.4 owns the latency decomposition this chapter adds a term to. Chapter 10.1 opens the module that owns the MAC/PHY interfaces in their own right.
The claim this chapter defends: once correction is mandatory, the link's error rate stops being an indicator — because the correction was designed to make it insensitive to margin — and the design's obligation moves to measuring the input the correction consumes.
2. PAM4 — Two Bits per Symbol, and What They Cost
The whole trade is one division and one logarithm.
NRZ has two levels and one eye, which occupies the entire voltage swing the transmitter can produce. One symbol carries one bit.
PAM4 puts four levels in the same swing. Four levels create three eyes, and they share the swing, so each is one third of the height an NRZ eye had.
A third of the amplitude is a signal-to-noise penalty of
20 × log₁₀(1/3) = −9.542 dB
And two bits per symbol means half the symbols, so a 100 Gb/s lane needs 50 Gbaud in NRZ and 25 Gbaud in PAM4 — half the bandwidth from the channel.
| NRZ | PAM4 | |
|---|---|---|
| levels | 2 | 4 |
| eyes | 1 | 3 |
| eye height | full swing | ⅓ of the swing |
| bits per symbol | 1 | 2 |
| baud for a given bit rate | R | R/2 |
| SNR relative to NRZ | reference | −9.54 dB |
Which is the deal in one sentence: 9.54 dB of margin for a factor of two in bandwidth.
And it is worth taking, because the two quantities do not cost the same. Above about 25 Gbaud, channel loss on copper and board rises steeply with frequency, so the last factor of two in bandwidth is enormously expensive — better materials, shorter reach, more retimers. Ten decibels of SNR, by contrast, can be bought back by a code, and RS(544,514) buys most of it at 5.84% of the line rate.
3. The Rate Build-Up, Computed
A PAM4 lane rate is built in four steps, and two of them are overheads that pay for each other.
Step 1 — transcode. Chapter 9.4's 64B/66B costs 3.125%, which is a lot of line rate to give to a FEC that also needs parity. So four 66-bit blocks are transcoded into one 257-bit block:
4 × 66 = 264 bits, carrying 4 × 64 = 256 payload bits
264 → 257 bits, a saving of 7 bits per 256 payload bits
The overhead falls from 66/64 = 3.125% to 257/256 = 0.3906% — and the freed 2.73 percentage points are most of what the FEC's parity will cost.
Step 2 — add FEC parity. RS(544,514) adds 30 parity symbols to 514, an overhead of 30/514 = 5.8366%.
Step 3 — divide by the lane count. Step 4 — divide by two, because PAM4 carries two bits per symbol.
| Variant | Aggregate | × 257/256 | × 544/514 | Lanes | Gb/s per lane | ÷ 2 → baud |
|---|---|---|---|---|---|---|
| 200GBASE-R4 | 200 G | 200.7813 | 212.5 | 4 | 53.125 | 26.5625 GBd |
| 400GBASE-R8 | 400 G | 401.5625 | 425.0 | 8 | 53.125 | 26.5625 GBd |
| 800G, 8 lanes | 800 G | 803.1250 | 850.0 | 8 | 106.25 | 53.125 GBd |
Read the first two rows together. 200 and 400 Gigabit have identical per-lane rates — 53.125 Gb/s, 26.5625 GBd — and differ only in how many lanes. This is Chapter 9.5's strategy again: a proven serialiser, replicated, with the aggregate as the quotient.
And 800 Gigabit is where the per-lane rate finally doubles, to 53.125 GBd — which is why 800G needed a new generation of SerDes and 400G did not.
4. RTL 1 — Three Eyes Are Not One Eye Measured Three Times
// SYNTHESIZABLE.
//
// Per-eye margin for a PAM4 lane.
//
// PAM4 has FOUR levels and therefore THREE decision thresholds:
//
// level 3 ---- eye 2 (upper)
// level 2 ---- eye 1 (middle)
// level 1 ---- eye 0 (lower)
// level 0 ----
//
// The three eyes are NOT interchangeable, and a single aggregate margin
// destroys the distinction:
//
// ALL THREE EQUALLY SMALL -- loss, reach, or additive noise. A link
// level problem.
// OUTER EYES WORSE -- transmitter compression or a nonlinear
// driver: the extreme levels are squeezed toward each other.
// MIDDLE EYE WORSE -- level mismatch or a DC offset: the
// middle levels have drifted off their nominal spacing.
//
// Same aggregate margin, three different work orders. Which is why the
// counters below are per eye and never summed inside this module.
package pam4_link_pkg;
localparam int unsigned EYES = 3;
localparam int unsigned LEVELS = 4;
// 20*log10(3) = 9.5424 dB, in hundredths, for reporting.
localparam int unsigned PAM4_SNR_PENALTY_CDB = 954;
// RS(544,514) over 10-bit symbols (KP4).
localparam int unsigned FEC_N = 544;
localparam int unsigned FEC_K = 514;
localparam int unsigned FEC_SYM_BITS = 10;
localparam int unsigned FEC_T = (FEC_N - FEC_K) / 2; // 15
endpackage
module pam4_level_monitor
import pam4_link_pkg::*;
#(
parameter int unsigned MARGIN_W = 12,
parameter int unsigned CNT_W = 24,
parameter int unsigned WINDOW_BITS = 20,
// Below this, an eye is closing rather than merely small.
parameter int unsigned EYE_FLOOR = 12'd200
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic [EYES-1:0] eye_sample_valid,
input logic [EYES-1:0][MARGIN_W-1:0] eye_margin,
output logic [EYES-1:0][MARGIN_W-1:0] worst_eye_margin,
output logic [EYES-1:0] eye_closing,
output logic window_valid,
// WHICH eye is worst, and the shape of the degradation. These two
// outputs are the module: the shape names the cause.
output logic [1:0] worst_eye,
output logic outer_eyes_worse,
output logic middle_eye_worse,
output logic all_eyes_equal,
output logic [EYES-1:0] ever_closing
);
logic [WINDOW_BITS-1:0] win_q;
integer i;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
win_q <= '0; window_valid <= 1'b0; eye_closing <= '0;
ever_closing <= '0;
for (i = 0; i < EYES; i = i + 1) worst_eye_margin[i] <= '1;
end else if (clear) begin
win_q <= '0; window_valid <= 1'b0; eye_closing <= '0;
for (i = 0; i < EYES; i = i + 1) worst_eye_margin[i] <= '1;
// ever_closing survives: an eye that has closed is a fact about
// this transmitter, receiver or channel, and reading a counter
// did not change any of them.
end else begin
win_q <= win_q + 1'b1;
if (&win_q) window_valid <= 1'b1;
for (i = 0; i < EYES; i = i + 1) begin
if (eye_sample_valid[i]) begin
if (eye_margin[i] < worst_eye_margin[i])
worst_eye_margin[i] <= eye_margin[i];
if (eye_margin[i] < MARGIN_W'(EYE_FLOOR)) begin
eye_closing[i] <= 1'b1;
ever_closing[i] <= 1'b1;
end
end
end
end
end
// THE SHAPE. Comparing the outer eyes (0 and 2) with the middle (1)
// is what turns three numbers into a cause.
always_comb begin
logic [MARGIN_W-1:0] outer_min, mid;
outer_min = (worst_eye_margin[0] < worst_eye_margin[2])
? worst_eye_margin[0] : worst_eye_margin[2];
mid = worst_eye_margin[1];
worst_eye = (outer_min < mid)
? ((worst_eye_margin[0] < worst_eye_margin[2]) ? 2'd0 : 2'd2)
: 2'd1;
// A quarter difference is a shape; anything smaller is measurement
// noise on three nominally identical eyes.
outer_eyes_worse = window_valid && (mid > (outer_min + (outer_min >> 2)));
middle_eye_worse = window_valid && (outer_min > (mid + (mid >> 2)));
all_eyes_equal = window_valid && !outer_eyes_worse && !middle_eye_worse;
end
endmoduleClassification: synthesizable.
What it teaches: that the shape of the degradation names the cause, and an aggregate margin destroys the shape. Three equally small eyes is loss or additive noise — a link-level fact. Outer eyes worse is transmitter compression, because a nonlinear driver squeezes the extreme levels toward each other. Middle eye worse is a level or offset error. All three report the same worst-case margin, and only the comparison separates them.
Deliberately simplified: eye_margin arrives as a per-eye number. In a real receiver it comes from a slicer's soft information or from an on-die eye scan, and the scan is a mode rather than a continuous measurement.
Production implication: ever_closing is per eye and survives clear. A link that intermittently closes its upper eye and only its upper eye has a transmitter that is compressing when it warms up — and that is a completely different investigation from a link whose three eyes shrink together in the afternoon. The aggregate number is identical in both cases; the per-eye history is the whole diagnosis.
5. Why the Three Eyes Diverge
Section 4 asserts that the eyes degrade differently. Here is why, mechanism by mechanism, because the shapes are what make the monitor worth building.
Additive noise closes all three equally. Thermal noise, crosstalk and jitter-induced amplitude noise do not care which level a symbol is at. Three equal reductions, and the cause is the channel or the environment.
Transmitter nonlinearity closes the outer eyes. A driver approaching its supply rails compresses: the distance from level 2 to level 3 shrinks more than the distance from level 1 to level 2, because the top of the swing is where the amplifier runs out of headroom. The outer eyes go first, and they go first on the transmitter's side of the link.
Level mismatch closes the middle eye. The four levels are meant to be equally spaced; a DC offset or a mis-set level ratio moves the middle two toward each other while leaving the outer gaps alone.
And there is a fourth asymmetry that is designed in. Real PAM4 transmitters deliberately do not space the levels equally — the outer levels are pushed slightly apart to compensate for the nonlinearity above. So a receiver measuring three exactly equal eyes on a real link is measuring something wrong, and the monitor's tolerance band exists partly for that reason.
| Shape observed | Mechanism | Where the fault is |
|---|---|---|
| all three equally small | additive noise, loss, reach | the channel or environment |
| outer eyes worse | driver compression, nonlinearity | the far transmitter |
| middle eye worse | level mismatch, DC offset | level setting, either end |
| one outer eye only | asymmetric clipping | one rail of the far driver |
6. RTL 2 — Building the Codeword
// SYNTHESIZABLE shell around a Reed-Solomon encoder; the Galois-field
// arithmetic is abstracted and the FRAMING is the subject.
//
// RS(544,514) over GF(2^10) -- "KP4", and mandatory for every PAM4
// Ethernet interface.
//
// symbol size : 10 bits
// message : 514 symbols = 5140 bits
// parity : 30 symbols = 300 bits
// codeword : 544 symbols = 5440 bits
// corrects : t = (544-514)/2 = 15 symbols
// worst-case burst it can absorb: 15 x 10 = 150 bits, IF the errors
// fall inside 15 distinct symbols
// parity overhead: 30/514 = 5.8366%
//
// THE PART THAT MATTERS FOR RTL: the encoder cannot emit anything until
// it has 514 message symbols, and the decoder cannot correct anything
// until it has all 544. That is not an implementation choice -- it is
// what a block code is, and it is where the latency in Section 8 comes
// from.
module rs_fec_codeword_builder
import pam4_link_pkg::*;
#(
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic [FEC_SYM_BITS-1:0] sym_in,
input logic sym_valid,
output logic [FEC_SYM_BITS-1:0] sym_out,
output logic sym_out_valid,
output logic is_parity,
// Where in the codeword we are. Exported because the deskew and
// latency blocks both need to know when a codeword boundary passes.
output logic [9:0] sym_index,
output logic codeword_start,
output logic codeword_end,
// The encoder is mid-codeword and the source has stalled. A block
// code cannot emit a partial codeword, so the stall propagates -- and
// a design that does not report this attributes the resulting gap to
// the wrong stage.
output logic stalled_mid_codeword,
output logic [CNT_W-1:0] c_codewords
);
logic [9:0] idx_q;
logic [FEC_SYM_BITS-1:0] parity_q [30];
integer i;
assign sym_index = idx_q;
assign codeword_start = sym_valid && (idx_q == 10'd0);
assign codeword_end = sym_out_valid && (idx_q == 10'(FEC_N - 1));
// Placeholder for the GF(2^10) parity accumulation. The real encoder
// is a linear feedback structure over the code's generator
// polynomial; what matters at this level is that parity is a function
// of ALL 514 message symbols and therefore cannot be emitted early.
function automatic logic [FEC_SYM_BITS-1:0] parity_step
(input logic [FEC_SYM_BITS-1:0] acc, input logic [FEC_SYM_BITS-1:0] s);
parity_step = {acc[FEC_SYM_BITS-2:0], acc[FEC_SYM_BITS-1]} ^ s;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
idx_q <= 10'd0; sym_out <= '0; sym_out_valid <= 1'b0;
is_parity <= 1'b0; stalled_mid_codeword <= 1'b0;
c_codewords <= '0;
for (i = 0; i < 30; i = i + 1) parity_q[i] <= '0;
end else begin
sym_out_valid <= 1'b0;
// A block code has no partial output. If the source stalls
// mid-codeword the whole pipeline stalls, and saying so is what
// stops the gap being blamed on the MAC.
stalled_mid_codeword <= !sym_valid && (idx_q != 10'd0)
&& (idx_q < 10'(FEC_K));
if (sym_valid && (idx_q < 10'(FEC_K))) begin
// MESSAGE SYMBOLS pass through unchanged and feed the parity.
sym_out <= sym_in;
sym_out_valid <= 1'b1;
is_parity <= 1'b0;
for (i = 0; i < 30; i = i + 1)
parity_q[i] <= parity_step(parity_q[i], sym_in);
idx_q <= idx_q + 10'd1;
end else if (idx_q >= 10'(FEC_K)) begin
// PARITY SYMBOLS. Emitted without needing input, because they
// are already a function of everything that came before.
sym_out <= parity_q[idx_q - 10'(FEC_K)];
sym_out_valid <= 1'b1;
is_parity <= 1'b1;
if (idx_q == 10'(FEC_N - 1)) begin
idx_q <= 10'd0;
for (i = 0; i < 30; i = i + 1) parity_q[i] <= '0;
if (!(&c_codewords)) c_codewords <= c_codewords + 1'b1;
end else begin
idx_q <= idx_q + 10'd1;
end
end
end
end
endmoduleClassification: synthesizable shell; the Galois-field arithmetic is abstracted.
What it teaches: that a block code's boundary is a hard structural fact with consequences in three other blocks. Parity is a function of all 514 message symbols, so nothing can be emitted early, and the decoder cannot correct until it holds all 544. That single property produces the latency of Section 8, the stall behaviour above, and the reason the error statistics of Section 10 are naturally per codeword rather than per bit.
Deliberately simplified: the parity accumulation is a placeholder, and one symbol is processed per cycle. At 425 Gb/s a real encoder processes many symbols per clock, which changes the arithmetic but not the boundary.
Production implication: stalled_mid_codeword exists because a block code turns an upstream stall into a downstream gap at a place nothing else would attribute correctly. Without it, a MAC that pauses mid-codeword produces idle time on the wire that looks like a PHY problem — and a throughput investigation starts at the wrong end of the pipeline. The block boundary is invisible above the FEC and it shapes everything below it.
7. Why KP4 Rather Than KR4
Two Reed–Solomon codes appear in Ethernet, they differ only in parity, and PAM4 is the reason the stronger one won.
| KR4 — RS(528,514) | KP4 — RS(544,514) | |
|---|---|---|
| message symbols | 514 | 514 |
| parity symbols | 14 | 30 |
| codeword | 528 symbols = 5280 bits | 544 symbols = 5440 bits |
| corrects | (528−514)/2 = 7 symbols | (544−514)/2 = 15 symbols |
| worst-case burst absorbed | 70 bits | 150 bits |
| parity overhead | 14/514 = 2.7237% | 30/514 = 5.8366% |
KP4 costs 3.11 percentage points more of line rate and corrects more than twice as many symbols. On an NRZ link with a raw error rate low enough that KR4 has ample headroom, that extra parity is a waste. On a PAM4 link it is the difference between working and not.
And the reason is the 9.54 dB. A PAM4 link at the same received power as an NRZ link has a raw error rate orders of magnitude higher — around 2.4 × 10⁻⁴ at the specified operating point, roughly one bit in four thousand. Seven correctable symbols per 544 is not enough margin at that input rate; fifteen is.
Which is why FEC stops being a medium's choice. In earlier generations a medium specification could require FEC or not, depending on its reach and loss. With PAM4 the raw link does not meet the error expectation on any conforming channel, so the correction moves into the base specification and every PAM4 interface has it.
8. RTL 3 — Accounting for the Latency the FEC Adds
// SYNTHESIZABLE.
//
// Measures what the FEC actually costs in delay, rather than quoting a
// datasheet number.
//
// WHY IT IS STRUCTURAL, not an implementation cost:
// ENCODE -- parity is a function of all 514 message symbols, so the
// encoder holds a whole message before it emits any parity.
// DECODE -- the decoder cannot correct ANY symbol until it holds all
// 544, because a symbol arriving last can change which symbols
// the decoder concludes were wrong.
//
// THE FLOOR, computed:
// codeword = 544 x 10 = 5440 bits
// 400G line rate (after both overheads) = 425 Gb/s
// one fill = 5440 / 425e9 = 12.8 ns
// encode fill + decode fill = 25.6 ns
//
// And a real KP4 contribution is several times that, because the
// decoder's syndrome, root-finding and error-evaluation stages are
// pipelines of their own. So the module MEASURES rather than assumes.
module fec_latency_accountant
import pam4_link_pkg::*;
#(
parameter int unsigned TS_W = 32,
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic clear,
// A symbol entering the encoder and the same symbol leaving the
// decoder, tagged so the two can be matched.
input logic tag_in_valid,
input logic [TS_W-1:0] tag_in_time,
input logic tag_out_valid,
input logic [TS_W-1:0] tag_out_time,
output logic [TS_W-1:0] last_latency,
output logic [TS_W-1:0] worst_latency,
output logic [TS_W-1:0] best_latency,
// Accumulated, so a mean can be computed by whoever reads -- keeping
// a divider out of a path that runs at line rate.
output logic [TS_W+CNT_W-1:0] total_latency,
output logic [CNT_W-1:0] c_samples,
// The measured delay exceeded the budget this stage was allotted in
// the system's latency decomposition. Reported, because a FEC that
// silently costs more than its budget shows up as a system-level
// latency miss with no stage attached to it.
input logic [TS_W-1:0] budget_cycles,
output logic over_budget,
output logic ever_over_budget
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
last_latency <= '0; worst_latency <= '0; best_latency <= '1;
total_latency <= '0; c_samples <= '0;
over_budget <= 1'b0; ever_over_budget <= 1'b0;
end else if (clear) begin
worst_latency <= '0; best_latency <= '1;
total_latency <= '0; c_samples <= '0; over_budget <= 1'b0;
// ever_over_budget survives.
end else begin
over_budget <= 1'b0;
if (tag_out_valid) begin
automatic logic [TS_W-1:0] d = tag_out_time - tag_in_time;
last_latency <= d;
total_latency <= total_latency + (TS_W+CNT_W)'(d);
if (!(&c_samples)) c_samples <= c_samples + 1'b1;
if (d > worst_latency) worst_latency <= d;
if (d < best_latency) best_latency <= d;
if (d > budget_cycles) begin
over_budget <= 1'b1;
ever_over_budget <= 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that worst_latency and best_latency must both be kept, because the FEC's delay is not constant. A codeword that needed no correction leaves the decoder earlier than one that consumed the full error-evaluation pipeline — so a link with rising error rates has rising latency, and the two numbers together show it. A single mean hides it entirely.
Deliberately simplified: the tag arrives with a matching timestamp. Real measurement uses a periodic marked symbol and matches at the far end, which is what makes the number a system measurement rather than a block one.
Production implication: over_budget connects this block to Chapter 8.4's decomposition. A FEC that quietly costs more than its allotted term produces a system-level latency miss with no stage attached to it — and 8.4's argument was precisely that a latency budget is only useful if each term names its owner. A stage that can exceed its budget without saying so removes itself from the decomposition.
9. The Latency Cost, Against a Real Budget
Compute the structural floor first, because it is the part nobody can implement away.
| Step | Working | Result |
|---|---|---|
| codeword size | 544 × 10 | 5440 bits |
| 400G line rate | 400 × 257/256 × 544/514 | 425 Gb/s |
| one codeword fill | 5440 ÷ 425 × 10⁹ | 12.8 ns |
| encode fill + decode fill | 2 × 12.8 | 25.6 ns |
at 800G (850 Gb/s) | 2 × 5440 ÷ 850 × 10⁹ | 12.8 ns |
And the realised figure is several times that — KP4 is commonly cited at roughly 100 to 200 ns — because the decoder's syndrome computation, root finding and error evaluation are pipelines on top of the fill, and interleaving adds more.
Now put 100 ns beside Chapter 8.4's other terms at 400 Gigabit.
| Term | Value at 400G | Working |
|---|---|---|
| serialization of a 1518-octet frame | 30.36 ns | 1518 × 8 ÷ 400 × 10⁹ |
| FEC contribution | ≈ 100 ns | 3.3× a whole maximum frame |
| propagation over 100 m of fibre | 489.5 ns | 100 ÷ (0.681 × 3 × 10⁸) |
| the FEC expressed as fibre | ≈ 20.4 m | 100 × 10⁻⁹ × 0.681 × 3 × 10⁸ |
Read the second row against the first. At 400 Gigabit, the FEC costs more than three times the time it takes to serialise an entire maximum-length frame. Chapter 8.4 established that serialization shrinks with rate while the other terms do not — and the FEC is a term that appeared as the rate rose, and which does not shrink with it in the same way.
And read it against the third. The FEC is worth about 20 metres of fibre — trivial in a long-haul link, and significant inside a data-centre row where 20 metres is a meaningful fraction of the total path.
10. RTL 4 — Measuring the Input the Correction Consumes
// SYNTHESIZABLE.
//
// The margin reporter, and the most important module in this chapter.
//
// THE PROBLEM IT SOLVES:
// Post-FEC errors on a healthy 400G link are approximately zero, and
// they stay approximately zero as the link degrades -- until they
// don't, and then the link is gone. Section 11 computes the curve.
//
// So "no errors" is not a margin measurement. It is the answer a
// perfect link and an almost-dead link both give.
//
// WHAT DOES CARRY MARGIN INFORMATION:
// the number of symbols the decoder CORRECTED per codeword.
// That number is monotone in the pre-FEC error rate, moves smoothly,
// and is available every codeword. A histogram of it is the link's
// margin, directly.
//
// t = 15, so a codeword correcting 2 symbols has enormous headroom
// and one correcting 14 is one bad symbol from being lost -- and both
// report zero post-FEC errors.
module fec_margin_reporter
import pam4_link_pkg::*;
#(
parameter int unsigned CNT_W = 24,
parameter int unsigned BINS = FEC_T + 2 // 0..15 corrected, plus uncorrectable
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic cw_done,
input logic [4:0] corrected_symbols, // 0..15
input logic uncorrectable,
// The histogram IS the margin. Exported whole, because a summary of
// it is exactly the information this module exists to preserve.
output logic [BINS-1:0][CNT_W-1:0] hist,
output logic [4:0] worst_corrected,
output logic [CNT_W-1:0] c_codewords,
output logic [CNT_W-1:0] c_uncorrectable,
// Corrected symbols summed, so a pre-FEC symbol error rate can be
// derived: total_corrected / (c_codewords x 544).
output logic [CNT_W+5:0] total_corrected,
// The link is running close to the code's limit. This is the ALARM
// that a post-FEC error count cannot produce, because by the time a
// post-FEC error appears the margin is already gone.
output logic near_limit,
output logic ever_near_limit,
output logic ever_uncorrectable
);
// Within two symbols of t is close enough that ordinary variation
// will reach it.
localparam int unsigned NEAR_LIMIT_AT = FEC_T - 2; // 13
integer i;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
worst_corrected <= 5'd0; c_codewords <= '0; c_uncorrectable <= '0;
total_corrected <= '0; near_limit <= 1'b0;
ever_near_limit <= 1'b0; ever_uncorrectable <= 1'b0;
for (i = 0; i < BINS; i = i + 1) hist[i] <= '0;
end else if (clear) begin
worst_corrected <= 5'd0; c_codewords <= '0; c_uncorrectable <= '0;
total_corrected <= '0; near_limit <= 1'b0;
for (i = 0; i < BINS; i = i + 1) hist[i] <= '0;
// ever_near_limit and ever_uncorrectable survive. They are the
// two facts a later reader needs and the two a clear destroys.
end else begin
near_limit <= 1'b0;
if (cw_done) begin
if (!(&c_codewords)) c_codewords <= c_codewords + 1'b1;
if (uncorrectable) begin
// The overflow bin. An uncorrectable codeword is not a bigger
// version of a corrected one -- the decoder does not know how
// many symbols were wrong, only that it was more than t.
if (!(&hist[BINS-1])) hist[BINS-1] <= hist[BINS-1] + 1'b1;
if (!(&c_uncorrectable)) c_uncorrectable <= c_uncorrectable + 1'b1;
ever_uncorrectable <= 1'b1;
end else begin
if (!(&hist[corrected_symbols]))
hist[corrected_symbols] <= hist[corrected_symbols] + 1'b1;
total_corrected <= total_corrected + (CNT_W+6)'(corrected_symbols);
if (corrected_symbols > worst_corrected)
worst_corrected <= corrected_symbols;
if (corrected_symbols >= 5'(NEAR_LIMIT_AT)) begin
near_limit <= 1'b1;
ever_near_limit <= 1'b1;
end
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the corrected-symbol count is the margin and the post-FEC error count is not. corrected_symbols is monotone in the pre-FEC error rate, moves smoothly, and is available on every codeword. A histogram concentrated at 1 or 2 is a link with enormous headroom; one with mass at 13 and 14 is a link about to fail — and both report zero uncorrectable codewords.
Deliberately simplified: the decoder supplies a correction count. Real RS decoders produce it naturally as the degree of the error-locator polynomial, so this is one of the few telemetry outputs that is genuinely free.
Production implication: ever_near_limit is the alarm the post-FEC counter cannot give. By the time an uncorrectable codeword appears the margin is already gone, because Section 11's curve is a cliff rather than a slope. A histogram sampled weekly shows the mass drifting from bin 2 toward bin 13 over months, which is a maintenance window; an uncorrectable-codeword counter shows nothing, nothing, nothing, and then an outage.
11. The Waterfall
Compute the curve, because the shape is the argument.
For RS(544,514) over 10-bit symbols, a bit error rate p gives a symbol error rate of 1 − (1 − p)¹⁰, and a codeword is lost when more than 15 of its 544 symbols are wrong.
| Pre-FEC BER | Symbol error rate | Mean errors per codeword | P(uncorrectable codeword) |
|---|---|---|---|
| 1 × 10⁻⁵ | 1.00 × 10⁻⁴ | 0.054 | 2.1 × 10⁻³⁴ |
| 5 × 10⁻⁵ | 5.00 × 10⁻⁴ | 0.272 | 2.7 × 10⁻²³ |
| 1 × 10⁻⁴ | 1.00 × 10⁻³ | 0.544 | 1.4 × 10⁻¹⁸ |
| 2.4 × 10⁻⁴ | 2.40 × 10⁻³ | 1.304 | 8.2 × 10⁻¹³ |
| 5 × 10⁻⁴ | 4.99 × 10⁻³ | 2.714 | 2.8 × 10⁻⁸ |
| 1 × 10⁻³ | 9.96 × 10⁻³ | 5.416 | 1.5 × 10⁻⁴ |
| 2 × 10⁻³ | 1.98 × 10⁻² | 10.783 | 7.9 × 10⁻² |
| 4 × 10⁻³ | 3.93 × 10⁻² | 21.372 | 9.1 × 10⁻¹ |
Read the last column down the page. From a pre-FEC BER of 10⁻⁴ to 2 × 10⁻³ — a factor of 20 in the input — the codeword loss probability rises from 1.4 × 10⁻¹⁸ to 7.9 × 10⁻², a factor of about 6 × 10¹⁶.
That is not a slope. It is a cliff, and the useful operating range sits entirely on the flat part.
Now read the fourth column instead. Mean errors per codeword goes 0.054 → 0.272 → 0.544 → 1.30 → 2.71 → 5.42 → 10.78. Smooth, monotone, and proportional to the input over the whole range.
Which is the entire design consequence. A link at 10⁻⁴ and a link at 5 × 10⁻⁴ — a fivefold difference in margin — both report zero uncorrectable codewords for as long as anyone will watch. They report 0.54 and 2.71 mean corrected symbols, and that difference is visible in seconds.
12. RTL 5 — Per-Lane, Per-Eye, Because the Codeword Is Striped
// SYNTHESIZABLE.
//
// Combines Chapter 9.5's per-lane attribution with this chapter's
// per-eye attribution, because at 400G BOTH have been destroyed by the
// time anything above the FEC can see an error.
//
// THE TWO COMBINING STEPS, and what each one erases:
// 1. The FEC decoder turns "which symbols were wrong" into "corrected
// or not". Symbol identity is gone.
// 2. The re-interleaver turns 8 lanes into one stream (Chapter 9.5).
// Lane identity is gone.
// 3. And within a lane, min() over three eyes destroys the shape
// (Section 4). Eye identity is gone.
//
// So a design that wants to know WHERE its margin went must capture all
// three below their respective combines. There is no fourth chance.
module pam4_eye_margin_reporter
import pam4_link_pkg::*;
#(
parameter int unsigned LANES = 8,
parameter int unsigned MARGIN_W = 12,
parameter int unsigned CNT_W = 20
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic [LANES-1:0] lane_sample_valid,
input logic [LANES-1:0][EYES-1:0][MARGIN_W-1:0] lane_eye_margin,
input logic [LANES-1:0][4:0] lane_corrected_symbols,
input logic [LANES-1:0] lane_cw_done,
output logic [LANES-1:0][EYES-1:0][MARGIN_W-1:0] worst,
output logic [LANES-1:0][CNT_W-1:0] lane_corrected_total,
// The two comparisons that name a cause.
output logic [2:0] worst_lane,
output logic [1:0] worst_eye_of_worst_lane,
output logic single_lane_outlier,
output logic same_eye_across_lanes,
output logic window_valid,
output logic [LANES-1:0] ever_lane_degraded
);
logic [CNT_W-1:0] win_q;
integer l, e;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
win_q <= '0; window_valid <= 1'b0; ever_lane_degraded <= '0;
for (l = 0; l < LANES; l = l + 1) begin
lane_corrected_total[l] <= '0;
for (e = 0; e < EYES; e = e + 1) worst[l][e] <= '1;
end
end else if (clear) begin
win_q <= '0; window_valid <= 1'b0;
for (l = 0; l < LANES; l = l + 1) begin
lane_corrected_total[l] <= '0;
for (e = 0; e < EYES; e = e + 1) worst[l][e] <= '1;
end
// ever_lane_degraded survives.
end else begin
win_q <= win_q + 1'b1;
if (&win_q) window_valid <= 1'b1;
for (l = 0; l < LANES; l = l + 1) begin
if (lane_sample_valid[l]) begin
for (e = 0; e < EYES; e = e + 1)
if (lane_eye_margin[l][e] < worst[l][e])
worst[l][e] <= lane_eye_margin[l][e];
end
if (lane_cw_done[l]) begin
if (!(&lane_corrected_total[l]))
lane_corrected_total[l] <= lane_corrected_total[l]
+ CNT_W'(lane_corrected_symbols[l]);
end
end
end
end
// Two questions, two answers, and they point at different hardware.
always_comb begin
logic [CNT_W-1:0] hi, lo;
logic [2:0] hi_idx;
logic [MARGIN_W-1:0] wm;
logic [1:0] we;
int same_count;
hi = '0; lo = '1; hi_idx = 3'd0;
for (int p = 0; p < LANES; p = p + 1) begin
if (lane_corrected_total[p] > hi) begin
hi = lane_corrected_total[p]; hi_idx = 3'(p);
end
if (lane_corrected_total[p] < lo) lo = lane_corrected_total[p];
end
worst_lane = hi_idx;
// ONE lane far worse than the rest: that lane's optics or path.
single_lane_outlier = window_valid && (hi > (lo << 2));
wm = '1; we = 2'd0;
for (int q = 0; q < EYES; q = q + 1)
if (worst[hi_idx][q] < wm) begin wm = worst[hi_idx][q]; we = 2'(q); end
worst_eye_of_worst_lane = we;
// THE SAME eye worst on every lane: not a lane fault at all. It is
// a level-setting or driver characteristic shared by the whole
// device, and replacing a fibre will not touch it.
same_count = 0;
for (int r = 0; r < LANES; r = r + 1) begin
logic [MARGIN_W-1:0] m; logic [1:0] idx;
m = '1; idx = 2'd0;
for (int q2 = 0; q2 < EYES; q2 = q2 + 1)
if (worst[r][q2] < m) begin m = worst[r][q2]; idx = 2'(q2); end
if (idx == we) same_count = same_count + 1;
end
same_eye_across_lanes = window_valid && (same_count == int'(LANES));
end
endmoduleClassification: synthesizable.
What it teaches: that same_eye_across_lanes distinguishes a device problem from a path problem, and nothing else does. If the upper eye is worst on every lane, the cause is shared — a level-setting choice, a driver characteristic, a supply — and replacing a fibre will not change it. If one lane is worse and its worst eye differs from the others', the cause is that lane's optics or path.
Deliberately simplified: eight lanes and a fixed outlier ratio. Production telemetry reports per-lane pre-FEC error rates and per-lane eye scans directly, and the comparison is made in software.
Production implication: three combining steps sit between a PAM4 eye and a frame error — min() over eyes, the FEC decoder, and the lane re-interleaver — and each one destroys an attribution permanently. A frame's FCS failure at 400 Gigabit carries no lane, no eye and no symbol, so all three have to be captured below their respective combines. This is the fourth time the track reaches that conclusion, after Chapter 9.3's pairs, Chapter 9.4's byte lanes and Chapter 9.5's PCS lanes — and here three of them are stacked on one link.
13. Properties Worth Asserting, and One Worth Refusing
The organising split here is between the codeword's structure, which this design produces and controls, and the link's error statistics, which it only observes. Everything in the first group is unconditionally assertable. Everything in the second group can only be asserted as a property of the reporting.
Codeword structure
// P1. A codeword is exactly FEC_N symbols. The index never runs past
// the code's length, whatever the source does.
property p_codeword_length;
@(posedge clk) disable iff (!rst_n)
sym_index < 10'(FEC_N);
endproperty
a_codeword_length: assert property (p_codeword_length);
// P2. The first FEC_K symbols are message, the rest parity. The
// boundary is structural and never moves.
property p_parity_after_message;
@(posedge clk) disable iff (!rst_n)
sym_out_valid |-> (is_parity == (sym_index >= 10'(FEC_K)));
endproperty
a_parity_after_message: assert property (p_parity_after_message);
// P3. No parity is emitted before the whole message has been consumed.
// The property that makes the Section 9 latency structural rather than
// an implementation choice.
property p_no_early_parity;
@(posedge clk) disable iff (!rst_n)
is_parity |-> ($past(sym_index) >= 10'(FEC_K - 1));
endproperty
a_no_early_parity: assert property (p_no_early_parity);
// P4. A codeword boundary is a pulse, once per FEC_N symbols.
property p_codeword_end_periodic;
@(posedge clk) disable iff (!rst_n)
codeword_end |=> (sym_index == 10'd0);
endproperty
a_codeword_end_periodic: assert property (p_codeword_end_periodic);
// P5. A stall mid-codeword is REPORTED. A block code turns an upstream
// pause into a downstream gap that nothing else attributes correctly.
property p_stall_reported;
@(posedge clk) disable iff (!rst_n)
(!sym_valid && (sym_index != 10'd0) && (sym_index < 10'(FEC_K)))
|=> stalled_mid_codeword;
endproperty
a_stall_reported: assert property (p_stall_reported);Margin reporting — the honesty properties
// P6. Corrected symbols never exceed the code's capability. A decoder
// reporting 18 corrections on a t=15 code has miscounted, and the
// number is being used as a margin measure.
property p_corrections_within_t;
@(posedge clk) disable iff (!rst_n)
(cw_done && !uncorrectable) |-> (corrected_symbols <= 5'(FEC_T));
endproperty
a_corrections_within_t: assert property (p_corrections_within_t);
// P7. Corrected and uncorrectable are exclusive. An uncorrectable
// codeword has NO correction count -- the decoder does not know how
// many symbols were wrong, only that it was more than t.
property p_uncorrectable_has_no_count;
@(posedge clk) disable iff (!rst_n)
(cw_done && uncorrectable) |=> $stable(total_corrected);
endproperty
a_uncorrectable_no_count: assert property (p_uncorrectable_has_no_count);
// P8. Every completed codeword lands in exactly one histogram bin. The
// histogram is the margin, so a codeword that misses it is margin
// information destroyed.
property p_every_codeword_binned;
@(posedge clk) disable iff (!rst_n)
cw_done |=> (c_codewords == $past(c_codewords) + 1'b1);
endproperty
a_every_codeword_binned: assert property (p_every_codeword_binned);
// P9. THE ALARM PROPERTY. A codeword correcting within two of the limit
// raises near_limit -- which is the warning a post-FEC error count
// cannot give, because by then the margin is gone.
property p_near_limit_raised;
@(posedge clk) disable iff (!rst_n)
(cw_done && !uncorrectable && (corrected_symbols >= 5'(FEC_T - 2)))
|=> near_limit;
endproperty
a_near_limit_raised: assert property (p_near_limit_raised);
// P10. Margin history survives clear. ever_near_limit and
// ever_uncorrectable are the two facts a later reader needs and the two
// a counter reset destroys.
property p_margin_history_sticky;
@(posedge clk) disable iff (!rst_n)
ever_near_limit |=> ever_near_limit;
endproperty
a_margin_history_sticky: assert property (p_margin_history_sticky);Per-eye and per-lane attribution
// P11. The worst eye is always a real eye. A shape report that names
// nothing is an aggregate margin with extra steps.
property p_worst_eye_valid;
@(posedge clk) disable iff (!rst_n)
window_valid |-> (worst_eye < 2'(EYES));
endproperty
a_worst_eye_valid: assert property (p_worst_eye_valid);
// P12. The three shape classifications are mutually exclusive -- a
// degradation has one shape, and a design reporting two has a
// comparison bug rather than an interesting link.
property p_shapes_exclusive;
@(posedge clk) disable iff (!rst_n)
$onehot0({outer_eyes_worse, middle_eye_worse});
endproperty
a_shapes_exclusive: assert property (p_shapes_exclusive);
// P13. A worst-eye margin only ever falls. It is a running minimum, and
// a minimum that rises has been reset by something that should not
// have.
property p_worst_margin_monotone;
@(posedge clk) disable iff (!rst_n)
!clear |=> (worst_eye_margin[0] <= $past(worst_eye_margin[0]));
endproperty
a_worst_margin_monotone: assert property (p_worst_margin_monotone);
// P14. Eye-closure history survives clear, because reading a counter
// did not change the transmitter.
property p_eye_history_sticky;
@(posedge clk) disable iff (!rst_n)
(|ever_closing) |=> (|ever_closing);
endproperty
a_eye_history_sticky: assert property (p_eye_history_sticky);
// P15. The worst lane is always named when an outlier is declared.
property p_outlier_names_lane;
@(posedge clk) disable iff (!rst_n)
single_lane_outlier |-> (worst_lane < 3'(LANES));
endproperty
a_outlier_names_lane: assert property (p_outlier_names_lane);Latency accounting
// P16. Worst is never below best. The two bounds must bracket, or the
// FEC's variable delay is being reported by a broken pair of registers.
property p_latency_bounds_ordered;
@(posedge clk) disable iff (!rst_n)
(c_samples > 0) |-> (worst_latency >= best_latency);
endproperty
a_latency_bounds_ordered: assert property (p_latency_bounds_ordered);
// P17. Exceeding the allotted budget is REPORTED. A stage that can
// overrun silently has removed itself from Chapter 8.4's decomposition.
property p_over_budget_reported;
@(posedge clk) disable iff (!rst_n)
(tag_out_valid && ((tag_out_time - tag_in_time) > budget_cycles))
|=> over_budget;
endproperty
a_over_budget_reported: assert property (p_over_budget_reported);14. Verification Scenarios
Group by what the environment is doing to the link, because at 400 Gigabit the design's own logic is simple and the interesting failures are all about what it observes and reports.
Codeword structure
- A continuous symbol stream — codewords of exactly 544 symbols, 514 message then 30 parity, forever.
- A stall at message symbol 0 — no
stalled_mid_codeword; a gap between codewords is legal. - A stall at message symbol 300 —
stalled_mid_codewordhigh, and no parity is emitted until the message completes. - A stall during the parity symbols — parity continues, because it needs no input. The distinction the module's
else ifencodes. sym_indexacross a full codeword — 0 to 543, then 0. Never 544.- Back-to-back codewords with no gap —
codeword_endand the nextcodeword_startin consecutive cycles. - Reset mid-codeword — the index and parity accumulator both clear; a partial codeword is abandoned, not resumed.
Margin reporting
- Codewords with 0 corrections — bin 0 fills,
total_correctedunchanged,near_limitlow. - Codewords with exactly
t = 15corrections — bin 15,near_limithigh, and not counted as uncorrectable. The boundary that must be corrected. - Codewords reported uncorrectable — the overflow bin,
c_uncorrectablerises, andtotal_correcteddoes not move. P7 directly: an uncorrectable codeword has no correction count. - A decoder reporting 16 corrections on a
t = 15code — P6 fires. The property exists because this number is used as a margin measure and a miscount corrupts the measure. - 13 corrections —
near_limithigh atFEC_T − 2, two symbols before the limit. - 12 corrections —
near_limitlow. The alarm's boundary, tested from both sides. - A mix drifting from bin 2 to bin 13 over a long run — the histogram's mass moves;
c_uncorrectablestays at zero throughout. The scenario that demonstrates why the histogram exists. clearafter anear_limit— bins and counters clear,ever_near_limitsurvives.
Per-eye shape
- All three eyes reduced equally —
all_eyes_equalhigh, neither shape flag set. - Outer eyes 30% worse than the middle —
outer_eyes_worse,worst_eyenaming eye 0 or 2. Transmitter compression. - Middle eye 30% worse —
middle_eye_worse,worst_eye = 1. Level mismatch. - A 10% difference between outer and middle — neither shape flag. Inside the tolerance band, because real PAM4 levels are deliberately unequal.
- Only eye 2 degraded —
worst_eye = 2,outer_eyes_worse. Asymmetric clipping on one rail. worst_eye_marginunder a rising then falling input — monotone non-increasing. P13.clearafter eye closure —ever_closingsurvives per eye.
Latency and multi-lane
- Codewords needing no correction versus codewords needing 15 —
best_latencyandworst_latencydiffer, because the error-evaluation pipeline runs only when there is something to evaluate. - A measured delay one cycle over
budget_cycles—over_budgetpulses,ever_over_budgetsticks. - One lane correcting four times as many symbols as the others —
single_lane_outlierhigh,worst_lanenaming it. - All eight lanes correcting similarly —
single_lane_outlierlow, with the identical aggregate correction count as scenario 25. Same total, opposite diagnosis. - The upper eye worst on all eight lanes —
same_eye_across_laneshigh. A device characteristic, not a path fault. - The worst eye differing per lane, one lane an outlier —
same_eye_across_laneslow,single_lane_outlierhigh. That lane's optics.
15. Debugging: Nothing Above the FEC Tells You Anything
At 400 Gigabit, three combining steps sit between a physical defect and a frame error, and each one erases an attribution. The table sorts faults by which telemetry, below which combine, distinguishes them.
| Observation | Likely cause | The distinguishing check |
|---|---|---|
| link up, zero errors, no margin data collected | unknown — and it will stay unknown | there is no diagnosis available; start collecting the histogram |
| histogram mass at bins 0–1 | a healthy link with full margin | worst_corrected low; nothing to do |
| histogram mass drifting toward 13–15 over weeks | margin being consumed — ageing, temperature, a connector | trend total_corrected ÷ c_codewords; schedule before the cliff |
near_limit firing, c_uncorrectable still zero | the last warning the link will give | act now; the next step is an outage, not more errors |
c_uncorrectable non-zero | already past the cliff | the margin went some time ago and nothing was watching |
| one lane correcting 4× the others | that lane's optics, fibre or connector | single_lane_outlier, worst_lane |
| all lanes correcting similarly | reach, plant, temperature, supply | single_lane_outlier low — same total, different work order |
| the same eye worst on every lane | a device characteristic — levels, driver, supply | same_eye_across_lanes high; replacing a fibre changes nothing |
| outer eyes worse on one lane | that lane's transmitter compressing | outer_eyes_worse with worst_lane |
| middle eye worse | level setting or DC offset | middle_eye_worse |
| system latency misses its target, no stage owns the miss | the FEC exceeding its budget | worst_latency against budget_cycles; ever_over_budget |
Three habits.
First, never treat a zero error count as a margin measurement. Section 11 computed it: a fivefold loss of margin leaves c_uncorrectable at zero for any window an operator watches. The clean counter is the FEC succeeding, and it will keep succeeding until it does not.
Second, trend the corrected-symbol mean, and write down its value when the link was commissioned. total_corrected ÷ c_codewords is one division of two exported registers, it is monotone in margin, and the difference between 0.5 and 2.7 is the difference between a link with headroom and a link on borrowed time. Neither shows up anywhere else.
Third, ask whether the worst eye is the same eye on every lane before touching any fibre. Same eye everywhere is a device — levels, driver, supply — and no amount of re-cabling will move it. A different worst eye on an outlier lane is that path.
16. Common Misconceptions
"PAM4 is a better modulation than NRZ."
The wrong model: a technical improvement, adopted because it is superior.
What it costs: you cannot explain why FEC became mandatory at the same moment, or why NRZ is still used wherever the channel allows it.
The corrected model: it is a trade, and an expensive one. Two bits per symbol halves the baud rate and therefore the bandwidth the channel must carry — and it costs 20 × log₁₀(3) = 9.54 dB of signal-to-noise ratio, because four levels in one voltage swing give three eyes of a third the height. It is taken because above about 25 Gbaud the last factor of two in channel bandwidth is enormously expensive and ten decibels of SNR can be bought back by a code.
"FEC is an optional extra that improves reliability."
The wrong model: a safety margin bolted on top of a working link.
What it costs: you misread the link's error statistics completely, because you expect a healthy link to have a low raw error rate.
The corrected model: the raw link does not work. A conforming 400 Gigabit link runs at a pre-FEC bit error rate of about 2.4 × 10⁻⁴ — roughly one bit in four thousand — and that is the operating point, not a fault. RS(544,514) is mandatory in the base specification for every PAM4 interface, and its 5.84% is not a tax on a working link: it is part of the mechanism that makes one.
"Zero post-FEC errors means the link is healthy."
The wrong model: a clean error counter is a clean link.
What it costs: everything — it is the reason margin loss is discovered as an outage.
The corrected model: the FEC was built to make its output insensitive to its input, and it succeeds. Section 11's table: from a pre-FEC BER of 10⁻⁴ to 5 × 10⁻⁴ — a fivefold loss of margin — the codeword loss probability goes from 10⁻¹⁸ to 10⁻⁸, both of which read as zero on any dashboard. The metric that moves is the corrected-symbol count, which goes from 0.54 to 2.71 per codeword over the same range.
"An uncorrectable codeword is a slightly worse corrected one."
The wrong model: a continuum, so the counters can be added together.
What it costs: a corrupted margin measure, because the two events carry different information.
The corrected model: a corrected codeword tells you exactly how many symbols were wrong; an uncorrectable one tells you only that it was more than 15. The decoder does not know the count, and inventing one — or folding it into a total — poisons the only monotone margin signal the link produces. Which is what P7 asserts: an uncorrectable codeword contributes nothing to total_corrected.
"All three PAM4 eyes are the same, so one margin number is enough."
The wrong model: three measurements of the same thing.
What it costs: every PAM4 fault gets diagnosed as "loss," because the aggregate cannot distinguish the causes.
The corrected model: the shape names the cause. Three eyes equally small is additive noise or reach — the channel. Outer eyes worse is driver compression, on the far transmitter. Middle eye worse is a level or offset error. All three report the same minimum margin, and only the comparison separates them — and if the same eye is worst on every lane, the cause is the device and no fibre change will touch it.
17. Interview Reasoning
"Why did the industry move to PAM4, and what did it cost?"
The weak answer says it is faster. The answer that ends the topic states it as a trade with both sides computed. Two bits per symbol halves the baud rate for a given bit rate, which halves the bandwidth the channel must support — and four levels in one voltage swing give three eyes of a third the height, which is 20 × log₁₀(3) = 9.54 dB of SNR. The reason the trade is worth taking is that the two quantities do not cost the same: above about 25 Gbaud, channel loss rises steeply with frequency so the last factor of two in bandwidth is very expensive, while ten decibels can be bought back by a code at 5.84% of the line rate. The finishing point: the penalty is a ratio of amplitudes, so it is exactly 9.54 dB on every medium at every rate — and it is a floor, with nonlinearity and level-dependent noise adding to it.
"Why is FEC mandatory at 400 Gigabit when it was optional before?"
Because the 9.54 dB penalty puts the raw link's error rate at about 2.4 × 10⁻⁴ on a conforming channel — one bit in four thousand — which is not an Ethernet link. In earlier generations a medium specification could require FEC or not, depending on its reach; with PAM4 no conforming channel meets the error expectation unaided, so the correction moves into the base specification. The strong answer adds why it is KP4 rather than KR4: RS(544,514) corrects 15 symbols per codeword against RS(528,514)'s 7, for 5.84% overhead instead of 2.72%, and seven is not enough headroom at a 10⁻⁴ input rate. The finishing observation: the transcoding from 64B/66B to 256B/257B frees 2.73 points, so the FEC costs about 3.1 points net rather than 5.84.
"How much latency does the FEC add, and where does it come from?"
Structurally, from the block. Parity is a function of all 514 message symbols, so the encoder holds a whole message before emitting any; and the decoder cannot correct any symbol until it holds all 544, because a symbol arriving last can change which symbols the decoder concludes were wrong. Compute the floor: the codeword is 544 × 10 = 5440 bits, the 400G line rate after both overheads is 400 × 257/256 × 544/514 = 425 Gb/s, so one fill is 12.8 ns and encode-plus-decode is 25.6 ns. The realised KP4 figure is several times that — commonly cited around 100 ns — because the syndrome, root-finding and error-evaluation stages are pipelines. Put it in context: 100 ns is 3.3 times the 30.36 ns it takes to serialise a whole 1518-octet frame at 400G, and equivalent to about 20 metres of fibre. The finishing point: it is the first latency term Ethernet added rather than inherited, and it does not shrink with rate the way serialization does.
"Would you assert that the post-FEC error rate meets the target?"
No, on two grounds, and the second is the one that matters. First, it is unevaluable — 10⁻¹² needs 10¹² codewords, so the property passes because nothing ran long enough to fail it. Second, and worse: the metric is flat across the entire operating range. From a pre-FEC BER of 10⁻⁴ to 5 × 10⁻⁴ — a link that has lost most of its margin — uncorrectable codewords stay at zero for any window anyone watches. The FEC was built to make its output insensitive to its input, so asserting on the output asserts on the one quantity the mechanism was designed to flatten. And a design graded on that property has no reason to build the corrected-symbol histogram, which is the only warning the link will ever give. Assert the reporting instead: corrections never exceed t, an uncorrectable codeword contributes no count, every codeword is binned, near_limit fires two symbols before the limit, and the history survives a clear.
18. Understanding Check
Half the bandwidth, for 9.54 dB of margin.
What is bought. PAM4 carries two bits per symbol, so a given bit rate needs half the baud — a 100 Gb/s lane is 50 Gbaud in NRZ and 25 Gbaud in PAM4 — and baud rate is what the channel has to support.
What is paid. Four levels share the same voltage swing, so there are three eyes where there was one, each one third of the height:
20 × log₁₀(1/3) = −9.542 dB
And the trade is worth taking because the two are not equally expensive. Above about 25 Gbaud, channel loss on copper and board rises steeply with frequency, so the last factor of two in bandwidth costs better materials, shorter reach and more retimers. Ten decibels of SNR, by contrast, is recoverable by a code — RS(544,514) at 5.84% of the line rate.
Two more things worth knowing. The penalty is a ratio of amplitudes, so it is exactly 9.54 dB on every medium at every rate, with no qualification. And it is a floor — real PAM4 adds level-dependent noise, transmitter nonlinearity and tighter timing sensitivity on top, which is why Section 4 measures the three eyes separately rather than assuming they match.
19. What's Next
The claim this chapter defended: once correction is mandatory, the link's error rate stops being an indicator.
PAM4 buys half the channel bandwidth and pays 20 × log₁₀(3) = 9.54 dB for it, because three eyes share the swing one eye used to have. At that penalty a conforming 400 Gigabit link runs at a raw error rate of about 2.4 × 10⁻⁴ — one bit in four thousand — which is the operating point, not a fault. So RS(544,514) is in the base specification for every PAM4 interface, the transcoding to 256B/257B frees 2.73 points to help pay for its 5.84, and the line rate works out at 400 × 257/256 × 544/514 = 425 Gb/s, or 53.125 Gb/s and 26.5625 GBd on each of eight lanes.
The code adds a latency term Ethernet never had, and it is structural: a block code cannot correct until it holds a whole codeword, giving a floor of 25.6 ns at 400G and a realised figure several times larger — 3.3 times the time it takes to serialise a maximum-length frame.
And it flattens the metric everyone watches. From a pre-FEC BER of 10⁻⁴ to 2 × 10⁻³, uncorrectable codewords move by 6 × 10¹⁶ while reading as zero across the entire useful range — so a link with full margin and a link one step from an outage report the same thing. The quantity that moves smoothly is the corrected-symbol count, which is why the design's real obligation is to keep its distribution and why the rejected property here asserts on the output the mechanism was built to make constant.
Module 9 is complete. Six generations, from a shared coax to eight PAM4 lanes, and the pattern held throughout: mechanisms attached to a medium died with it, and mechanisms attached to a frame were inherited by every link that carries frames.
Chapter 10.1 — MII opens the module that owns the MAC/PHY interfaces in their own right.
Module 9 cited them in passing — Chapter 9.3 §11 gave GMII's width arithmetic, Chapter 9.4 §4 gave XGMII's. Module 10 gives the pin lists, the clocking, the latency each one adds, and the bring-up failures each one is famous for — starting with the original 4-bit MII, its two clock domains, and the signals that carry carrier sense and collision to a MAC that, on most links since, has had no use for either.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
Line Modulation — NRZ and PAM4
PAM4 does not create bandwidth. At the same data rate it halves the symbol rate, recovering horizontal margin from frequency-dependent loss, and pays for it by cutting the vertical eye to one third — a 9.54 dB penalty that no circuit recovers and that forward error correction must.
- Related topic
Forward Error Correction
FEC converts a gradual degradation into a cliff and hides the gradient behind it. The pre-correction error rate is the link's health metric and gives months of warning; the corrected output reads zero until the moment it collapses, and can be silently wrong when a decoder miscorrects.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
- Related topic
CSMA/CD, Collision Domains and Slot Time
Slot time is the parameter the whole half-duplex MAC hangs on: it bounds medium acquisition, bounds a collision fragment, and is the retransmission quantum. Deriving it from round-trip propagation plus jam is what fixes Ethernet's minimum frame size — a timing constant wearing a frame-format costume.
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.
