Skip to content
VLSI Mentor

Ethernet · Module 4

Elastic Buffering and Clock Compensation

Two independent oscillators differ by a bounded amount forever, and a bounded rate difference still accumulates without limit unless something discharges it. The interframe gap is that opportunity — which is why it is not negotiable and why the buffer is far smaller than intuition suggests.

Chapter 2.6 listed elastic buffering as one of the PHY's responsibilities and said it absorbs the difference between two independent oscillators. Chapter 4.1 §7 and Chapter 4.3 both said the compensation happens in the interframe gap, and both treated that as a given.

Nobody has yet asked the question that makes the mechanism intelligible.

Two clocks that differ never stop differing. A receiver whose recovered clock is slightly slower than the transmitter's falls behind by a little every symbol, and it keeps falling behind — for as long as the link is up, which may be years. There is no equilibrium and no self-correction. A finite buffer cannot absorb an accumulation that grows without limit, so either the accumulation is bounded, or the mechanism does not work.

The usual answer is that the difference is small — a hundred parts per million — and small differences are easy. That answer is wrong in an interesting way, because a small difference accumulating forever is still unbounded. Slow accumulation reaches any depth eventually; it just takes longer.

Why is the accumulation bounded, how much buffer does the bound actually require, and what happens when the thing that bounds it is taken away?

1. Scope — What This Chapter Owns

This chapter owns: why two independent clocks differ permanently; why the difference is bounded and what bounds it; the derivation of buffer depth from a clock tolerance in parts per million and a maximum frame length; idle insertion and deletion as the discharge mechanism; what happens when discharge opportunity is removed; and the distinction between clock-difference depth and burst depth.

This chapter does not own: the clock-domain crossing itself — the synchronisers, the pointer encoding, the metastability handling — which is Chapter 4.6. Nor the burst sizing, which Chapter 4.3 §10 owns and Section 12 here only relates to. Nor the gap's role in access control, which is Chapter 2.5's.

The debt it repays: Chapter 2.6 named the mechanism, Chapter 4.1 §7 and Chapter 4.3 §2 both relied on the gap being available for it, and none of the three said why the arithmetic works. This chapter is that arithmetic.

2. Why Two Clocks Differ Permanently

The two ends of an Ethernet link have no shared time reference. Each has its own oscillator, and no oscillator is exact.

The transmitter's clock sets the rate at which symbols leave. The receiver recovers a clock from the arriving signal — Chapter 2.6's clock recovery — so its receive side runs at the transmitter's rate. But the data must then cross into the receiver's own clock domain, and that domain runs at the receiver's oscillator rate.

Those two rates are not equal and never will be. The difference does not oscillate around zero or average out; if the local oscillator is 40 ppm fast, it is 40 ppm fast for as long as it is powered.

3. The Bound Within a Frame

Section 2 showed the accumulation is unbounded over time. Within a single frame it is not, and that is the first half of the mechanism.

A frame has a maximum length, so the slip accumulated during one frame is bounded by that length times the relative difference.

The derivation:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
worst-case relative difference = 100 ppm + 100 ppm = 200 ppm = 2e-4
 
slip over one frame = frame_octets × 2e-4

Worked for three frame sizes:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
minimum frame,   64 octets:   64 × 2e-4 = 0.013 octets
maximum basic, 1518 octets: 1518 × 2e-4 = 0.30  octets
jumbo,         9000 octets: 9000 × 2e-4 = 1.80  octets

Read the middle line. Over a maximum-length standard frame, two clocks at opposite tolerance extremes drift apart by less than a third of an octet. Even a 9,000-octet jumbo frame produces under two octets of slip.

Which gives the depth requirement for the within-frame case directly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
depth_clock ≥ ceil(max_frame_octets × 2e-4) + margin
            = ceil(1.80) + margin  for jumbo
            = 2 + margin octets

4. The Discharge — Where the Gap Comes In

Section 3 bounded the slip within a frame. Between frames, the accumulated slip has to be repaid, or Section 2's unbounded growth resumes across frame boundaries.

The interframe gap is where it is repaid, and the reason is that the gap carries nothing anyone needs.

Chapter 3.4 §9 and Chapter 3.5 §4 established that a PHY never goes silent — the gap is filled with idle, which is transmitted so the far end's clock recovery does not starve. Idle carries no information. So removing an idle symbol, or adding one, changes nothing the far end cares about — and that is exactly what a discharge needs.

  • If the local clock is slower than the incoming rate, the buffer fills. Delete an idle from the gap: one fewer octet to store, and the fill drops by one.
  • If the local clock is faster, the buffer empties. Insert an idle into the gap: one more octet to consume, and the fill rises by one.

The capacity available per gap:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
minimum interframe gap = 12 octets
octets that may be removed while keeping the gap usable ≈ a few

Published guidance for one gigabit family is that idle removal must not leave fewer than 8 octets between frames including the end-of-frame and start-of-frame markers — so on a 12-octet gap, a small number of deletions per gap is available.

Now put that against Section 3's numbers. A maximum basic frame accumulates 0.3 octets of slip. One deleted idle repays roughly three such frames. Even jumbo frames at 1.8 octets of slip need only one deletion every gap or two.

A five-stage cycle. Two clocks differ by a bounded amount. Slip accumulates during a frame. The offset from centre crosses a trigger. A gap arrives, and an idle symbol is deleted or inserted. The offset returns toward centre and the cycle repeats. Removing the gap breaks the cycle at stage four.Accumulate, trigger, discharge, repeat1Clocks differat most 200 ppm, permanently2Slip accumulates0.3 octets per maximum frame3Offset crosses the triggercompensation is requested4A gap arrivesthe only place removal is harmless5Offset returns to centreone idle repays three frames
Figure 1 — the cycle that makes a linear accumulation survivable.

The discharge capacity exceeds the requirement by a comfortable margin, which is why the mechanism works at all — and why the failure mode is not "not enough discharge capacity" but "no discharge opportunity", which is a completely different thing.

5. RTL 1 — The Depth Calculator

