Skip to content
VLSI Mentor

Ethernet · Module 16

Hardware Timestamping at the MAC/PHY Boundary

A Sync must carry the time of its own transmission. One-step rewrites the field in flight and patches the CRC with a linear map; two-step sends a second message.

Chapter 16.2 §2 named the problem and Section 5 of that chapter worked around it. This chapter solves it.

A Sync message must carry t1 — the moment it left the wire. That moment is not known until the message is already leaving, by which time its destination address, its source address, its EtherType and thirty-four octets of PTP header are physically gone.

There are exactly two answers.

Two-step sends the value afterwards, in a Follow_Up. Simple, costs a message, and Chapter 16.2 §5 built the slave side of it.

One-step rewrites the field as the frame streams past — and then must fix the FCS, because Chapter 6.3's residue check will fail on every frame whose content changed after its CRC was computed.

And fixing the FCS is where the chapter's engineering lives. The naive answer is to buffer the frame, patch it, and recompute — which adds a full store-and-forward delay: 512 ns at 1 Gb/s, on a mechanism whose whole purpose is nanosecond accuracy. The right answer costs about 1248 XOR gates and no latency at all, and it works because Chapter 6.2's CRC is linear over GF(2).

1. Scope — What This Chapter Owns

This chapter owns the timestamp unit: where it sits, what it captures, the two-step report path, the one-step in-flight rewrite, the FCS patch that makes one-step possible, the transmit-timestamp FIFO, and the pairing of a timestamp to the frame it belongs to.

It does not own why the boundary is the right place. Chapter 16.1 §13 derived it: capture at the earliest point whose remaining delay to the wire is characterisable, which is the MAC/PHY boundary at Chapter 5.2's Start Frame Delimiter. This chapter takes that as settled.

It does not own the arithmetic. Chapter 16.2 §6 derived the offset and path-delay equations from t1 through t4. This chapter produces t1 and t3 and consumes neither.

It does not own the correction field's content. The unit built in Section 10 can add to correctionField in flight; what value a transparent clock puts there, and why, is Chapter 16.4 §12. Section 10 builds the mechanism and states the requirement.

And it does not own the residual error. What a characterised PHY delay leaves behind, and what Chapter 4.4's elastic buffer contributes, is Chapter 16.5. Section 4 here names the terms and does not price them.

2. The Problem Chapter 16.1 Left Open

State it precisely, because the imprecise version has an easy and wrong solution.

The imprecise version: "a Sync must contain the time it was sent." The easy wrong answer is to read the clock, write it into the frame, and transmit — which records the time the software decided to send, not the time the frame left. Chapter 16.1 §9 priced that path at 9 µs typical and 10.5 ms of jitter.

The precise version: a Sync must contain the value of a counter sampled at the instant its own Start Frame Delimiter crosses the MAC/PHY boundary.

And that instant is after the frame's first fifteen octets are already transmitted.

OctetWhat it isTransmitted at 1 Gb/s
−8 … −1preamble0 – 64 ns
−1the SFD — the capture instant56 – 64 ns
0 … 5destination address64 – 112 ns
6 … 11source address112 – 160 ns
12 … 13EtherType 0x88F7160 – 176 ns
14 … 47the PTP common header176 – 448 ns
48 … 57originTimestamp — where t1 must go448 – 528 ns
58 … 59pad528 – 544 ns
60 … 63the FCS544 – 576 ns

The capture happens at 64 ns and the field it must fill begins at 448 ns. So there are 384 ns of slack at 1 Gb/s — plenty — and the field ends 16 ns before the FCS begins.

That 16 ns is the constraint, and it shrinks with line rate:

Line rateCapture to fieldField end to FCS start
1 Gb/s384 ns16.0 ns
10 Gb/s38.4 ns1.60 ns
25 Gb/s15.4 ns0.64 ns
100 Gb/s3.84 ns0.16 ns

==

A Sync frame's Start Frame Delimiter crosses the MAC-PHY boundary at the end of the preamble, which is the capture instant. The destination address, source address, EtherType and thirty-four octets of PTP common header follow, so the originTimestamp field at octets 48 through 57 begins 384 nanoseconds later at 1 Gb/s — ample slack for the captured value to be substituted. But the field ends only two octets before the frame check sequence begins, which is 16 nanoseconds at 1 Gb/s, 1.6 at 10, 0.64 at 25 and 0.16 at 100 gigabit. Read serially that appears to make one-step impossible at high rate; read in the clock domain the logic runs in, a 512-bit datapath at 195 megahertz puts the field, the pad and the FCS all in the same 5.12 nanosecond cycle.SFD — the captureoctet -1, at 64 nsDA, SA, ET, header48 octetsoriginTimestampoctets 48-57The FCSoctets 60-63384 ns of slackcapture to field, 1 Gb/s16 ns at 1 Gb/s0.16 ns at 100One 5.12 ns cycle512-bit datapath12
Figure 1 — where the capture instant falls relative to the field it must fill, and how little room remains before the FCS.

At 100 Gb/s there are 160 picoseconds between the last octet of the timestamp and the first octet of the FCS, which is less than one gate delay in any process this design would be built in. Read serially, one-step is impossible at high rate.

It is not impossible, and Section 8's callout explains why: the datapath is not serial. At 100 Gb/s a MAC moves 64 octets per cycle at 195 MHz, so the entire 64-octet Sync frame — timestamp field, pad and FCS — arrives in one cycle, and the question is not "how much time between two octets" but "can the patch be computed combinationally in 5.12 ns". Which it can.

3. RTL 1 — The Timestamp Unit at the Boundary

The capture itself, on both directions, with the classification that decides whether to capture at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tsu_pkg -- shared types for the timestamp unit.
// -----------------------------------------------------------------------
package tsu_pkg;

  localparam int SEC_W = 48;
  localparam int NS_W  = 32;

  typedef struct packed {
    logic [SEC_W-1:0] sec;
    logic [NS_W-1:0]  ns;
  } ts_t;

  // The PTP originTimestamp wire format: 6 octets of seconds then 4
  // of nanoseconds. 80 bits, and the width of every patch in this
  // chapter.
  localparam int OTS_BITS = 80;
  typedef logic [OTS_BITS-1:0] ots_t;

  function automatic ots_t to_wire(input ts_t t);
    to_wire = {t.sec, t.ns};
  endfunction

  // Where the interesting fields sit, as offsets from the first octet
  // of the PTP header. 16.2 section 3's layout.
  localparam int OFF_CORRECTION = 8;    // 8 octets
  localparam int OFF_ORIGIN_TS  = 34;   // 10 octets

  // A transmit timestamp is meaningless without knowing which frame it
  // belongs to. 16.1 section 12's simplification, fixed here.
  typedef struct packed {
    logic        valid;
    logic [15:0] frame_tag;    // assigned by the transmit scheduler
    ts_t         ts;
  } tx_ts_entry_t;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tsu_capture_point -- captures the free-running clock at the SFD, on
// receive and on transmit, for EVENT messages only.
//
// The classification is one bit -- 16.2 section 4: message types 0x0
// to 0x3 are event messages and the top bit of messageType is clear.
// Everything else needs no timestamp and must not consume a FIFO slot.
// -----------------------------------------------------------------------
module tsu_capture_point
  import tsu_pkg::*;
(
  input  logic       clk,               // the MAC clock for this direction
  input  logic       rst_n,

  input  logic       sfd,               // 4.2's reconciliation sublayer
  input  logic [3:0] sfd_lane,          // which lane of the word -- 16.1 section 12
  input  ts_t        now,

  // Late classification: the message type is not known at the SFD.
  // The timestamp is captured UNCONDITIONALLY and discarded if the
  // frame turns out not to need it.
  input  logic       class_valid,
  input  logic       class_is_event,
  input  logic [15:0] class_frame_tag,

  // Fixed, characterised corrections for everything below this point.
  input  logic [15:0] cfg_phy_delay_ns,
  input  logic [15:0] cfg_lane_ps,

  output logic       ts_valid,
  output ts_t        ts_out,
  output logic [15:0] ts_frame_tag,
  output logic [31:0] c_captured,
  output logic [31:0] c_discarded,
  output logic       overrun
);

  ts_t  held;
  logic held_valid;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      held <= '0; held_valid <= 1'b0;
      ts_valid <= 1'b0; ts_out <= '0; ts_frame_tag <= '0;
      c_captured <= '0; c_discarded <= '0; overrun <= 1'b0;
    end else begin
      ts_valid <= 1'b0;

      // The capture is unconditional and one cycle. 16.1 section 19's
      // p_capture_latency_is_fixed: a VARIABLE capture latency
      // reintroduces exactly the jitter this whole module removes.
      if (sfd) begin
        if (held_valid) overrun <= 1'b1;
        held <= '{ sec: now.sec,
                   ns:  now.ns
                        - NS_W'(cfg_phy_delay_ns)
                        - NS_W'((32'(cfg_lane_ps) * 32'(sfd_lane)) / 1000) };
        held_valid <= 1'b1;
      end

      // Classification arrives later, once the header has been parsed.
      // An event message keeps its timestamp; anything else frees it.
      if (class_valid && held_valid) begin
        held_valid <= 1'b0;
        if (class_is_event) begin
          ts_valid     <= 1'b1;
          ts_out       <= held;
          ts_frame_tag <= class_frame_tag;
          c_captured   <= c_captured + 1;
        end else begin
          c_discarded <= c_discarded + 1;
        end
      end
    end
  end

endmodule

Classification: a capture register with deferred classification. One flop's worth of decision and the chapter's foundation.

What it teaches: that the capture must precede the classification, and the ordering is forced. The SFD arrives at octet −1; the messageType field arrives at octet 14 — fifteen octets and 120 ns later at 1 Gb/s. A design that waits to know whether a frame needs a timestamp has already lost the instant it would have recorded. So every frame's SFD is captured and most captures are thrown away, and c_discarded will be far larger than c_captured on any link carrying ordinary traffic.

And it teaches that the lane term is in picoseconds rather than nanoseconds for a reason. Chapter 10.6's XGMII presents four or eight octets per clock, so one lane position is 0.8 ns at 10 Gb/s and 0.08 ns at 100 — and a correction expressed in integer nanoseconds truncates the whole term to zero. Chapter 16.1 §12's simplification was exactly this, and the fix is one unit change.

Deliberately simplified: one held capture with no queue, so two SFDs before a classification lose one. Section 12 builds the FIFO this needs, and the reason it needs one is not back-to-back PTP frames — those are rare — but the classification latency: at 100 Gb/s with a 64-octet datapath, the SFD and the messageType are in the same cycle and the pipeline behind them is several cycles deep.

Production implication: overrun is the signal that a timestamp was silently overwritten, and its consequence is worse than a lost timestamp. With frame tags, an overrun loses one measurement and is detected; without them, the next classification claims a timestamp belonging to a different frame — and the resulting t1 is wrong by the interval between two frames, which on a busy link is nanoseconds and therefore invisible, and on a quiet one is milliseconds and therefore catastrophic. The tag is 16 bits and it converts a silent corruption into a counter.

4. Where Exactly, and What Is Below It

Section 3 captured at "the boundary". Two questions remain: which side of the boundary, and what the characterised correction actually covers.

The capture is on the MAC side and the correction accounts for everything on the PHY side.

Below the capture pointMagnitudeConstant?
the MII/XGMII interface itselfa few nsyes
the PCSChapter 3.4tens of nsyes, per mode
64B/66B encode or decodeChapter 3.5tens of nsyes
FEC, when enabledChapter 3.7hundreds of nsyes, and mode-dependent
the PMA serialisernsyes
Chapter 4.4's elastic buffer5–20 nsNO — it varies
the cable5 ns/myes

