Skip to content
VLSI Mentor

Ethernet · Module 16

The PTP Message Exchange

Why synchronisation needs four messages rather than one, what each equation assumes, and the half-the-imbalance error that survives every measurement.

Chapter 16.1 §5 established the asymmetry that shapes this whole chapter: drift survives an unknown path and offset does not.

A device can estimate its rate error across a path it knows nothing about, because drift is a difference of differences and a constant path delay cancels in the subtraction. It cannot estimate its phase error at all without knowing that path's delay — and the path's delay is exactly the thing a single message cannot reveal.

So the exchange has four messages, and the four exist to produce two equations in two unknowns.

One equation measures the round trip and gives the path delay. The other measures the imbalance and gives the offset. Neither works alone, and both rest on one assumption that the arithmetic cannot avoid: that the path is the same length in both directions.

It is not, and the residual is exactly half the imbalance. Ten metres of fibre length difference is 25 ns of offset error — two and a half times Chapter 16.1 §18's endpoint residual — and no amount of averaging, filtering or faster hardware touches it, because the samples are not noisy. They are consistently displaced.

1. Scope — What This Chapter Owns

This chapter owns the exchange: the message set, the common header, the four timestamps and the two equations derived from them, sequence pairing, the symmetry assumption and its cost, the peer-delay alternative, and the announce/BMCA selection of a master.

It does not own where a timestamp comes from. Chapter 16.1 §13 derived that it must be captured at the MAC/PHY boundary at Chapter 5.2's Start Frame Delimiter, and §12 sketched the capture register. This chapter assumes those timestamps exist and uses them.

It does not own how a Sync message carries its own transmit timestamp. That value is not known until the message is already leaving, and the two answers — one-step and two-step — are Chapter 16.3. Section 5 here uses two-step because it is the simpler one to present and says so.

It does not own the correction. How a slave's clock is disciplined from the offset this chapter computes, and how a switch reports the time it held a message, is Chapter 16.4. Section 7 produces the numbers that chapter consumes.

And it does not own the error budget. Section 8 establishes the symmetry assumption and prices the obvious sources; Chapter 16.5 takes the full accounting and the calibration that removes what can be removed.

2. Why Four Messages and Not One

The obvious protocol is one message. It fails, and understanding exactly how it fails is the whole design rationale.

Suppose the master sends one message saying "it is now T". The slave receives it and sets its clock to T.

The slave's clock is now wrong by the transit time, which for a 100 m link plus two PHYs plus a switch is anywhere from 700 ns to 12 µs. And the slave has no way to know what that transit time was, because the message contains no information about its own journey.

Adding a second message does not help either, if it goes the same way. Two Syncs give two (t1, t2) pairs, and their difference gives the driftChapter 16.1 §5's finding, that a constant delay cancels in the subtraction. The offset does not cancel; it is present, identically, in both samples.

So the missing information is the transit time, and the only way to measure a transit time with two clocks that disagree is to send something back.

MessagesWhat is knownWhat is not
1 — Synct2 − t1 = d_ms + offsetone equation, two unknowns
2 — two Syncsthe driftthe offset, and the path
2 — Sync + Delay_Reqt2−t1 and t4−t3two equations, two unknowns — solvable
4 — with Follow_Up and Delay_Respthe same, with the timestamps actually delivered

The third row is the protocol. The fourth row is the third row implemented, and the extra two messages exist purely to carry timestamps that could not be carried in the messages they describe.

t1 is the moment the Sync left. The Sync cannot contain it — the value is not known until transmission is under way. So either the hardware rewrites the field mid-flight (one-step) or a second message carries it afterwards (Follow_Up, two-step).

t4 is the moment the master received the Delay_Req. The Delay_Req is already gone, so the master sends it back in a Delay_Resp. There is no alternative; the slave has no other way to learn it.

Which is why the count is four and not two, and why the two extra messages are asymmetric in kind: Follow_Up is optional and one-step removes it, while Delay_Resp is structural and nothing removes it.

3. RTL 1 — The PTP Common Header