The arithmetic of Sections 3 and 4 should be computed by the design, not written into a comment and forgotten.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Depth derived, not asserted.
//
// The arithmetic of Sections 3 and 4, as elaboration-time constants. A
// hand-computed depth is correct exactly once -- until someone enables
// jumbo frames, or specifies a cheaper oscillator, or raises the rate.
// Deriving it means those changes update it.
//
// TWO INDEPENDENT REQUIREMENTS ARE COMPUTED SEPARATELY AND ADDED. They have
// different causes and different failure modes, and a design that computes
// one total loses the ability to say which one it ran out of.
package elastic_pkg;
 
  // ── Inputs to the derivation. All illustrative defaults; a real design
  // takes them from its own oscillator specification and configuration. ────
  parameter int unsigned PPM_LOCAL   = 100;   // IEEE 802.3 specifies +/-100
  parameter int unsigned PPM_REMOTE  = 100;
  parameter int unsigned MAX_FRAME   = 1518;  // octets; 9000 for jumbo
  parameter int unsigned MIN_GAP     = 12;    // octets
  // Octets that must remain in the gap for frame delineation. Published
  // guidance for one gigabit family is not fewer than 8 including the
  // end and start markers.
  parameter int unsigned GAP_RESERVE = 8;
 
  // ── Derived: the clock-difference requirement ──────────────────────────
  // Worst case is both ends at opposite tolerance extremes.
  parameter int unsigned PPM_TOTAL = PPM_LOCAL + PPM_REMOTE;   // 200
 
  // Slip in octets over one maximum frame, scaled by 1e6 to stay integral,
  // then rounded up. For 1518 octets at 200 ppm this is 0.3036 -> 1.
  parameter int unsigned SLIP_SCALED   = (MAX_FRAME * PPM_TOTAL);       // 303600
  parameter int unsigned SLIP_OCTETS   = (SLIP_SCALED + 999_999) / 1_000_000;
 
  // Both directions plus margin: the buffer must absorb slip in EITHER
  // direction from its centre, so the usable depth is twice the one-way
  // slip, and the centre itself must exist.
  parameter int unsigned CLOCK_MARGIN  = 2;
  parameter int unsigned DEPTH_CLOCK   = (2 * SLIP_OCTETS) + CLOCK_MARGIN;
 
  // ── Derived: discharge capacity per gap ────────────────────────────────
  parameter int unsigned DISCHARGE_PER_GAP = (MIN_GAP > GAP_RESERVE)
                                           ? (MIN_GAP - GAP_RESERVE) : 0;
 
  // Frames that may pass between discharges before the slip exceeds what
  // one gap can repay. Large is good: it means the mechanism has headroom.
  parameter int unsigned FRAMES_PER_DISCHARGE =
      (SLIP_OCTETS == 0) ? 1_000_000 : (DISCHARGE_PER_GAP * 1_000_000)
                                      / (SLIP_SCALED / MAX_FRAME == 0 ? 1
                                         : (SLIP_SCALED / MAX_FRAME));
 
endpackage
 
module elastic_depth_calc
  import elastic_pkg::*;
#(
  // The burst requirement comes from Chapter 4.3 §10 and is a completely
  // separate calculation with a different cause. It is an INPUT here so the
  // two are visibly added rather than silently merged.
  parameter int unsigned DEPTH_BURST = 2048
) (
  output int unsigned depth_clock,
  output int unsigned depth_burst,
  output int unsigned depth_total,
  output int unsigned discharge_per_gap,
  output int unsigned frames_per_discharge,
 
  // Asserted at elaboration if the discharge capacity cannot keep up with
  // the slip. On any standard configuration this is comfortably false --
  // and a configuration where it is true is one where the gap has been
  // shortened or the tolerance loosened beyond what the mechanism supports.
  output logic        discharge_insufficient
);
 
  assign depth_clock          = DEPTH_CLOCK;
  assign depth_burst          = DEPTH_BURST;
  // ADDED, not maxed. The two accumulations are independent and can be
  // adverse simultaneously: a burst can arrive while the buffer is already
  // displaced from centre by clock slip.
  assign depth_total          = DEPTH_CLOCK + DEPTH_BURST;
  assign discharge_per_gap    = DISCHARGE_PER_GAP;
  assign frames_per_discharge = FRAMES_PER_DISCHARGE;
  assign discharge_insufficient = (DISCHARGE_PER_GAP == 0);
 
endmodule

Classification: synthesizable, with the derivation at elaboration time.

What it teaches: that the two depth requirements are added, not maximised. They are independent and can be adverse at the same moment — a burst can arrive while the buffer is already displaced from its centre by accumulated clock slip — so taking the larger of the two leaves no room for the other.

It also teaches that DEPTH_CLOCK is twice the one-way slip plus margin. The buffer must absorb drift in either direction from its centre, because which clock is faster is not known at design time and can differ per link.

Deliberately simplified: integer arithmetic scaled by a million to avoid floating point, and a fixed margin rather than one derived from start-up transients. Real designs also account for the buffer's own latency in the centring calculation.

Production implication: frames_per_discharge is the headroom figure and it is the one to check when a configuration changes. On a standard setup it is large — one discharge repays several maximum-length frames — which is why the mechanism has never been the limiting factor. A change that makes it small has not broken anything yet, and will.

Later ownership: the burst depth is Chapter 4.3 §10's; the clock-domain crossing that carries the pointers is Chapter 4.6.

6. RTL 2 — The Elastic FIFO

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The elastic buffer.
//
// THE DIFFERENCE FROM AN ORDINARY FIFO, and it is the whole design:
//
//   An ordinary FIFO starts EMPTY. Empty is its correct idle state, and it
//   fills when the producer is ahead.
//
//   An elastic FIFO starts HALF FULL, and half full is its correct idle
//   state. It must absorb drift in EITHER direction -- the local clock may
//   be faster or slower and which is not known at design time -- so it
//   needs room to both grow and shrink from wherever it sits.
//
// Every decision here is made against DISTANCE FROM CENTRE, never against
// fullness. A design that thinks in terms of "how full" will insert when it
// should delete on half the links it is deployed on.
module elastic_fifo
  import elastic_pkg::*;