Six of the seven rows are constant and removable — Chapter 16.1 §9's callout: a constant delay of any size is an offset. The fourth row is the largest and it is still removable: FEC adds hundreds of nanoseconds and adds the same hundreds every frame, so a correctly characterised design subtracts it and loses nothing.

The sixth row is the one that is not, and it is the floor.

And cfg_phy_delay_ns must therefore be a table rather than a constant, because four of the rows depend on the operating mode:

ModeWhat changes
1000BASE-T against 1000BASE-Xdifferent PCS, different delay
10GBASE-R with and without FEChundreds of ns of difference
25G with RS-FECthe largest correction in the table
a link that renegotiated — Chapter 11.2the mode changed under the correction

The last row is the operational trap. A link that auto-negotiates down from 10 Gb/s to 1 Gb/s has changed its PHY delay by tens of nanoseconds, and a design whose correction was set at commissioning is now wrong by that amount — silently, monotonically, with every check passing. Chapter 16.1 §17's cfg_phy_delay_zero catches an unset correction; nothing catches a stale one, which is why the correction must be indexed by the live link mode rather than configured once.

And the values come from the PHY vendor rather than from measurement, for the reason Chapter 16.1 §19 established: measuring them needs an instrument better than the device. A datasheet figure with a stated uncertainty is the honest input; a number obtained by tuning until the offset looked right has absorbed the path's asymmetry into the PHY correction, and the two are then inseparable.

5. RTL 2 — Two-Step: Capture and Report

The simpler answer. The frame goes out untouched and a second message carries the value.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// twostep_reporter -- takes a transmit timestamp from the FIFO and
// builds the Follow_Up that carries it.
//
// The Sync's own originTimestamp field is transmitted as ZERO and the
// twoStepFlag is set, so a conformant slave knows to wait. 16.2
// section 5's parked t2 is the other end of this.
// -----------------------------------------------------------------------
module twostep_reporter
  import tsu_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  // A Sync has been transmitted and its timestamp has come back.
  input  logic        tx_ts_valid,
  input  logic [15:0] tx_ts_tag,
  input  ts_t         tx_ts,

  // The scheduler's record of which tag was which Sync.
  input  logic [15:0] pending_tag,
  input  logic [15:0] pending_seq,
  input  logic        pending_valid,

  input  logic        tx_ready,

  output logic        fu_request,
  output logic [15:0] fu_seq,
  output ots_t        fu_precise_origin,
  output logic [31:0] c_followups,
  output logic [31:0] c_orphan_ts,     // a timestamp with no pending Sync
  output logic [31:0] latency_cycles   // capture to Follow_Up transmitted
);

  logic [31:0] age;
  logic        armed;
  logic [15:0] armed_seq;
  ots_t        armed_ts;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      armed <= 1'b0; age <= '0;
      fu_request <= 1'b0; fu_seq <= '0; fu_precise_origin <= '0;
      c_followups <= '0; c_orphan_ts <= '0; latency_cycles <= '0;
      armed_seq <= '0; armed_ts <= '0;
    end else begin
      fu_request <= 1'b0;

      if (tx_ts_valid) begin
        if (pending_valid && (pending_tag == tx_ts_tag)) begin
          armed     <= 1'b1;
          armed_seq <= pending_seq;
          armed_ts  <= to_wire(tx_ts);
          age       <= '0;
        end else begin
          // A transmit timestamp whose frame nobody is waiting for.
          // Section 13: without tags this case is undetectable and
          // the timestamp is attributed to the wrong frame.
          c_orphan_ts <= c_orphan_ts + 1;
        end
      end

      if (armed) begin
        age <= age + 1;
        if (tx_ready) begin
          fu_request        <= 1'b1;
          fu_seq            <= armed_seq;
          fu_precise_origin <= armed_ts;
          armed             <= 1'b0;
          c_followups       <= c_followups + 1;
          latency_cycles    <= age;
        end
      end
    end
  end

endmodule

Classification: a one-deep report queue. No datapath, no timing pressure, and that is its entire advantage.

What it teaches: that two-step's latency does not matter and this is worth saying explicitly. latency_cycles can be thousands — a Follow_Up delayed by a millisecond loses nothing, because Chapter 16.2 §4 established that a general message carries a number and numbers do not decay. So the module has no timing requirement at all, which is why two-step is the answer for any device whose transmit path is already tight.

And it teaches that c_orphan_ts is the counter that catches the missing frame tag. A transmit timestamp arriving with no matching pending Sync means the scheduler's record and the timestamp unit's record have diverged — and without tags there is nothing to compare, so the timestamp is silently attributed to whatever Sync is pending. Section 13 is that problem in full.

Deliberately simplified: one armed slot, so a second transmit timestamp arriving before the first's Follow_Up is sent is lost. At 128 Sync/s and a transmit path that is ready within microseconds this never happens; on a device whose transmit queue is congested — Chapter 14.1 — it does, and the remedy is the same small FIFO Section 12 builds for the other direction.

Production implication: the Follow_Up must carry the same sequence ID as the Sync it describes, and pending_seq is where that comes from. A design that generates a fresh sequence ID for the Follow_Up produces a message the slave cannot matchChapter 16.2 §5's c_orphan_fu at the far end — and the slave's c_stale_sync climbs while its c_followup also climbs, which is a confusing pair of symptoms for a one-line bug.

6. What Two-Step Costs in Messages

The cost is exactly one extra frame per Sync, and its size is where the accounting starts.

Sync aloneSync + Follow_Up
frames12
octets on the wire84168
at 1 Sync/s672 bit/s1344 bit/s
at 16 Sync/s10 752 bit/s21 504 bit/s
at 128 Sync/s86 016 bit/s172 032 bit/s
transmit timestamps needed1 per Sync1 per Sync — unchanged
transmit frames the master must schedule12

Row six is the one people get wrong. Two-step does not need more timestamps — the Sync's transmit timestamp is captured either way. What it needs is a second frame to carry it, which is a scheduling and bandwidth cost rather than a timestamping one.

And the delta at the deployment scale that matters:

DeploymentOne-stepTwo-stepExtra
1 slave, 16 Sync/s33 382 bit/s44 134 bit/s10 752 bit/s
1 slave, 128 Sync/s266 342 bit/s352 358 bit/s86 016 bit/s
1024 slaves, 128 Sync/s131 072 msg/s131 200 msg/s128 msg/s

The last row is the surprise and it is a consequence of multicast. Sync and Follow_Up are both sent to the PTP multicast group, so one Follow_Up serves every slave — the extra cost of two-step at a thousand slaves is 128 messages per second, not 131 072. Chapter 16.2 §18's master load is dominated by unicast Delay_Resps, and two-step barely touches it.

Which reverses the intuition about where one-step pays. It does not pay at scale on the master's message count. It pays on a link whose bandwidth is genuinely tight — a 10 Mb/s industrial segment where 21 504 bit/s is 0.2% of the link — and on a slave-side transmit path where every frame scheduled is a frame not carrying data.

And it pays in one more place that the table cannot show: a two-step exchange has a window in which the slave holds t2 and does not yet have t1Chapter 16.2 §5's parked slot, 48 octets of state and a c_stale_sync counter. One-step publishes on the Sync itself and has no pending state at all.

7. RTL 3 — One-Step: Rewriting a Field in Flight

The harder answer, and the module that makes the rest of the chapter necessary.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// onestep_field_rewriter -- substitutes the captured transmit timestamp
// into the outgoing frame's originTimestamp field as it streams past.
//
// The datapath is W octets wide. The field is 10 octets at a known
// offset, so it may span one or two beats. Both cases must be handled
// and the two-beat case is the one that is usually wrong.
// -----------------------------------------------------------------------
module onestep_field_rewriter
  import tsu_pkg::*;