Every PTP message begins with the same 34 octets, and two of its fields do work the rest of the protocol depends on.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ptp_pkg -- shared types for the IEEE 1588 message exchange.
// -----------------------------------------------------------------------
package ptp_pkg;

  localparam logic [15:0] PTP_ETHERTYPE = 16'h88F7;
  // Two reserved multicast groups: one for messages a transparent clock
  // must NOT forward unchanged, one for the rest.
  localparam logic [47:0] PTP_PRIMARY_DA = 48'h01_1B_19_00_00_00;
  localparam logic [47:0] PTP_PDELAY_DA  = 48'h01_80_C2_00_00_0E;

  typedef enum logic [3:0] {
    MSG_SYNC          = 4'h0,
    MSG_DELAY_REQ     = 4'h1,
    MSG_PDELAY_REQ    = 4'h2,
    MSG_PDELAY_RESP   = 4'h3,
    MSG_FOLLOW_UP     = 4'h8,
    MSG_DELAY_RESP    = 4'h9,
    MSG_PDELAY_RESP_FU= 4'hA,
    MSG_ANNOUNCE      = 4'hB,
    MSG_SIGNALING     = 4'hC,
    MSG_MANAGEMENT    = 4'hD
  } msg_type_e;

  // Messages 0x0..0x3 are EVENT messages: they are timestamped on
  // transmit and on receive. Everything else is a GENERAL message and
  // carries no timestamp of its own. The distinction decides which
  // messages 16.3's timestamp unit must act on.
  function automatic bit is_event(input msg_type_e t);
    is_event = (t[3] == 1'b0);
  endfunction

  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;

  // A port's identity: an 8-octet clock identity plus a 2-octet port
  // number. Every message says who sent it, and the pairing in
  // section 9 depends on it.
  typedef struct packed {
    logic [63:0] clock_id;
    logic [15:0] port_num;
  } port_id_t;

  // correctionField is signed nanoseconds in a 16.16 fixed-point form:
  // the low 16 bits are fractional nanoseconds. 16.4's transparent
  // clock accumulates residence time here.
  typedef logic signed [63:0] correction_t;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ptp_header_parser -- recognises a PTP message and extracts the common
// header. It is a filter first and a parser second.
// -----------------------------------------------------------------------
module ptp_header_parser
  import ptp_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        rx_valid,
  input  logic [7:0]  rx_octet,
  input  logic        rx_sop,
  input  logic        rx_eop,
  input  logic        rx_fcs_ok,
  input  logic        rx_tagged,      // 13.2's tag shifts every offset

  output logic        hdr_valid,
  output msg_type_e   msg_type,
  output logic        is_event_msg,
  output logic [15:0] msg_length,
  output logic [7:0]  domain,
  output logic [15:0] flags,
  output correction_t correction,
  output port_id_t    src_port,
  output logic [15:0] seq_id,

  output logic [31:0] c_by_type [16],
  output logic [31:0] c_wrong_domain,
  output logic [31:0] c_bad_fcs
);

  // 13.2 section 7: a tag moves every offset after octet 12 by four.
  // A parser with hard-coded offsets silently mis-parses a tagged PTP
  // frame, which is legal and common on a trunk.
  logic [7:0] base;
  assign base = rx_tagged ? 8'd18 : 8'd14;

  logic [7:0] idx;
  logic       in_frame, et_ok;
  logic [15:0] et_q;

  msg_type_e   mt_q;
  logic [15:0] len_q, flg_q, seq_q;
  logic [7:0]  dom_q;
  correction_t corr_q;
  port_id_t    sp_q;

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      idx <= '0; in_frame <= 1'b0; et_ok <= 1'b0; et_q <= '0;
      hdr_valid <= 1'b0; c_wrong_domain <= '0; c_bad_fcs <= '0;
      for (i = 0; i < 16; i++) c_by_type[i] <= '0;
    end else begin
      hdr_valid <= 1'b0;

      if (rx_valid && rx_sop) begin
        idx <= 8'd1; in_frame <= 1'b1; et_ok <= 1'b0;
      end else if (rx_valid && in_frame) begin
        idx <= idx + 1'b1;

        if (idx == (base - 2)) et_q[15:8] <= rx_octet;
        if (idx == (base - 1)) et_ok <= ({et_q[15:8], rx_octet} == PTP_ETHERTYPE);

        if (idx == base + 8'd0)  mt_q  <= msg_type_e'(rx_octet[3:0]);
        if (idx == base + 8'd2)  len_q[15:8] <= rx_octet;
        if (idx == base + 8'd3)  len_q[7:0]  <= rx_octet;
        if (idx == base + 8'd4)  dom_q <= rx_octet;
        if (idx == base + 8'd6)  flg_q[15:8] <= rx_octet;
        if (idx == base + 8'd7)  flg_q[7:0]  <= rx_octet;

        // correctionField, octets 8..15 of the header.
        if ((idx >= base + 8'd8) && (idx <= base + 8'd15))
          corr_q[63 - 8*(idx - base - 8)-: 8] <= rx_octet;

        // sourcePortIdentity, octets 20..29.
        if ((idx >= base + 8'd20) && (idx <= base + 8'd27))
          sp_q.clock_id[63 - 8*(idx - base - 20) -: 8] <= rx_octet;
        if (idx == base + 8'd28) sp_q.port_num[15:8] <= rx_octet;
        if (idx == base + 8'd29) sp_q.port_num[7:0]  <= rx_octet;

        if (idx == base + 8'd30) seq_q[15:8] <= rx_octet;
        if (idx == base + 8'd31) seq_q[7:0]  <= rx_octet;
      end

      if (rx_valid && rx_eop) begin
        in_frame <= 1'b0;
        if (et_ok) begin
          if (!rx_fcs_ok) c_bad_fcs <= c_bad_fcs + 1;
          else begin
            c_by_type[mt_q] <= c_by_type[mt_q] + 1;
            hdr_valid    <= 1'b1;
            msg_type     <= mt_q;
            is_event_msg <= is_event(mt_q);
            msg_length   <= len_q;
            domain       <= dom_q;
            flags        <= flg_q;
            correction   <= corr_q;
            src_port     <= sp_q;
            seq_id       <= seq_q;
          end
        end
      end
    end
  end

endmodule

Classification: a receive-side header decoder with a tag-aware offset base. One pass, no state beyond the current frame.

What it teaches: that the event/general split is a property of the message type's top bit and it decides the entire hardware interface. Message types 0x0 to 0x3 are event messages — they are timestamped on transmit and receive — and everything else is general and carries no timestamp of its own. A timestamp unit does not need to parse PTP; it needs one bit, and is_event(t) = !t[3] is that bit. Chapter 16.3's unit sits in the datapath and must decide in a handful of cycles whether to capture, and this encoding is why it can.

And it teaches that correctionField is in the common header rather than in any message body, which is the structural decision that makes transparent clocks possible. A switch that wants to report how long it held a message does not need to know what kind of message it is — it adds to a field at a fixed offset, present in all of them. Chapter 16.4 §12 is the consequence.

Deliberately simplified: rx_tagged is an input, and a real parser must determine it from the frame. Chapter 13.2 §7 established that a tag moves every offset after octet 12 by four, and a PTP parser with hard-coded untagged offsets silently mis-parses every tagged PTP frame — reading the message type from the middle of the header, matching nothing, and counting nothing. On a trunk port that is every PTP frame.

Production implication: c_wrong_domain exists because PTP domains are the mechanism by which two independent synchronisation systems share a network, and a device listening on the wrong domain hears everything and uses none of it. The symptom is a slave that never locks with a healthy link and rising c_by_type counters — which looks like a protocol fault and is a single-octet configuration difference.

4. The Message Set, Field by Field

Ten message types, of which five carry the exchange and the rest are infrastructure.

MessageTypeEvent?BodyFrameWirePurpose
Sync0x0yes106484carries t1, or announces that a Follow_Up will
Delay_Req0x1yes106484its transmission is t3; its arrival is t4
Pdelay_Req0x2yes207292the peer-delay alternative — Section 12
Pdelay_Resp0x3yes207292
Follow_Up0x8no106484carries t1 when the hardware could not
Delay_Resp0x9no207292carries t4 back to the slave
Pdelay_Resp_Follow_Up0xAno207292
Announce0xBno3082102the master's credentials — Section 14
Signaling0xCnovariesnegotiation of rates
Management0xDnovariesout of scope

Three of these are 64-octet frames and the reason is Chapter 5.6's minimum payload: a 34-octet header plus a 10-octet body is 44 octets, two short of the 46 minimum, so two octets of pad make the frame 64. The protocol's smallest messages are at Ethernet's floor.

And the event/general column is the one that shapes the hardware.

An event message is timestamped both on transmit and on receive. Its arrival on the wire is the thing being measured, so Chapter 16.1 §13's capture point applies to it and to nothing else.

A general message carries data about a previous event and is not itself timed. A Follow_Up may be delayed by a millisecond and lose nothing — it is carrying a number, and the number does not decay. Which is exactly Chapter 16.1's distinction between a fact about the transport and a fact carried over it, appearing here as a two-way split in the message set.

==

PTP message types zero through three are event messages: Sync, Delay Request, Peer Delay Request and Peer Delay Response. Their arrival on the wire is the quantity being measured, so they must be timestamped at the MAC-PHY boundary on the Start Frame Delimiter. Message types eight and above are general messages: Follow Up, Delay Response, Announce, Signaling and Management. They carry data about a previous event and are not themselves timed, so a Follow Up delayed by a millisecond loses nothing because it carries a number and numbers do not decay. The split is exactly the top bit of the message type field, which is why a timestamp unit in the datapath needs one bit rather than a PTP parser.messageType4 bits0x0 - 0x3: EVENTtop bit clearTimed on the wire16.1's SFD captureLateness is fatalthe event IS the value0x8+: GENERALtop bit setCarries a numbernot timed itselfLateness is freenumbers do not decay12
Figure 2 — the message set split by whether a message is itself timed, and what that split means for hardware.

So the load on the timestamp unit is smaller than the message count suggests:

At 16 Sync/s and 16 Delay_Req/sMessages/sTimestamped
slave, two-step4932 — Sync RX and Delay_Req TX
slave, one-step3332 — the same
master, one slave4932
master, 1024 slaves at 128/s131 328>16 000

The last row is the number Chapter 16.1 §6's callout predicted, and it is why a large deployment's oscillator choice becomes the master's processing requirement. A fabric of a thousand slaves on 100 ppm crystals needs a master generating and timestamping over a hundred thousand messages a second; the same fabric on OCXOs needs a few thousand.

5. RTL 2 — Sync and Follow_Up

The slave's receive path for the master-to-slave half of the exchange. It holds two values that arrive in different messages and must be matched.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// sync_followup_tracker -- pairs a Sync's RECEIVE timestamp with the
// TRANSMIT timestamp that arrives afterwards.
//
// Two-step is used here because it is the simpler shape to present:
// the Sync's own t1 field is ignored and a Follow_Up carries it. One-
// step removes the second message and is chapter 16.3's subject.
// -----------------------------------------------------------------------
module sync_followup_tracker
  import ptp_pkg::*;
#(
  parameter int PENDING = 4          // outstanding Syncs awaiting a Follow_Up
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        hdr_valid,
  input  msg_type_e   msg_type,
  input  logic [15:0] flags,
  input  logic [15:0] seq_id,
  input  port_id_t    src_port,
  input  correction_t correction,
  input  ts_t         body_ts,        // originTimestamp or preciseOrigin
  input  ts_t         rx_hw_ts,       // 16.1 section 12's capture

  input  port_id_t    cfg_master,     // whose Syncs we honour

  output logic        pair_valid,
  output logic [15:0] pair_seq,
  output ts_t         t1,
  output ts_t         t2,
  output correction_t t1_correction,
  output logic [31:0] c_sync,
  output logic [31:0] c_followup,
  output logic [31:0] c_orphan_fu,    // a Follow_Up with no Sync
  output logic [31:0] c_stale_sync    // a Sync evicted unmatched
);

  typedef struct packed {
    logic        busy;
    logic [15:0] seq;
    ts_t         t2;
    logic        twostep;
  } slot_t;

  slot_t slots [PENDING];
  logic [$clog2(PENDING)-1:0] wp;

  // flagField bit 9 (twoStepFlag) says a Follow_Up is coming. A design
  // that ignores it and always waits will stall on a one-step master;
  // one that ignores it and never waits will use an empty t1 field.
  logic two_step;
  assign two_step = flags[9];

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < PENDING; i++) slots[i] <= '0;
      wp <= '0; pair_valid <= 1'b0;
      c_sync <= '0; c_followup <= '0;
      c_orphan_fu <= '0; c_stale_sync <= '0;
    end else begin
      pair_valid <= 1'b0;

      if (hdr_valid && (src_port.clock_id == cfg_master.clock_id)) begin
        unique case (msg_type)
          MSG_SYNC: begin
            c_sync <= c_sync + 1;
            if (!two_step) begin
              // One-step: t1 is in this frame's own body, already
              // rewritten by the master's hardware. Publish at once.
              pair_valid    <= 1'b1;
              pair_seq      <= seq_id;
              t1            <= body_ts;
              t2            <= rx_hw_ts;
              t1_correction <= correction;
            end else begin
              // Two-step: park t2 and wait.
              if (slots[wp].busy) c_stale_sync <= c_stale_sync + 1;
              slots[wp].busy    <= 1'b1;
              slots[wp].seq     <= seq_id;
              slots[wp].t2      <= rx_hw_ts;
              slots[wp].twostep <= 1'b1;
              wp <= wp + 1'b1;
            end
          end

          MSG_FOLLOW_UP: begin
            automatic bit hit;
            c_followup <= c_followup + 1;
            hit = 1'b0;
            for (i = 0; i < PENDING; i++) begin
              if (slots[i].busy && (slots[i].seq == seq_id)) begin
                hit           = 1'b1;
                pair_valid    <= 1'b1;
                pair_seq      <= seq_id;
                t1            <= body_ts;
                t2            <= slots[i].t2;
                t1_correction <= correction;
                slots[i].busy <= 1'b0;
              end
            end
            if (!hit) c_orphan_fu <= c_orphan_fu + 1;
          end

          default: ;
        endcase
      end
    end
  end

endmodule

Classification: a small associative buffer keyed on sequence ID, with a one-step bypass. Four slots, one match per message.

What it teaches: that the two-step flag must be honoured rather than assumed, and both wrong assumptions produce a slave that never locks. A design that always waits for a Follow_Up stalls for ever against a one-step master — c_sync rises, c_followup stays at zero, and no pair is ever published. A design that never waits reads the Sync's originTimestamp field, which a two-step master transmits as zero, and computes an offset of roughly the current epoch.

And it teaches why the pending buffer needs more than one slot. A Sync and its Follow_Up are two frames, and a second Sync can be transmitted before the first's Follow_Up arrives — legally, at any Sync rate, and inevitably when the two messages take different paths through a switch's queues. A single-slot design silently drops the older pair, and c_stale_sync is how that becomes visible rather than mysterious.

Deliberately simplified: the slot search is a linear scan over four entries, evaluated combinationally in one cycle. At 128 Sync/s this is comfortable; a boundary clock serving a thousand slaves needs a proper CAM or a hash, and the sequence ID is 16 bits, so the natural structure is Chapter 12.5's set-associative table indexed by the low bits.

Production implication: c_orphan_fu is the counter that separates the master is not sending Follow_Ups from we are dropping Syncs. A Follow_Up arriving with no matching Sync means the Sync was lost or was never parsed — a tagged frame against an untagged parser, Section 3's simplification — and the two have completely different remedies. Without the counter both present as a slave that does not lock.

6. The Four Timestamps, Derived

Two equations fall out of four measurements, and it is worth doing the algebra rather than quoting the result, because the assumption enters at a specific line.

Define:

offset = the slave's clock minus the master's clock. Positive means the slave is ahead.

d_ms = the one-way delay from master to slave. d_sm = the one-way delay from slave to master.

The Sync leaves the master at t1 on the master's clock and arrives at the slave at t2 on the slave's clock:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
t2 = t1 + d_ms + offset
  so  t2 - t1 = d_ms + offset                         ... (1)

The Delay_Req leaves the slave at t3 on the slave's clock and arrives at the master at t4 on the master's:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
t4 = t3 - offset + d_sm
  so  t4 - t3 = d_sm - offset                         ... (2)

Add them, and the offset cancels:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(t2 - t1) + (t4 - t3) = d_ms + d_sm = the round trip  ... (3)

Subtract them, and the round trip does not:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
(t2 - t1) - (t4 - t3) = d_ms - d_sm + 2 * offset      ... (4)

Equation (3) is a genuine measurement. It is made entirely of four numbers two clocks observed, and it is correct regardless of what the offset is or how asymmetric the path is. The round-trip delay is known.

Equation (4) has two unknowns in it — the offset, and the difference between the two one-way delays — and four timestamps cannot separate them.

So the protocol assumes d_ms = d_sm, at which point:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mean_path = ((t2 - t1) + (t4 - t3)) / 2               ... from (3)
offset    = ((t2 - t1) - (t4 - t3)) / 2               ... from (4)

And the error in the second is exactly:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
offset_error = (d_sm - d_ms) / 2

==

The master transmits a Sync at t1 on its own clock and the slave receives it at t2 on the slave's clock, giving t2 minus t1 equals the master-to-slave delay plus the offset. The slave transmits a Delay Request at t3 and the master receives it at t4, giving t4 minus t3 equals the slave-to-master delay minus the offset. Adding the two cancels the offset and yields the round trip, which is a genuine measurement made entirely of observed values and carries no assumption. Subtracting the two leaves both the offset and the difference between the one-way delays, which four timestamps cannot separate, so the protocol assumes the two one-way delays are equal. Under that premise the offset is half the difference; when the premise fails the offset is wrong by exactly half the imbalance.t1 — master TX Syncmaster's clockt2 — slave RX Syncslave's clockt2 - t1 = d_ms +offsetone equationSum: the round tripoffset cancels — EXACTt3 — slave TXDelay_Reqslave's clockt4 — master RXmaster's clockt4 - t3 = d_sm -offsetthe secondDifference: offsetonly if d_ms = d_smError = (d_sm -d_ms)/2half the imbalance12
Figure 1 — four timestamps, two equations, and the one line where a premise enters.

Half the imbalance. Not a fraction of it that depends on the traffic, not something that averages down, not something a better clock improves — exactly half, deterministically, for as long as the imbalance exists.

ImbalanceOffset error
1 m of fibre — 5 ns2.5 ns
10 m of fibre — 50 ns25 ns
100 m of fibre — 500 ns250 ns
asymmetric PHY TX vs RX — 40 ns20 ns
a switch that queued 12.14 µs one way6.07 µs

The last row is why Chapter 16.1 §15 ended where it did. A store-and-forward switch that holds a Sync for a full frame time and a Delay_Req for almost none is an asymmetry of 12.14 µs at 1 Gb/s, and the offset is wrong by half of it — six microseconds, from one switch, on a system whose endpoints are good to ten nanoseconds.

7. RTL 3 — Computing Offset and Path Delay

Section 6's algebra in hardware, with the correction fields folded in and the sign conventions made explicit.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// offset_delay_calc -- turns four timestamps into an offset and a mean
// path delay.
//
// The correctionField from each message is SUBTRACTED from the
// corresponding interval: it holds the residence time a transparent
// clock accumulated, which is delay that did not happen on the wire.
// 16.4 section 12 is where those values come from.
// -----------------------------------------------------------------------
module offset_delay_calc
  import ptp_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        ms_valid,        // a (t1,t2) pair from section 5
  input  ts_t         t1,
  input  ts_t         t2,
  input  correction_t corr_ms,

  input  logic        sm_valid,        // a (t3,t4) pair from section 11
  input  ts_t         t3,
  input  ts_t         t4,
  input  correction_t corr_sm,

  output logic        result_valid,
  output logic signed [63:0] offset_ns,
  output logic signed [63:0] mean_path_ns,
  output logic signed [63:0] round_trip_ns,
  output logic        negative_path,    // the round trip came out < 0
  output logic [31:0] c_results,
  output logic [31:0] c_negative
);

  // Both halves must be present before either equation is evaluated.
  // A design that computes an offset from a fresh (t1,t2) and a stale
  // (t3,t4) has mixed two path measurements -- section 10.
  logic ms_have, sm_have;
  logic signed [63:0] d_ms, d_sm;

  function automatic logic signed [63:0] ts_diff(input ts_t a, input ts_t b);
    ts_diff = ($signed(64'(a.sec)) - $signed(64'(b.sec))) * 64'sd1_000_000_000
            + ($signed(64'(a.ns))  - $signed(64'(b.ns)));
  endfunction

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      ms_have <= 1'b0; sm_have <= 1'b0;
      d_ms <= '0; d_sm <= '0;
      result_valid <= 1'b0; offset_ns <= '0;
      mean_path_ns <= '0; round_trip_ns <= '0;
      negative_path <= 1'b0; c_results <= '0; c_negative <= '0;
    end else begin
      result_valid  <= 1'b0;
      negative_path <= 1'b0;

      // correctionField is 16.16 fixed point; take its integer part.
      if (ms_valid) begin
        d_ms    <= ts_diff(t2, t1) - (corr_ms >>> 16);
        ms_have <= 1'b1;
      end
      if (sm_valid) begin
        d_sm    <= ts_diff(t4, t3) - (corr_sm >>> 16);
        sm_have <= 1'b1;
      end

      if (ms_have && sm_have && (ms_valid || sm_valid)) begin
        automatic logic signed [63:0] a, b, rt;
        a  = ms_valid ? (ts_diff(t2, t1) - (corr_ms >>> 16)) : d_ms;
        b  = sm_valid ? (ts_diff(t4, t3) - (corr_sm >>> 16)) : d_sm;
        rt = a + b;

        round_trip_ns <= rt;
        // Equation (3): the round trip, with no assumption in it.
        mean_path_ns  <= rt >>> 1;
        // Equation (4): the offset, ASSUMING d_ms == d_sm.
        offset_ns     <= (a - b) >>> 1;
        result_valid  <= 1'b1;
        c_results     <= c_results + 1;

        // A negative round trip is arithmetically impossible and
        // therefore evidence about the INPUTS -- section 10.
        if (rt < 0) begin
          negative_path <= 1'b1;
          c_negative    <= c_negative + 1;
        end
      end
    end
  end

endmodule

Classification: two subtractions, an add, a subtract and two arithmetic shifts. The cheapest module in the chapter and the one that carries the assumption.

What it teaches: that the correction field is subtracted, not added, and the sign convention is the most commonly inverted thing in a PTP implementation. correctionField accumulates the time a message spent inside switches — residence time, which is delay that did not happen on the wire and must not be attributed to the path. Subtracting it gives the true wire delay; adding it doubles the error the transparent clocks were correcting. The symptom of the inversion is an offset that gets worse when transparent clocks are enabled.

And it teaches that negative_path is a diagnostic rather than an error case. (t2−t1) + (t4−t3) is a physical round trip and cannot be negative, so a negative result means one of the four timestamps is wrong — a stale pair matched to a fresh one, a Follow_Up matched to the wrong Sync, or a clock that was stepped between t2 and t3. The arithmetic detects an input fault it cannot otherwise see.

Deliberately simplified: ts_diff multiplies a seconds difference by 10⁹, which for a 48-bit seconds field is a 64-bit multiply. In practice the two timestamps of a pair are within a second of each other almost always, so a production design compares the seconds fields first and takes a fast path. The slow path still has to exist, because the first pair after a cold start spans an arbitrary interval.

Production implication: the two shifts are arithmetic right shifts on signed values, so they round toward negative infinity rather than toward zero. On a single sample that is half a nanosecond and irrelevant; on a servo integrating millions of samples it is a systematic bias of −0.5 nsChapter 16.1 §11's point that a one-sided error has a removable mean, appearing here as an artefact of the arithmetic rather than of the physics. Round-to-nearest costs one adder.

8. The Assumption the Arithmetic Cannot Avoid

Section 6 introduced d_ms = d_sm in one line and everything after it depends on that line. It is worth being precise about what kind of statement it is.

It is not a measurement. It is not an approximation with an error bar. It is the only way to get two numbers out of one equation, and the protocol adopts it because there is no alternative available to four timestamps.

And it is worth showing that no fifth or sixth message helps.

Additional measurementWhat it addsDoes it separate d_ms from d_sm?
more Syncsmore (t1,t2) pairsno — every one has d_ms + offset
more Delay_Reqsmore (t3,t4) pairsno — every one has d_sm − offset
averaging bothless noiseno — the bias is in every sample
a faster clockfiner resolutionno
a third devicetwo more pathsno — three more unknowns with them
measuring the cablethe physical lengthyes — and it is not a network measurement

Row three is the one that matters operationally, and Section 22's fourth misconception is exactly it. Averaging reduces the variance of the offset estimate and leaves its mean displaced by (d_sm − d_ms)/2, because every sample is displaced by the same amount. A thousand samples give a beautifully tight estimate of the wrong number.

Row six is the honest answer and it is a commissioning procedure rather than a protocol feature. Measure the asymmetry once, with a known-good reference or with a physical cable measurement, store it as a constant, and subtract it. That is what a calibrated deployment does and an uncalibrated one does not — Chapter 16.5 §11.

Which gives the chapter's central claim in its strongest form:

The PTP exchange measures the round trip exactly and the offset only up to half the path's asymmetry. The first is a measurement; the second is a measurement plus a premise. And the premise is about cables and queues, which the protocol has no access to.

9. RTL 4 — Sequence IDs and Pairing

Four timestamps are useless unless they belong to the same exchange, and nothing about their arrival order guarantees that.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// seqid_pairing_tracker -- matches a Delay_Resp back to the Delay_Req
// it answers, and detects the pairings that silently go wrong.
//
// The sequenceId is 16 bits and wraps. The requestingPortIdentity in a
// Delay_Resp names WHICH slave asked, which matters because the
// Delay_Resp is multicast on most deployments and every slave sees it.
// -----------------------------------------------------------------------
module seqid_pairing_tracker
  import ptp_pkg::*;
#(
  parameter int OUTSTANDING = 8,
  parameter int TIMEOUT_CYCLES = 2_500_000_000/1000   // 5 ms at 500 MHz
)(
  input  logic        clk,
  input  logic        rst_n,

  // Our own Delay_Req, as it leaves.
  input  logic        dreq_sent,
  input  logic [15:0] dreq_seq,
  input  ts_t         dreq_t3,

  // An arriving Delay_Resp.
  input  logic        dresp_valid,
  input  logic [15:0] dresp_seq,
  input  port_id_t    dresp_requesting,
  input  ts_t         dresp_t4,
  input  correction_t dresp_correction,

  input  port_id_t    cfg_self,

  output logic        sm_valid,
  output ts_t         t3,
  output ts_t         t4,
  output correction_t corr_sm,
  output logic [31:0] c_matched,
  output logic [31:0] c_not_for_us,
  output logic [31:0] c_unmatched,
  output logic [31:0] c_timed_out
);

  typedef struct packed {
    logic        busy;
    logic [15:0] seq;
    ts_t         t3;
    logic [31:0] age;
  } entry_t;

  entry_t q [OUTSTANDING];
  logic [$clog2(OUTSTANDING)-1:0] wp;

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < OUTSTANDING; i++) q[i] <= '0;
      wp <= '0; sm_valid <= 1'b0;
      c_matched <= '0; c_not_for_us <= '0;
      c_unmatched <= '0; c_timed_out <= '0;
    end else begin
      sm_valid <= 1'b0;

      // Age every outstanding request; a Delay_Resp that never comes
      // must free its slot or the tracker fills and stops matching.
      for (i = 0; i < OUTSTANDING; i++) begin
        if (q[i].busy) begin
          q[i].age <= q[i].age + 1;
          if (q[i].age == TIMEOUT_CYCLES-1) begin
            q[i].busy   <= 1'b0;
            c_timed_out <= c_timed_out + 1;
          end
        end
      end

      if (dreq_sent) begin
        q[wp].busy <= 1'b1;
        q[wp].seq  <= dreq_seq;
        q[wp].t3   <= dreq_t3;
        q[wp].age  <= '0;
        wp <= wp + 1'b1;
      end

      if (dresp_valid) begin
        // Gate 1 -- is this answering US? A multicast Delay_Resp is
        // seen by every slave on the segment and answers one of them.
        if (dresp_requesting.clock_id != cfg_self.clock_id) begin
          c_not_for_us <= c_not_for_us + 1;
        end else begin
          automatic bit hit;
          hit = 1'b0;
          for (i = 0; i < OUTSTANDING; i++) begin
            if (q[i].busy && (q[i].seq == dresp_seq)) begin
              hit       = 1'b1;
              sm_valid  <= 1'b1;
              t3        <= q[i].t3;
              t4        <= dresp_t4;
              corr_sm   <= dresp_correction;
              q[i].busy <= 1'b0;
              c_matched <= c_matched + 1;
            end
          end
          if (!hit) c_unmatched <= c_unmatched + 1;
        end
      end
    end
  end

endmodule

Classification: an outstanding-transaction tracker with an age-out. Eight entries, one match per response.

What it teaches: that requestingPortIdentity is a gate and not a field to record. On most deployments the Delay_Resp is sent to the same multicast group as everything else, so every slave on the segment receives every other slave's answer — and a design that matches on sequence ID alone will happily pair its own t3 with another slave's t4 whenever the two sequence numbers coincide. With 16-bit sequence IDs and a dozen slaves that is not rare; it is routine.

And it teaches that the age-out is what keeps the tracker alive under loss. A Delay_Resp that never arrives holds a slot for ever, and eight lost responses stop the mechanism permanently with no counter moving — the slave keeps sending Delay_Reqs, keeps receiving nothing it can match, and reports a stale path delay indefinitely. c_timed_out turns a silent stall into a rate.

Deliberately simplified: the slot scan is linear over eight entries and the age counter is per entry. A boundary clock tracking a thousand slaves needs one tracker per port, not one per device, and the natural structure is again Chapter 12.5's set-associative table — with the interesting difference that a collision here is a correctness failure rather than a capacity one, so associativity must cover the worst case rather than the typical one.

Production implication: c_not_for_us rising fast is normal on a multicast deployment and is itself a useful measurement: it counts the other slaves on this segment. A device seeing zero is either alone or is on a unicast deployment; one seeing thousands per second is sharing a segment with a large population, which is the first thing to know when the master's load is in question — Section 4's last row.

10. What a Lost Message Costs

Every message in the exchange can be lost, and the four losses have four different consequences.

LostImmediate effectRecoveryCounter
Syncno fresh (t1,t2)the next Syncc_sync gap
Follow_Upt2 is parked and never usedthe next Sync/Follow_Up pairc_stale_sync
Delay_Reqthe master never answersthe next Delay_Reqc_timed_out
Delay_Respt3 is held with no t4the next exchangec_timed_out

Every row recovers on the next exchange, which is the protocol's most important structural property and it is the same one Chapter 14.4 §2 and Chapter 15.3 §24 both arrived at: periodic complete state rather than transactional exchange.

A lost PTP message leaves nothing incomplete. There is no retransmission, no acknowledgement and no state machine waiting for something that will not come — the pairing ages out, the slot frees, and the next Sync starts over.

What the loss costs is not correctness. It is drift, accumulated for one extra interval:

Sync rateIntervalDrift at 100 ppmat 20 ppmat 0.1 ppm
1/s1000 ms100 µs20 µs0.1 µs
16/s62.5 ms6.25 µs1.25 µs0.006 µs
128/s7.81 ms0.78 µs0.16 µs0.001 µs

Read the first column against a 1 µs requirement and the message rate's real job appears. It is not to keep the offset estimate fresh for its own sake — it is to bound the damage a single lost message does. At 1/s on a commodity crystal, one lost Sync puts the slave 100 µs out and the requirement is violated until the next one lands.

Which is a different argument for a fast rate than Chapter 16.1 §6's. That one said the rate must cover the drift between messages. This one says it must cover the drift between messages that arrive, and on a lossy path those are not the same interval. A design sizing its Sync rate from the nominal interval has assumed a loss rate of zero.

And the loss rate is not zero, for a specific reason. PTP messages are small, low-rate and — on a network without Chapter 13.4 §9's priority mapping configured for them — in the same queue as bulk traffic. Chapter 14.1's drop is indifferent to what a frame carries. A Sync discarded by a congested egress is a Sync lost, and the congestion that caused it is exactly when accurate time matters.

11. RTL 5 — Delay_Req and Delay_Resp

The slave-to-master half. Its shape is dictated by one fact: the slave must transmit a message whose only purpose is to be timestamped by somebody else.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// delay_req_resp_engine -- issues Delay_Req messages at a randomised
// interval and consumes the Delay_Resp that answers them.
//
// The randomisation is not cosmetic. Every slave on a segment that
// issues Delay_Req on a fixed period synchronised to the Sync it just
// received will transmit at the SAME instant, and the master receives
// N requests in one burst -- section 12's callout.
// -----------------------------------------------------------------------
module delay_req_resp_engine
  import ptp_pkg::*;
#(
  parameter int CLK_HZ = 500_000_000,
  parameter int LOG_INTERVAL = 0        // 2^0 = 1 s between requests
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        sync_received,     // a fresh Sync landed
  input  logic [15:0] rand_bits,         // from an LFSR
  input  logic        tx_ready,

  output logic        dreq_tx,
  output logic [15:0] dreq_seq,
  input  ts_t         dreq_hw_ts,        // 16.3's TX capture
  input  logic        dreq_hw_ts_valid,

  output logic        dreq_sent,
  output logic [15:0] dreq_sent_seq,
  output ts_t         dreq_t3,

  output logic [31:0] c_issued,
  output logic [31:0] c_no_tx_ts,
  output logic [31:0] interval_ms
);

  localparam int NOMINAL = CLK_HZ;                 // 2^LOG_INTERVAL sec
  localparam int PERIOD  = NOMINAL <<< LOG_INTERVAL;

  logic [31:0] cnt, target;
  logic [15:0] seq_q;
  logic        awaiting_ts;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      cnt <= '0; seq_q <= '0; dreq_tx <= 1'b0;
      awaiting_ts <= 1'b0; dreq_sent <= 1'b0;
      c_issued <= '0; c_no_tx_ts <= '0;
      // The first interval is randomised too, so a rack of devices
      // powering up together does not stay in lockstep.
      target <= PERIOD;
      interval_ms <= '0;
    end else begin
      dreq_tx   <= 1'b0;
      dreq_sent <= 1'b0;

      cnt <= cnt + 1;
      if (cnt >= target) begin
        if (tx_ready && !awaiting_ts) begin
          dreq_tx     <= 1'b1;
          dreq_seq    <= seq_q;
          seq_q       <= seq_q + 1'b1;
          awaiting_ts <= 1'b1;
          c_issued    <= c_issued + 1;
          cnt         <= '0;
          // Uniform over [0.75, 1.25] of the nominal period. The
          // standard requires the mean to be the configured interval
          // and the individual intervals to be spread.
          target      <= PERIOD - (PERIOD >> 2)
                       + ((PERIOD >> 1) * 32'(rand_bits)) / 32'd65536;
          interval_ms <= target / (CLK_HZ / 1000);
        end
      end

      // The TRANSMIT timestamp arrives from the hardware after the
      // frame has left. Until it does, t3 does not exist -- which is
      // the same structural problem 16.3 solves for Sync.
      if (awaiting_ts) begin
        if (dreq_hw_ts_valid) begin
          awaiting_ts   <= 1'b0;
          dreq_sent     <= 1'b1;
          dreq_sent_seq <= seq_q - 1'b1;
          dreq_t3       <= dreq_hw_ts;
        end else if (cnt == target) begin
          // A whole interval with no transmit timestamp: the capture
          // was lost. The request is unusable and must be abandoned.
          awaiting_ts <= 1'b0;
          c_no_tx_ts  <= c_no_tx_ts + 1;
        end
      end
    end
  end

endmodule

Classification: a randomised periodic transmitter with an outstanding-timestamp interlock.

What it teaches: that the randomised interval prevents a synchronised burst, and the failure it prevents is a self-inflicted denial of service. Every slave on a segment receives the same multicast Sync at the same instant. A slave that issues its Delay_Req a fixed delay afterwards will collide with every other slave doing the same — so the master receives N Delay_Reqs within microseconds, timestamps them all, and must generate N Delay_Resps in a burst. At Section 4's thousand slaves that is a thousand messages in one window, on a device sized for an average.

And it teaches that t3 does not exist when the Delay_Req is sent. The engine transmits, then waits for its own hardware to report when the frame actually leftChapter 16.1 §12's capture, on the transmit side. This is the same structural problem a Sync has and the reason Section 2 said the count is four: a message cannot contain the time of its own transmission. Delay_Req solves it by not tryingt3 never goes on the wire, it stays at the slave.

Deliberately simplified: rand_bits is an input and a real design needs an LFSR seeded differently per device. Seeding every device identically re-creates the burst the randomisation was added to prevent, and the usual seed source is the low bits of the clock identity, which is the device's MAC address and is guaranteed distinct.

Production implication: c_no_tx_ts catches a transmit-timestamp capture that was lost — Chapter 16.1 §12's overrun, seen from the protocol side. Without the interlock the engine would issue a second Delay_Req while the first's timestamp was still outstanding, and the arriving Delay_Resp would be matched to a t3 belonging to the wrong frame. The resulting path delay is wrong by the interval between the two requests — up to a second — and every check in Section 7 passes.

12. Peer Delay Against End-to-End Delay

The exchange in Sections 6 and 11 measures the delay from the slave to the master, across however many switches lie between. There is a second mechanism that measures each link instead, and the difference is architectural rather than cosmetic.

end-to-end — Delay_Req/Resppeer delay — Pdelay_Req/Resp
what is measuredslave to master, whole pathone link, between neighbours
who participatesthe two endpointsevery device, with its neighbour
messages per measurement22 or 3
load at the masterN slaves × ratenone — it is peer to peer
a switch in the pathmust be a transparent clockmust be a peer-delay-capable clock
asymmetrythe whole path'seach link's, separately
what a topology change costsevery slave re-measures the pathonly the changed link re-measures

Row four is why large deployments use peer delay. End-to-end puts N × rate Delay_Reqs on one master; peer delay distributes the measurement across every link and the master sees none of it. Section 4's 131 328 messages per second becomes, under peer delay, two messages per second per link regardless of how many slaves are downstream.

And row six is the more interesting difference. End-to-end asymmetry is the whole path's asymmetry and it accumulates across every hop; peer delay measures each link's asymmetry separately, so a calibration can be applied per link — which is the only form in which a calibration is maintainable, because a link's asymmetry is a property of a cable and a path's is a property of a topology that changes.

==

Under end-to-end delay measurement the slave exchanges Delay Request and Delay Response with the master across the whole path, so a thousand slaves at 128 requests per second put 131328 messages per second on one master, which must generate and timestamp a message every 7.6 microseconds; every switch in the path must be a transparent clock, and the asymmetry measured is the whole path's and accumulates across hops. Under peer delay each device measures only the link to its immediate neighbour, so the master sees 256 messages per second regardless of how many slaves are downstream, a topology change re-measures only the changed link, and the asymmetry is localised to one cable where a calibration is maintainable. The cost is that every switch must implement the protocol: end-to-end degrades on non-participating equipment while peer delay does not work at all without it.End-to-endendpoints only131 328 msg/s1024 slaves at 128/sWhole-pathasymmetryaccumulates per hopDegrades gracefullyworks badly on any switchPeer delaylink by link256 msg/sindependent of slavecountPer-link asymmetrycalibration ismaintainableNeeds every switchor it does not work atall12
Figure 3 — end-to-end concentrates the measurement at the master; peer delay distributes it across every link.

The cost is that every device must implement it. An end-to-end deployment works with ordinary switches in the path, badly — Section 6's 6.07 µs from one store-and-forward hop. A peer-delay deployment requires every switch to speak the protocol, and one that does not breaks the chain entirely rather than degrading it.

Which is the choice, stated plainly: end-to-end degrades gracefully on equipment that does not participate, and peer delay does not work at all without it. The second is better wherever the whole path is under one organisation's control — the same condition Chapter 14.4 §17 found for lossless Ethernet, arriving from a completely different direction.

13. RTL 6 — The Announce Message and the Dataset

Everything so far assumed a master. Choosing one is a separate mechanism with no election, no voting and no message that carries a result.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// announce_dataset -- holds the local clock's advertised quality and
// the best remote dataset seen, and decides which is better.
//
// There is no election. Every clock advertises what it is; every clock
// independently applies the same comparison; and the one that finds
// nothing better than itself becomes master. Section 14.
// -----------------------------------------------------------------------
module announce_dataset
  import ptp_pkg::*;
#(
  parameter int ANNOUNCE_TIMEOUT = 3     // 3 announce intervals
)(
  input  logic        clk,
  input  logic        rst_n,

  // What we are.
  input  logic [7:0]  local_priority1,
  input  logic [7:0]  local_class,
  input  logic [7:0]  local_accuracy,
  input  logic [15:0] local_variance,
  input  logic [7:0]  local_priority2,
  input  logic [63:0] local_clock_id,

  // An arriving Announce.
  input  logic        ann_valid,
  input  logic [7:0]  rx_priority1,
  input  logic [7:0]  rx_class,
  input  logic [7:0]  rx_accuracy,
  input  logic [15:0] rx_variance,
  input  logic [7:0]  rx_priority2,
  input  logic [63:0] rx_clock_id,
  input  logic [7:0]  rx_steps_removed,

  input  logic        announce_tick,     // one per announce interval

  output logic        we_are_master,
  output logic [63:0] best_master_id,
  output logic [7:0]  best_steps_removed,
  output logic [31:0] c_announces,
  output logic [31:0] c_master_changes,
  output logic [31:0] c_timeouts
);

  // The best master clock algorithm's comparison, in order. Each field
  // is only consulted if every earlier one ties. The LAST tiebreak is
  // the clock identity, which is unique by construction -- so the
  // comparison is TOTAL and two clocks can never both decide they win.
  function automatic bit remote_is_better(
      input logic [7:0]  rp1, input logic [7:0]  rc,
      input logic [7:0]  ra,  input logic [15:0] rv,
      input logic [7:0]  rp2, input logic [63:0] rid,
      input logic [7:0]  lp1, input logic [7:0]  lc,
      input logic [7:0]  la,  input logic [15:0] lv,
      input logic [7:0]  lp2, input logic [63:0] lid);
    begin
      if (rp1 != lp1) return (rp1 < lp1);
      if (rc  != lc)  return (rc  < lc);
      if (ra  != la)  return (ra  < la);
      if (rv  != lv)  return (rv  < lv);
      if (rp2 != lp2) return (rp2 < lp2);
      return (rid < lid);          // total order, always decides
    end
  endfunction

  logic [7:0]  b_p1, b_cls, b_acc, b_p2;
  logic [15:0] b_var;
  logic [63:0] b_id;
  logic        have_best;
  logic [7:0]  age;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      have_best <= 1'b0; age <= '0;
      b_p1 <= '0; b_cls <= '0; b_acc <= '0; b_var <= '0;
      b_p2 <= '0; b_id <= '0;
      we_are_master <= 1'b1;        // alone until told otherwise
      best_master_id <= '0; best_steps_removed <= '0;
      c_announces <= '0; c_master_changes <= '0; c_timeouts <= '0;
    end else begin
      if (ann_valid) begin
        c_announces <= c_announces + 1;
        age         <= '0;

        // Is this Announce better than the best we hold?
        if (!have_best ||
            remote_is_better(rx_priority1, rx_class, rx_accuracy,
                             rx_variance, rx_priority2, rx_clock_id,
                             b_p1, b_cls, b_acc, b_var, b_p2, b_id)) begin
          have_best <= 1'b1;
          b_p1 <= rx_priority1; b_cls <= rx_class;
          b_acc <= rx_accuracy; b_var <= rx_variance;
          b_p2 <= rx_priority2; b_id <= rx_clock_id;
          if (b_id != rx_clock_id) c_master_changes <= c_master_changes + 1;
          best_master_id     <= rx_clock_id;
          best_steps_removed <= rx_steps_removed;
        end
      end

      if (announce_tick) begin
        if (have_best) begin
          age <= age + 1'b1;
          if (age == ANNOUNCE_TIMEOUT-1) begin
            // The master went quiet. Fall back to ourselves and let
            // the comparison run again from scratch.
            have_best  <= 1'b0;
            c_timeouts <= c_timeouts + 1;
          end
        end
      end

      // We are master exactly when nothing better has been heard.
      we_are_master <= !have_best ||
        !remote_is_better(b_p1, b_cls, b_acc, b_var, b_p2, b_id,
                          local_priority1, local_class, local_accuracy,
                          local_variance, local_priority2, local_clock_id);
    end
  end

endmodule

Classification: a comparison against a held best, with a timeout. No state machine, no negotiation, one total order.

What it teaches: that the comparison must be a total order and the clock identity is what makes it one. Every earlier field can tie — two devices with the same priority, class, accuracy and variance are entirely plausible, and are the common case in a rack of identical switches. The final tiebreak is the clock identity, which is derived from a MAC address and is unique by construction, so the comparison always decides. A design that stops at priority2 has a comparison that can return "equal", and two devices that each find nothing better than themselves both become master.

And it teaches that this is Chapter 15.3 §9's structure again, exactly. LACP agreed on who decides rather than on the selected set, and derived the set from that. BMCA agrees on a comparison function and each device applies it independently — no message carries the outcome, and correctness depends on every device evaluating the same function from the same inputs. Two implementations that order the fields differently produce two masters on one domain.

Deliberately simplified: only one best dataset is held, so a device with several ports sees them as one population. A boundary clock must run the comparison per port and then decide which port is its slave port — the one facing the best master — with every other port becoming a master port. That is the whole of a boundary clock's control plane and it is one comparison per port plus a selection across them.

Production implication: c_master_changes is the counter that turns a mysterious synchronisation outage into a topology event. A master change discards the servo's accumulated state — a new master means a new offset and possibly a new path delay — so every change costs a re-lock, which at Chapter 16.1 §4's holding times is seconds of degraded accuracy. A network where c_master_changes climbs steadily has two clocks alternating, usually because their datasets tie on every field an implementation compares and differ on one it does not.

14. BMCA — Choosing a Master Nobody Elected

The selection has five ordered fields and a tiebreak, and each field exists to express a different kind of claim.

FieldOctetsWhat it claimsSet by
priority11"use me" or "do not"the operator
clockClass1what I am traceable tothe clock's own state
clockAccuracy1how good I think I amthe implementation
offsetScaledLogVariance2how stable I ammeasured
priority21the operator's tiebreakthe operator
clockIdentity8which device I amunique — the final tiebreak

priority1 is first because it is the override. An operator who wants a specific device to be master sets its priority1 low and every other device's high, and no amount of clock quality overrides it. Which is deliberate: the best clock in the room is frequently not the one you want as master, because mastership is also a topology decision.

clockClass is the one that changes at run time and is the most informative. It says what the clock is currently traceable to:

clockClassMeaning
6a primary reference — GPS, locked
7a primary reference in holdover
13an application-specific source, locked
14the same, in holdover
187was a primary reference, now free-running
248the default — no traceability at all

A GPS-locked grandmaster advertises 6. The same device with its antenna unplugged advertises 7, then 187, and the BMCA moves mastership away from it automatically — which is the mechanism by which a network notices that its time source has gone blind. Without clockClass a failed GPS receiver keeps its mastership and keeps serving increasingly wrong time.

And the subtlety worth knowing: clockClass degrades on a schedule the device chooses. A device in holdover is still a better master than a free-running peer, for as long as its oscillator's drift keeps it inside the requirement — which is Chapter 16.1 §6's holdover arithmetic, used here as a protocol input. An OCXO can honestly hold class 7 for hours; a crystal cannot hold it for a minute, and a device that advertises the same holdover class regardless of its oscillator is lying to the algorithm.

==

Three mechanisms in this track exchange inputs and agree on a function rather than on a result. LACP's port selection exchanges each end's priorities and identity and agrees on whose ordering wins, with both ends computing the subset independently; a disagreement degrades to a smaller aggregate and is visible in the collecting and distributing state bits. The best master clock algorithm exchanges each clock's dataset and agrees on the comparison function, with every clock deciding independently; a disagreement produces two masters on one domain, detectable because the best master identity alternates in the Announce stream. This chapter's offset computation exchanges four timestamps and agrees on a formula, with the slave computing alone; its premise of path symmetry is something the network must satisfy rather than something a message conveys, so a disagreement produces a wrong number with no symptom anywhere.Agree on a functionnot on an answer15.3's selectionwhose ordering winsBMCAthe comparisonSection 6's offsetthe formulaA smaller aggregatevisible in the statebitsTwo mastersdetectable in AnnounceA wrong numberno symptom at allThe function is theinterfaceand needs its ownevidence12
Figure 4 — three mechanisms that agree on a function rather than on an answer, and how each one's disagreement shows up.

15. RTL 7 — Exchange Telemetry

Six numbers, and the useful ones say whether the exchange's premise is holding rather than whether its messages are arriving.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// exchange_telemetry -- what an operator needs to decide whether a PTP
// exchange is healthy, and — separately — whether its results can be
// trusted.
//
// The second question is the one Section 8 makes necessary: an
// exchange can be perfectly healthy and produce an offset that is
// wrong by half the path's asymmetry.
// -----------------------------------------------------------------------
module exchange_telemetry
  import ptp_pkg::*;
#(
  parameter int HISTORY = 16
)(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        result_valid,
  input  logic signed [63:0] offset_ns,
  input  logic signed [63:0] mean_path_ns,
  input  logic signed [63:0] round_trip_ns,

  input  logic [31:0] c_sync,
  input  logic [31:0] c_stale_sync,
  input  logic [31:0] c_timed_out,
  input  logic [31:0] c_negative,
  input  logic        window_tick,

  output logic signed [63:0] path_spread_ns,   // max - min of mean_path
  output logic signed [63:0] path_median_ns,
  output logic [15:0] loss_pct_x100,
  output logic        path_is_stable,
  output logic        results_trustworthy,
  output logic [31:0] c_windows
);

  logic signed [63:0] hist [HISTORY];
  logic [$clog2(HISTORY)-1:0] wp;
  logic [$clog2(HISTORY):0]   fill;
  logic [31:0] sync_base, lost_base;

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < HISTORY; i++) hist[i] <= '0;
      wp <= '0; fill <= '0;
      path_spread_ns <= '0; path_median_ns <= '0;
      loss_pct_x100 <= '0; path_is_stable <= 1'b0;
      results_trustworthy <= 1'b0; c_windows <= '0;
      sync_base <= '0; lost_base <= '0;
    end else begin
      if (result_valid) begin
        hist[wp] <= mean_path_ns;
        wp <= wp + 1'b1;
        if (fill != HISTORY) fill <= fill + 1'b1;
      end

      if (window_tick) begin
        automatic logic signed [63:0] mx, mn;
        automatic logic [31:0] syncs, lost;
        mx = hist[0]; mn = hist[0];
        for (i = 1; i < HISTORY; i++) begin
          if (hist[i] > mx) mx = hist[i];
          if (hist[i] < mn) mn = hist[i];
        end
        path_spread_ns <= mx - mn;
        // A crude median: the midpoint of the observed range is good
        // enough to spot a path that has changed, which is the use.
        path_median_ns <= (mx + mn) >>> 1;

        syncs = c_sync - sync_base;
        lost  = (c_stale_sync + c_timed_out) - lost_base;
        loss_pct_x100 <= (syncs == 0) ? 16'd0
                       : 16'((lost * 10000) / syncs);
        sync_base <= c_sync;
        lost_base <= c_stale_sync + c_timed_out;

        // A stable path is one whose measured delay is not moving.
        // A moving path delay means the asymmetry is also moving, so
        // no stored calibration applies.
        path_is_stable <= ((mx - mn) < 64'sd1000) && (fill == HISTORY);

        // Trustworthy means: messages are arriving, the arithmetic is
        // sane, and the path is stable enough for a calibration to
        // mean anything. It does NOT mean the offset is correct --
        // section 8, and section 19's rejected property.
        results_trustworthy <= (fill == HISTORY) &&
                               (c_negative == '0) &&
                               ((mx - mn) < 64'sd1000);
        c_windows <= c_windows + 1;
      end
    end
  end

endmodule

Classification: a windowed spread estimator over the measured path delay, plus a loss rate and two judgements.

What it teaches: that path_spread_ns is the measurement that decides whether anything else in the chapter is usable, for the same reason Chapter 16.1 §5's offset_spread_ns was. A path whose measured delay varies by 20 µs between samples has queueing in itChapter 12.6's store-and-forward plus Chapter 14.1's backlog — and its asymmetry varies with it, so no stored calibration applies and Section 8's residual is not even constant.

And it teaches why results_trustworthy deliberately does not include the offset. The offset is the output; its correctness depends on a premise this device cannot evaluate. What the bit says is that the inputs are sound and the path is stable enough for the premise to be stably wrong — which is the condition under which a calibration is worth storing. A device reporting results_trustworthy high and a 6 µs error has a constant asymmetry waiting to be calibrated out.

Deliberately simplified: the median is the midpoint of the range, which is not a median and is badly affected by a single outlier. A production design sorts or uses a running quantile estimator; the midpoint is used here because the use is detecting a path that moved, and a path that moved moves both ends of the range.

Production implication: loss_pct_x100 is the counter that connects Section 10's arithmetic to reality. At 1 Sync/s on a 100 ppm crystal, a 1% loss rate means the slave spends 1% of its time more than 100 µs out of specification — which for a requirement of 1 µs is a violation one second in a hundred, invisible in any average. The remedy is not a better servo; it is Chapter 13.4 §9's priority mapping configured to put PTP in a queue that does not drop.

16. RTL 8 — Conformance for an Exchange

The monitor checks that the exchange was conducted correctly. It deliberately does not check that its answer is right.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// exchange_conformance_monitor -- one bit.
//
// Every check is evaluable from messages this device sent or received.
// Section 19's rejected property is the one check that is not, and it
// is the one an operator most wants this bit to mean.
// -----------------------------------------------------------------------
module exchange_conformance_monitor
  import ptp_pkg::*;
(
  input  logic       clk,
  input  logic       rst_n,

  input  logic       negative_round_trip,
  input  logic       paired_across_masters,   // t1,t2 and t3,t4 from different masters
  input  logic       twostep_flag_ignored,
  input  logic       resp_not_for_us_used,    // matched on seq alone
  input  logic       correction_sign_wrong,   // added instead of subtracted
  input  logic       cfg_domain_mismatch,
  input  logic       cfg_no_master_selected,
  input  logic       dreq_interval_not_random,

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

  logic v_neg, v_cross, v_flag, v_wrong_resp, v_sign, v_dom, v_nomaster, v_burst;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_neg <= 1'b0; v_cross <= 1'b0; v_flag <= 1'b0;
      v_wrong_resp <= 1'b0; v_sign <= 1'b0; v_burst <= 1'b0;
      v_dom <= 1'b0; v_nomaster <= 1'b0; c_violations <= '0;
    end else begin
      // Runtime violations, sticky: each is a real defect and one
      // occurrence is the finding.
      if (negative_round_trip)   begin v_neg        <= 1'b1; c_violations <= c_violations + 1; end
      if (paired_across_masters) begin v_cross      <= 1'b1; c_violations <= c_violations + 1; end
      if (twostep_flag_ignored)  begin v_flag       <= 1'b1; c_violations <= c_violations + 1; end
      if (resp_not_for_us_used)  begin v_wrong_resp <= 1'b1; c_violations <= c_violations + 1; end
      if (correction_sign_wrong) begin v_sign       <= 1'b1; c_violations <= c_violations + 1; end

      // Standing configuration properties -- wrong from power-on,
      // not from the first message. 14.1 section 17's argument.
      v_dom      <= cfg_domain_mismatch;
      v_nomaster <= cfg_no_master_selected;
      v_burst    <= dreq_interval_not_random;
    end
  end

  assign conformant = !(v_neg || v_cross || v_flag || v_wrong_resp ||
                        v_sign || v_dom || v_nomaster || v_burst);
  assign fault_vector = {v_burst, v_nomaster, v_dom, v_sign,
                         v_wrong_resp, v_flag, v_cross, v_neg};

endmodule

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

What it teaches: that paired_across_masters is a check nothing else would catch and it matters at exactly the wrong moment. A BMCA master change between a Sync and a Delay_Resp leaves the slave holding (t1, t2) from one master and (t3, t4) from another — two different clocks, two different paths, and Section 7's arithmetic combines them without complaint. The resulting offset is nonsense and the round trip may well be positive, so negative_round_trip does not fire. The check is a comparison of two sourcePortIdentity fields the design already parsed.

And it teaches that correction_sign_wrong is detectable locally, which is not obvious. Adding the correction field instead of subtracting it makes the computed path delay grow when transparent clocks are enabled, and the sign of the change is the evidence: enabling residence-time correction must reduce the measured wire delay or leave it alone. A design that measures mean_path_ns before and after can catch its own inverted sign without a reference clock.

Deliberately simplified: dreq_interval_not_random is presented as a configuration input. Detecting it properly means measuring the variance of the device's own Delay_Req intervals and comparing against the standard's requirement, which is a few counters — and worth having, because a burst-generating slave is invisible to itself and visible only as load at the master.

Production implication: conformant here means the exchange was conducted to specification and says nothing about the accuracy achieved. A slave behind one store-and-forward switch is fully conformant and 6.07 µs wrong, which is correct and is the thing operators most want the bit to mean. Section 15's results_trustworthy and path_spread_ns are where the accuracy question lives, and the two outputs are separate because one can be true while the other is false in both directions.

17. What the Exchange Can and Cannot Promise

Put the guarantees and the non-guarantees side by side, because the boundary is where every argument about PTP accuracy sits.

ClaimStatus
the round-trip delay is measured exactlyguaranteed — equation (3), no premise
the drift is measurable across an unknown pathguaranteedChapter 16.1 §5
a lost message costs one interval's drift and nothing elseguaranteed — Section 10
the offset is measured exactlyonly if d_ms = d_sm
the error is exactly half the imbalanceguaranteed — and that is the useful form
both ends pick the same masteronly if both implement the same comparison
the master is the best clockno — priority1 overrides quality
averaging improves the offsetits variance, not its mean

Rows four and five are the same fact stated pessimistically and constructively, and the constructive form is the one to carry.

The offset may be wrong is unactionable. "The offset is wrong by exactly (d_sm − d_ms)/2" is a specification: it says the error is constant while the path is, which means it is calibratable, which is why Chapter 16.5 §11 has a procedure and not an apology.

And row eight is the one that wastes the most engineering time. Section 8's table: averaging reduces the variance of the offset estimate and leaves its mean displaced by the same amount every sample was displaced by. A thousand samples give a tight estimate of the wrong number, and the tightness is frequently reported as accuracy.

Which gives the honest summary of the exchange:

Four timestamps measure a round trip and split it by assumption. The measurement is exact and the split is not, and everything the protocol achieves beyond half-the-asymmetry comes from somewhere elseChapter 16.4's residence-time correction removing the switches' contribution, and Chapter 16.5's calibration removing the cables'.

18. The Cost of the Exchange, Accounted

The protocol is cheap on the wire and the cost lands somewhere else entirely.

ComponentAt 16/s per slaveAgainst what
Sync + Follow_Up168 octets/s
Delay_Req + Delay_Resp176 octets/s
Announce at 1/8 s12.75 octets/s
two-step total5517 octets/s — 44 134 bit/s4.4 × 10⁻³% of 1 Gb/s
one-step total4173 octets/s — 33 382 bit/ssaves 10 752 bit/s
at 1/s, two-step2854 bit/s2.9 × 10⁻⁴%
at 128/s, two-step352 358 bit/s3.5 × 10⁻²%
state at the slave4 Sync slots + 8 Delay slots≈220 octets
the master's load, 1024 slaves at 128/s131 328 msg/sthe real cost

The last row is the cost and it is not bandwidth. 131 328 messages per second is 107 Mb/s of control traffic on a 100 Gb/s fabric — negligible — and it is a device that must generate, timestamp and transmit a message every 7.6 microseconds. At that rate the master's timestamp unit, not its link, is the constraint.

And Section 12's peer delay changes that row and only that row:

end-to-endpeer delay
master's messages/s, 1024 slaves at 128/s131 328256
messages per link22 or 3
requires every switch to participatenoyes

Which is the trade in one line: end-to-end concentrates the load at the master and works on ordinary switches badly; peer delay distributes it and does not work on ordinary switches at all.

Compared against the batch's other mechanisms, the exchange is the cheapest thing in Module 16:

MechanismStateWhat it buys
the exchange — this chapter≈220 octetsan offset, up to half the asymmetry
LACP — Chapter 15.3 §17≈320 octetsthe mis-cable, the one-way failure
distribution — Chapter 15.2 §183.8 KiB25% instead of 74.9% at a failover
PFC — Chapter 14.4 §181.45 MiBcollateral 88% → 11%

Two hundred and twenty octets, and the thing it produces is wrong by half a cable's asymmetry. Which is the right shape: the protocol is the cheap part and the accuracy is bought by the timestamp unit, the transparent clocks and the calibration — the three chapters that follow.

19. Properties Worth Asserting, and One Worth Refusing

The properties divide by what they protect: the header, the pairing, the arithmetic, the sequence tracking, the BMCA, and the configuration.

Group 1 — the header.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. A PTP message is recognised only on the PTP EtherType.
property p_ethertype_gate;
  @(posedge clk) disable iff (!rst_n)
  hdr_valid |-> $past(et_ok);
endproperty

// P2. Nothing is published on a bad FCS.
property p_no_commit_bad_fcs;
  @(posedge clk) disable iff (!rst_n)
  (rx_eop && !rx_fcs_ok) |-> !hdr_valid;
endproperty

// P3. The event/general split is exactly the top bit of the message
// type. 16.3's timestamp unit depends on this being cheap.
property p_event_is_top_bit;
  @(posedge clk) disable iff (!rst_n)
  hdr_valid |-> (is_event_msg == !msg_type[3]);
endproperty

// P4. A tagged frame is parsed at the shifted offsets. 13.2 section 7.
property p_tag_shifts_offsets;
  @(posedge clk) disable iff (!rst_n)
  (hdr_valid && rx_tagged) |-> (base == 8'd18);
endproperty

// P5. Exactly one type counter moves per accepted message.
property p_type_counter_partition;
  @(posedge clk) disable iff (!rst_n)
  hdr_valid |=> ($countones(type_counter_changed) == 1);
endproperty

Group 2 — Sync and Follow_Up pairing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P6. A one-step Sync publishes immediately and never parks.
property p_onestep_is_immediate;
  @(posedge clk) disable iff (!rst_n)
  (hdr_valid && (msg_type == MSG_SYNC) && !flags[9]) |=> pair_valid;
endproperty

// P7. A two-step Sync never publishes on its own.
property p_twostep_waits;
  @(posedge clk) disable iff (!rst_n)
  (hdr_valid && (msg_type == MSG_SYNC) && flags[9]) |=> !pair_valid;
endproperty

// P8. A published pair's t2 came from the Sync with the SAME
// sequence id as the Follow_Up that completed it.
property p_pair_is_same_sequence;
  @(posedge clk) disable iff (!rst_n)
  pair_valid |-> (pair_seq == $past(seq_id));
endproperty

// P9. Only the configured master's Syncs are honoured.
property p_only_our_master;
  @(posedge clk) disable iff (!rst_n)
  pair_valid |-> ($past(src_port.clock_id) == cfg_master.clock_id);
endproperty

// P10. An evicted unmatched Sync is always counted.
property p_eviction_counted;
  @(posedge clk) disable iff (!rst_n)
  (hdr_valid && (msg_type == MSG_SYNC) && flags[9] && slots[wp].busy)
    |=> $changed(c_stale_sync);
endproperty

Group 3 — the arithmetic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P11. The round trip is non-negative. It is a physical duration and
// a negative value is evidence about the inputs.
property p_round_trip_is_physical;
  @(posedge clk) disable iff (!rst_n)
  (result_valid && !negative_path) |-> (round_trip_ns >= 0);
endproperty

// P12. mean_path is exactly half the round trip -- equation (3),
// which carries no premise.
property p_mean_path_is_half_round_trip;
  @(posedge clk) disable iff (!rst_n)
  result_valid |-> (mean_path_ns == (round_trip_ns >>> 1));
endproperty

// P13. The correction field is SUBTRACTED. Adding it doubles the
// error transparent clocks were correcting.
property p_correction_is_subtracted;
  @(posedge clk) disable iff (!rst_n)
  (ms_valid && (corr_ms > 0)) |=> (d_ms < ts_diff(t2, t1));
endproperty

// P14. No result is published until BOTH halves have been seen.
property p_needs_both_halves;
  @(posedge clk) disable iff (!rst_n)
  result_valid |-> (ms_have && sm_have);
endproperty

// P15. A negative round trip is always flagged and counted.
property p_negative_flagged;
  @(posedge clk) disable iff (!rst_n)
  (result_valid && (round_trip_ns < 0)) |-> negative_path;
endproperty

// P16. Under the premise, the offset is exact. The premise is an
// explicit antecedent -- section 19's rejected property is what
// happens when it is left implicit.
property p_offset_correct_when_symmetric;
  @(posedge clk) disable iff (!rst_n)
  (result_valid && (tb_d_ms == tb_d_sm)) |->
    (offset_ns == (tb_slave_ns - tb_master_ns));
endproperty

// P17. And when the premise fails, the error is EXACTLY half the
// imbalance. A stronger and more useful claim than the one refused.
property p_error_is_half_the_imbalance;
  @(posedge clk) disable iff (!rst_n)
  result_valid |-> ((offset_ns - (tb_slave_ns - tb_master_ns)) ==
                    ((tb_d_sm - tb_d_ms) >>> 1));
endproperty

Group 4 — sequence tracking.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P18. A Delay_Resp addressed to another slave is never used.
property p_resp_must_be_for_us;
  @(posedge clk) disable iff (!rst_n)
  sm_valid |-> ($past(dresp_requesting.clock_id) == cfg_self.clock_id);
endproperty

// P19. A matched pair's t3 came from the request with that sequence id.
property p_t3_matches_sequence;
  @(posedge clk) disable iff (!rst_n)
  sm_valid |-> ($past(dresp_seq) inside {outstanding_seqs});
endproperty

// P20. Every outstanding request eventually frees its slot -- matched
// or aged out. Without this the tracker fills and stops silently.
property p_slots_always_free;
  @(posedge clk) disable iff (!rst_n)
  $rose(q[i].busy) |-> ##[1:TIMEOUT_CYCLES+1] !q[i].busy;
endproperty

// P21. A Delay_Req is never issued while its predecessor's transmit
// timestamp is outstanding.
property p_one_outstanding_tx_ts;
  @(posedge clk) disable iff (!rst_n)
  (dreq_tx) |-> !$past(awaiting_ts);
endproperty

// P22. The Delay_Req interval is randomised: consecutive intervals
// are not equal. A fixed interval produces section 11's burst.
property p_interval_is_randomised;
  @(posedge clk) disable iff (!rst_n)
  dreq_tx |=> (target != $past(target));
endproperty

Group 5 — BMCA.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P23. The comparison is a TOTAL order: it never returns "equal",
// because the clock identity always decides.
property p_comparison_is_total;
  @(posedge clk) disable iff (!rst_n)
  (a_id != b_id) |-> (remote_is_better(a) != remote_is_better(b));
endproperty

// P24. priority1 dominates everything. An operator override is not
// overridden by clock quality.
property p_priority1_dominates;
  @(posedge clk) disable iff (!rst_n)
  (rx_priority1 < local_priority1) |=> !we_are_master;
endproperty

// P25. We are master exactly when nothing better has been heard.
property p_master_iff_best;
  @(posedge clk) disable iff (!rst_n)
  we_are_master <-> (!have_best || !remote_is_better(best, local));
endproperty

// P26. An announce timeout falls back to ourselves rather than
// holding a master that has gone quiet.
property p_timeout_releases_master;
  @(posedge clk) disable iff (!rst_n)
  (announce_tick && (age == ANNOUNCE_TIMEOUT-1)) |=> !have_best;
endproperty

// P27. Every master change is counted -- each one costs a re-lock.
property p_master_change_counted;
  @(posedge clk) disable iff (!rst_n)
  $changed(best_master_id) |=> $changed(c_master_changes);
endproperty

Group 6 — configuration and conformance.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P28. Standing property: the domain matches. A device on the wrong
// domain hears everything and uses none of it.
property p_domain_matches;
  @(posedge clk) disable iff (!rst_n)
  !cfg_domain_mismatch;
endproperty

// P29. A pair is never assembled across a master change.
property p_no_cross_master_pairing;
  @(posedge clk) disable iff (!rst_n)
  result_valid |-> !paired_across_masters;
endproperty

// P30. results_trustworthy requires a stable path -- it is about the
// premise's stability, not about the offset's correctness.
property p_trustworthy_needs_stable_path;
  @(posedge clk) disable iff (!rst_n)
  results_trustworthy |-> (path_spread_ns < 64'sd1000);
endproperty

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

P11 through P15 are about the measurement, P16 and P17 about the premise, and P23 through P27 about a comparison every participant must evaluate identically. None of them says the computed offset is the true offset — which is the property this chapter refuses, and the reason is a kind of unavailability the series has not met before.

20. Verification Scenarios

Seventy-two scenarios. Several have expected outcomes in which every message is correct, every check passes, and the offset is wrong by six microseconds.

The header

#ScenarioExpected
1Untagged PTP frameparsed at base 14
2802.1Q-tagged PTP frameparsed at base 18
3Same, hard-coded untagged parsermessage type read from mid-header — matches nothing
4Same, on a trunk portevery PTP frame mis-parsed
5Sync, type 0x0is_event_msg high
6Follow_Up, type 0x8is_event_msg low
7Announce, type 0xBgeneral
8Bad FCSnothing published, c_bad_fcs + 1
9Wrong domainc_wrong_domain; slave never locks on a healthy link
10correctionField at header octets 8–15present in every message type
11Sync frame size64 octets, 84 on the wire
12Delay_Resp frame size72 octets, 92 on the wire
13Announce frame size82 octets, 102 on the wire

Sync and Follow_Up

#ScenarioExpected
14Two-step master, Sync then Follow_Uppair published on the Follow_Up
15One-step master, Sync alonepair published immediately
16Design always waits, one-step masterc_sync rises, c_followup = 0, no pair ever
17Design never waits, two-step masterreads originTimestamp = 0
18Sameoffset ≈ the current epoch
19Two Syncs before the first Follow_Upboth parked — needs 2+ slots
20Same, single-slot designolder pair silently dropped
21Same, with the counterc_stale_sync
22Follow_Up with no matching Syncc_orphan_fu
23Sync from a non-selected masterignored

The four timestamps

#ScenarioExpected
24Symmetric path, 500 ns each wayoffset exact, mean_path = 500 ns
25Same, offset = +40 µsrecovered exactly
26Asymmetric: d_ms = 500, d_sm = 550mean_path = 525 — exact
27Sameoffset error = +25 ns — half the imbalance
2810 m of fibre imbalance25 ns
29100 m of fibre imbalance250 ns
30Asymmetric PHY, 40 ns20 ns
31One store-and-forward switch, 12.14 µs one way6.07 µs
321000 samples averaged, asymmetric pathvariance falls, mean unchanged
33Faster clock, asymmetric pathno change
34A third device addedno help — three more unknowns
35Cable measured physicallythe asymmetry, and it is not a network measurement
36correctionField subtractedmean_path falls with transparent clocks
37correctionField addedmean_path rises — the inverted sign
38Clock stepped between t2 and t3negative_path
39Fresh (t1,t2) with stale (t3,t4)two path measurements mixed
40Arithmetic shift on a signed value−0.5 ns systematic bias

Pairing and loss

#ScenarioExpected
41Delay_Resp for another slave, same seqc_not_for_us, not used
42Same, matched on seq aloneour t3 with their t4
43Delay_Resp never arrivesslot ages out, c_timed_out
44Eight lost responses, no age-outtracker full, mechanism stops silently
45Lost Sync at 1/s, 100 ppm100 µs of drift accrued
46Lost Sync at 16/s, 100 ppm6.25 µs
47Lost Sync at 128/s, 100 ppm0.78 µs
48Lost Sync at 16/s, 0.1 ppm0.006 µs
491% loss at 1/s, 100 ppm1% of the time, >100 µs out
50Same, in an averageinvisible
51PTP in the bulk queue, congested egressSyncs dropped by Chapter 14.1
52PTP in a priority queuenot dropped
53Delay_Req at a fixed offset after Syncall slaves transmit together
541024 slaves, same seeda 1024-message burst at the master
55Randomised intervalspread over 0.75–1.25 of nominal
56TX timestamp lostc_no_tx_ts, request abandoned
57Same, no interlockt3 from the wrong frame

BMCA

#ScenarioExpected
58Two identical switches, no identity tiebreakboth become master
59Same, with the identity tiebreakexactly one
60priority1 lower on a worse clockthe worse clock wins
61GPS lockedclockClass 6
62Antenna unplugged7, then 187
63Samemastership moves away automatically
64Same, no clockClass degradationa blind master keeps serving
65Crystal advertising holdover class 7lying to the algorithm
66Master goes quiet, 3 announce intervalsc_timeouts, fall back to self
67Two implementations ordering fields differentlytwo masters, both conformant
68Master change between Sync and Delay_Resppaired_across_masters
69Sameround trip may be positivenegative_path does not fire
70Master changec_master_changes, servo re-locks
71Boundary clock, one dataset for all portscannot choose a slave port
72Same, per-port comparisoncorrect

The directed test random stimulus will not produce

A path asymmetry is not a traffic condition, a fault, or a distribution — it is a property of the testbench's own delay model, and a random stimulus generator varies frames rather than cable lengths. And the finding is the chapter's central claim: that the round trip is measured exactly while the offset is not, which requires driving the identical exchange through two different delay models and comparing. No coverage metric asks for that.

Setup: one master, one slave, a delay model with independently settable d_ms and d_sm. The slave's true offset is fixed at exactly +40 000 ns — known to the testbench, which Chapter 16.1 §19 established is the only place it can exist. Two-step master, 16 Sync/s, 16 Delay_Req/s, 1000 exchanges per run.

Stimulus, four runs. Run A — symmetric: d_ms = d_sm = 500 ns. Run B — symmetric, large: d_ms = d_sm = 12 640 ns, a store-and-forward hop each way. Run C — asymmetric, small: d_ms = 500, d_sm = 550 ns — 10 m of fibre. Run D — asymmetric, large: d_ms = 12 640, d_sm = 500 ns — a switch that queued one direction and not the other.

Oracle:

#ObservableA — sym 500B — sym 12 640C — asym 50D — asym 12 140
1round_trip_ns100025 280105013 140
2round_trip_ns error0000
3mean_path_ns50012 6405256570
4true d_ms50012 64050012 640
5mean_path error00+25−6070
6offset_ns40 00040 00040 02533 930
7offset error00+25−6070
8error against (d_sm−d_ms)/2exactexactexactexact
91000 samples averaged, offset40 00040 00040 02533 930
10offset variance after averaging→ 0→ 0→ 0→ 0
11negative_pathlowlowlowlow
12path_spread_ns≈0≈0≈0≈0
13results_trustworthyhighhighhighhigh
14conformanthighhighhighhigh
15c_violations0000
16rerun C with the imbalance stored and subtractederror 0

Rows 1 and 2 are the first finding: the round trip is exact in all four runs, including both asymmetric ones. Equation (3) carries no premise and the measurement proves it.

Rows 6 to 8 are the second: the offset error is exactly (d_sm − d_ms)/2, every time, to the nanosecond. Run B shows that a large symmetric delay costs nothing — 12.64 µs each way, offset exact — and Run D shows that a large asymmetry costs half of itself. The two runs have almost the same total path and opposite outcomes.

Rows 9 to 15 are the third and the most important operationally. A thousand samples drive the variance to zero while the mean stays displaced; path_spread_ns reads near zero because the path is stable; results_trustworthy and conformant are both high; no violation counter moves. Run D is a fully conformant exchange producing an offset 6.07 µs wrong, and every instrument in Sections 15 and 16 says it is healthy.

Row 16 is the remedy and the reason results_trustworthy is defined the way it is: a stable path means a constant error, which is exactly the condition under which a stored calibration works.

21. Debugging a PTP Exchange

Five questions, in order. The first three are about messages and the last two are about the premise.

Step 1 — are the messages arriving and being parsed? c_by_type across all sixteen types. All zeros on a link carrying PTP means the parser is not recognising them — a tagged frame against an untagged parser is the common cause, and Section 3's simplification is exactly that bug. A domain mismatch presents identically from the protocol's side and is separated by c_wrong_domain.

Step 2 — are pairs being assembled? c_sync, c_followup, c_stale_sync, c_orphan_fu. c_sync rising with c_followup at zero is a one-step master against a design that waits. c_stale_sync rising means Syncs are arriving faster than Follow_Ups are being matched. c_orphan_fu rising means Syncs are being lost or not parsed.

Step 3 — is the slave-to-master half working? c_matched, c_timed_out, c_not_for_us. c_timed_out rising means Delay_Resps are not coming back — a master that is overloaded, Section 4's last row, or a unicast/multicast configuration mismatch. c_not_for_us is normal and counts the other slaves on the segment.

Step 4 — is the arithmetic sound? negative_path and c_negative. A negative round trip is physically impossible and therefore evidence about the inputs: a cross-master pairing, a wrong Follow_Up match, or a clock stepped mid-exchange. paired_across_masters separates the first from the others.

Step 5 — is the premise holding? path_spread_ns and mean_path_ns. This is the step that is almost never taken and it is where the accuracy lives. A mean_path_ns of 6.5 µs on a link whose cable is 100 m — 500 ns — says 6 µs of the measured path is queueing, which means the asymmetry is that large too and the offset is wrong by half of it. A path_spread_ns of 20 µs says the asymmetry is not even constant, so no calibration applies.

And the finding that ends an investigation without a fault: messages flowing, pairs assembling, negative_path low, conformant high, path_spread_ns near zero — and an offset error that matches path_asymmetry / 2. That is the protocol working exactly as specified, and the remaining error belongs to Chapter 16.4's transparent clocks and Chapter 16.5's calibration rather than to anything in this chapter.

22. Common Misconceptions

1 — "PTP measures the one-way delay."

The wrong model: the exchange determines how long a message takes to travel.

What it costs: confidence in a number that is half measurement and half assumption. Equation (3) measures the round trip exactly and carries no premise. The one-way delay is half of it, and halving it is the assumption — the only way to get a one-way number out of a two-way measurement.

The corrected model: mean_path is a measurement of (d_ms + d_sm)/2 and is exact. It equals d_ms only if the path is symmetric, and the difference propagates straight into the offset at half the imbalance.

2 — "More messages would let PTP measure the asymmetry."

The wrong model: the limitation is a shortage of data.

What it costs: designs that raise the message rate expecting accuracy and get only freshness. Section 8's table: more Syncs, more Delay_Reqs, averaging, a faster clock and even a third device all fail, because every (t1,t2) sample contains d_ms + offset and every (t3,t4) sample contains d_sm − offset. Four unknowns remain three equations short whatever the sample count.

The corrected model: the asymmetry is measurable only outside the protocol — by measuring the cable, or against a reference known to be symmetric. That is a commissioning procedure, and a deployment that did not perform it carries the error for ever.

3 — "A big path delay is a big error."

The wrong model: accuracy degrades with distance.

What it costs: effort shortening paths that were never the problem. Run B of Section 20's directed test: 12.64 µs each way, symmetric, and the offset is exact to the nanosecond. Run D has almost the same total path, asymmetrically distributed, and is 6.07 µs wrong.

The corrected model: only the imbalance matters, and it enters at exactly half. A 10 km symmetric fibre pair is better than a 10 m asymmetric one, which is the opposite of the intuition and is why a deliberately matched pair of fibres is worth specifying.

4 — "We averaged a thousand samples, so we are accurate to nanoseconds."

The wrong model: a tight estimate is an accurate one.

What it costs: a reported accuracy that is a measure of the estimator rather than of the clock. Averaging drives the variance toward zero and leaves the mean displaced by (d_sm − d_ms)/2 — because every sample is displaced by the same amount. Section 20's rows 9 and 10: variance → 0, error unchanged at 6.07 µs.

The corrected model: report the spread and name the bias separately. A tight spread with a constant path is good news — it means the error is constant and therefore calibratable — but it is not accuracy, and results_trustworthy is deliberately defined to say the former and not the latter.

5 — "Both ends run 1588, so they will agree on a master."

The wrong model: conformance implies identical behaviour.

What it costs: two masters on one domain, each serving a different time, with every device individually conformant. BMCA has no election and no message carrying the result — each clock applies the comparison independently, so correctness requires every implementation to order the same fields the same way and to reach the identity tiebreak.

The corrected model: the comparison function is part of the interface. A design that stops before the clockIdentity tiebreak has a comparison that can return "equal", and two identical switches — the common case in a rack — both find nothing better than themselves.

6 — "PTP will work over the existing network."

The wrong model: it is an application protocol.

What it costs: Section 20's Run D. A single store-and-forward switch that queues one direction and not the other introduces 12.14 µs of asymmetry at 1 Gb/s, and the offset is wrong by 6.07 µs — three orders of magnitude worse than the endpoints' own residual. And the Syncs themselves are in whatever queue Chapter 13.4 §9's mapping put them in, so they are dropped by Chapter 14.1's allocator exactly when time matters most.

The corrected model: sub-microsecond PTP requires every device in the path to participate — as a transparent clock reporting its residence time, or as a boundary clock terminating and re-originating the exchange. Chapter 16.1 §15 said it as an endpoint result; this chapter says it as an arithmetic one.

23. Interview Reasoning

Q1 — Why does PTP need four messages?

Because one message gives one equation in two unknowns. t2 − t1 = d_ms + offset — and a second Sync does not help, because the offset appears identically in both samples while a constant delay cancels, which is Chapter 16.1 §5's drift-survives-offset-does-not. Sending something back gives a second, independent equationt4 − t3 = d_sm − offset — and two equations in two unknowns are solvable. The extra two messages exist to carry timestamps that could not ride in the messages they describe: t1 is not known when the Sync is transmitted, and t4 is known only at the master.

Q2 — Derive the offset and the path delay.

Add and subtract. (t2−t1) + (t4−t3) = d_ms + d_sm — the round trip, with the offset cancelled, and this is a genuine measurement. (t2−t1) − (t4−t3) = d_ms − d_sm + 2·offsettwo unknowns, and four timestamps cannot separate them. So the protocol assumes d_ms = d_sm, giving mean_path = sum/2 and offset = diff/2, and the offset's residual error is exactly (d_sm − d_ms)/2 — half the imbalance.

Q3 — How much does asymmetry cost, concretely?

Exactly half of itself. 10 m of fibre length difference is 50 ns of imbalance and 25 ns of offset error; 100 m is 250 ns; an asymmetric PHY at 40 ns is 20 ns. And one store-and-forward switch that queues a Sync for a full frame time and a Delay_Req for nothing is 12.14 µs at 1 Gb/s — 6.07 µs of error, three orders of magnitude worse than a good endpoint's own residual. It does not average out, because the samples are not noisy; they are consistently displaced.

Q4 — Can any amount of measurement find the asymmetry?

No, not from inside the protocol. More Syncs give more samples of d_ms + offset; more Delay_Reqs give more of d_sm − offset; averaging reduces variance and leaves the mean where it was; a faster clock refines a number that is already wrong; and a third device adds two more paths with three more unknowns. The asymmetry is measurable only outside — a physical cable measurement, or a comparison against a reference known to be symmetric. That is a commissioning step, and it produces a constant to store and subtract.

Q5 — How does PTP choose a master, and what breaks it?

There is no election. Each clock advertises a dataset in Announce messages — priority1, clockClass, clockAccuracy, offsetScaledLogVariance, priority2, clockIdentityand every clock independently applies the same ordered comparison. The one that finds nothing better than itself becomes master. It works because the comparison is a total order: the clock identity, unique by construction, always decides. What breaks it is two implementations comparing the fields in a different order, or stopping before the identity tiebreak — at which point two identical switches each find nothing better than themselves and there are two masters.

Q6 — Why can't you assert that the computed offset is correct?

Because the formula is a theorem conditional on a premise about cables and queues. offset = ((t2−t1) − (t4−t3))/2 is exactly right when d_ms = d_sm and wrong by half the difference otherwise, and that difference is outside the design, outside the protocol, and outside anything four timestamps can observe. So the assertion passes vacuously on a symmetric testbench, fails by a fixed amount on an asymmetric one, and is absent in silicon. The properties that hold are: the round trip is exact; the formula is evaluated correctly; the result is right when the path is symmetric; and — the strongest — the error is exactly (d_sm − d_ms)/2, which catches sign and shift bugs at any asymmetry.

24. Understanding Check

25. What's Next

This chapter produced two numbers and deliberately left three problems open.

The numbers: a round-trip delay that is exact, and an offset that is exact up to half the path's asymmetry.

The first problem is the one Section 2 named and Section 5 worked around. A Sync must carry the time of its own transmission, and that time is not known until the message is already leaving. Two-step sends it afterwards in a Follow_Up; one-step rewrites the field in flight and must then fix the FCS mid-frame. Chapter 16.3 — Hardware Timestamping at the MAC/PHY Boundary builds both, prices the FCS recomputation against Chapter 12.1 §12's frame budget, and places the unit at the boundary Chapter 16.1 §13 derived.

The second is the correction this chapter subtracted without producing. correctionField arrives holding residence time, and something has to put it there. Chapter 16.4 — The Servo closes the loop on the offset using Chapter 16.1 §3's rate trim, derives the loop bandwidth from §11's noise-against-drift trade, and builds the transparent clock that measures how long a switch held each message — which is what turns Section 20's Run D from 6.07 µs into something much smaller.

And the third is the residual nothing so far removes. Chapter 16.5 — What Limits Accuracy takes the asymmetry this chapter derived, the granularity Chapter 16.1 §14 bounded, and the jitter neither of them could filter, and prices all three with the protocol in place — then gives the calibration procedure that removes the part of the asymmetry that is constant.

One thread carries into all three. This chapter's arithmetic was exact and its premise was not. Every chapter that follows is an attempt to make the premise more nearly true — by measuring the switches, by calibrating the cables, and by admitting what is left.

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.