#(
  parameter int unsigned W     = 8,
  parameter int unsigned DEPTH = 16,
  parameter int unsigned PTR_W = $clog2(DEPTH),
  // Distance from centre at which compensation is requested. Small enough
  // that a gap arrives before the buffer is in trouble; large enough that
  // it does not compensate on every gap for no reason.
  parameter int unsigned TRIGGER = 2
) (
  input  logic clk_wr,
  input  logic clk_rd,
  input  logic rst_n,
 
  // Write side, in the recovered clock domain.
  input  logic         wr_en,
  input  logic [W-1:0] wr_data,
 
  // Read side, in the local clock domain.
  input  logic         rd_en,
  output logic [W-1:0] rd_data,
 
  // Compensation requests, evaluated in the READ domain because that is
  // where the local clock lives and where the discharge is applied.
  output logic         request_delete,   // buffer above centre: drop an idle
  output logic         request_insert,   // buffer below centre: add an idle
  input  logic         in_gap,           // a gap is available now
 
  // Distance from centre, signed. THIS is the health metric, not fill --
  // Section 9 explains why the trend of this number is what matters.
  output logic signed [PTR_W:0] offset_from_centre,
 
  output logic         overrun,
  output logic         underrun
);
 
  localparam int unsigned CENTRE = DEPTH / 2;
 
  logic [W-1:0]     mem [DEPTH];
  logic [PTR_W-1:0] wptr_q, rptr_q;
  logic [PTR_W:0]   count_q;
 
  // The count is maintained in the read domain here for readability. A real
  // design must carry the pointers across the domain boundary with proper
  // synchronisation -- Chapter 4.6 owns that, and Section 13's rejected
  // property is about exactly why this simplification is dangerous to
  // reason from.
  always_ff @(posedge clk_wr or negedge rst_n) begin
    if (!rst_n) wptr_q <= '0;
    else if (wr_en) begin
      mem[wptr_q] <= wr_data;
      wptr_q      <= wptr_q + 1'b1;
    end
  end
 
  always_ff @(posedge clk_rd or negedge rst_n) begin
    if (!rst_n) begin
      rptr_q   <= '0;
      // START AT CENTRE. An elastic buffer that starts empty underruns on
      // its first read if the local clock is the faster one.
      count_q  <= (PTR_W+1)'(CENTRE);
      overrun  <= 1'b0;
      underrun <= 1'b0;
    end else begin
      if (rd_en) rptr_q <= rptr_q + 1'b1;
 
      // Fill tracking, simplified as noted above.
      case ({wr_en, rd_en})
        2'b10: count_q <= count_q + 1'b1;
        2'b01: count_q <= count_q - 1'b1;
        default: ;   // both or neither: no net change
      endcase
 
      overrun  <= (count_q >= (PTR_W+1)'(DEPTH - 1)) && wr_en && !rd_en;
      underrun <= (count_q == '0) && rd_en && !wr_en;
    end
  end
 
  assign rd_data            = mem[rptr_q];
  assign offset_from_centre = (PTR_W+1)'(count_q) - (PTR_W+1)'(CENTRE);
 
  // Compensation is requested on DISTANCE FROM CENTRE and applied only when
  // a gap is available. Requesting mid-frame would mean deleting a data
  // octet, which is not compensation -- it is corruption.
  assign request_delete = in_gap && (offset_from_centre >  (PTR_W+1)'(TRIGGER));
  assign request_insert = in_gap && (offset_from_centre < -(PTR_W+1)'(TRIGGER));
 
endmodule
Data arrives in the recovered clock domain and is written to an elastic buffer. It is read out in the local clock domain. The buffer's offset from its centre drives a compensation request, which is gated by gap availability and applied as an idle insertion or deletion. Without a gap the request is deferred and the offset keeps growing.Recovered clockthe far end's rate, viathe PHYElastic bufferidles at centre, not emptyLocal clockthis device's ownoscillatorToward the MACone coherent rateOffset from centresigned, and the healthmetricGap available?no gap means deferredInsert or deleteidlethe discharge12
Figure 2 — two clock domains, one buffer, and a discharge path that only opens in a gap.

Classification: synthesizable.

What it teaches: that an elastic buffer's correct idle state is half full, and every decision is made against distance from centre rather than fullness. A design reasoning in terms of "how full" will insert when it should delete on half the links it is deployed on, because which end has the faster oscillator is not knowable at design time and differs per installation.

Deliberately simplified: the fill count is maintained in one domain, which a real dual-clock FIFO cannot do. Proper pointer synchronisation is Chapter 4.6's, and Section 13's rejected property is specifically about why reasoning from this simplification is dangerous.

Production implication: request_delete and request_insert are gated on in_gap and this gating is not optional. Deleting an octet mid-frame is not compensation; it is corruption — the frame loses an octet, its check value fails, and the loss is attributed to the link. The gap is the only place where removing a symbol changes nothing, which is the entire reason Section 4's mechanism works there and nowhere else.

Later ownership: the clock-domain crossing is Chapter 4.6.

7. RTL 3 — The Idle Insertion and Deletion Controller

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Idle insertion and deletion within a gap.
//
// Two constraints the controller must respect, and the second is the one
// designs get wrong:
//
//   1. Only in a gap. Deleting an octet mid-frame is corruption.
//   2. The gap has a RESERVE that must survive. Published guidance for one
//      gigabit family is that removal must not leave fewer than 8 octets
//      including the end and start markers -- so deletion capacity per gap
//      is finite, and a controller that deletes greedily will shorten a gap
//      below what the far end needs to delineate frames.
//
// Constraint 2 is why compensation is spread across gaps rather than done
// all at once, and why Section 5's frames_per_discharge headroom matters.
module idle_compensator
  import elastic_pkg::*;
#(
  parameter int unsigned CNT_W       = 24,
  parameter int unsigned GAP_MIN     = 12,
  parameter int unsigned GAP_RESERVE_LOCAL = 8,
  parameter int unsigned GAP_W       = $clog2(GAP_MIN + 34)
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic in_frame,
  input  logic request_delete,
  input  logic request_insert,
 
  // Idle octets seen so far in the current gap.
  input  logic idle_octet,
 
  output logic do_delete,
  output logic do_insert,
 
  // ── Observability ───────────────────────────────────────────────────────
  output logic [CNT_W-1:0] c_deleted,
  output logic [CNT_W-1:0] c_inserted,
 
  // A compensation was needed and no gap was available to do it in. This is
  // the early warning that the discharge opportunity is disappearing, and
  // it fires LONG BEFORE the buffer overruns -- which is the difference
  // between a diagnosable fault and a mysterious one.
  output logic             deferred,
  output logic [CNT_W-1:0] c_deferred,
  output logic [15:0]      longest_deferral,
 
  // A gap arrived that was already at or below the reserve, so nothing
  // could be removed from it even though removal was wanted. Distinct from
  // `deferred` -- this one names the MAC as the cause.
  output logic [CNT_W-1:0] c_gap_below_reserve
);
 
  logic [GAP_W-1:0] gap_len_q;
  logic [15:0]      defer_run_q;
  logic             in_gap_c;
 
  assign in_gap_c = !in_frame;
 
  // Deletion is permitted only while the gap still exceeds its reserve.
  wire can_delete_c = in_gap_c && (gap_len_q > GAP_W'(GAP_RESERVE_LOCAL));
  // Insertion always has room -- lengthening a gap is harmless.
  wire can_insert_c = in_gap_c;
 
  assign do_delete = request_delete && can_delete_c;
  assign do_insert = request_insert && can_insert_c;
  assign deferred  = (request_delete && !can_delete_c)
                  || (request_insert && !can_insert_c);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      gap_len_q           <= '0;
      c_deleted           <= '0;
      c_inserted          <= '0;
      c_deferred          <= '0;
      defer_run_q         <= '0;
      longest_deferral    <= '0;
      c_gap_below_reserve <= '0;
    end else begin
      if (clear) begin
        c_deleted           <= '0;
        c_inserted          <= '0;
        c_deferred          <= '0;
        longest_deferral    <= '0;
        c_gap_below_reserve <= '0;
      end
 
      // Gap length accounting.
      if (in_frame) begin
        // A gap just ended. If it was below the reserve and we wanted to
        // delete, the MAC gave us nowhere to work.
        if (gap_len_q != '0) begin
          if ((gap_len_q <= GAP_W'(GAP_RESERVE_LOCAL)) && !(&c_gap_below_reserve))
            c_gap_below_reserve <= c_gap_below_reserve + 1'b1;
        end
        gap_len_q <= '0;
      end else if (idle_octet && !(&gap_len_q)) begin
        gap_len_q <= gap_len_q + 1'b1;
      end
 
      if (do_delete) begin
        if (!(&c_deleted)) c_deleted <= c_deleted + 1'b1;
        // A deletion consumes one octet of the gap.
        if (gap_len_q != '0) gap_len_q <= gap_len_q - 1'b1;
      end
      if (do_insert && !(&c_inserted)) c_inserted <= c_inserted + 1'b1;
 
      if (deferred) begin
        if (!(&c_deferred)) c_deferred <= c_deferred + 1'b1;
        defer_run_q <= defer_run_q + 1'b1;
        if (defer_run_q + 1'b1 > longest_deferral)
          longest_deferral <= defer_run_q + 1'b1;
      end else begin
        defer_run_q <= '0;
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that deletion capacity per gap is finite, because a reserve must survive for the far end to delineate frames. That finiteness is why compensation is spread across gaps rather than performed all at once, and it is why Section 5's frames_per_discharge headroom is worth computing.

Production implication: c_deferred is the early warning and it is the most valuable counter in the chapter. A deferred compensation means the buffer wanted to discharge and had nowhere to do it — which is Section 4's failure beginning, long before the buffer overruns. On a gigabit link with a modest buffer, deferrals start milliseconds before an overrun; with a generous buffer, minutes. Either way the deferral is the cause and the overrun is the symptom, and only one of them names the right subsystem.

And c_gap_below_reserve is distinct from c_deferred deliberately. Deferral says compensation could not happen. Gap-below-reserve says why: the MAC left a gap too short to work in. Merging them loses the attribution, and Chapter 4.1 §14 showed what that costs.

8. RTL 4 — The Drift Monitor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION.
//
// FILL IS A READING. DRIFT IS THE HEALTH METRIC.
//
// A buffer sitting at 60 percent tells you nothing -- it may have sat there
// for a year. A buffer whose offset from centre has been growing by one
// octet per second for an hour is a link that will fail, and it is still
// reporting healthy on every instantaneous check.
//
// This is the same argument Chapter 3.3 made for margin, Chapter 3.7 for
// the pre-correction error rate, and Chapter 3.8 for skew margin. At every
// layer: measure what degrades continuously, and treat what degrades
// discontinuously as an alarm of last resort.
module drift_monitor #(
  parameter int unsigned PTR_W  = 5,
  parameter int unsigned WINDOW = 1_000_000,
  parameter int unsigned WIN_W  = $clog2(WINDOW + 1),
  parameter int unsigned CNT_W  = 24
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic signed [PTR_W:0] offset_from_centre,
  input  logic                  do_delete,
  input  logic                  do_insert,
 
  // Offset at the start and end of the window that just closed. Their
  // difference is the NET drift the compensation failed to remove -- which
  // should be near zero on a healthy link.
  output logic signed [PTR_W:0] window_start_offset,
  output logic signed [PTR_W:0] window_end_offset,
  output logic signed [PTR_W+1:0] window_net_drift,
  output logic                  window_valid,
 
  // Compensation applied during the window. The ratio of deletions to
  // insertions says WHICH clock is faster, which is a fact about the
  // installation and worth knowing.
  output logic [CNT_W-1:0] window_deletes,
  output logic [CNT_W-1:0] window_inserts,
 
  // Extremes since reset. Survive `clear`: they are properties of the
  // installed link, not of a window an operator happened to choose.
  output logic signed [PTR_W:0] worst_high,
  output logic signed [PTR_W:0] worst_low,
 
  // Net drift did not return to near zero across the window, meaning
  // compensation is not keeping up. This is the finding, and it precedes
  // any overrun by a wide margin.
  output logic                  drift_uncompensated
);
 
  logic [WIN_W-1:0] win_q;
  logic [CNT_W-1:0] del_q, ins_q;
  logic signed [PTR_W:0] start_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      win_q               <= '0;
      del_q               <= '0;
      ins_q               <= '0;
      start_q             <= '0;
      window_start_offset <= '0;
      window_end_offset   <= '0;
      window_deletes      <= '0;
      window_inserts      <= '0;
      window_valid        <= 1'b0;
      worst_high          <= '0;
      worst_low           <= '0;
    end else begin
      if (clear) begin
        win_q        <= '0;
        del_q        <= '0;
        ins_q        <= '0;
        window_valid <= 1'b0;
        // worst_high and worst_low deliberately survive.
      end else begin
        if (do_delete && !(&del_q)) del_q <= del_q + 1'b1;
        if (do_insert && !(&ins_q)) ins_q <= ins_q + 1'b1;
 
        if (offset_from_centre > worst_high) worst_high <= offset_from_centre;
        if (offset_from_centre < worst_low)  worst_low  <= offset_from_centre;
 
        if (win_q == WIN_W'(WINDOW - 1)) begin
          window_start_offset <= start_q;
          window_end_offset   <= offset_from_centre;
          window_deletes      <= del_q;
          window_inserts      <= ins_q;
          window_valid        <= 1'b1;
          start_q             <= offset_from_centre;
          del_q               <= '0;
          ins_q               <= '0;
          win_q               <= '0;
        end else begin
          win_q <= win_q + 1'b1;
        end
      end
    end
  end
 
  assign window_net_drift = (PTR_W+2)'(window_end_offset) - (PTR_W+2)'(window_start_offset);
  // On a healthy link compensation returns the offset to near where it
  // started. A persistent net drift means it is not keeping up.
  assign drift_uncompensated = window_valid
                            && ((window_net_drift > 2) || (window_net_drift < -2));
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that net drift across a window is the metric, not fill. On a healthy link, compensation returns the offset to roughly where it started each window — the mechanism is working. A persistent net drift means compensation is not keeping up, and it appears long before any overrun.

Deliberately simplified: one window and a fixed threshold. Production designs keep a histogram of offsets, which reveals the distribution rather than only its endpoints.

Production implication: the ratio of window_deletes to window_inserts says which end has the faster oscillator, which is a fact about the installation worth recording. Deletions dominating means the incoming rate exceeds the local one; insertions dominating means the reverse. A link whose ratio changes has had an oscillator drift or a component swapped — and that is a finding no other metric produces.

And this is the third layer at which the track's recurring argument appears. Chapter 3.3 measured margin, Chapter 3.7 the pre-correction error rate, Chapter 3.8 skew margin — and here, drift. Measure what degrades continuously; treat what degrades discontinuously as an alarm of last resort.

9. RTL 5 — Overrun and Underrun Are Different Faults

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION.
//
// OVERRUN and UNDERRUN are not two sides of one fault:
//
//   OVERRUN  -- the incoming rate exceeded the local one and the buffer
//               filled. DATA IS LOST. The local clock is slower, or
//               deletions were deferred.
//
//   UNDERRUN -- the local rate exceeded the incoming one and the buffer
//               emptied. NO DATA IS LOST -- the read side must emit
//               something, and what it emits is idle. The local clock is
//               faster, or insertions were deferred.
//
// One destroys data and the other does not. A shared counter makes a lossy
// link and a merely mistimed one read identically.
module buffer_fault_classifier #(
  parameter int unsigned PTR_W = 5,
  parameter int unsigned CNT_W = 24
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic overrun,
  input  logic underrun,
  input  logic in_frame,
  input  logic deferred_delete,
  input  logic deferred_insert,
 
  output logic [CNT_W-1:0] c_overrun,
  output logic [CNT_W-1:0] c_underrun,
 
  // An overrun that happened INSIDE a frame destroys that frame. One in a
  // gap costs nothing. Same event, completely different consequence.
  output logic [CNT_W-1:0] c_overrun_in_frame,
  output logic [CNT_W-1:0] c_overrun_in_gap,
 
  // Was a compensation deferred before the fault? If so, the cause is
  // upstream -- a gap too short -- rather than the buffer being too small.
  // This is the attribution that decides which team owns the problem.
  output logic             preceded_by_deferral,
  output logic [CNT_W-1:0] c_fault_after_deferral,
 
  // First fault kind since reset, held. During an incident both will be
  // set; which came first says which clock is the faster one.
  output logic             first_was_overrun,
  output logic             first_fault_valid
);
 
  logic deferral_recent_q;
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_overrun              <= '0;
      c_underrun             <= '0;
      c_overrun_in_frame     <= '0;
      c_overrun_in_gap       <= '0;
      c_fault_after_deferral <= '0;
      deferral_recent_q      <= 1'b0;
      first_was_overrun      <= 1'b0;
      first_fault_valid      <= 1'b0;
    end else begin
      if (clear) begin
        c_overrun              <= '0;
        c_underrun             <= '0;
        c_overrun_in_frame     <= '0;
        c_overrun_in_gap       <= '0;
        c_fault_after_deferral <= '0;
      end else begin
        c_overrun  <= bump(c_overrun,  overrun);
        c_underrun <= bump(c_underrun, underrun);
        c_overrun_in_frame <= bump(c_overrun_in_frame, overrun &&  in_frame);
        c_overrun_in_gap   <= bump(c_overrun_in_gap,   overrun && !in_frame);
        c_fault_after_deferral <= bump(c_fault_after_deferral,
                                       (overrun || underrun) && deferral_recent_q);
      end
 
      // A recent deferral makes the fault upstream's, not the buffer's.
      if (deferred_delete || deferred_insert) deferral_recent_q <= 1'b1;
      else if (overrun || underrun)           deferral_recent_q <= 1'b0;
 
      if (!first_fault_valid && (overrun || underrun)) begin
        first_fault_valid <= 1'b1;
        first_was_overrun <= overrun;
      end
    end
  end
 
  assign preceded_by_deferral = deferral_recent_q;
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that overrun destroys data and underrun does not. When an elastic buffer empties, the read side still has to emit something, and what it emits is idle — which is harmless in a gap and merely a stretched gap. When it fills, an arriving octet has nowhere to go and is lost. Treating them as one fault class loses that distinction entirely.

Deliberately simplified: a single recency bit for deferral rather than a window. Production designs correlate on a timestamp.

Production implication: c_overrun_in_frame against c_overrun_in_gap is the split that matters most. An overrun inside a frame destroys that frame; one in a gap costs nothing at all, because the octet lost was idle. A design reporting only a total overrun count will alarm on harmless events and hide damaging ones behind the same number.

And preceded_by_deferral is the attribution. A fault preceded by a deferred compensation is not a buffer sizing problem — the buffer was the right size and was denied its discharge opportunity. The cause is upstream, in whatever closed the gap, and Section 4 showed the delay between that cause and this symptom can be minutes. Without this bit the investigation starts at the buffer and stays there.

10. Two Sizing Arguments, Added Not Confused

Section 3 gave a clock-difference depth of a few octets. Chapter 4.3 §10 gave a burst depth of kilobytes. They are different requirements with different causes, and they add.

Clock differenceBurst
causetwo independent oscillatorstraffic pattern — back-to-back minimum frames
magnitudea few octetskilobytes
grows withtime, without dischargethe number of frames in a burst
bounded bythe discharge opportunitythe client's drain rate
failure timescalemilliseconds to minuteswithin one burst
fixed byrestoring the gapmore depth, or faster drain
early warningc_deferred, net driftfifo_high_water trend

Why they add rather than one dominating. The two accumulations are independent and can be adverse simultaneously: a burst can arrive while the buffer is already displaced from centre by accumulated clock slip. Taking the larger of the two leaves no room for the other, and the combination is exactly when a marginal design fails.

11. Assertions

Some properties below rest on published figures — the ±100 ppm clock tolerance and the 12-octet minimum gap are specified by IEEE 802.3. The state machines, counters and trigger thresholds are implementation choices, and each property says which it is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ─── Safety: compensation happens only in a gap ────────────────────────────
// The single most important property here. Deleting an octet mid-frame is
// not compensation -- it is corruption, and the frame's check value then
// fails for a reason unrelated to anything the link did.
property p_compensate_only_in_gap;
  @(posedge clk) disable iff (!rst_n)
  (do_delete || do_insert) |-> !in_frame;
endproperty
 
// ─── Safety: the gap reserve survives ──────────────────────────────────────
// Rests on the published requirement that removal must leave enough for
// frame delineation. Catches a greedy controller that shortens a gap below
// what the far end needs.
property p_reserve_survives_deletion;
  @(posedge clk) disable iff (!rst_n)
  do_delete |-> (gap_len_q > GAP_RESERVE_LOCAL);
endproperty
 
// ─── Causation: compensation direction follows the offset ──────────────────
// Catches the classic inversion -- deleting when the buffer is below centre
// -- which is correct-looking logic that accelerates the drift it is
// supposed to remove.
property p_delete_only_when_above_centre;
  @(posedge clk) disable iff (!rst_n)
  request_delete |-> (offset_from_centre > 0);
endproperty
 
property p_insert_only_when_below_centre;
  @(posedge clk) disable iff (!rst_n)
  request_insert |-> (offset_from_centre < 0);
endproperty
 
// ─── Safety: the buffer starts at centre, not empty ────────────────────────
// Catches an elastic buffer initialised like an ordinary FIFO, which
// underruns on its first read whenever the local clock is the faster one --
// so it works on half the links it is deployed on.
property p_starts_at_centre;
  @(posedge clk) disable iff (!rst_n)
  $rose(rst_n) |-> (offset_from_centre == 0);
endproperty
 
// ─── Conservation: depth is the sum, not the maximum ───────────────────────
// An elaboration-time check. Catches a design that takes the larger of the
// two requirements, leaving no room for the other when both are adverse.
property p_depth_is_sum;
  @(posedge clk) disable iff (!rst_n)
  (depth_total == depth_clock + depth_burst);
endproperty
 
// ─── Safety: a deferral is recorded, never dropped ─────────────────────────
// The early warning. Catches a controller that silently gives up on a
// compensation it could not perform, removing the only signal that precedes
// an overrun by a useful margin.
property p_deferral_is_counted;
  @(posedge clk) disable iff (!rst_n)
  deferred |=> (c_deferred == $past(c_deferred) + 1);
endproperty
 
// ─── Mutual exclusion: overrun and underrun cannot coincide ────────────────
// Catches a fill calculation that can report both, which means the count is
// wrong rather than that the buffer is in trouble.
property p_not_both_faults;
  @(posedge clk) disable iff (!rst_n)
  !(overrun && underrun);
endproperty
 
// ─── Causation: an in-frame overrun is classified as such ──────────────────
// Catches a total-only overrun counter, which alarms on harmless gap
// overruns and hides frame-destroying ones behind the same number.
property p_overrun_classified;
  @(posedge clk) disable iff (!rst_n)
  overrun |-> (c_overrun_in_frame != $past(c_overrun_in_frame))
           || (c_overrun_in_gap   != $past(c_overrun_in_gap));
endproperty
 
// ─── Stability: the historical extremes survive a clear ────────────────────
// Catches worst_high and worst_low folded into the clear branch, destroying
// the record of how far this link has ever drifted.
property p_extremes_survive_clear;
  @(posedge clk) disable iff (!rst_n)
  clear |=> ((worst_high >= $past(worst_high)) && (worst_low <= $past(worst_low)));
endproperty
 
// ─── Bounded response: a window always closes ──────────────────────────────
// Catches a window counter that can be starved, so the drift trend never
// produces a data point and the early warning never arrives.
property p_drift_window_closes;
  @(posedge clk) disable iff (!rst_n)
  $rose(rst_n) |-> ##[1:WINDOW+1] window_valid;
endproperty
 
// ─── Causation: a fault after a deferral is attributed upstream ────────────
// Catches the attribution being lost, after which every buffer fault looks
// like a sizing problem and the investigation never reaches the gap.
property p_deferral_attribution_held;
  @(posedge clk) disable iff (!rst_n)
  (deferred_delete || deferred_insert) |=> preceded_by_deferral;
endproperty

12. Verification

Conceptual — accumulate during a frame, discharge in the gap

10 cycles
A conceptual timing diagram over ten periods. A frame is in progress for the first six, during which the buffer offset from centre grows steadily. A gap follows, during which a delete is performed and the offset returns toward centre. The cycle then repeats.offset grows during the frameoffset grows during theframegap begins — discharge is possiblegap begins — discharge ispossibledelete an idle; offset returnsdelete an idle; offsetreturnsand it begins againand it begins againperiodin_frame1111110011offsetdo_deletet0t1t2t3t4t5t6t7t8t9
Figure 3 — conceptual: the offset walks away from centre and a gap returns it.

This figure is conceptual and labelled so. Real slip is a third of an octet per maximum frame, not two — the figure exaggerates the offset so the shape is visible. The shape is what matters: accumulate, discharge, repeat.

Scenarios

  1. Matched clocks. Drive both domains at exactly the same rate and verify the offset stays at centre, no compensation is requested, and no fault occurs.
  2. Local clock slower by 200 ppm. Verify the offset grows, request_delete asserts once it exceeds the trigger, and a deletion in a gap returns it toward centre.
  3. Local clock faster by 200 ppm. The mirror. Verify request_insert and that the offset rises back toward centre.
  4. The direction check. In both scenarios above, verify compensation is never applied in the wrong direction — Section 11's inversion property is the guard, and the inversion is correct-looking logic that accelerates the drift.
  5. Reset state. Verify offset_from_centre is zero after reset, not that the buffer is empty. An elastic buffer initialised empty underruns on its first read whenever the local clock is faster.
  6. Compensation requested mid-frame. Drive the offset past the trigger while in_frame is high and verify nothing happens until a gap arrives. This is the corruption guard.
  7. A gap exactly at the reserve. Verify no deletion occurs and c_gap_below_reserve advances.
  8. A gap one octet above the reserve. Verify exactly one deletion is permitted, and the gap is not shortened further.
  9. A long run with no gaps at all. Close the gap entirely and verify c_deferred advances, longest_deferral grows, and the overrun — when it arrives — has preceded_by_deferral set.
  10. Depth derivation. Elaborate with 1518 and 9000 octet maxima and verify depth_clock changes accordingly. Then elaborate at 50 ppm and verify it changes again.
  11. Depth is a sum. Verify depth_total equals the sum of the two terms and not the maximum.
  12. Discharge headroom. Elaborate with a shortened gap and verify frames_per_discharge falls and discharge_insufficient asserts at the boundary.
  13. Drift window, compensated. Run long with compensation enabled and verify window_net_drift returns to near zero each window.
  14. Drift window, uncompensated. Disable compensation and verify drift_uncompensated asserts well before any overrun. Record the margin — that margin is the early warning the design provides.
  15. Overrun inside a frame against inside a gap. Two runs. Verify the classifier separates them, and confirm the in-gap overrun destroyed nothing.
  16. Underrun. Verify no data is lost — the read side emits idle — and that it is counted separately from overrun.
  17. First-fault ordering. Force an underrun, then many overruns. Verify first_was_overrun still reports the underrun.
  18. Clear during operation. Verify counters zero and worst_high and worst_low do not.

What the checker must own

  • Two genuinely independent clock generators with configurable ppm offset. A testbench driving both domains from one clock cannot produce this chapter's subject at all, and it is the most common way an elastic buffer reaches silicon unverified.
  • A gap-length shaper, not just a gap-length randomiser. Scenario 19 needs uniformly marginal gaps, which randomisation destroys.
  • A drift reference that independently integrates the ppm offset over time and predicts the expected offset, so the design's offset_from_centre can be checked rather than merely observed.
  • Coverage crosses of offset band against gap availability against frame state. The bin (offset above trigger, in frame, no compensation applied) must be well populated — it is the correct behaviour of Scenario 6 — and (compensation applied, in frame) must be unreachable.

13. Debugging — Deferral Before Depth

The symptom: a PHY reporting buffer overruns on a link that worked yesterday.

Step 1 — read c_deferred and preceded_by_deferral before anything else. This is the whole method in one instruction, and it inverts the instinct to look at buffer depth:

ReadingWhat it meansWho owns it
c_deferred at zero, overruns occurringgenuine depth shortage — a burst problemChapter 4.3's sizing
c_deferred rising, c_gap_below_reserve risingthe gap is too shortthe MAC, or whatever shapes traffic
c_deferred rising, gaps finedischarge capacity exceeded — check ppmthe oscillator specification
drift_uncompensated set, no overruns yetthe warning stage — act nowas above, but with time to spare

Row two is the common one and it is the misattribution Chapter 4.1 §14 exists to prevent: the PHY reports the fault and the MAC caused it, hours or days apart.

Step 2 — read the deletion-to-insertion ratio. Deletions dominating means the incoming rate exceeds the local one; insertions dominating means the reverse. A ratio that has changed means an oscillator drifted or a component was swapped, and that is a finding no other metric produces.

Step 3 — separate overruns in frames from overruns in gaps. An in-gap overrun destroyed idle and cost nothing. Only c_overrun_in_frame corresponds to lost data, and a design reporting a total will have you chasing harmless events.

Step 4 — if depth genuinely looks short, check which requirement. Section 10's table is the guide. A burst failure is fixed with depth; a clock failure is not — more buffer only moves the failure later, converting an immediate fault into a delayed one that is harder to diagnose. fifo_high_water trending is the burst; net drift failing to return to zero is the clock.

Step 5 — only now measure the actual clock offset. By this point you know whether the problem is discharge opportunity or oscillator accuracy, and measuring is slow. Steps 1 to 4 are register reads.

The method stated once: deferral before depth. A buffer that could not discharge was denied an opportunity, and no amount of depth fixes that — while a buffer that never deferred and still overran is genuinely too small for its traffic.

14. Common Misconceptions

"The elastic buffer just needs to be big enough."

The wrong model: clock difference is an accumulation, so make the buffer large enough to hold it.

What it costs: you size for a duration and it fails anyway, just later. At 200 ppm and a gigabit rate the accumulation is 25,000 octets per second — a 64-octet buffer lasts under three milliseconds and a 64-kilobyte one lasts under three seconds. Making it a thousand times bigger buys three seconds, and converts an immediate failure into a delayed one that is harder to diagnose.

The corrected model: the accumulation is linear in time and buffer depth is a constant, so no depth solves it. The mechanism is discharge, not capacity — and the buffer only needs to hold the slip accumulated between discharge opportunities, which is a few octets.

"200 ppm is tiny, so this is a small problem."

The wrong model: a small rate difference is a small effect.

What it costs: you do not build the discharge mechanism, or you build it and do not protect its opportunity, because the numbers look negligible.

The corrected model: the rate difference is small; the accumulation is unbounded. One extra octet every 5,000 sounds harmless, and it means 25,000 octets per second on a gigabit link. Small differences accumulating forever reach any depth eventually — they just take longer.

"The interframe gap is a courtesy."

The wrong model: the gap is spacing, and shortening it slightly is a harmless optimisation that buys throughput.

What it costs: the most expensive fault in this chapter. Closing the gap removes the discharge opportunity, Section 2's unbounded accumulation resumes, and the buffer overruns hours or days later — reported by the PHY, caused by the MAC, with the delay defeating bisection.

The corrected model: the gap is where the clock difference is repaid. It is the only place a symbol can be removed without changing anything the far end cares about, because it carries idle and idle carries no information. It is a mechanism, not spacing.

"Overrun and underrun are the same fault mirrored."

The wrong model: the buffer went out of range in one direction or the other.

What it costs: one counter for both, so a link losing data and a link merely mistimed read identically. You alarm on harmless events and hide damaging ones.

The corrected model: overrun destroys data; underrun does not. When the buffer empties, the read side still emits something and what it emits is idle — harmless in a gap. When it fills, an arriving octet has nowhere to go. And an overrun inside a frame destroys that frame while one in a gap costs nothing, which is a second split the same argument requires.

"The fill level tells you the buffer's health."

The wrong model: read the occupancy, compare it against the depth, conclude.

What it costs: you miss every gradual failure. A buffer at 60 percent may have sat there for a year or may have been at 50 an hour ago and be heading for the wall.

The corrected model: fill is a reading; drift is the health metric. Net drift across a window should return to near zero on a healthy link, because compensation removes what accumulated. A persistent net drift means compensation is not keeping up, and it appears long before any overrun. This is the same argument Chapter 3.3 made for margin, Chapter 3.7 for the pre-correction rate and Chapter 3.8 for skew margin.

15. Interview Reasoning

"Two clocks differ by 100 ppm each. How big does the buffer need to be?"

The trap is to compute a duration and give a size. The answer that ends the topic reframes it: no fixed depth works, because the accumulation is linear in time and depth is a constant — at a gigabit rate, 200 ppm is 25,000 octets per second, so a 64-kilobyte buffer buys under three seconds. The buffer only needs to hold what accumulates between discharge opportunities, which for a maximum-length frame is about a third of an octet. The mechanism is the gap, not the depth.

"Why is the interframe gap not negotiable?"

Because it is where the clock difference is repaid. Idle carries no information, so removing or adding an idle symbol changes nothing the far end cares about — and that is exactly what a discharge needs. Close the gap and the accumulation resumes without bound. The strong follow-up is the timescale: the overrun arrives hours later, is reported by the PHY, and was caused by the MAC — which is why a deferral counter that fires at the moment of the cause is worth more than any buffer.

"A PHY reports buffer overruns. Where do you start?"

Not at the buffer. Read the deferral counter first: a buffer that could not discharge was denied an opportunity, and no depth fixes that. A buffer that never deferred and still overran is genuinely too small for its traffic — which is a different requirement, from Chapter 4.3's burst rather than from clock difference, with a different fix. Naming the two requirements and that they add rather than one dominating is what distinguishes a complete answer.

16. Understanding Check

Because a bounded rate difference produces an unbounded accumulation.

The rate difference is bounded — IEEE 802.3 specifies ±100 ppm at each end, so two ends differ by at most 200 ppm. That is one extra octet every 5,000, which sounds harmless.

But the accumulation is a running total with a non-zero mean, and that is unbounded. At a gigabit rate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
octet rate     = 125,000,000 octets/s
slip rate      = 125e6 × 200e-6 = 25,000 octets/s
64-octet buffer  overruns in about 2.6 ms
64-kbyte buffer  overruns in about 2.6 s

A thousand times more buffer buys under three seconds. No practical depth solves an accumulation that is linear in time.

So the mechanism cannot be capacity; it has to be discharge. Something must periodically remove the accumulated slip, and the buffer then only holds what builds up between discharges.

The follow-up to be ready for: what makes discharge possible at all? Idle. The gap between frames carries idle, idle carries no information, and removing or adding an idle symbol changes nothing the far end cares about. That is the only place in the stream where a symbol can be removed harmlessly.

17. What's Next

The claim this chapter defended: a bounded rate difference produces an unbounded accumulation unless something discharges it regularly, and the interframe gap is that opportunity.

The arithmetic is small and the conclusion is not. Two ends differ by at most 200 ppm, which over a maximum-length frame is about a third of an octet — so the clock-compensation depth requirement is a handful of octets, and real buffers are kilobytes for a completely different reason. What matters is not the depth but the discharge: no practical capacity absorbs an accumulation that is linear in time, and one deleted idle repays roughly three maximum-length frames.

Take the gap away and the bound disappears. The buffer fills at 25,000 octets per second on a gigabit link, the overrun arrives hours later, and the PHY reports a fault the MAC caused — which is why the deferral counter, firing at the moment of the cause, is worth more than any amount of buffer.

One thing was deliberately deferred throughout. Section 6's buffer maintains its fill count in a single domain and says so, and Section 11's rejected property is entirely about why reasoning from that simplification is dangerous. A real dual-clock FIFO cannot do it — there is no instant at which both pointers are simultaneously valid, and everything that follows from pretending otherwise is wrong.

Chapter 4.5 — PHY Management comes next and takes a different subject: the second interface at this boundary. Chapter 4.1 §5 insisted that data-path status and diagnostic detail belong on separate paths with different contracts, and named the management path without describing it. 4.5 owns it — the MDIO frame, its address model, and why a slow serial interface with a completely different failure model sits alongside a fast parallel one.

Then Chapter 4.6 assembles the module and finally owns the clock-domain crossing this chapter kept pointing at.

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.