#(
  parameter int W = 8                      // octets per beat
)(
  input  logic              clk,
  input  logic              rst_n,

  // The outgoing stream, before substitution.
  input  logic              in_valid,
  input  logic [W*8-1:0]    in_data,
  input  logic              in_sop,
  input  logic              in_eop,
  input  logic [$clog2(W+1)-1:0] in_bytes,   // valid octets on eop

  // Which frame this is and where its field sits.
  input  logic              frame_is_onestep_sync,
  input  logic [15:0]       field_offset,      // octets from frame start
  input  ots_t              new_ts,            // to_wire(captured t1)

  output logic              out_valid,
  output logic [W*8-1:0]    out_data,
  output logic              out_sop,
  output logic              out_eop,
  output logic [$clog2(W+1)-1:0] out_bytes,

  // The delta the FCS patch needs: old XOR new, positioned.
  output logic              delta_valid,
  output ots_t              delta_bits,
  output logic [15:0]       delta_offset,
  output logic [31:0]       c_rewritten,
  output logic [31:0]       c_split_across_beats
);

  logic [15:0] octet_ctr;
  ots_t        old_captured;
  logic [$clog2(OTS_BITS+1)-1:0] captured_bits;

  // For each octet lane in this beat, is it inside the field?
  logic [W-1:0]      lane_in_field;
  logic [W-1:0][7:0] lane_new;
  logic [W-1:0][7:0] lane_old;

  always_comb begin
    int L, pos;
    for (L = 0; L < W; L++) begin
      pos = int'(octet_ctr) + L;
      lane_in_field[L] = frame_is_onestep_sync &&
                         (pos >= int'(field_offset)) &&
                         (pos <  int'(field_offset) + 10);
      lane_old[L] = in_data[8*L +: 8];
      // Octet (pos - field_offset) of the 10-octet field, MSB first.
      lane_new[L] = lane_in_field[L]
                  ? new_ts[OTS_BITS - 8*(pos - int'(field_offset)) - 1 -: 8]
                  : in_data[8*L +: 8];
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int L;
    if (!rst_n) begin
      octet_ctr <= '0; out_valid <= 1'b0; out_data <= '0;
      out_sop <= 1'b0; out_eop <= 1'b0; out_bytes <= '0;
      delta_valid <= 1'b0; delta_bits <= '0; delta_offset <= '0;
      old_captured <= '0; captured_bits <= '0;
      c_rewritten <= '0; c_split_across_beats <= '0;
    end else begin
      out_valid   <= in_valid;
      out_sop     <= in_sop;
      out_eop     <= in_eop;
      out_bytes   <= in_bytes;
      delta_valid <= 1'b0;

      if (in_valid) begin
        // Substitute.
        for (L = 0; L < W; L++) out_data[8*L +: 8] <= lane_new[L];

        // Accumulate the OLD field content, which the FCS patch needs.
        for (L = 0; L < W; L++) begin
          if (lane_in_field[L]) begin
            automatic int k;
            k = int'(octet_ctr) + L - int'(field_offset);
            old_captured[OTS_BITS - 8*k - 1 -: 8] <= lane_old[L];
            captured_bits <= captured_bits + 8;
          end
        end

        octet_ctr <= in_sop ? 16'(W) : (octet_ctr + 16'(W));

        // Once the whole field has passed, the delta is complete and
        // the patch can be computed. On a wide datapath this is the
        // SAME beat the FCS is in -- section 8's callout.
        if (frame_is_onestep_sync &&
            ((int'(octet_ctr) + W) >= (int'(field_offset) + 10)) &&
            ((int'(octet_ctr))     <  (int'(field_offset) + 10))) begin
          delta_valid  <= 1'b1;
          delta_offset <= field_offset;
          c_rewritten  <= c_rewritten + 1;
          if (int'(octet_ctr) <= int'(field_offset))
            ; // the field fitted in one beat
          else
            c_split_across_beats <= c_split_across_beats + 1;
        end

        if (in_eop) begin
          octet_ctr     <= '0;
          captured_bits <= '0;
        end
      end
    end
  end

  // delta = old XOR new, which is zero outside the field by
  // construction. This is the only nonzero input the patch needs.
  always_comb delta_bits = old_captured ^ new_ts;

endmodule

Classification: a per-lane multiplexer with an accumulator for the displaced content. Combinational substitution, registered output.

What it teaches: that the old field content must be captured, not discarded, and that requirement is what makes the FCS patch cheap. The delta is old XOR new, and a CRC's linearity means the patch depends only on the delta and its position — Section 8. A design that overwrites the field without keeping what was there must recompute the CRC from the whole frame, which needs the frame buffered and costs 512 ns at 1 Gb/s.

And it teaches that a 10-octet field on a W-octet datapath spans one beat or two, and the two-beat case is where implementations break. With W = 8, the field at offset 48 occupies octets 48–57, which is beats 6 and 7 — so the substitution, the old-content capture and the patch's readiness all straddle a beat boundary. c_split_across_beats exists to make the case visible in simulation, because a design tested only with a field that happens to align is a design tested on half its logic.

Deliberately simplified: field_offset is an input, computed elsewhere from the frame's type and whether it carries a Chapter 13.2 tag. That computation is the same tag-dependent offset arithmetic Chapter 16.2 §3's parser needed, and getting it wrong here is worse: the rewriter substitutes ten octets of timestamp into the middle of the PTP header, producing a frame that is well-formed, correctly checksummed after the patch, and semantically destroyed.

Production implication: frame_is_onestep_sync must be false for every frame that is not a one-step Sync, and the classification arrives from a parser several cycles upstream. A design that gates on "is this a Sync" without checking the one-step flag will rewrite a two-step Sync's originTimestamp — which a conformant slave ignores, so the damage is invisible until the deployment changes to one-step and the field is now written twice.

8. RTL 4 — Fixing the FCS Mid-Frame

The patch. It is the chapter's centre and it is six lines of arithmetic resting on one property of Chapter 6.2's CRC.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// fcs_patcher -- corrects the frame check sequence after octets were
// substituted, WITHOUT recomputing it from the frame.
//
// CRC-32 is linear over GF(2):   CRC(A xor B) = CRC(A) xor CRC(B).
// So if the frame changed by `delta` at a known position, the new FCS
// is the old FCS xor CRC(delta placed at that position) -- and that is
// a FIXED linear map from 80 delta bits to 32 FCS bits.
//
// No frame buffer. No full CRC engine. One matrix multiply.
// -----------------------------------------------------------------------
module fcs_patcher
  import tsu_pkg::*;
#(
  // Distance in octets from the END of the substituted field to the
  // start of the FCS. For a 64-octet Sync with the field at 48..57
  // and the FCS at 60..63, this is 2.
  parameter int TRAILING_OCTETS = 2
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        delta_valid,
  input  ots_t        delta_bits,        // old XOR new, 80 bits

  input  logic        fcs_valid,
  input  logic [31:0] fcs_in,

  output logic        fcs_out_valid,
  output logic [31:0] fcs_out,
  output logic [31:0] c_patched,
  output logic        patch_without_delta   // an FCS arrived unpatched
);

  // The GF(2) matrix. Row j of PATCH_M is the set of delta bits whose
  // parity forms bit j of the CRC contribution. It depends only on the
  // polynomial and on TRAILING_OCTETS, so it is a compile-time
  // constant -- generated the same way 6.4's parallel CRC tables are.
  //
  // Shown symbolically: a real design emits the 32 x 80 bit pattern
  // from a script, exactly as chapter 6.4 emits its per-width tables.
  function automatic logic [31:0] crc32_of_positioned_delta(input ots_t d);
    logic [31:0] r;
    int i;
    begin
      // Feed the delta through the CRC shift register, then advance
      // TRAILING_OCTETS zero octets to position it. Written serially
      // for clarity; synthesis unrolls it into the matrix above.
      r = 32'h0000_0000;                    // a DELTA has no init value
      for (i = OTS_BITS-1; i >= 0; i--) begin
        if (r[31] ^ d[i]) r = {r[30:0], 1'b0} ^ 32'h04C1_1DB7;
        else              r = {r[30:0], 1'b0};
      end
      for (i = 0; i < TRAILING_OCTETS*8; i++) begin
        if (r[31]) r = {r[30:0], 1'b0} ^ 32'h04C1_1DB7;
        else       r = {r[30:0], 1'b0};
      end
      crc32_of_positioned_delta = r;
    end
  endfunction

  logic [31:0] held_patch;
  logic        have_patch;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      held_patch <= '0; have_patch <= 1'b0;
      fcs_out_valid <= 1'b0; fcs_out <= '0;
      c_patched <= '0; patch_without_delta <= 1'b0;
    end else begin
      fcs_out_valid       <= 1'b0;
      patch_without_delta <= 1'b0;

      if (delta_valid) begin
        // A zero delta means the field already held the right value --
        // possible, and the patch is then the identity.
        held_patch <= crc32_of_positioned_delta(delta_bits);
        have_patch <= 1'b1;
      end

      if (fcs_valid) begin
        fcs_out_valid <= 1'b1;
        if (have_patch) begin
          fcs_out    <= fcs_in ^ held_patch;
          have_patch <= 1'b0;
          c_patched  <= c_patched + 1;
        end else begin
          // No substitution happened: pass the FCS through unchanged.
          fcs_out <= fcs_in;
        end
      end

      // A one-step Sync whose FCS arrived with no delta computed is a
      // frame that will be discarded by every receiver -- 6.3's
      // residue check. It must be counted, loudly.
      if (fcs_valid && !have_patch && delta_expected)
        patch_without_delta <= 1'b1;
    end
  end

endmodule

Classification: a GF(2) matrix multiply and an XOR. Zero added latency, no frame buffer, and it replaces a full CRC engine.

What it teaches: that linearity is the whole trick and it is worth stating as an identity. CRC(A ⊕ B) = CRC(A) ⊕ CRC(B) — so a frame that changed from F to F ⊕ Δ has a CRC that changed from CRC(F) to CRC(F) ⊕ CRC(Δ). The patch is CRC(Δ), and Δ is zero everywhere except the ten octets that changed. So the map from 80 delta bits to 32 patch bits is a fixed 32 × 80 GF(2) matrix, computable at elaboration exactly as Chapter 6.4 computes its per-width tables.

And it teaches why r starts at zero rather than at 0xFFFFFFFF. Chapter 6.2's CRC-32 initialises its register to all ones and complements its output, and both of those are constants that cancel in a difference. The patch is a CRC of a delta, not of a frame — so the initial value and the final complement must be omitted, and including them produces a patch that is wrong by a fixed 32-bit value on every frame. The symptom is that every patched frame fails its receiver's residue check, which at least fails loudly.

Deliberately simplified: the CRC loop is written bit-serially for readability and is intended to be fully unrolled. The real implementation emits the 32 × 80 bit matrix from a generator script — the same tooling Chapter 6.4 uses — and the resulting logic is 32 XOR trees over an average of 40 terms each: roughly 1248 XOR2 gates and a depth of 6.

Production implication: TRAILING_OCTETS is a parameter and it must match the actual distance from the field's end to the FCS. For a 64-octet untagged Sync it is 2; for a tagged one the frame is 68 octets and it is still 2; for a Delay_Resp with the field elsewhere it differs — and a wrong value produces a patch that is a valid CRC of a differently-positioned delta, so the frame is consistently and wrongly checksummed. Every receiver discards it, patch_without_delta does not fire, and the symptom is a Sync stream that vanishes.

==

Two ways to repair the frame check sequence after octets are substituted. The naive approach buffers the whole frame, patches it and recomputes the CRC with a full engine, adding 512 nanoseconds of store-and-forward latency at 1 gigabit — which, applied to Sync messages and not to Delay Requests, creates 512 nanoseconds of path asymmetry and therefore 256 nanoseconds of offset error. The patch approach exploits the fact that CRC-32 is linear over GF(2), so the CRC of a frame changed by a delta is the old CRC exclusive-ORed with the CRC of the delta alone. The delta is old content exclusive-ORed with new and is zero outside the ten substituted octets, so the map from 80 delta bits to 32 patch bits is a fixed 32-by-80 matrix: about 1248 XOR gates at depth 6, with no frame buffer and no added latency at all.10 octetssubstitutedthe FCS is now staleBuffer andrecomputea full CRC engine+512 ns at 1 Gb/sstore-and-forward256 ns of offseterrorasymmetry it createdCRC is linearCRC(A xor B) = CRC(A) xorCRC(B)32 x 80 GF(2)matrix1248 XOR2, depth 6+0 nsno buffer12
Figure 2 — the FCS patch: linearity turns a recomputation into a fixed matrix multiply and an XOR.

9. The FCS Recomputation, Priced

Two ways to make the FCS right after a substitution, and they differ by a factor that decides whether one-step is viable at all.

A — buffer and recomputeB — patch the FCS
what it needsthe whole frame bufferedthe delta and its position
logica full CRC-32 engineChapter 6.4a 32 × 80 GF(2) matrix
gatesthousands≈1248 XOR2
storage64 octets — 512 flops80 flops for the old content
added latency at 1 Gb/s512 ns0
added latency at 100 Gb/s5.12 ns0
against Chapter 12.1 §12's 28 ns budget18× over at 1 Gb/sfits trivially

Row five is the one that settles it. A store-and-forward buffer for the patch adds 512 ns at 1 Gb/s to a mechanism whose target is 10 ns — and it adds it after the timestamp was captured, so it does not corrupt the value, it delays the frame. Which matters because Chapter 16.2 §6's asymmetry is exactly a difference in delay between two directions, and a device that buffers Syncs and not Delay_Reqs has created 512 ns of asymmetry and 256 ns of offset error by trying to be careful.

Approach B has no such effect because it adds nothing to the frame's transit at all.

And the timing question is the interesting one, because the serial view in Section 2 said there were 0.16 ns at 100 Gb/s.

Line rateDatapathClockPeriodA 64-octet Sync spans
1 Gb/s8-bit125 MHz8.00 ns64 beats
10 Gb/s64-bit156.25 MHz6.40 ns8 beats
25 Gb/s64-bit390.6 MHz2.56 ns8 beats
100 Gb/s512-bit195.3 MHz5.12 ns1 beat

At 100 Gb/s the whole frame is one beat, so the field, the pad and the FCS are all in the same cycle — and the 0.16 ns serial gap does not exist as a timing constraint. What exists instead is a requirement to compute the patch combinationally within one 5.12 ns cycle, from a delta formed in the same cycle.

A depth-6 XOR tree in a modern process is a few hundred picoseconds. The substitution's multiplexers are one level. The whole path — extract old, XOR with new, matrix multiply, XOR into the FCS — is comfortably inside 5.12 ns, and it is comfortably inside 2.56 ns at 25 Gb/s too.

Which is the resolution of Section 2's apparent impossibility, and the general lesson is worth extracting: a serial timing analysis of a wide datapath produces constraints that do not exist. The 0.16 ns figure is real as a statement about the wire and meaningless as a statement about the logic, because the logic never sees those two octets separately.

10. RTL 5 — The Correction Field and Its Arithmetic

The same in-flight rewrite, applied to a different field, by a device in the middle rather than at the end.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// correction_field_updater -- adds a value to the 8-octet
// correctionField as a frame streams past, and patches the FCS.
//
// This is section 7's rewriter and section 8's patcher applied to a
// different offset and a different width. The VALUE added is 16.4
// section 12's residence time; this module is the mechanism.
//
// correctionField is signed 16.16 fixed point: nanoseconds in the
// upper 48 bits, fractional nanoseconds in the lower 16.
// -----------------------------------------------------------------------
module correction_field_updater
  import tsu_pkg::*;
#(
  parameter int W = 8
)(
  input  logic              clk,
  input  logic              rst_n,

  input  logic              in_valid,
  input  logic [W*8-1:0]    in_data,
  input  logic              in_sop,
  input  logic              in_eop,

  input  logic              frame_is_ptp_event,
  input  logic [15:0]       correction_offset,     // octets from frame start
  input  logic signed [63:0] add_value,            // 16.16 fixed point

  output logic              out_valid,
  output logic [W*8-1:0]    out_data,
  output logic              out_sop,
  output logic              out_eop,

  output logic              delta_valid,
  output logic [63:0]       delta_bits,
  output logic              overflow,              // the field saturated
  output logic [31:0]       c_updated,
  output logic [31:0]       c_overflow
);

  logic [15:0]  octet_ctr;
  logic [63:0]  old_field, new_field;
  logic [W-1:0] lane_in_field;

  always_comb begin
    int L, pos;
    for (L = 0; L < W; L++) begin
      pos = int'(octet_ctr) + L;
      lane_in_field[L] = frame_is_ptp_event &&
                         (pos >= int'(correction_offset)) &&
                         (pos <  int'(correction_offset) + 8);
    end
  end

  // The added value is signed and the field is signed, so the sum can
  // overflow in either direction. Saturating is wrong -- it produces a
  // plausible number -- so the design must SAY it overflowed.
  logic signed [64:0] sum_ext;
  always_comb begin
    sum_ext   = $signed({old_field[63], old_field}) + $signed({add_value[63], add_value});
    new_field = sum_ext[63:0];
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int L;
    if (!rst_n) begin
      octet_ctr <= '0; old_field <= '0;
      out_valid <= 1'b0; out_data <= '0;
      out_sop <= 1'b0; out_eop <= 1'b0;
      delta_valid <= 1'b0; delta_bits <= '0;
      overflow <= 1'b0; c_updated <= '0; c_overflow <= '0;
    end else begin
      out_valid   <= in_valid;
      out_sop     <= in_sop;
      out_eop     <= in_eop;
      delta_valid <= 1'b0;
      overflow    <= 1'b0;

      if (in_valid) begin
        for (L = 0; L < W; L++) begin
          automatic int pos, k;
          pos = int'(octet_ctr) + L;
          k   = pos - int'(correction_offset);
          if (lane_in_field[L]) begin
            old_field[63 - 8*k -: 8] <= in_data[8*L +: 8];
            out_data[8*L +: 8]       <= new_field[63 - 8*k -: 8];
          end else begin
            out_data[8*L +: 8]       <= in_data[8*L +: 8];
          end
        end

        octet_ctr <= in_sop ? 16'(W) : (octet_ctr + 16'(W));

        if (frame_is_ptp_event &&
            ((int'(octet_ctr) + W) >= (int'(correction_offset) + 8)) &&
            ((int'(octet_ctr))     <  (int'(correction_offset) + 8))) begin
          delta_valid <= 1'b1;
          delta_bits  <= old_field ^ new_field;
          c_updated   <= c_updated + 1;
          // Sign-extension mismatch between the 65-bit sum and the
          // 64-bit field means the correction no longer fits.
          if (sum_ext[64] != sum_ext[63]) begin
            overflow   <= 1'b1;
            c_overflow <= c_overflow + 1;
          end
        end

        if (in_eop) octet_ctr <= '0;
      end
    end
  end

endmodule

Classification: a read-modify-write on a frame in flight, with an overflow flag. The same shape as Section 7 with an adder in the middle.

What it teaches: that the correction field is accumulated rather than written, and that is what lets an arbitrary number of transparent clocks compose. Each switch adds its own residence time to whatever is already there, so a message crossing five transparent clocks arrives with the sum of five residence times — and Chapter 16.2 §7's calculation subtracts the total. No switch needs to know how many others there are, or which position it occupies in the path.

And it teaches that saturating on overflow is the wrong behaviour, which is unusual. Most datapath arithmetic saturates because a clipped value is closer to the truth than a wrapped one. Here a clipped correction produces a plausible path delay that is silently too small, and Chapter 16.2 §7's arithmetic will accept it. An overflow must be reported, because the correct response is to drop the message rather than to deliver a wrong one — a 64-bit signed field in 16.16 format covers ±140 years, so an overflow means something is badly wrong rather than that the path is long.

Deliberately simplified: the field is read and written in the same pass, which requires the whole 8-octet field to be visible before the substitution is emitted. With W = 8 and an aligned offset that is one beat and works; with a misaligned offset it spans two beats and the first beat's output must be emitted before the second beat's input is seen. Production designs delay the stream by one beat — which costs 6.4 ns at 10 Gb/s and is the honest price of a read-modify-write on a streaming interface.

Production implication: frame_is_ptp_event gates this on event messages only, and applying it to a general message is a real bug with a subtle signature. A Follow_Up's correctionField is meaningful — it corrects the Sync it describes — but its residence time is not part of the path measurement, because the Follow_Up is not the frame that was timed. A transparent clock that adds its residence time to a Follow_Up corrupts a correction that was already correct, and the error is exactly one switch's residence time per hop.

11. One-Step Against Two-Step, Accounted

Both are conformant, both are deployed, and the choice is decided by which resource a device is short of.

Two-stepOne-step
extra frames per Sync10
bandwidth at 16 Sync/s+10 752 bit/s
bandwidth at 128 Sync/s+86 016 bit/s
extra messages, 1024 slaves at 128/s+128/s — multicast serves all
transmit timestamps captured1 per Sync1 per Sync — same
slave pending state48 octets + c_stale_syncnone
transmit datapath logicnone≈1248 XOR2, 80 muxes, 80 flops
transmit timing pathuntoucheddepth-6 XOR in the output cycle
frame bufferingnonenone, with the patch
tolerance to a late reportunlimitednot applicable
a lost reportc_stale_sync, recover next Syncimpossible
risk of a wrong field offsetlowa corrupted header — Section 7

Rows seven and eight are one-step's entire cost and they are both in the transmit path, which is the resource a small device is short of. Row six is two-step's, and it is in the slave.

Which produces a clean allocation and it is the one real deployments use:

A grandmaster or a switch — a device with a wide datapath, a hardware transmit scheduler and plenty of area — takes one-step. The gates are cheap at that size, the timing fits, and removing the Follow_Up removes a message the master must schedule per Sync per domain.

A small embedded slave takes two-step, because it has no transmit-side PTP hardware at all and 48 octets of pending state is nothing. And it does not matter: a slave sends Delay_Reqs, whose t3 never goes on the wireChapter 16.2 §11 — so a slave needs no in-flight rewrite in either mode.

And the third case is the one that makes the flag necessary. A network mixes them. A master may be one-step and a slave must handle both — which is why Chapter 16.2 §5's twoStepFlag check is not optional and why both of its wrong assumptions produce a slave that never locks.

One more asymmetry is worth naming. The correctionField rewrite in Section 10 is not optional for a transparent clock — a switch cannot report its residence time in a separate message, because there is no message to send it in. So a transparent clock is always a one-step device for that field, even if the grandmaster it forwards for is two-step. The mechanism this chapter built for one-step Sync is required regardless.

12. RTL 6 — The Transmit Timestamp FIFO

A transmit timestamp arrives after the frame that caused it. Something must hold it, and something must know which frame it belongs to.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tx_timestamp_fifo -- holds transmit timestamps with their frame tags
// until the protocol layer collects them.
//
// 16.1 section 12's simplification was a single capture register with
// no tag. This module fixes both halves: a queue, so a burst does not
// lose entries, and a tag, so an entry cannot be attributed to the
// wrong frame.
// -----------------------------------------------------------------------
module tx_timestamp_fifo
  import tsu_pkg::*;
#(
  parameter int DEPTH = 8
)(
  input  logic        clk,
  input  logic        rst_n,

  // From the capture point.
  input  logic        push,
  input  logic [15:0] push_tag,
  input  ts_t         push_ts,

  // The protocol layer asks for a specific tag rather than taking the
  // head. A burst can complete out of order and order is not the
  // property that matters -- identity is.
  input  logic        lookup_valid,
  input  logic [15:0] lookup_tag,

  output logic        hit,
  output ts_t         hit_ts,

  output logic [$clog2(DEPTH+1)-1:0] occupancy,
  output logic        full,
  output logic [31:0] c_pushed,
  output logic [31:0] c_dropped,      // pushed while full
  output logic [31:0] c_evicted,      // aged out unclaimed
  output logic [31:0] c_miss          // looked up and not present
);

  tx_ts_entry_t e [DEPTH];
  logic [15:0]  age [DEPTH];
  logic [$clog2(DEPTH+1)-1:0] occ;

  // Any entry nobody claims must eventually leave, or a single
  // abandoned frame permanently consumes a slot.
  localparam int MAX_AGE = 16'hF000;

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    automatic bit placed;
    if (!rst_n) begin
      for (i = 0; i < DEPTH; i++) begin e[i] <= '0; age[i] <= '0; end
      occ <= '0; hit <= 1'b0; hit_ts <= '0;
      c_pushed <= '0; c_dropped <= '0; c_evicted <= '0; c_miss <= '0;
    end else begin
      hit <= 1'b0;

      for (i = 0; i < DEPTH; i++) begin
        if (e[i].valid) begin
          age[i] <= age[i] + 1'b1;
          if (age[i] == 16'(MAX_AGE)) begin
            e[i].valid <= 1'b0;
            occ        <= occ - 1'b1;
            c_evicted  <= c_evicted + 1;
          end
        end
      end

      if (push) begin
        placed = 1'b0;
        for (i = 0; i < DEPTH; i++) begin
          if (!e[i].valid && !placed) begin
            placed      = 1'b1;
            e[i].valid  <= 1'b1;
            e[i].frame_tag <= push_tag;
            e[i].ts     <= push_ts;
            age[i]      <= '0;
            occ         <= occ + 1'b1;
            c_pushed    <= c_pushed + 1;
          end
        end
        // Dropping the NEWEST is the right policy: an old entry may
        // still be claimed, and a dropped new one is detectable at
        // the protocol layer as a missing timestamp for a known frame.
        if (!placed) c_dropped <= c_dropped + 1;
      end

      if (lookup_valid) begin
        automatic bit found;
        found = 1'b0;
        for (i = 0; i < DEPTH; i++) begin
          if (e[i].valid && (e[i].frame_tag == lookup_tag)) begin
            found      = 1'b1;
            hit        <= 1'b1;
            hit_ts     <= e[i].ts;
            e[i].valid <= 1'b0;
            occ        <= occ - 1'b1;
          end
        end
        if (!found) c_miss <= c_miss + 1;
      end
    end
  end

  assign occupancy = occ;
  assign full      = (occ == DEPTH);

endmodule

Classification: a small tag-addressed store with an age-out. Eight entries, associative lookup, not a FIFO despite the name.

What it teaches: that order is not the property that matters and a true FIFO is the wrong structure. Transmit completions can arrive out of order — a transmit scheduler with several queues, Chapter 13.4 §11, completes a high-priority frame before a low-priority one that was queued earlier — and the protocol layer asks for a specific frame's timestamp rather than for the oldest. Lookup by tag is correct; pop is not.

And it teaches that dropping the newest on overflow is deliberate and counter-intuitive. The usual policy is to drop the oldest and keep the fresh data. Here an old entry may still be claimed by a protocol layer that is a few microseconds behind, and a dropped new entry surfaces immediately at the protocol layer as c_miss for a frame it knows it sent. Dropping the oldest converts a detectable miss into a silent substitution.

Deliberately simplified: the lookup is a linear scan over eight entries evaluated in one cycle, which is fine at eight and not at sixty-four. A grandmaster serving a thousand slaves needs depth for a Delay_Resp burstChapter 16.2 §11's callout — and at that depth the structure becomes Chapter 12.5's set-associative table indexed by the tag's low bits, with the same interesting property Section 9 of that chapter described: a collision here is a correctness failure rather than a capacity one.

Production implication: c_evicted and c_miss together diagnose a protocol layer that is too slow. c_evicted rising means timestamps are being captured and never collected — the software or firmware that should read them is not keeping up, and the Follow_Ups it should have generated are simply absent. At the far end that is Chapter 16.2 §5's c_stale_sync climbing with c_sync, which looks like a network problem and is a local scheduling one.

13. Pairing a Timestamp to a Frame

Section 12 solved this with a 16-bit tag. This section is the argument for why the tag is not optional, because a great many designs omit it and appear to work.

Without a tag, a timestamp and a frame are associated by order: the nth timestamp belongs to the nth frame.

That is correct until any one of five things happens.

EventWhat order-based pairing does
a capture overrunChapter 16.1 §12every later pairing is off by one
a FIFO dropthe same
out-of-order transmit completiontwo pairings swapped
a frame aborted after its SFDone extra timestamp, all later ones shifted
the protocol layer restartingits count and the hardware's diverge

Rows one, two and four are the ones that occur, and their consequence is the same: a permanent off-by-one that no check detects.

And the resulting error's size is the part worth internalising, because it is not small and it is not constant.

Interval between the two framest1 error
back-to-back at 1 Gb/s672 ns
back-to-back at 100 Gb/s6.72 ns
one Sync interval at 128/s7.81 ms
one Sync interval at 1/s1 second

The last two rows are the common case, because PTP messages are not back-to-back. A Sync stream at 1/s with an off-by-one pairing gives every Sync the previous Sync's transmit timestamp — so t1 is one second stale and the computed offset is off by one second. Which, unusually, is loud: the slave's servo sees a one-second error and refuses to lock.

The dangerous case is the middle rows. On a busy link where a Sync happens to be adjacent to other frames, an off-by-one gives t1 a value from a frame microseconds or nanoseconds away — and the resulting offset error is a few hundred nanoseconds, which looks exactly like a path asymmetry and is indistinguishable from one. Chapter 16.2 §8's calibration procedure will faithfully absorb it into a stored constant, and the constant will be wrong the moment the traffic pattern changes.

So the tag's 16 bits buy the difference between an error that is diagnosable and an error that is absorbed into a calibration.

And the tag has to come from the right place: the transmit scheduler, not the protocol layer. The scheduler is what decides the order frames actually leave in — Chapter 13.4 §11 — so it is the only component that can label a frame in a way the capture point and the protocol layer will both agree on. A tag assigned by the protocol layer and carried through a reordering scheduler is a tag that identifies intent rather than reality.

==

A transmit timestamp arrives after the frame that caused it, so it must be paired back to that frame. Order-based pairing associates the nth timestamp with the nth frame and is correct until a capture overrun, a FIFO drop, an aborted frame or an out-of-order transmit completion occurs, after which every subsequent pairing is permanently off by one and nothing detects it. The resulting t1 error equals the interval between the two frames: one second at a one-per-second Sync rate, which is loud enough that the servo refuses to lock, but only a few hundred nanoseconds on a busy link, which is indistinguishable from a path asymmetry and will be absorbed into a stored calibration constant that is then wrong the moment the traffic pattern changes. Tagged pairing attaches a sixteen-bit identifier assigned by the transmit scheduler, so a lost timestamp is one lost measurement and is reported as a miss.A transmittimestamparrives after its framePaired by orderthe nth to the nthOne overrun, onedropoff by one, for everLooks likeasymmetrya few hundred nsPaired by tag16 bits, from theschedulerA miss is reportedone lost measurementThe calibrationstays validnothing absorbed12
Figure 4 — order-based pairing against tagged pairing, and the error each produces when something goes wrong.

14. RTL 7 — Timestamp Unit Telemetry

Six numbers, and the useful ones describe the unit's discipline rather than its output.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tsu_telemetry -- what an operator or an integrator needs to decide
// whether a timestamp unit is behaving, without a reference clock.
//
// Every quantity here is derived from the unit's own behaviour.
// 16.1 section 19 established that accuracy cannot be among them.
// -----------------------------------------------------------------------
module tsu_telemetry
  import tsu_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        capture,
  input  logic        classify,
  input  logic        ts_published,
  input  logic        overrun,
  input  logic [31:0] c_patched,
  input  logic [31:0] c_rewritten,
  input  logic [31:0] c_dropped,
  input  logic [31:0] c_miss,
  input  logic [31:0] c_evicted,
  input  logic [$clog2(9)-1:0] fifo_occupancy,
  input  logic        window_tick,

  output logic [15:0] capture_to_classify_cycles,
  output logic [15:0] worst_capture_to_classify,
  output logic [15:0] peak_occupancy,
  output logic [15:0] patch_ratio_x100,     // patched / rewritten
  output logic        pairing_is_sound,
  output logic        unit_is_keeping_up,
  output logic [31:0] c_windows
);

  logic [15:0] lat;
  logic        timing;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      lat <= '0; timing <= 1'b0;
      capture_to_classify_cycles <= '0;
      worst_capture_to_classify <= '0;
      peak_occupancy <= '0; patch_ratio_x100 <= '0;
      pairing_is_sound <= 1'b0; unit_is_keeping_up <= 1'b0;
      c_windows <= '0;
    end else begin
      // The interval between capturing and classifying sets the FIFO
      // depth the unit needs. It is a pipeline property and nobody
      // measures it, so the depth is usually a guess.
      if (capture) begin timing <= 1'b1; lat <= '0; end
      else if (timing) begin
        lat <= lat + 1'b1;
        if (classify) begin
          timing <= 1'b0;
          capture_to_classify_cycles <= lat;
          if (lat > worst_capture_to_classify)
            worst_capture_to_classify <= lat;
        end
      end

      if (16'(fifo_occupancy) > peak_occupancy)
        peak_occupancy <= 16'(fifo_occupancy);

      if (window_tick) begin
        // Every rewritten frame must have been patched. A ratio other
        // than 100 means frames left with a stale FCS and every
        // receiver discarded them -- 6.3's residue check.
        patch_ratio_x100 <= (c_rewritten == 0) ? 16'd100
                          : 16'((c_patched * 100) / c_rewritten);

        // Pairing is sound if nothing was dropped, evicted or missed.
        // Any of the three means a timestamp and a frame may have
        // been mismatched -- section 13.
        pairing_is_sound <= (c_dropped == '0) && (c_evicted == '0) &&
                            (c_miss == '0);

        // The unit keeps up if the queue never approached full and
        // no capture was overwritten.
        unit_is_keeping_up <= (peak_occupancy < 16'd7) && !overrun;

        c_windows <= c_windows + 1;
      end
    end
  end

endmodule

Classification: a latency and occupancy monitor with two derived judgements. It measures the unit, not the time.

What it teaches: that worst_capture_to_classify is the measurement that sizes the FIFO, and it is almost never taken. Section 12's depth of eight is a guess; the correct depth is the worst capture-to-classify latency times the peak event-message rate, and the first factor is a property of the parser pipeline that only the design knows. A design that measures it can size the FIFO from evidence — and can discover that at 100 Gb/s with a deep parse pipeline the answer is larger than eight.

And it teaches that patch_ratio_x100 must be exactly 100 and any other value is a silent catastrophe. A rewritten frame whose FCS was not patched fails Chapter 6.3's residue check at every receiver and is discarded — so the symptom is a Sync stream that vanishes, with the transmitting device's counters all healthy. The ratio is two counters and a divide, and it converts an invisible failure into a number.

Deliberately simplified: unit_is_keeping_up compares the peak occupancy against a hard-coded 7 out of 8. A production design parameterises the margin and, more usefully, reports the peak as a fractiona unit running at 90% of its queue depth is one traffic change away from dropping, and "did not drop" is not the same finding as "has headroom".

Production implication: pairing_is_sound is a single bit that gates whether any offset computed downstream is worth believing, and it is the bit Section 13's argument makes necessary. A device reporting pairing_is_sound low has possibly mismatched a timestamp to a frame, and the resulting error looks like a path asymmetry — which Chapter 16.2 §8's calibration would absorb into a stored constant. So the bit's real job is to prevent a calibration from being taken while the pairing is unreliable.

15. RTL 8 — Conformance for a Timestamp Unit in Flight

The monitor checks the discipline that makes accuracy possible, and deliberately not accuracy.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tsu_conformance_monitor -- one bit.
//
// It asserts: fixed capture latency, correct classification, complete
// patching, sound pairing, and a characterised configuration. 16.1
// section 19 established why accuracy is not on the list and section
// 19 here refuses a different property for a different reason.
// -----------------------------------------------------------------------
module tsu_conformance_monitor
  import tsu_pkg::*;
(
  input  logic       clk,
  input  logic       rst_n,

  input  logic       variable_capture_latency,
  input  logic       rewrote_a_general_message,
  input  logic       rewrote_a_twostep_sync,
  input  logic       patch_without_delta,
  input  logic       correction_overflow,
  input  logic       pairing_unsound,
  input  logic       cfg_phy_delay_unset,
  input  logic       cfg_phy_delay_stale,     // the link mode changed
  input  logic       cfg_lane_in_ns,          // truncates to zero

  output logic       conformant,
  output logic [15:0] fault_vector,
  output logic [31:0] c_violations
);

  logic v_lat, v_gen, v_two, v_patch, v_ovf, v_pair;
  logic v_unset, v_stale, v_lane;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_lat <= 1'b0; v_gen <= 1'b0; v_two <= 1'b0;
      v_patch <= 1'b0; v_ovf <= 1'b0; v_pair <= 1'b0;
      v_unset <= 1'b0; v_stale <= 1'b0; v_lane <= 1'b0;
      c_violations <= '0;
    end else begin
      // A variable capture latency is the one violation that destroys
      // the mechanism rather than degrading it -- it reintroduces
      // exactly the jitter 16.1 removed.
      if (variable_capture_latency) begin v_lat <= 1'b1; c_violations <= c_violations + 1; end
      if (rewrote_a_general_message) begin v_gen <= 1'b1; c_violations <= c_violations + 1; end
      if (rewrote_a_twostep_sync)    begin v_two <= 1'b1; c_violations <= c_violations + 1; end
      if (patch_without_delta)       begin v_patch <= 1'b1; c_violations <= c_violations + 1; end
      if (correction_overflow)       begin v_ovf <= 1'b1; c_violations <= c_violations + 1; end
      if (pairing_unsound)           begin v_pair <= 1'b1; c_violations <= c_violations + 1; end

      // Standing configuration properties. 14.1 section 17's argument:
      // these are wrong from elaboration, not from the first frame.
      v_unset <= cfg_phy_delay_unset;
      v_stale <= cfg_phy_delay_stale;
      v_lane  <= cfg_lane_in_ns;
    end
  end

  assign conformant = !(v_lat || v_gen || v_two || v_patch || v_ovf ||
                        v_pair || v_unset || v_stale || v_lane);
  assign fault_vector = {7'b0, v_lane, v_stale, v_unset, v_pair,
                         v_ovf, v_patch, v_two, v_gen, v_lat};

endmodule

Classification: a sticky aggregator with six runtime violations and three standing configuration terms.

What it teaches: that cfg_phy_delay_stale is a fault class Chapter 16.1 §17 did not have, and Section 4 is why it exists. That chapter's cfg_phy_delay_zero catches a correction nobody set. This one catches a correction that was right and is no longer — a link that renegotiated from 10 Gb/s to 1 Gb/s, or that enabled FEC, has changed its PHY delay by tens or hundreds of nanoseconds under a constant that was correct at commissioning. The check is a comparison of the live link mode against the mode the correction was indexed for.

And it teaches that rewrote_a_twostep_sync is separate from rewrote_a_general_message because the two have different blast radii. Rewriting a general message corrupts a field that mattered — Section 10's Follow_Up case. Rewriting a two-step Sync's originTimestamp corrupts a field every conformant slave ignores, so it is harmless today and becomes a double-write the day the deployment switches to one-step. A latent fault deserves its own bit.

Deliberately simplified: variable_capture_latency is presented as an input, and producing it means asserting that the interval from sfd to the capture register's update is always exactly one cycle. That is Chapter 16.1 §19's p_capture_latency_is_fixed as a runtime monitor, which is cheap — one comparator on a two-cycle history — and is worth having in silicon because a synthesis or placement change can introduce a retiming stage that no functional test detects.

Production implication: conformant here means the unit's discipline is sound: the capture is fixed-latency, the classification is right, every rewrite was patched, and the pairing is trustworthy. It says nothing about the accuracy achievedChapter 16.2 §8's asymmetry is still there, Chapter 16.5's terms are still there, and a fully conformant unit behind an uncorrected store-and-forward switch still delivers 6.07 µs of error. Which is the same separation Chapter 15.3 §15 drew between a fault and a finding, arriving here as a separation between discipline and outcome.

16. Where the Unit Sits in the Datapath

Three blocks have been built and they do not all sit in the same place. Putting them on the datapath makes the timing argument concrete.

BlockDirectionWhereWhy there
Section 3's capturebothat the MAC/PHY boundarythe earliest characterisable point
Section 7's rewritertransmitafter the MAC, before the PHYit must see the frame after the FCS was computed
Section 8's patchertransmiton the FCS octets specificallyit patches, it does not recompute
Section 10's correction updatertransmitthe same place, a different fielda transparent clock's only mechanism
Section 12's FIFOtransmitbeside the datapath, not in itit holds values, not frames

Row two is the one that decides the architecture and it is counter-intuitive. The rewriter must sit downstream of the MAC's CRC generator, because its whole premise is that a frame with a valid FCS arrives and leaves with a different valid FCS. A rewriter placed upstream of the CRC generator needs no patcher at all — the MAC would compute the CRC over the already-substituted content.

And that placement is available and is sometimes the right answer. Its cost is that the substitution now happens before the frame's transmit timestamp exists:

Rewriter placementNeeds a patcher?Can substitute the real t1?
upstream of the CRC generatornono — t1 is not known yet
downstream, before the PHYyesyes

Which is the whole reason the patcher exists. The timestamp is only available once the frame is physically leaving, and by then the CRC has been computed — so either the substitution happens too early to be correct, or it happens late and the CRC must be repaired. There is no third placement.

And the correction updater has the same constraint for a different reason. A transparent clock's residence time is the difference between a frame's arrival and its departure, so it is not known until the frame departs — the same self-referential shape, resolved the same way.

One consequence for verification is worth stating. The rewriter and the patcher are downstream of the MAC, so a testbench that drives the MAC and checks its output never exercises them. They must be verified on the PHY-side interface, with a model that recomputes the FCS from the emitted frame and compares — which is Section 20's directed test.

==

A field rewriter can sit upstream or downstream of the MAC's CRC generator. Placed upstream, the MAC computes the CRC over the already-substituted content so no patcher is needed at all — but at that point in the pipeline the frame has not begun transmitting, so the transmit timestamp does not exist and the substituted value cannot be the real one. Placed downstream, between the MAC and the PHY, the timestamp is available because the frame's Start Frame Delimiter has already crossed the boundary, but the CRC was computed over the old content and must be repaired. There is no third placement, which is why the patcher exists. A consequence for verification is that both the rewriter and the patcher sit outside the MAC, so a testbench that drives the MAC and checks its output never exercises either of them.The MACcomputes the CRCRewriter upstreamno patcher neededt1 does not existyetthe frame has not leftRewriter downstreambefore the PHYt1 is availablethe SFD has passedThe FCS must berepairedhence the patcherVerify on the PHYsidethe MAC never sees it12
Figure 3 — the two placements for a field rewriter, and why only one of them can carry a real timestamp.

17. What the Unit Can and Cannot Promise

ClaimStatus
the capture is at the SFDguaranteed
the capture latency is exactly one cycleguaranteed, and it is the load-bearing one
the characterised PHY delay is subtractedguaranteed, if the constant is right
a rewritten frame's FCS is validguaranteed — Section 8, and patch_ratio_x100 proves it
a timestamp belongs to the frame claimedguaranteed with tags; not without
an arbitrary number of transparent clocks composeguaranteed — the correction accumulates
the timestamp equals the true instantno — a constant offset and the elastic buffer remain
the achieved accuracynot a property of this unit

Row two is the one everything else rests on, and it is worth saying why it is the structural promise rather than an implementation detail. Chapter 16.1 §9 established that a constant delay is removable and a variable one is not. A fixed capture latency is precisely the guarantee that the remaining delay is constant — which is what makes Section 4's characterisation meaningful. A unit with a variable capture latency has no characterisable delay and therefore no accuracy at any calibration.

And row five is the one designs omit. Section 13's arithmetic: without a tag, an off-by-one pairing produces an error of a few hundred nanoseconds on a busy link — indistinguishable from a path asymmetry, and absorbed by a calibration that will then be wrong.

Row seven is the honest limit and it has two parts. The constant part is the PHY delay, removable if characterised and stale-checked. The variable part is Chapter 4.4's elastic buffer at 5 to 20 ns, and nothing in this chapter touches it — Chapter 16.5 prices it with the protocol in place.

Which gives the chapter's closing position: this unit makes accuracy possible and does not deliver it. It removes the jitter Chapter 16.1 spent nine sections rejecting, and what it leaves behind is a constant to be characterised, an elastic buffer that cannot be, and Chapter 16.2 §8's asymmetry that this chapter never touched.

18. The Cost, Accounted

ComponentCostAgainst what
capture register, per direction80 flops + a subtract
transmit FIFO — 8 × (16 + 80) bits96 octets
Section 7's rewriter — 10-octet mux80 2:1 muxes + 80 flops
Section 8's patcher — 32 × 80 GF(2)≈1248 XOR2, depth 6replaces a full CRC-32 engine
Section 10's correction updater64-bit adder + 64 muxes + a 32 × 64 matrix≈1000 XOR2
telemetry and conformance≈40 flops
total, one port, both directions≈2500 XOR2 + ≈400 flopstiny against a MAC
added frame latencyzeroagainst 512 ns for buffer-and-recompute
added timing depth6 XOR levelsinside 5.12 ns at 100 Gb/s; tight at 2.56 ns

Row eight is the number that makes one-step viable and it is the chapter's central engineering result. The naive implementation adds 512 ns at 1 Gb/s — and does so asymmetrically, which by Chapter 16.2 §6's arithmetic creates 256 ns of offset error in the act of trying to be precise. The patch adds nothing.

And the comparison across the module puts it in proportion:

MechanismCostWhere it lands
Chapter 16.2's exchange≈220 octetsarithmetic — nowhere
this unit≈2500 XOR2 + 400 flopsthe transmit datapath's timing
Chapter 16.4's transparent clocka residence timer per portevery switch in the path
Chapter 16.5's calibrationa constant per linka commissioning procedure

Two and a half thousand gates is a rounding error in a MACChapter 19.1's block is orders of magnitude larger — and the cost that actually constrains a design is the timing depth, which is why the row above quotes it against two clock periods rather than against an area budget.

19. Properties Worth Asserting, and One Worth Refusing

The properties divide by block: the capture, the classification, the rewrite, the patch, the correction, the FIFO, and the configuration.

Group 1 — the capture.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. THE load-bearing property: the capture latency is exactly one
// cycle, always. A variable latency reintroduces the jitter the whole
// of chapter 16.1 was about removing.
property p_capture_latency_is_one;
  @(posedge clk) disable iff (!rst_n)
  sfd |=> held_valid;
endproperty

// P2. And nothing else ever writes the capture register.
property p_no_spurious_capture;
  @(posedge clk) disable iff (!rst_n)
  $changed(held) |-> $past(sfd);
endproperty

// P3. The lane correction is applied in picoseconds, so it does not
// truncate to zero -- 16.1 section 12's simplification, fixed.
property p_lane_correction_nonzero;
  @(posedge clk) disable iff (!rst_n)
  (sfd && (sfd_lane != '0) && (cfg_lane_ps != '0))
    |=> (held.ns != (now.ns - NS_W'(cfg_phy_delay_ns)));
endproperty

// P4. A second SFD before a classification sets overrun.
property p_overrun_flagged;
  @(posedge clk) disable iff (!rst_n)
  (sfd && held_valid) |=> overrun;
endproperty

// P5. Captures are monotonic -- checkable with no reference.
property p_captures_monotonic;
  @(posedge clk) disable iff (!rst_n)
  (ts_valid && $past(ts_valid)) |-> ((ts_out.ns >= $past(ts_out.ns)) ||
                                     (ts_out.sec > $past(ts_out.sec)));
endproperty

Group 2 — the classification.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P6. Only event messages publish a timestamp. 16.2 section 4's split.
property p_only_events_publish;
  @(posedge clk) disable iff (!rst_n)
  ts_valid |-> $past(class_is_event);
endproperty

// P7. A general message's capture is discarded, not leaked.
property p_general_discards;
  @(posedge clk) disable iff (!rst_n)
  (class_valid && !class_is_event && held_valid) |=> $changed(c_discarded);
endproperty

// P8. Every capture is eventually resolved -- published or discarded.
property p_every_capture_resolved;
  @(posedge clk) disable iff (!rst_n)
  $rose(held_valid) |-> ##[1:$] !held_valid;
endproperty

// P9. Classification always follows capture, never precedes it.
property p_capture_precedes_classify;
  @(posedge clk) disable iff (!rst_n)
  class_valid |-> $past(held_valid) || overrun;
endproperty

Group 3 — the rewrite.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P10. Only a one-step Sync's originTimestamp is rewritten.
property p_rewrite_only_onestep_sync;
  @(posedge clk) disable iff (!rst_n)
  delta_valid |-> frame_is_onestep_sync;
endproperty

// P11. Octets outside the field pass through unchanged.
property p_outside_field_untouched;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && !lane_in_field[L]) |=> (out_data[8*L +: 8] == $past(in_data[8*L +: 8]));
endproperty

// P12. The substituted content is exactly the captured timestamp in
// wire order -- MSB of seconds first.
property p_substitution_is_the_timestamp;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && lane_in_field[L]) |=>
    (out_data[8*L +: 8] == new_ts[OTS_BITS - 8*k - 1 -: 8]);
endproperty

// P13. The delta is zero outside the field, by construction.
property p_delta_is_confined;
  @(posedge clk) disable iff (!rst_n)
  delta_valid |-> ((delta_bits & ~field_mask) == '0);
endproperty

// P14. A field spanning two beats is handled and counted.
property p_split_field_counted;
  @(posedge clk) disable iff (!rst_n)
  (delta_valid && (octet_ctr > field_offset)) |=> $changed(c_split_across_beats);
endproperty

Group 4 — the patch.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P15. Every rewritten frame's FCS is patched. A ratio other than
// 100% means frames left with a stale FCS and every receiver
// discarded them -- 6.3's residue check.
property p_every_rewrite_is_patched;
  @(posedge clk) disable iff (!rst_n)
  delta_valid |-> ##[1:$] (fcs_out_valid && $past(have_patch));
endproperty

// P16. The patch is linear: patching with a zero delta is the
// identity.
property p_zero_delta_is_identity;
  @(posedge clk) disable iff (!rst_n)
  (fcs_valid && have_patch && (held_patch == '0)) |=> (fcs_out == $past(fcs_in));
endproperty

// P17. Linearity, stated directly: the patch for A xor B is the xor
// of the patches. This is the property the whole module rests on.
property p_patch_is_linear;
  @(posedge clk) disable iff (!rst_n)
  (crc32_of_positioned_delta(a ^ b) ==
   (crc32_of_positioned_delta(a) ^ crc32_of_positioned_delta(b)));
endproperty

// P18. The patch uses no initial value and no final complement --
// both cancel in a difference, and including them makes every
// patched frame fail its receiver's check.
property p_no_init_no_complement;
  @(posedge clk) disable iff (!rst_n)
  (crc32_of_positioned_delta('0) == 32'h0000_0000);
endproperty

// P19. The emitted frame's FCS is the correct CRC of the emitted
// frame -- the end-to-end check, on the PHY-side interface.
property p_emitted_frame_is_valid;
  @(posedge clk) disable iff (!rst_n)
  out_eop |-> (emitted_fcs == crc32_of(emitted_frame_body));
endproperty

// P20. An FCS arriving with no patch when one was expected is
// counted, loudly.
property p_missing_patch_flagged;
  @(posedge clk) disable iff (!rst_n)
  (fcs_valid && !have_patch && delta_expected) |=> patch_without_delta;
endproperty

Group 5 — the correction field.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P21. The correction ACCUMULATES -- it is added, never replaced, so
// an arbitrary number of transparent clocks compose.
property p_correction_accumulates;
  @(posedge clk) disable iff (!rst_n)
  delta_valid |-> (new_field == (old_field + add_value));
endproperty

// P22. Only event messages are updated. A Follow_Up's correction is
// already correct and must not gain this switch's residence time.
property p_correction_only_events;
  @(posedge clk) disable iff (!rst_n)
  delta_valid |-> frame_is_ptp_event;
endproperty

// P23. Overflow is reported, never saturated. A clipped correction is
// a plausible wrong number that section 7 of chapter 16.2 accepts.
property p_overflow_is_reported;
  @(posedge clk) disable iff (!rst_n)
  (sum_ext[64] != sum_ext[63]) |-> overflow;
endproperty

Group 6 — the FIFO and pairing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P24. A lookup returns the entry with that tag, or a miss. It never
// returns a different frame's timestamp.
property p_lookup_is_by_tag;
  @(posedge clk) disable iff (!rst_n)
  hit |-> ($past(e[i].frame_tag) == $past(lookup_tag));
endproperty

// P25. A hit consumes its entry exactly once.
property p_hit_consumes_once;
  @(posedge clk) disable iff (!rst_n)
  hit |=> !e[i].valid;
endproperty

// P26. Overflow drops the NEWEST, so an old entry can still be
// claimed and the loss surfaces as a miss at the protocol layer.
property p_overflow_drops_newest;
  @(posedge clk) disable iff (!rst_n)
  (push && full) |=> ($stable(e) && $changed(c_dropped));
endproperty

// P27. Every entry eventually leaves -- claimed or aged out.
property p_no_permanent_entries;
  @(posedge clk) disable iff (!rst_n)
  $rose(e[i].valid) |-> ##[1:MAX_AGE+1] !e[i].valid;
endproperty

// P28. Occupancy equals the number of valid entries.
property p_occupancy_is_exact;
  @(posedge clk) disable iff (!rst_n)
  occupancy == $countones(entry_valid_vector);
endproperty

Group 7 — configuration.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P29. Standing property: the PHY delay is characterised.
property p_phy_delay_characterised;
  @(posedge clk) disable iff (!rst_n)
  !cfg_phy_delay_unset;
endproperty

// P30. And is not stale -- the live link mode matches the mode the
// correction was indexed for. Section 4's operational trap.
property p_phy_delay_is_current;
  @(posedge clk) disable iff (!rst_n)
  !cfg_phy_delay_stale;
endproperty

// P31. The lane term is in picoseconds, not nanoseconds.
property p_lane_units_are_ps;
  @(posedge clk) disable iff (!rst_n)
  !cfg_lane_in_ns;
endproperty

// P32. conformant is exactly the conjunction it claims.
property p_conformant_definition;
  @(posedge clk) disable iff (!rst_n)
  conformant <-> (fault_vector == 16'h0000);
endproperty

P1 is the property the mechanism rests on, P17 is the identity that makes the patch possible, and P19 is the end-to-end check that catches every way the rewrite-and-patch pair can go wrong together. None of them says the timestamp arrives in time to be written — which is the property this chapter refuses, and its impossibility is structural in a way none of the earlier refusals were.

20. Verification Scenarios

Seventy-four scenarios. The ones that matter are on the PHY-side interface, because a testbench that stops at the MAC never sees the rewrite or the patch at all.

The capture

#ScenarioExpected
1SFD detectedtimestamp in exactly one cycle
2A retiming stage added by synthesistwo cycles — variable_capture_latency
3Same, functional test onlypasses — the jitter is back and invisible
4SFD at lane 0no lane correction
5SFD at lane 5, XGMII, cfg_lane_ps = 8004.0 ns subtracted
6Same, lane term in integer nstruncates to zero
7Two SFDs before a classificationoverrun
81000 ordinary frames, no PTPc_captured = 0, c_discarded = 1000
9Captures compared pairwisemonotonic
10cfg_phy_delay_ns = 0timestamps 200 ns early, all checks pass
11Link renegotiates 10 G → 1 Gcfg_phy_delay_stale
12FEC enabled after commissioninghundreds of ns of stale correction

Classification

#ScenarioExpected
13Sync, type 0x0published
14Follow_Up, type 0x8discarded
15Announce, type 0xBdiscarded
16Pdelay_Req, type 0x2published — it is an event
17Classification 15 octets after the SFD384 ns at 1 Gb/s, 3.84 ns at 100
18Deep parse pipeline at 100 Gb/smore than 8 captures outstanding
19Same, depth-8 FIFOc_dropped

The rewrite

#ScenarioExpected
20One-step Sync, field at octet 48, W = 8spans beats 6 and 7
21Samec_split_across_beats
22Field aligned to a beat boundaryone beat — and half the logic untested
23Octets outside the fieldunchanged
24Substituted octetsthe captured timestamp, MSB of seconds first
25Two-step Syncnot rewritten
26Same, rewritten anywayharmless today; a double write after switching to one-step
27A general message rewrittenten octets into the PTP header
28Samewell-formed, correctly checksummed, semantically destroyed
29Tagged Sync, untagged field_offsetthe header overwritten
30Old field content not capturedthe patch is impossible

The patch

#ScenarioExpected
31Delta of zeropatch is the identity
32CRC(a ⊕ b) against CRC(a) ⊕ CRC(b)equal — linearity
33Patch computed with 0xFFFFFFFF initwrong by a fixed 32-bit value, every frame
34Sameevery receiver discardsChapter 6.3's residue
35Patch with the final complement appliedthe same
36TRAILING_OCTETS = 2, untagged Synccorrect
37TRAILING_OCTETS = 3a valid CRC of a mis-positioned delta
38Sameframe consistently and wrongly checksummed; patch_without_delta silent
39Emitted frame checked on the PHY interfaceFCS valid
40Testbench stopping at the MACthe patch never exercised
41FCS arrives, no delta, none expectedpassed through unchanged
42FCS arrives, no delta, one expectedpatch_without_delta
43patch_ratio_x100exactly 100, or frames are vanishing
44Patch matrix, 10-octet field32 × 80, depth 6, ≈1248 XOR2

Latency and timing

#ScenarioExpected
45Buffer-and-recompute at 1 Gb/s+512 ns
46Same, applied to Syncs only256 ns of offset errorChapter 16.2 §6
47Patch approach+0 ns
481 Gb/s, 8-bit datapath64 beats per Sync, field-to-FCS = 2 beats
4910 Gb/s, 64-bit8 beats, adjacent cycles, 6.40 ns
5025 Gb/s, 64-bit8 beats, 2.56 ns — tight
51100 Gb/s, 512-bit1 beat — field and FCS in the same cycle, 5.12 ns
52Serial reading at 100 Gb/s0.16 ns — a constraint that does not exist

The correction field

#ScenarioExpected
53Five transparent clocks in the paththe five residence times sum
54Same, no switch knowing the countcorrect — it accumulates
55Correction replaced instead of addedonly the last switch counted
56Correction overflow, 64-bit signed 16.16reported, not saturated
57Same, saturateda plausible path delay that is too small
58Applied to a Follow_Upcorrupts a correction that was correct
59Same, per hopone switch's residence time of error per hop
60Misaligned 8-octet field, W = 8spans two beats — needs a one-beat delay

Pairing

#ScenarioExpected
61Tagged pairing, overrun occursone measurement lost, detected
62Order-based pairing, overrun occursevery later pairing off by one
63Same, back-to-back at 1 Gb/s672 ns of t1 error
64Same, one Sync interval at 1/s1 second of error — loud, servo refuses to lock
65Same, adjacent to bulk traffica few hundred ns — looks like asymmetry
66Same, calibration takenthe asymmetry constant absorbs it
67Traffic pattern changesthe calibration is now wrong
68Out-of-order transmit completiontwo pairings swapped — a FIFO pops the wrong one
69Tag from the protocol layer, reordering scheduleridentifies intent, not reality
70Tag from the transmit schedulercorrect
71Lookup for an absent tagc_miss
72Entry never claimedages out, c_evicted
73Protocol layer too slowc_evicted rising, Follow_Ups absent
74Same, seen at the far endc_stale_sync climbing with c_sync

The directed test random stimulus will not produce

Random stimulus will not exercise the rewrite-and-patch pair correctly, for a structural reason: the blocks sit downstream of the MAC, so a testbench that drives the MAC and checks its output never reaches them. Section 16's last paragraph. And the check that matters is not "did the field change" but "is the emitted frame's FCS the correct CRC of the emitted frame's body" — a recomputation from the wire, which is a checker rather than a stimulus and which no coverage metric requests.

Setup: one transmit path, W = 8 octets per beat at 156.25 MHz — 10 Gb/s. A PHY-side monitor that captures every emitted frame, recomputes CRC-32 over octets 0 through n−5, and compares against the emitted FCS. The capture point is driven with a known SFD instant. cfg_phy_delay_ns = 200, cfg_lane_ps = 800.

Stimulus, five phases. Phase 1: 1000 ordinary frames of random length, no PTP. Phase 2: 100 two-step Syncs. Phase 3: 100 one-step Syncs, field at octet 48 — spanning beats 6 and 7. Phase 4: 100 one-step Syncs on a tagged VLAN, field at octet 52. Phase 5: 100 one-step Syncs interleaved with ordinary frames such that the transmit scheduler completes them out of order.

Oracle:

#ObservablePhase 1Phase 2Phase 3Phase 4Phase 5
1frames emitted with a valid FCS1000100100100100
2any frame with an invalid FCS00000
3c_captured0100100100100
4c_discarded1000000~100
5c_rewritten00100100100
6c_patched00100100100
7patch_ratio_x100100100100100100
8c_split_across_beats00100100100
9emitted originTimestampn/aall zerothe captured valuethe captured valuethe captured value
10captured value against the driven SFDn/aSFD − 200 ns − lanethe samethe samethe same
11twoStepFlag in the emitted framen/asetclearclearclear
12c_dropped, c_miss, c_evicted00000
13timestamps paired to the right framen/ayesyesyesyes — by tag
14rerun phase 5 with order-based pairingpairings swapped
15rerun phase 3 with TRAILING_OCTETS = 3every FCS invalid
16rerun phase 3 with 0xFFFFFFFF initevery FCS invalid
17rerun phase 4 with untagged field_offsetPTP header corrupted, FCS valid
18added latency, all phases00000

Row 2 is the whole test and rows 15 to 17 are why it has to exist. All three of those misconfigurations produce frames that are internally consistent and wrong: rows 15 and 16 emit frames every receiver discards, and row 17 emits a frame with a perfectly valid FCS and a destroyed PTP header — which no CRC check anywhere will catch.

Row 8 is the second finding. Phases 3 and 4 both split the field across two beats, which is the case a naive implementation gets wrong and which an aligned-offset test never reaches. Row 17's tagged variant is the same logic at a different offset, and it is the one a deployment actually hits.

And row 14 is Section 13's argument reduced to an experiment: the same stimulus, the same hardware, one design decision different, and the timestamps end up on the wrong frames.

21. Debugging a Timestamp Unit

Five questions, in order. The first two need no reference clock and eliminate most cases.

Step 1 — is the capture disciplined? variable_capture_latency and worst_capture_to_classify. A capture latency that is not exactly one cycle has reintroduced the jitter the whole of Chapter 16.1 was about removing, and it is a synthesis or retiming artefact that no functional test detects. The worst capture-to-classify latency is what sizes the FIFO, and a design that has not measured it has guessed.

Step 2 — are frames surviving? patch_ratio_x100 and the receiving end's frame-error counters. A ratio other than 100 means rewritten frames left with a stale FCS and every receiver discarded them — so the symptom is a Sync stream that vanishes with the transmitter's counters all healthy. The far end's c_bad_fcs, not ours, is where it shows.

Step 3 — is the pairing sound? pairing_is_sound, c_dropped, c_miss, c_evicted. Any of the three non-zero means a timestamp may have been attributed to the wrong frame, and Section 13's arithmetic says the resulting error looks exactly like a path asymmetry. This step must precede any calibration, because a calibration taken while the pairing is unreliable stores the pairing error as a constant.

Step 4 — is the configuration current? cfg_phy_delay_unset, cfg_phy_delay_stale, cfg_lane_in_ns. The stale case is the operational one: a link that renegotiated or enabled FEC has changed its PHY delay under a constant that was right at commissioning, and nothing but this check notices. The lane check catches a correction that truncated to zero.

Step 5 — what is left? cfg_phy_delay_ns against the vendor's figure, and the elastic buffer. Once steps 1 to 4 are clean, the unit's residual is a characterised constant plus Chapter 4.4's 5 to 20 ns, and any error larger than that is not in this unit — it is Chapter 16.2 §8's asymmetry or a switch in the path.

And the finding that ends an investigation here: fixed capture latency, patch_ratio_x100 at 100, pairing_is_sound high, no stale configuration, peak_occupancy well below full. That is a timestamp unit doing everything available to it — and Section 17's last row says the accuracy achieved is then somebody else's problem.

22. Common Misconceptions

1 — "One-step is impossible at high line rates."

The wrong model: there are only 0.16 ns between the timestamp field and the FCS at 100 Gb/s.

What it costs: two-step chosen on a device where one-step was the better answer, on the strength of a timing analysis that does not apply. The 0.16 ns figure is a property of the wire; the logic runs on a 512-bit datapath at 195 MHz, where the field, the pad and the FCS are all in the same 5.12 ns cycle.

The corrected model: convert wire-serial figures into cycle counts before reasoning about them. A wider datapath has more time, not less — 5.12 ns against 8-bit-at-1-Gb/s's 16 ns is comparable, and both are far more than 0.16 ns. The real constraint is a depth-6 XOR tree inside one cycle, which is tight at 25 Gb/s and comfortable at 100.

2 — "Rewriting the timestamp means recomputing the CRC."

The wrong model: a changed frame needs a fresh checksum.

What it costs: a store-and-forward buffer — 512 ns at 1 Gb/s — on a mechanism targeting 10 ns. And worse: applied to Syncs and not to Delay_Reqs, it creates 512 ns of path asymmetry and, by Chapter 16.2 §6's arithmetic, 256 ns of offset error in the act of trying to be precise.

The corrected model: Chapter 6.2's CRC is linear over GF(2), so CRC(F ⊕ Δ) = CRC(F) ⊕ CRC(Δ). The patch is a fixed 32 × 80 GF(2) matrix on the delta — about 1248 XOR2 gates, depth 6, and zero added latency. No buffer, no CRC engine.

3 — "Two-step costs a lot at scale."

The wrong model: an extra message per Sync per slave.

What it costs: one-step implemented on a device whose transmit path could not comfortably take it, to save a cost that is not there. Sync and Follow_Up are both multicast, so one Follow_Up serves every slave — the extra cost at a thousand slaves and 128 Sync/s is 128 messages per second, against a total of 131 072.

The corrected model: two-step's cost is bandwidth on a tight link — 21 504 bit/s at 16 Sync/s — and 48 octets of pending state at the slave. One-step's cost is gates and a timing path. Choose by which resource is short, and note that a master's message load is dominated by unicast Delay_Resps either way.

4 — "The timestamp goes in and the frame is done."

The wrong model: a timestamp is a value written into a field.

What it costs: Section 13's off-by-one, and it is the most damaging error in the chapter because it hides. A timestamp attributed to the wrong frame produces a t1 error equal to the interval between the two frames — on a busy link that is a few hundred nanoseconds, which looks exactly like a path asymmetry. Chapter 16.2 §8's calibration then absorbs it into a stored constant, and the constant is wrong the moment the traffic changes.

The corrected model: a timestamp is a (value, frame) pair, and the frame half needs a 16-bit tag assigned by the transmit scheduler — the only component that knows the order frames actually leave in. Order-based pairing is correct until the first overrun, drop, abort or out-of-order completion, and permanently wrong afterwards.

5 — "The PHY delay is a constant you set once."

The wrong model: characterise at commissioning and store.

What it costs: tens or hundreds of nanoseconds of silent, monotonic error. Four of the delay's components depend on the operating mode — the PCS, the block coding, FEC, and the negotiated speed — so a link that renegotiated from 10 Gb/s to 1 Gb/s, or that enabled FEC, has invalidated the constant. Chapter 16.1 §17's check catches an unset correction; nothing catches a stale one.

The corrected model: the correction is a table indexed by the live link mode, and cfg_phy_delay_stale compares the two. And the values come from the vendor's characterisation, not from tuning — a number obtained by adjusting until the offset looked right has absorbed the path's asymmetry into the PHY constant, and the two are then inseparable.

6 — "We verified the MAC, so the timestamping is verified."

The wrong model: the timestamp unit is inside the MAC.

What it costs: the rewriter and the patcher untested. Both sit downstream of the MAC's CRC generator — they must, because their premise is that a frame with a valid FCS arrives and leaves with a different valid FCS — so a testbench that drives the MAC and checks its output never reaches them.

The corrected model: verify on the PHY-side interface, with a monitor that recomputes CRC-32 from the emitted octets and compares. Section 20's row 2 is the whole test, and rows 15 to 17 show three misconfigurations that produce internally consistent, entirely wrong frames — including one with a valid FCS and a destroyed PTP header, which no checksum anywhere catches.

23. Interview Reasoning

Q1 — A Sync must carry the time it was transmitted. What is the problem and what are the two answers?

The value does not exist when the field needs it. The capture instant is the SFD, at octet −1; the originTimestamp field is at octets 48–57 — so there is 384 ns of slack at 1 Gb/s and the field is satisfiable. What is not available is the time the frame finished, which is the naive reading. Two-step sends the captured value afterwards in a Follow_Up — one extra multicast frame, unlimited tolerance to lateness, 48 octets of pending state at the slave. One-step substitutes it into the field as the frame streams and must then repair the FCS, because the CRC was computed over the old content.

Q2 — How do you fix the FCS without buffering the frame?

Exploit linearity. Chapter 6.2's CRC-32 is linear over GF(2), so CRC(F ⊕ Δ) = CRC(F) ⊕ CRC(Δ). The frame changed by Δ = old ⊕ new, which is zero outside the ten substituted octets, so the new FCS is old_fcs ⊕ CRC(Δ positioned)a fixed 32 × 80 GF(2) matrix, about 1248 XOR2 gates at depth 6, computable at elaboration exactly like Chapter 6.4's parallel CRC tables. Zero added latency, no frame buffer, no CRC engine. And the patch uses no initial value and no final complement — both cancel in a difference.

Q3 — At 100 Gb/s there are 0.16 ns between the field and the FCS. How is one-step possible?

Because the datapath is 512 bits wide. At 100 Gb/s a MAC moves 64 octets per cycle at 195 MHz, so the entire 64-octet Sync — field, pad and FCS — is one beat, and the question is not "how long between two octets" but "can the patch be computed combinationally in 5.12 ns". A depth-6 XOR tree plus two levels of muxing comfortably fits. The 0.16 ns is a statement about the wire and is meaningless about the logic — and the trap runs the other way too: at 25 Gb/s with a 64-bit datapath the budget is 2.56 ns, which is tight.

Q4 — Why does a transmit timestamp need a frame tag?

Because without one it is paired by order, and order breaks permanently on the first anomaly. A capture overrun, a FIFO drop, an aborted frame or an out-of-order transmit completion shifts every subsequent pairing by one, and nothing detects it. The resulting t1 error is the interval between the two frames: one second at 1 Sync/s — loud, the servo refuses to lock — but a few hundred nanoseconds on a busy link, which is indistinguishable from a path asymmetry and will be absorbed into a calibration that is then wrong. And the tag must come from the transmit scheduler, which is the only component that knows the order frames actually left in.

Q5 — Where does the rewriter sit, and why can't it go earlier?

Downstream of the MAC's CRC generator, before the PHY. Placing it upstream removes the need for a patcher entirely — the MAC would compute the CRC over the substituted content — but at that point the transmit timestamp does not exist yet. There are exactly two placements and they trade the patcher against the value's availability, so the patcher exists because the timestamp is only available once the frame is physically leaving. A consequence for verification: a testbench that stops at the MAC output never exercises the rewrite or the patch.

Q6 — Why can't you assert that the field holds the time the frame was sent?

Because the property is self-referential in time: the value it asserts about measures the event the assertion is part of. A field cannot contain a measurement of anything later than its own transmission, and "the time the frame was sent", read as the last octet leaving, is later. There is a second folded impossibility too: substituting the timestamp invalidates the FCS and patching the FCS depends on the substituted value, so the pre-patch stream is a frame no receiver accepts. The fix is to change the temporal reference, not the tightness: assert that the field holds the captured value, that the captured value is the SFD instant — which strictly precedes the field — and that the emitted frame's FCS is valid, plus a composition check that the patch belonged to this frame.

24. Understanding Check

25. What's Next

This chapter produced t1 and t3 correctly and built the mechanism a transparent clock needs without saying what to put in it.

Chapter 16.2's exchange now has trustworthy inputs. The capture is fixed-latency at the MAC/PHY boundary, the PHY delay is characterised and stale-checked, the pairing is tagged, and a one-step Sync leaves with a valid FCS and the right value in it. What remains is the two things this chapter deliberately did not do.

Chapter 16.4 — The Servo takes the offset Chapter 16.2 §7 computes and closes the loop on it, using Chapter 16.1 §3's rate trim and never its step — and derives the loop bandwidth from §11's noise-against-drift trade, which gave the shape and stopped short of the controller. Then it fills in Section 10's add_value: the residence time a switch measures between a frame's arrival and its departure, which is what turns Chapter 16.2 §20's 6.07 µs into something much smaller.

And Chapter 16.5 — What Limits Accuracy takes what is left. Three terms survive everything both chapters do: Chapter 16.2 §8's path asymmetry, entering at half the imbalance; Chapter 16.1 §14's quantisation at period/√12; and Chapter 4.4's elastic buffer, which this chapter named as its own floor and did not price.

One thread runs from here into both. This chapter's central trick was that a value which does not exist yet can still be delivered — by sending it later, or by rewriting the frame in flight and repairing what the rewrite broke. Chapter 16.4's residence time is exactly the same shape: a switch cannot know how long it held a frame until the frame leaves, and the mechanism it uses to report it is the one built in Section 10.

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.