Ethernet · Module 17
Time-Aware Shaping (802.1Qbv)
A schedule turns an unbounded interference term into a configuration parameter, and then spends up to 19.5% of the cycle on a guard band to stay safe.
Chapter 17.1 §16 named the three things a bounded path needs — a schedule, a clock and a guard band — and built none of them. This chapter builds the first and the third; Module 16 already built the second.
The schedule's job is one substitution. Chapter 17.1 §8's interference term is bounded by how much higher-priority traffic arrives, which nothing limits. A gate schedule shuts every other class's gate during a window, so a frame transmitted in its own window waits for nothing — and a frame that misses its window waits at most one cycle, which is a number an operator chose.
| before | with a schedule | |
|---|---|---|
| interference, inside the window | unbounded | zero |
| interference, having missed a window | unbounded | cycle − window |
| what sets it | the traffic | the configuration |
And the price is the guard band, which Chapter 17.1 §17 derived as MTU/R + 2 × sync_error and this chapter spends.
At 1 Gb/s that is 12.192 µs, and it is subtracted from every window in every cycle. So a design that shortens its cycle to improve the latency bound pays the guard band more often — and the two move in opposite directions:
| Cycle | Window | Worst wait | Guard band, per cycle | Usable fraction |
|---|---|---|---|---|
| 2000 µs | 200 µs | 1800 µs | 0.61% | 9.39% |
| 1000 µs | 100 µs | 900 µs | 1.22% | 8.78% |
| 250 µs | 50 µs | 200 µs | 4.88% | 15.12% |
| 125 µs | 25 µs | 100 µs | 9.75% | 10.25% |
| 62.5 µs | 12.5 µs | 50 µs | 19.51% | 0.49% |
Read the last row's last two columns together. A 12.5 µs window at 1 Gb/s is 12.192 µs of guard band and 0.308 µs of usable transmission — the guard band has eaten the window, and the schedule delivers 0.49% of the link.
Which is the whole argument for Chapter 17.3, and this chapter's last section is where that argument becomes a number.
1. Scope — What This Chapter Owns
This chapter owns the schedule: the gate-control list, the cycle and its windows, the guard band's implementation, the transmission-eligibility check, the schedule's own contribution to latency, and the installation of a new schedule across a path.
It does not own the requirement. Chapter 17.1 established that two of a hop's seven latency terms have no bound and that five sum to 37.12 µs per hop at 1 Gb/s. This chapter bounds the sixth and seventh.
It does not own the clock. Chapter 16.5 §16 assembled a 24.2 ns budget; Sections 7 and 14 consume it — once in the guard band and once in the installation window.
And it does not own the guard band's dominant term. Chapter 17.1 §18 showed the MTU contributing 12.144 µs of a 12.192 µs band at 1 Gb/s. Section 18 prices what removing it would buy and Chapter 17.3 removes it.
2. What a Schedule Is
Four objects, and the whole mechanism is their arithmetic.
| Object | What it is |
|---|---|
| the cycle | a repeating interval, the same on every device |
| the gate-control list | a sequence of (gate vector, interval) entries summing to the cycle |
| a gate | one bit per traffic class: open or shut |
| the guard band | the interval before a gate shuts during which nothing new is admitted |
A gate-control list is executed cyclically. At each entry the eight gates take the entry's vector, and the entry's interval says how long before the next. The intervals sum to the cycle, so the list repeats exactly.
And "the same on every device" is the requirement that makes it work. A frame transmitted during class 6's window at hop 1 must arrive at hop 2 during class 6's window there — otherwise it waits for the next one, and the bound becomes N × cycle rather than cycle.
Which gives the three things Section 1 said this chapter consumes, in the order they are needed:
A shared cycle, so every device's list repeats with the same period.
A shared phase, so entry k begins at the same instant everywhere — and this is where Chapter 16.5's 24.2 ns enters.
And a guard band, because Chapter 12.6 §8 established that a frame in flight cannot be aborted, so a gate that shuts while a frame is transmitting has admitted something that will overrun into the next window.
The gates themselves are trivial — eight bits and a comparator. Everything difficult in this chapter is one of those three.
3. RTL 1 — The Gate-Control List
The list is a small memory and a sequencer, and the one subtlety is that it must be replaceable while it is running.
// -----------------------------------------------------------------------
// tas_pkg -- shared types for 802.1Qbv time-aware shaping.
// -----------------------------------------------------------------------
package tas_pkg;
localparam int NUM_CLASSES = 8; // 13.2's PCP, 13.4's queues
localparam int GCL_DEPTH = 64;
localparam int NS_W = 32;
typedef logic [NUM_CLASSES-1:0] gate_vec_t;
typedef logic [NS_W-1:0] ns_t;
// One gate-control entry: which gates are open, and for how long.
typedef struct packed {
gate_vec_t gates;
ns_t interval_ns;
} gcl_entry_t;
// A schedule is a list plus the cycle it repeats on and the instant
// it becomes effective. The third field is section 13's subject.
typedef struct packed {
logic valid;
logic [$clog2(GCL_DEPTH+1)-1:0] length;
ns_t cycle_ns;
logic [47:0] base_sec; // PTP time -- 16.2's timescale
ns_t base_ns;
} schedule_hdr_t;
endpackage// -----------------------------------------------------------------------
// gate_control_list -- holds two schedules and executes one.
//
// Two, because a schedule must be REPLACED atomically at an instant
// agreed across the path -- section 13. A single-buffered list cannot
// be changed without a window in which it is neither the old schedule
// nor the new one.
// -----------------------------------------------------------------------
module gate_control_list
import tas_pkg::*;
(
input logic clk,
input logic rst_n,
// Configuration writes land in the SHADOW bank, never the live one.
input logic cfg_we,
input logic [$clog2(GCL_DEPTH)-1:0] cfg_idx,
input gcl_entry_t cfg_entry,
input logic cfg_hdr_we,
input schedule_hdr_t cfg_hdr,
// The swap, from section 13's installer.
input logic swap_now,
// Execution.
input logic advance, // the interval expired
output gate_vec_t gates,
output ns_t current_interval_ns,
output logic [$clog2(GCL_DEPTH)-1:0] current_index,
output schedule_hdr_t live_hdr,
output logic wrapped, // the list returned to entry 0
output logic [31:0] c_swaps,
output logic [31:0] c_cycles,
output logic cfg_sum_mismatch // intervals do not sum to the cycle
);
gcl_entry_t bank_a [GCL_DEPTH];
gcl_entry_t bank_b [GCL_DEPTH];
schedule_hdr_t hdr_a, hdr_b;
logic live_is_a;
logic [$clog2(GCL_DEPTH)-1:0] idx;
// The intervals must sum to the cycle, or the list drifts against
// every other device's. This is checkable at configuration time and
// is the most common schedule error.
ns_t sum_shadow;
always_comb begin
int i;
sum_shadow = '0;
for (i = 0; i < GCL_DEPTH; i++)
if (i < int'(live_is_a ? hdr_b.length : hdr_a.length))
sum_shadow = sum_shadow +
(live_is_a ? bank_b[i].interval_ns : bank_a[i].interval_ns);
end
assign cfg_sum_mismatch =
(live_is_a ? hdr_b.valid : hdr_a.valid) &&
(sum_shadow != (live_is_a ? hdr_b.cycle_ns : hdr_a.cycle_ns));
always_ff @(posedge clk or negedge rst_n) begin
int i;
if (!rst_n) begin
for (i = 0; i < GCL_DEPTH; i++) begin
bank_a[i] <= '0;
bank_b[i] <= '0;
end
hdr_a <= '0; hdr_b <= '0;
live_is_a <= 1'b1; idx <= '0;
c_swaps <= '0; c_cycles <= '0; wrapped <= 1'b0;
end else begin
wrapped <= 1'b0;
// Writes always target the shadow bank.
if (cfg_we) begin
if (live_is_a) bank_b[cfg_idx] <= cfg_entry;
else bank_a[cfg_idx] <= cfg_entry;
end
if (cfg_hdr_we) begin
if (live_is_a) hdr_b <= cfg_hdr;
else hdr_a <= cfg_hdr;
end
// The swap is a single flop toggle, so the transition is atomic
// AT THIS DEVICE. Section 14 is about the fact that it is not
// atomic across devices.
if (swap_now) begin
live_is_a <= !live_is_a;
idx <= '0;
c_swaps <= c_swaps + 1;
end else if (advance) begin
if (idx == ((live_is_a ? hdr_a.length : hdr_b.length) - 1)) begin
idx <= '0;
wrapped <= 1'b1;
c_cycles <= c_cycles + 1;
end else begin
idx <= idx + 1'b1;
end
end
end
end
assign gates = live_is_a ? bank_a[idx].gates : bank_b[idx].gates;
assign current_interval_ns =
live_is_a ? bank_a[idx].interval_ns : bank_b[idx].interval_ns;
assign current_index = idx;
assign live_hdr = live_is_a ? hdr_a : hdr_b;
endmoduleClassification: a double-buffered configuration memory with a cyclic sequencer. Two banks, one toggle.
What it teaches: that double buffering is not an optimisation here, it is the only way a schedule can be replaced at all. A single-banked list being rewritten while it executes runs a mixture of the old and new schedules — entries 0 to k new, k+1 onward old — and the mixture's intervals do not sum to any cycle. The gates then drift against every other device's until the write completes, which for a 64-entry list over a management bus is milliseconds.
And it teaches that cfg_sum_mismatch is a configuration check with a specific and severe failure. The intervals must sum to the cycle exactly. If they sum to less, this device's list wraps early and its phase advances against the path's; if more, it wraps late. Either way the drift accumulates every cycle, so a 1 µs error on a 1000 µs cycle puts the device a full window out of phase after 100 cycles — 0.1 seconds.
Deliberately simplified: the sum is computed combinationally over all 64 entries, which is a 64-input adder tree evaluated continuously. A production design computes it once when the shadow bank's header is written and latches the result — the check is a configuration-time property, not a runtime one, and Chapter 14.1 §17's argument applies: it is wrong from the moment it is written, not from the first frame.
Production implication: c_cycles and wrapped are what let an operator verify that two devices are running the same schedule at the same phase. Comparing c_cycles across a path, sampled at a common PTP instant, shows a device whose list is drifting — and a device whose count differs by N after a known interval has an interval-sum error of N × cycle over that interval. Without the counter the drift is invisible until a frame misses a window.
4. The Cycle, the Windows and the Gates
A worked schedule makes the objects concrete, and the arithmetic in it is the arithmetic of the whole chapter.
Take a 1000 µs cycle at 1 Gb/s with three classes: 6 for control, 5 for audio, and 0 for everything else.
| Entry | Gates open | Interval | What happens |
|---|---|---|---|
| 0 | class 6 only | 100 µs | control transmits; nothing else may start |
| 1 | class 5 only | 150 µs | audio transmits |
| 2 | class 0 only | 750 µs | bulk traffic gets the rest |
| — | total | 1000 µs | must equal the cycle exactly |
Three things follow immediately and each one is a section of this chapter.
A class-6 frame that is ready at the start of entry 0 waits for nothing — every other gate is shut, so Chapter 17.1 §8's interference term is zero.
A class-6 frame that becomes ready one microsecond after entry 0 ends waits 900 µs for the next cycle's entry 0. That is the schedule's own latency contribution, and it is cycle − window.
And entry 0's last 12.192 µs cannot admit a new frame, because a maximum frame started then would still be transmitting when the gate shuts. So the usable part of a 100 µs window is 87.8 µs, and Section 8 is that subtraction.
The gates themselves are the easy part:
| Class | Gate bit | Open during |
|---|---|---|
| 6 | gates[6] | entry 0 |
| 5 | gates[5] | entry 1 |
| 0 | gates[0] | entry 2 |
| 1–4, 7 | — | never — configured, and starved by construction |
The last row is worth noticing because it is a real configuration and a real hazard. A class whose gate is never open is a class whose traffic is never transmitted, and Chapter 13.4 §11's strict-priority starvation returns in a new form — except that here it is deliberate, permanent, and produced by a configuration that looks complete.
==
5. RTL 2 — The Cycle Timer
The list from Section 3 needs something to advance it, and that something is where the schedule is anchored to real time.
// -----------------------------------------------------------------------
// cycle_timer -- advances the gate-control list and keeps its phase
// locked to the PTP timescale.
//
// The list's phase is NOT free-running. It is derived from the
// schedule's base time and the cycle, so every device in the path
// computes the same entry boundary from the same clock -- 16.5's
// clock, with 16.5's 24.2 ns of disagreement.
// -----------------------------------------------------------------------
module cycle_timer
import tas_pkg::*;
(
input logic clk,
input logic rst_n,
// 16.1 section 3's free-running clock, disciplined by 16.4's servo.
input logic [47:0] ptp_sec,
input ns_t ptp_ns,
input logic ptp_locked, // 16.4 section 9's S_LOCKED
input schedule_hdr_t live_hdr,
input ns_t current_interval_ns,
output logic advance,
output ns_t time_in_entry_ns,
output ns_t time_to_entry_end_ns,
output logic phase_valid,
output logic [31:0] c_advances,
output logic [31:0] c_phase_lost,
output logic signed [31:0] phase_error_ns
);
ns_t entry_start_ns;
ns_t elapsed;
// Time since the schedule's base, modulo the cycle. This is the
// position within the cycle, and every device computes it from the
// same PTP instant -- which is why they agree.
logic [63:0] since_base;
ns_t cycle_pos;
always_comb begin
since_base = ({16'd0, ptp_sec} - {16'd0, live_hdr.base_sec}) * 64'd1_000_000_000
+ {32'd0, ptp_ns} - {32'd0, live_hdr.base_ns};
cycle_pos = (live_hdr.cycle_ns == 0) ? '0
: ns_t'(since_base % {32'd0, live_hdr.cycle_ns});
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
entry_start_ns <= '0; elapsed <= '0; advance <= 1'b0;
phase_valid <= 1'b0; c_advances <= '0; c_phase_lost <= '0;
phase_error_ns <= '0;
end else begin
advance <= 1'b0;
// A schedule without a locked clock is a schedule whose phase is
// meaningless. 16.4 section 9's S_LOCKED gates it -- and the
// correct behaviour when it is lost is section 17's subject.
if (!ptp_locked || !live_hdr.valid) begin
if (phase_valid) c_phase_lost <= c_phase_lost + 1;
phase_valid <= 1'b0;
end else begin
phase_valid <= 1'b1;
elapsed <= cycle_pos - entry_start_ns;
if ((cycle_pos - entry_start_ns) >= current_interval_ns) begin
advance <= 1'b1;
entry_start_ns <= entry_start_ns + current_interval_ns;
c_advances <= c_advances + 1;
end
// How far this device's entry boundary sits from the ideal.
// On a correct schedule it is bounded by one clock period plus
// 16.5's synchronisation error.
phase_error_ns <= $signed({1'b0, cycle_pos}) -
$signed({1'b0, entry_start_ns});
end
end
end
assign time_in_entry_ns = elapsed;
assign time_to_entry_end_ns = (current_interval_ns > elapsed)
? (current_interval_ns - elapsed) : '0;
endmoduleClassification: a modulo counter anchored to an absolute timescale. No control, and its correctness is entirely in the modulo.
What it teaches: that the schedule's phase is computed from an absolute time rather than counted from a local reset, and that is what makes two devices agree. A free-running cycle counter started at power-on would drift against every other device's by the oscillator's tolerance — Chapter 16.1 §4's 100 ppm is 100 µs per second, so two devices would be a whole window apart within a second. Deriving the position as (now − base) mod cycle makes the phase a function of the shared clock, so the only disagreement is the clock's own: 24.2 ns.
And it teaches that ptp_locked must gate the schedule, and that gating it is not obviously the right behaviour. A device whose servo has lost lock has a clock drifting at its oscillator's rate, so its gates open at times nobody else agrees with — and opening them anyway means transmitting into another class's window. Section 17's table has the alternatives and none of them is good.
Deliberately simplified: since_base uses a 64-bit modulo, which synthesises into a divider. A production design exploits the fact that the cycle is constant between swaps and maintains the position incrementally, resynchronising to the absolute value once per cycle — which is one divide per cycle rather than one per clock.
Production implication: phase_error_ns is the measurement that says whether this device's gates are where the schedule says they should be. On a correct implementation it is bounded by one clock period plus Chapter 16.5's 24.2 ns; a value larger than that means the intervals do not sum to the cycle — Section 3's cfg_sum_mismatch, seen at run time rather than at configuration time — and the error accumulates, so the measurement grows without limit.
6. Where the Cycle's Start Comes From
Section 5 anchored the phase to base_sec/base_ns. This section is why that field exists and what happens when it is chosen badly.
The base time is an instant in Chapter 16.2's timescale at which entry 0 of the list begins. Every device computes its position as (now − base) mod cycle, so every device that has the same base and the same cycle is at the same entry — to within the clock's accuracy.
Three properties of the base matter and all three are configuration decisions.
It must be the same on every device. Two devices with bases differing by 500 µs on a 1000 µs cycle are half a cycle out of phase, and every frame transmitted in a window at one arrives outside the window at the other. Nothing detects this except the frames missing their deadlines.
It should be in the past. A base in the future means the schedule is not yet effective, and the standard's behaviour is to compute the position modulo the cycle anyway — so a base 1 hour in the future on a 1000 µs cycle gives a position that is arithmetically correct and semantically meaningless. A base in the past is unambiguous.
And it must be divisible into the cycle sensibly if several schedules interlock. A path whose hops run a 1000 µs cycle and one hop running 1024 µs has a relative phase that advances 24 µs per cycle and wraps in 41 cycles — so the path works for 41 milliseconds at a time.
Which gives a commissioning requirement that looks trivial and is not:
| Parameter | Must be | Failure if not |
|---|---|---|
| cycle | identical on every hop | relative phase drifts; works intermittently |
| base time | identical on every hop | a constant phase offset; frames always miss |
| intervals | sum to the cycle | Section 3's drift, accumulating |
| the clock | locked, on every hop | Section 5's phase_valid low |
Row two's failure is the one that is easiest to create and hardest to diagnose, because it produces a network in which everything is configured, every device reports a valid schedule, and every scheduled frame arrives one window late at the next hop. The latency is then N × cycle rather than cycle, which for five hops at 1000 µs is 5 ms against a 1 ms deadline.
7. RTL 3 — The Guard Band
Chapter 17.1 §17 derived it. This is the implementation, and the implementation has a choice in it that the derivation did not.
// -----------------------------------------------------------------------
// guard_band_calc -- computes how long before a gate shuts the port
// must stop admitting frames.
//
// Two variants, and the choice is a real one:
// STATIC : reserve for a maximum frame always -- simple, wasteful.
// LENGTH : admit a frame if THIS frame fits -- 12.6's length is
// known before transmission starts, so the check is exact.
// -----------------------------------------------------------------------
module guard_band_calc
import tas_pkg::*;
#(
parameter int LINE_RATE_MBPS = 1000,
parameter int MTU_OCTETS = 1518,
// 16.5 section 16's assembled budget. Doubled: two devices each
// wrong by that much, in opposite directions.
parameter int SYNC_ERROR_NS = 24,
parameter bit LENGTH_AWARE = 1'b1
)(
input logic clk,
input logic rst_n,
input ns_t time_to_gate_close_ns,
input logic frame_pending,
input logic [13:0] frame_octets,
output ns_t static_guard_ns,
output ns_t this_frame_ns,
output logic admit,
output ns_t wasted_this_window_ns,
output logic [31:0] c_admitted,
output logic [31:0] c_held,
output logic [15:0] guard_pct_x100 // of the window
);
// MTU/R + 2 x sync_error -- 17.1 section 17's formula, in hardware.
localparam int MTU_NS = (MTU_OCTETS * 8000) / LINE_RATE_MBPS;
localparam int GUARD_NS = MTU_NS + 2*SYNC_ERROR_NS;
ns_t need;
always_comb begin
// A length-aware guard admits a frame if THIS frame fits, which is
// exact because the length is known before transmission begins --
// 12.6's store-and-forward has the whole frame; even cut-through
// has the length field.
this_frame_ns = ns_t'((32'(frame_octets) * 8000) / LINE_RATE_MBPS)
+ ns_t'(2*SYNC_ERROR_NS);
need = LENGTH_AWARE ? this_frame_ns : ns_t'(GUARD_NS);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
admit <= 1'b0; c_admitted <= '0; c_held <= '0;
wasted_this_window_ns <= '0; guard_pct_x100 <= '0;
end else begin
admit <= 1'b0;
if (frame_pending) begin
if (time_to_gate_close_ns >= need) begin
admit <= 1'b1;
c_admitted <= c_admitted + 1;
end else begin
// The frame does not fit. It waits for the next window --
// which is section 10's latency contribution, arriving one
// frame at a time.
c_held <= c_held + 1;
wasted_this_window_ns <= time_to_gate_close_ns;
end
end
end
end
assign static_guard_ns = ns_t'(GUARD_NS);
endmoduleClassification: a comparator with two policies. The policies differ by a factor that depends entirely on the traffic's frame-size distribution.
What it teaches: that a length-aware guard band is exact and a static one is a worst case, and the difference is large on small frames. A 64-octet frame needs 0.56 µs at 1 Gb/s; the static guard reserves 12.192 µs. So a window's last 11.6 µs is usable by small frames and not by large ones — and a static guard refuses them all.
And it teaches that the length is always available in time, which is why the exact policy is implementable. Chapter 12.6's store-and-forward has the entire frame before it transmits anything; even cut-through has the length from Chapter 5.5's length field or from the descriptor. There is no case in which a transmitter must decide whether to admit a frame without knowing how long it is.
Deliberately simplified: SYNC_ERROR_NS is a parameter and Chapter 16.5 §14 produces a budget with a composition. A production design takes the live budget — which changes when a link renegotiates or a calibration goes stale — and a guard band computed from a commissioning-time constant is Chapter 16.3 §4's stale-correction problem in a new place.
Production implication: c_held is the count of frames that were ready and did not fit, and it is the measurement that says whether the window is sized correctly. A window in which c_held is a large fraction of c_admitted is a window whose guard band is consuming it — Section 8's last row — and the remedy is a longer window, a shorter frame, or Chapter 17.3's preemption.
8. The Guard Band, Recomputed With the Schedule
Chapter 17.1 §17 computed the band. This computes what it does to a schedule, which is a different and worse number.
The band is subtracted from every window in every cycle, so its cost is a fraction of time rather than a one-off.
| Line rate | Guard band | Of a 100 µs window | Of a 1000 µs cycle |
|---|---|---|---|
| 1 Gb/s | 12.192 µs | 12.19% | 1.22% |
| 10 Gb/s | 1.263 µs | 1.26% | 0.13% |
| 25 Gb/s | 0.534 µs | 0.53% | 0.05% |
| 100 Gb/s | 0.170 µs | 0.17% | 0.02% |
And the fraction that matters is the first column against the window, because that is the bandwidth the scheduled class loses.
Then the cycle's length enters, and it enters twice. A shorter cycle reduces the worst-case wait — Section 10 — and increases how often the band is paid:
| Cycle | Window | Worst wait | Guard band per cycle | Usable fraction of the link |
|---|---|---|---|---|
| 2000 µs | 200 µs | 1800 µs | 0.61% | 9.39% |
| 1000 µs | 100 µs | 900 µs | 1.22% | 8.78% |
| 500 µs | 75 µs | 425 µs | 2.44% | 12.56% |
| 250 µs | 50 µs | 200 µs | 4.88% | 15.12% |
| 125 µs | 25 µs | 100 µs | 9.75% | 10.25% |
| 62.5 µs | 12.5 µs | 50 µs | 19.51% | 0.49% |
The usable column has a maximum in it, which is the finding. It rises as the cycle shortens — more windows per second, each losing a fixed band — until the band approaches the window's length, and then it collapses. At 1 Gb/s the optimum is around a 250 µs cycle and the collapse is at about 12 µs of window.
And the last row is the one to internalise: a 12.5 µs window at 1 Gb/s is 12.192 µs of guard band and 0.308 µs of transmission. The schedule is correct, the bound is met, and 0.49% of the link is delivered.
Which is Chapter 17.3's entire justification, and Section 18 states it as an arithmetic rather than as a motivation.
==
9. RTL 4 — The Transmission-Eligibility Check
Three conditions, evaluated every cycle, and their conjunction is what a scheduled port actually does.
// -----------------------------------------------------------------------
// tx_eligibility_check -- decides which class may start a frame now.
//
// It sits between 13.4 section 11's scheduler and the transmit path,
// in exactly the position 14.4 section 9's PFC gate occupies -- and it
// is a second mask on the same eligibility vector.
// -----------------------------------------------------------------------
module tx_eligibility_check
import tas_pkg::*;
(
input logic clk,
input logic rst_n,
input gate_vec_t gates, // section 3's list
input logic phase_valid, // section 5
input gate_vec_t q_nonempty, // 13.4's eight queues
input gate_vec_t pfc_paused, // 14.4 section 8's timers
input logic guard_admits, // section 7
input logic tx_in_frame,
output gate_vec_t eligible,
output logic all_blocked,
output logic blocked_by_gate,
output logic blocked_by_guard,
output logic [31:0] c_gate_blocked [NUM_CLASSES],
output logic [31:0] c_guard_blocked,
output logic [31:0] c_no_phase
);
always_comb begin
int c;
for (c = 0; c < NUM_CLASSES; c++) begin
// Four conditions, all required. The gate is new; the other
// three were already there.
eligible[c] = gates[c] // the schedule permits it
&& q_nonempty[c] // there is something to send
&& !pfc_paused[c] // 14.4's neighbour agrees
&& guard_admits // section 7: it fits
&& phase_valid; // section 5: we know the time
end
end
assign blocked_by_gate = (q_nonempty & ~gates) != '0;
assign blocked_by_guard = (q_nonempty & gates) != '0 && !guard_admits;
assign all_blocked = (q_nonempty != '0) && (eligible == '0);
always_ff @(posedge clk or negedge rst_n) begin
int c;
if (!rst_n) begin
for (c = 0; c < NUM_CLASSES; c++) c_gate_blocked[c] <= '0;
c_guard_blocked <= '0; c_no_phase <= '0;
end else begin
for (c = 0; c < NUM_CLASSES; c++)
if (q_nonempty[c] && !gates[c]) c_gate_blocked[c] <= c_gate_blocked[c] + 1;
if (blocked_by_guard) c_guard_blocked <= c_guard_blocked + 1;
if (!phase_valid && (q_nonempty != '0)) c_no_phase <= c_no_phase + 1;
end
end
endmoduleClassification: a four-way conjunction on an eligibility vector. Combinational, and it is the third mask on the same signal.
What it teaches: that a scheduled port now has three independent gating mechanisms and they compose by conjunction. Chapter 13.4 §11's scheduler chooses among eligible classes; Chapter 14.4 §9's PFC gate removes paused ones; and this removes classes whose window is shut. All three subtract and none adds — so a class transmits only when all three permit it, and a frame held by any of them is held.
And it teaches that the three counters must be separate because they name different parties. c_gate_blocked[c] is our own schedule; pfc_paused is the neighbour's decision; c_guard_blocked is the frame's own length against the window's remainder. A single "blocked" counter sends every investigation to the same place, and two of the three are at different ends of the cable — which is Chapter 14.3 §15's attribution argument, arriving for the fourth time.
Deliberately simplified: the gate vector is applied to the class and not to the queue, which assumes Chapter 13.4 §9's mapping is the identity. With eight priorities compressed into four queues — that chapter's §10 — a gate must be derived from the queue's constituent classes, and the conservative derivation is that a queue transmits only when every class mapped to it has its gate open.
Production implication: c_no_phase counts cycles in which frames were waiting and the clock was not locked, and it is the counter that connects Module 16's health to Module 17's function. A device whose servo lost lock — Chapter 16.4 §9's S_HOLDOVER — has a schedule whose phase is drifting, and Section 17's table gives the three possible responses. Without this counter, a synchronisation outage presents as a latency problem.
==
10. The Schedule's Own Contribution to Latency
Chapter 17.1 §8's interference term is gone. What replaces it is a new term that is bounded, and the bound is worth deriving rather than quoting.
A frame becomes ready at some instant. Two cases:
It is inside its class's window and it fits. Then it transmits immediately and the schedule contributes zero.
It is outside its window, or inside but too close to the end. Then it waits until the next occurrence of its window, and the worst case is the whole cycle minus the window — a frame that became ready one nanosecond after its window ended.
schedule_wait_worst = cycle − windowAnd that is the entire new term. It is bounded, it is a configuration parameter, and it does not depend on the traffic at all.
Which lets Chapter 17.1's worst case be completed:
worst_case = N × bounded_terms + schedule_waitAt 1 Gb/s with five hops, Chapter 17.1 §4's bounded subtotal is 37.12 µs per hop — 185.6 µs.
| Cycle | Window | Schedule wait | Five-hop worst case | Against a 1 ms deadline |
|---|---|---|---|---|
| 2000 µs | 200 µs | 1800 µs | 1985.6 µs | missed |
| 1000 µs | 100 µs | 900 µs | 1085.6 µs | missed |
| 500 µs | 75 µs | 425 µs | 610.6 µs | met |
| 250 µs | 50 µs | 200 µs | 385.6 µs | met, with margin |
| 125 µs | 25 µs | 100 µs | 285.6 µs | met |
| 62.5 µs | 12.5 µs | 50 µs | 235.6 µs | met |
Row two is Chapter 17.1 §20's Run D exactly — 1085.6 µs against a 1000 µs deadline — and row four is that section's row 14, the cycle change that fixed it.
And the schedule wait appears once, not once per hop, which is the subtlety worth stating. A frame that catches its window at hop 1 catches it at every hop, provided the hops share a cycle and a phase — Section 6 — so the wait is paid at the first hop and nowhere else. A path whose hops have different bases pays it at every hop, and five hops at a 1000 µs cycle is 5 ms.
Which is why Section 6's base-time requirement is not a detail. The difference between one schedule wait and N of them is the difference between meeting the deadline and missing it by a factor of five, and both configurations look identical in every status output.
11. RTL 5 — Chapter 17.1 §20's Run D, Fixed
Section 10's table has the answer in it. This module is what lets a design find it without a spreadsheet.
// -----------------------------------------------------------------------
// schedule_latency_model -- computes a path's worst case from the
// schedule's parameters, and searches for a cycle that meets a
// deadline.
//
// The search is worth having in hardware because both terms move: a
// shorter cycle reduces the wait and raises the guard-band overhead --
// section 8 -- so the answer is a constrained optimum rather than a
// limit.
// -----------------------------------------------------------------------
module schedule_latency_model
import tas_pkg::*;
#(
parameter int LINE_RATE_MBPS = 1000,
parameter int GUARD_NS = 12192 // section 7, at 1 Gb/s
)(
input logic clk,
input logic rst_n,
input logic eval,
input logic [7:0] n_hops,
input ns_t bounded_per_hop_ns, // 17.1 section 4: 37 120
input ns_t cycle_ns,
input ns_t window_ns,
input logic hops_share_phase, // section 6
input ns_t deadline_ns,
output ns_t schedule_wait_ns,
output ns_t worst_case_ns,
output logic meets_deadline,
output ns_t margin_ns,
output logic [15:0] guard_overhead_x100,
output logic [15:0] usable_fraction_x100,
output ns_t suggested_cycle_ns,
output logic no_cycle_works
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
schedule_wait_ns <= '0; worst_case_ns <= '0;
meets_deadline <= 1'b0; margin_ns <= '0;
guard_overhead_x100 <= '0; usable_fraction_x100 <= '0;
suggested_cycle_ns <= '0; no_cycle_works <= 1'b0;
end else if (eval) begin
automatic ns_t wait_one, bounded, wc, try_cycle, try_win;
automatic bit found;
wait_one = (cycle_ns > window_ns) ? (cycle_ns - window_ns) : '0;
// Section 10: the wait is paid ONCE if the hops share a phase,
// and once per hop if they do not.
schedule_wait_ns <= hops_share_phase
? wait_one
: (wait_one * ns_t'(n_hops));
bounded = bounded_per_hop_ns * ns_t'(n_hops);
wc = bounded + (hops_share_phase ? wait_one
: (wait_one * ns_t'(n_hops)));
worst_case_ns <= wc;
meets_deadline <= (wc <= deadline_ns);
margin_ns <= (deadline_ns > wc) ? (deadline_ns - wc) : '0;
guard_overhead_x100 <= (cycle_ns == 0) ? 16'd0
: 16'((32'(GUARD_NS) * 10000) / 32'(cycle_ns));
// What the scheduled class actually gets: the window less the
// guard band, as a fraction of the cycle. Section 8's collapse.
usable_fraction_x100 <= ((cycle_ns == 0) || (window_ns <= ns_t'(GUARD_NS)))
? 16'd0
: 16'(((32'(window_ns) - 32'(GUARD_NS)) * 10000) /
32'(cycle_ns));
// Search: halve the cycle until the deadline is met, keeping the
// window at a tenth. Stop when the guard band consumes it.
found = 1'b0; try_cycle = cycle_ns;
repeat (8) begin
try_win = try_cycle / 10;
if (!found && (try_win > ns_t'(GUARD_NS)) &&
((bounded + (try_cycle - try_win)) <= deadline_ns)) begin
found = 1'b1;
suggested_cycle_ns <= try_cycle;
end
try_cycle = try_cycle >> 1;
end
no_cycle_works <= !found;
end
end
endmoduleClassification: a closed-form evaluator with a bounded search. Eight iterations, evaluated on demand.
What it teaches: that hops_share_phase multiplies the answer by the hop count, and it is a configuration property rather than a measurable one. Section 6: hops with different base times pay the schedule wait at every hop. At a 1000 µs cycle over five hops that is 4500 µs of wait against 900 — and the two configurations are indistinguishable in every per-device status output. The model has to be told, which is why the input exists and why commissioning must verify it.
And it teaches that the search terminates on try_win > GUARD_NS rather than on the deadline, which is Section 8's collapse as a stopping condition. A cycle short enough that its window is smaller than the guard band delivers nothing, so it is not a solution however well it meets the deadline — and no_cycle_works distinguishes "the deadline is unachievable" from "we did not look hard enough".
Deliberately simplified: the window is assumed to be a tenth of the cycle throughout the search. A real schedule allocates windows by class bandwidth requirement, so the search is over a two-dimensional space — and the honest version reports a frontier rather than a single suggestion.
Production implication: usable_fraction_x100 is the number that stops a design shortening the cycle indefinitely. At 1 Gb/s it peaks around a 250 µs cycle at 15.12% and collapses to 0.49% at 62.5 µs — so a design chasing latency by halving the cycle passes through the optimum and then loses the link. The counter makes the peak visible, and Chapter 17.3 moves it.
12. What a Cycle Change Costs
Section 10 showed a shorter cycle meeting a deadline that a longer one missed. This is the other side of that ledger.
Two quantities move in opposite directions as the cycle shortens:
schedule_wait = cycle − window falls linearly
guard_overhead = guard_band / cycle rises linearlyAnd the product of those two behaviours is a usable fraction with a maximum:
| Cycle | Wait | Guard overhead | Usable |
|---|---|---|---|
| 2000 µs | 1800 µs | 0.61% | 9.39% |
| 1000 µs | 900 µs | 1.22% | 8.78% |
| 500 µs | 425 µs | 2.44% | 12.56% |
| 250 µs | 200 µs | 4.88% | 15.12% |
| 125 µs | 100 µs | 9.75% | 10.25% |
| 62.5 µs | 50 µs | 19.51% | 0.49% |
Halving the cycle halves the latency contribution and doubles the guard-band overhead — which is a clean trade until the window approaches the band, at which point the usable fraction falls off a cliff rather than declining.
And there is a third cost that the table does not show and that a real deployment feels: a shorter cycle means more schedule entries per second to execute, more list wraps, and — Section 14 — more frequent opportunities for an installation to go wrong.
| Cycle | Cycles per second | GCL entries executed per second, 3-entry list |
|---|---|---|
| 2000 µs | 500 | 1500 |
| 1000 µs | 1000 | 3000 |
| 250 µs | 4000 | 12 000 |
| 62.5 µs | 16 000 | 48 000 |
None of those is difficult for hardware — 48 000 entry advances per second is a 21 µs period against a 2 ns clock — and all of them multiply the management burden, because a schedule change disrupts one cycle and a shorter cycle means more of them per unit time.
Which gives the sizing rule: choose the longest cycle that meets the deadline, not the shortest that is achievable. Section 11's search does exactly that — it halves until the deadline is met and stops — and a design that halved further bought margin it did not need with bandwidth it did.
13. RTL 6 — Installing a Schedule Atomically
The hardest part of the chapter, and it is hard for a reason that has nothing to do with the schedule.
// -----------------------------------------------------------------------
// schedule_installer -- swaps the live and shadow gate-control lists
// at an instant agreed across every device in the path.
//
// The swap is atomic AT THIS DEVICE -- section 3's single flop. It is
// not atomic ACROSS devices, because "the same instant" is bounded by
// 16.5's 24.2 ns and nothing closes that window.
// -----------------------------------------------------------------------
module schedule_installer
import tas_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [47:0] ptp_sec,
input ns_t ptp_ns,
input logic ptp_locked,
// The new schedule's effective instant, distributed to every device
// by management. It must be the SAME value everywhere.
input logic arm,
input logic [47:0] effective_sec,
input ns_t effective_ns,
input logic shadow_valid,
input logic shadow_sum_ok, // section 3's check
// 16.5 section 14's live budget, not a commissioning constant.
input logic [15:0] sync_budget_ns,
output logic swap_now,
output logic armed,
output logic install_refused,
output logic [7:0] refuse_reason,
output ns_t disagreement_window_ns,
output logic [31:0] c_installs,
output logic [31:0] c_refused,
output logic [31:0] c_late_arm // armed after the instant
);
logic [63:0] now_abs, eff_abs;
always_comb begin
now_abs = {16'd0, ptp_sec} * 64'd1_000_000_000 + {32'd0, ptp_ns};
eff_abs = {16'd0, effective_sec} * 64'd1_000_000_000 + {32'd0, effective_ns};
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
armed <= 1'b0; swap_now <= 1'b0;
install_refused <= 1'b0; refuse_reason <= '0;
c_installs <= '0; c_refused <= '0; c_late_arm <= '0;
end else begin
swap_now <= 1'b0;
install_refused <= 1'b0;
if (arm && !armed) begin
// Four preconditions. Each one, violated, produces a path in
// which the devices are running different schedules and no
// device is at fault.
if (!ptp_locked) begin
install_refused <= 1'b1; refuse_reason <= 8'd1;
c_refused <= c_refused + 1;
end else if (!shadow_valid) begin
install_refused <= 1'b1; refuse_reason <= 8'd2;
c_refused <= c_refused + 1;
end else if (!shadow_sum_ok) begin
install_refused <= 1'b1; refuse_reason <= 8'd3;
c_refused <= c_refused + 1;
end else if (eff_abs <= now_abs) begin
// The instant has already passed. Swapping now would put
// this device on the new schedule while its neighbours are
// still waiting -- a permanent phase split, not a transient.
install_refused <= 1'b1; refuse_reason <= 8'd4;
c_late_arm <= c_late_arm + 1;
c_refused <= c_refused + 1;
end else begin
armed <= 1'b1;
end
end
if (armed && (now_abs >= eff_abs)) begin
swap_now <= 1'b1;
armed <= 1'b0;
c_installs <= c_installs + 1;
end
end
end
// The window during which two devices may be on different
// schedules: each is wrong about `now` by up to the budget, in
// opposite directions. Section 14.
assign disagreement_window_ns = ns_t'(2 * 32'(sync_budget_ns));
endmoduleClassification: a deferred trigger with four preconditions. One comparison, and the comparison is against an absolute time.
What it teaches: that the swap is scheduled rather than commanded, and that is the only way a distributed configuration change can be coordinated at all. A management write that says swap now arrives at different devices at different times — a management bus is not synchronous and a software loop over ten switches takes milliseconds. A write that says swap at time T arrives whenever it arrives and takes effect together, to within the clock's accuracy.
And it teaches that refuse_reason = 4 — an instant already past — is a refusal rather than a best-effort swap, and the distinction matters enormously. A device that swaps late is on the new schedule while its neighbours are still on the old one, permanently — not for a window, but until the next install. Refusing leaves it on the old schedule with every neighbour, which is a consistent state, and c_late_arm says the management system's distribution was too slow.
Deliberately simplified: sync_budget_ns is taken as a scalar where Chapter 16.5 §14 produces a budget with random and systematic parts. The disagreement window should use the systematic part doubled plus a few sigma of the random — because two devices' random errors are independent and their systematic ones may not be. The scalar is conservative and the composition is better.
Production implication: disagreement_window_ns is an output rather than an internal value because it is the number a commissioning engineer must compare against the cycle. At Chapter 16.5's 24.2 ns it is 48.4 ns, which against a 62.5 µs cycle is 0.077% — negligible. On an uncalibrated deployment at 1 µs it is 2 µs — 3.2% of the cycle — and on a network synchronised by NTP at 1 ms it is 2 ms, which is thirty-two whole cycles. Section 14 is what happens then.
==
14. The Installation Window Nobody Can Close
Section 13's swap is atomic at one device. Across a path it is not, and the window's length is imported from Chapter 16.5 rather than chosen here.
Two adjacent devices each believe the effective instant has arrived when their clock says so. Each clock is wrong by up to the synchronisation budget, and the two errors can be in opposite directions — so there is an interval of up to twice the budget during which one device has swapped and the other has not.
| Synchronisation | Disagreement window | Of a 1000 µs cycle | Of a 62.5 µs cycle |
|---|---|---|---|
| Chapter 16.5's 24.2 ns | 48.4 ns | 0.005% | 0.077% |
| uncalibrated, 1 µs | 2 µs | 0.2% | 3.2% |
| NTP, 1 ms | 2 ms | 200% | 3200% — thirty-two cycles |
And what happens during the window depends entirely on how different the two schedules are.
If the new schedule differs only in an interval's length, a frame transmitted by the early device during what the late device still considers another class's window arrives into a shut gate and waits. One frame, one extra cycle of latency.
If the new schedule reassigns a class's window to a different part of the cycle, every frame in flight during the disagreement arrives at the wrong time, and the effect lasts for the window's duration.
And if the two devices' cycles differ — a schedule change that alters the cycle length — the disagreement is not a window at all. The devices' phases diverge until both have swapped, and if the management distribution failed on one device, they never reconverge.
Which gives the operational rule that follows from the arithmetic:
| Change | Disruption | Safe? |
|---|---|---|
| an interval's length, same cycle | frames in the window | yes, with a good clock |
| a window's position | one window's worth of frames | yes |
| the cycle length itself | phases diverge until both swap | only if every device swaps |
| adding a hop mid-install | the new hop is on no schedule | no — install before connecting |
Row three is why a cycle change is a maintenance operation rather than a tuning knob, which matters because Section 12's whole argument was about choosing the cycle. The choice is made once; changing it is an outage of at least one cycle on every device, and an unbounded one if the install fails anywhere.
And the deepest point is that the window cannot be closed by anything in this chapter. It is 2 × sync_error and the sync error is Chapter 16.5's assembled budget — which that chapter showed has an irreducible floor. A better schedule installer does not help; a better clock does, and it is the only thing that does.
15. RTL 7 — Schedule Telemetry
Six numbers, and the useful ones say whether this device's schedule agrees with the path's rather than whether it is executing.
// -----------------------------------------------------------------------
// schedule_telemetry -- what an operator needs to decide whether a
// scheduled port is working AND whether it agrees with its neighbours.
//
// The second question is the one that matters: a device can execute a
// perfectly valid schedule that nobody else is running.
// -----------------------------------------------------------------------
module schedule_telemetry
import tas_pkg::*;
(
input logic clk,
input logic rst_n,
input logic wrapped,
input logic [31:0] c_cycles,
input logic signed [31:0] phase_error_ns,
input logic [31:0] c_guard_blocked,
input logic [31:0] c_admitted,
input logic [31:0] c_gate_blocked [NUM_CLASSES],
input logic phase_valid,
input ns_t cycle_ns,
input ns_t window_ns,
input ns_t guard_ns,
input logic window_tick,
output logic [15:0] held_pct_x100,
output logic [15:0] usable_fraction_x100,
output logic signed [31:0] worst_phase_error_ns,
output logic guard_dominates_window,
output logic schedule_is_executing,
output logic [2:0] most_blocked_class,
output logic [31:0] c_windows
);
logic [31:0] adm_base, held_base, cyc_base;
always_ff @(posedge clk or negedge rst_n) begin
int c;
if (!rst_n) begin
held_pct_x100 <= '0; usable_fraction_x100 <= '0;
worst_phase_error_ns <= '0; guard_dominates_window <= 1'b0;
schedule_is_executing <= 1'b0; most_blocked_class <= '0;
c_windows <= '0; adm_base <= '0; held_base <= '0; cyc_base <= '0;
end else begin
if (phase_error_ns > worst_phase_error_ns)
worst_phase_error_ns <= phase_error_ns;
if (window_tick) begin
automatic logic [31:0] adm, held;
automatic int best; automatic logic [31:0] bestv;
adm = c_admitted - adm_base;
held = c_guard_blocked - held_base;
// What fraction of ready frames did not fit in their window.
// A large value means section 8's collapse is happening.
held_pct_x100 <= ((adm + held) == 0) ? 16'd0
: 16'((held * 10000) / (adm + held));
// Section 8's usable fraction, computed from the live values.
usable_fraction_x100 <= ((cycle_ns == 0) || (window_ns <= guard_ns))
? 16'd0
: 16'(((32'(window_ns) - 32'(guard_ns)) * 10000)
/ 32'(cycle_ns));
guard_dominates_window <= (window_ns <= (guard_ns + (guard_ns >> 2)));
// Executing means the list is advancing AND the phase is
// valid. A stalled list and a lost clock look identical from
// outside and have different remedies.
schedule_is_executing <= phase_valid &&
((c_cycles - cyc_base) != 32'd0);
best = 0; bestv = '0;
for (c = 0; c < NUM_CLASSES; c++)
if (c_gate_blocked[c] > bestv) begin bestv = c_gate_blocked[c]; best = c; end
most_blocked_class <= best[2:0];
adm_base <= c_admitted;
held_base <= c_guard_blocked;
cyc_base <= c_cycles;
c_windows <= c_windows + 1;
end
end
end
endmoduleClassification: a windowed rate calculator with two derived judgements. No control.
What it teaches: that guard_dominates_window is the warning Section 8's table needs as a signal. A window within 25% of the guard band's length is a window delivering almost nothing — the 62.5 µs row's 0.49% — and the condition is checkable from two configured values with no traffic at all. A design that shortened its cycle to meet a deadline and tripped this has met the deadline and lost the link, which no latency measurement would reveal.
And it teaches that schedule_is_executing must conjoin two conditions that look like one. A list that has stopped advancing and a clock that has lost lock both produce a port that transmits nothing scheduled, and they have entirely different remedies: the first is a configuration or logic fault here, the second is Chapter 16.4 §9's servo somewhere else. phase_valid separates them.
Deliberately simplified: most_blocked_class is a plain argmax over eight counters evaluated in one cycle. A production design would report the full vector — the argmax hides the case where two classes are equally starved, and a class blocked 100% of the time because its gate is never open is Section 4's last row, which the argmax will report and not explain.
Production implication: worst_phase_error_ns is the cross-device measurement in disguise. On a correct schedule it is bounded by one clock period plus Chapter 16.5's 24.2 ns; a value that grows without limit is Section 3's interval-sum error accumulating, and a value that is large and constant is a base-time mismatch — Section 6's row two, the failure that produces a path where every scheduled frame arrives one window late.
16. RTL 8 — Conformance for a Scheduled Port
The monitor checks that this port's schedule is well formed and correctly executed. It cannot check that the path agrees.
// -----------------------------------------------------------------------
// schedule_conformance_monitor -- one bit.
//
// It asserts the local properties: the list sums, the gates are
// honoured, the guard band is respected, and the swap was atomic here.
// Section 19's rejected property is the one that claims the PATH is
// consistent, which no device can check.
// -----------------------------------------------------------------------
module schedule_conformance_monitor
import tas_pkg::*;
(
input logic clk,
input logic rst_n,
input logic transmitted_with_gate_shut,
input logic overran_the_window,
input logic swapped_mid_cycle, // not at the effective instant
input logic admitted_without_guard,
input logic cfg_sum_mismatch,
input logic cfg_class_never_open, // section 4's last row
input logic cfg_window_below_guard, // section 8's collapse
input logic cfg_no_ptp_lock,
output logic conformant,
output logic [15:0] fault_vector,
output logic [31:0] c_violations
);
logic v_gate, v_over, v_swap, v_guard;
logic v_sum, v_never, v_win, v_lock;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
v_gate <= 1'b0; v_over <= 1'b0; v_swap <= 1'b0; v_guard <= 1'b0;
v_sum <= 1'b0; v_never <= 1'b0; v_win <= 1'b0; v_lock <= 1'b0;
c_violations <= '0;
end else begin
// Runtime violations. The second is the one the guard band
// exists to prevent, so it firing means the guard is wrong.
if (transmitted_with_gate_shut) begin v_gate <= 1'b1; c_violations <= c_violations + 1; end
if (overran_the_window) begin v_over <= 1'b1; c_violations <= c_violations + 1; end
if (swapped_mid_cycle) begin v_swap <= 1'b1; c_violations <= c_violations + 1; end
if (admitted_without_guard) begin v_guard <= 1'b1; c_violations <= c_violations + 1; end
// Standing configuration properties -- wrong from the moment
// the schedule is written, not from the first frame.
v_sum <= cfg_sum_mismatch;
v_never <= cfg_class_never_open;
v_win <= cfg_window_below_guard;
v_lock <= cfg_no_ptp_lock;
end
end
assign conformant = !(v_gate || v_over || v_swap || v_guard ||
v_sum || v_never || v_win || v_lock);
assign fault_vector = {8'b0, v_lock, v_win, v_never, v_sum,
v_guard, v_swap, v_over, v_gate};
endmoduleClassification: a sticky aggregator with four runtime violations and four standing configuration terms.
What it teaches: that overran_the_window firing means the guard band is wrong rather than that a frame misbehaved. The band exists precisely so that nothing is still transmitting when a gate shuts — so an overrun is a guard band computed from a stale MTU, a stale synchronisation budget, or a length-aware check that used the wrong length. The frame did what it was permitted to do.
And it teaches that cfg_class_never_open is a configuration fault rather than a policy choice. A class whose gate never opens in any entry is a class permanently starved, and the schedule looks complete because every interval is accounted for. The check is a bitwise OR over the list's gate vectors, and a zero in the result is a class nobody will ever hear from.
Deliberately simplified: swapped_mid_cycle is an input, and producing it means comparing the swap instant against the cycle boundary. The standard permits a swap at any point — it is not required to align with a cycle — but a swap mid-cycle leaves the list partway through a schedule whose intervals do not sum from that point, so a production design either aligns the swap to a wrap or accepts one short cycle and says so.
Production implication: conformant here means this port executed this schedule correctly. It does not mean the path is consistent — Section 14's disagreement window, Section 6's base-time requirement and a neighbour running a different cycle are all invisible from here. A fully conformant port on a path whose hops have different base times delivers N × cycle of latency, and Section 19's rejected property is precisely the attempt to claim otherwise.
17. What a Schedule Can and Cannot Promise
| Claim | Status |
|---|---|
| interference is zero inside the window | guaranteed — every other gate is shut |
a missed window costs cycle − window | guaranteed — a configuration parameter |
| nothing overruns a window | guaranteed, if the guard band is right |
| the swap is atomic at this device | guaranteed — one flop |
| the swap is atomic across the path | no — 2 × sync_error |
| the path shares a cycle and a phase | not checkable from here |
the wait is paid once rather than N times | only if the bases match |
| the window delivers usable bandwidth | only if it exceeds the guard band |
Rows five through seven are the ones no device can verify, and all three have the same shape: they are properties of the path's configuration rather than of any device's behaviour.
Row seven is the most consequential. A path whose hops share a base pays the schedule wait once; one whose hops do not pays it at every hop — and at a 1000 µs cycle over five hops that is 4500 µs against 900, a factor of five, with every device individually conformant and every status output identical.
And row eight is the one a design creates for itself. Section 8: at 1 Gb/s a window below about 12.2 µs is entirely guard band. The schedule is correct, the bound is met, and the class gets 0.49% of the link — which is a configuration that passes every check in Section 16 and delivers nothing.
Which gives the honest position: a schedule converts an unbounded term into a configured one, and every remaining risk is in the configuration rather than in the mechanism. The gates are eight bits and a comparator; the cycle, the base, the window sizing and the install are where deployments fail.
18. The Cost of a Schedule, Accounted
| Component | Cost | Note |
|---|---|---|
| gate-control list, 64 × (8 + 32) bits, two banks | 640 octets | double buffering is mandatory — Section 3 |
| schedule headers, two | 28 octets | — |
| cycle timer — a modulo and a comparator | ~100 flops | one divide per cycle in practice |
| guard-band calculator | a multiply and a comparator | — |
| eligibility check | an 8-bit AND tree | a third mask on the same vector |
| installer | a 64-bit comparator | — |
| telemetry and conformance | ≈80 flops | — |
| total state, one port | ≈700 octets | trivial |
| the guard band | 12.19% of a window at 1 Gb/s | 0.17% at 100 Gb/s |
| the usable fraction, at its optimum | 15.12% of the link | at a 250 µs cycle, 1 Gb/s |
Seven hundred octets and an AND tree, and the cost that matters is entirely bandwidth — which is Chapter 17.1 §18's finding confirmed: Module 17's mechanisms cost throughput rather than gates.
And the guard band's composition decides where the next chapter goes:
| Line rate | MTU term | Sync term | Band | MTU's share |
|---|---|---|---|---|
| 1 Gb/s | 12.144 µs | 0.048 µs | 12.192 µs | 99.6% |
| 10 Gb/s | 1.214 µs | 0.048 µs | 1.263 µs | 96.2% |
| 25 Gb/s | 0.486 µs | 0.048 µs | 0.534 µs | 90.9% |
| 100 Gb/s | 0.121 µs | 0.048 µs | 0.170 µs | 71.5% |
At every rate below 100 Gb/s the MTU term is over 90% of the band, so removing it is worth almost the whole band. Chapter 17.3 removes it by interrupting the frame in flight, replacing a maximum frame with a minimum fragment.
And the arithmetic of that replacement is the chapter's closing number. A 64-octet minimum fragment at 1 Gb/s is 0.512 µs, so a preemption-capable guard band is 0.512 + 0.048 = 0.560 µs against 12.192 — a factor of 21.8.
| Guard band | Of a 100 µs window | Usable at a 62.5 µs cycle | |
|---|---|---|---|
| without preemption | 12.192 µs | 12.19% | 0.49% |
| with preemption | 0.560 µs | 0.56% | 19.10% |
The last column is the one to carry into the next chapter: a 12.5 µs window at 1 Gb/s goes from delivering 0.49% of the link to delivering 19.10%. Which is not an optimisation — it is the difference between a schedule that works at short cycles and one that does not.
19. Properties Worth Asserting, and One Worth Refusing
The properties divide by object: the list, the timer, the guard band, the eligibility check, the installer, and the configuration.
Group 1 — the gate-control list.
// P1. The shadow bank is written and the live bank is not. A write to
// the live bank would mix two schedules -- section 3.
property p_writes_target_shadow;
@(posedge clk) disable iff (!rst_n)
(cfg_we && live_is_a) |=> $stable(bank_a);
endproperty
// P2. The swap is a single-cycle toggle: atomic at this device.
property p_swap_is_atomic_here;
@(posedge clk) disable iff (!rst_n)
swap_now |=> (live_is_a == !$past(live_is_a));
endproperty
// P3. A swap restarts the list at entry 0.
property p_swap_restarts_the_list;
@(posedge clk) disable iff (!rst_n)
swap_now |=> (current_index == '0);
endproperty
// P4. The list wraps exactly at its configured length.
property p_wraps_at_length;
@(posedge clk) disable iff (!rst_n)
(advance && (current_index == live_hdr.length - 1)) |=> (current_index == '0);
endproperty
// P5. The intervals sum to the cycle. A standing property, wrong from
// configuration time -- 14.1 section 17's argument.
property p_intervals_sum_to_cycle;
@(posedge clk) disable iff (!rst_n)
!cfg_sum_mismatch;
endpropertyGroup 2 — the cycle timer.
// P6. The phase is derived from ABSOLUTE time, not counted locally.
// A free-running counter drifts at the oscillator's tolerance.
property p_phase_is_absolute;
@(posedge clk) disable iff (!rst_n)
phase_valid |-> (cycle_pos == ((now_abs - base_abs) % live_hdr.cycle_ns));
endproperty
// P7. No phase without a locked clock. 16.4 section 9's S_LOCKED.
property p_no_phase_without_lock;
@(posedge clk) disable iff (!rst_n)
!ptp_locked |=> !phase_valid;
endproperty
// P8. An entry advances exactly when its interval has elapsed.
property p_advance_on_interval;
@(posedge clk) disable iff (!rst_n)
(phase_valid && (elapsed >= current_interval_ns)) |=> advance;
endproperty
// P9. The phase error is bounded on a correct schedule: one clock
// period plus 16.5's budget.
property p_phase_error_is_bounded;
@(posedge clk) disable iff (!rst_n)
(phase_valid && !cfg_sum_mismatch)
|-> (absv(phase_error_ns) <= (CLK_PERIOD_NS + SYNC_BUDGET_NS));
endproperty
// P10. Losing the phase is counted, so a synchronisation outage does
// not present as a latency problem.
property p_phase_loss_counted;
@(posedge clk) disable iff (!rst_n)
$fell(phase_valid) |=> $changed(c_phase_lost);
endpropertyGroup 3 — the guard band.
// P11. THE property the band exists for: nothing is admitted that
// cannot finish before the gate shuts.
property p_nothing_overruns;
@(posedge clk) disable iff (!rst_n)
admit |-> (time_to_gate_close_ns >= this_frame_ns);
endproperty
// P12. The static band is MTU/R + 2 x sync -- 17.1 section 17.
property p_static_band_formula;
@(posedge clk) disable iff (!rst_n)
(static_guard_ns == ns_t'(MTU_NS + 2*SYNC_ERROR_NS));
endproperty
// P13. A length-aware band admits a short frame the static one would
// refuse -- the whole reason for the variant.
property p_length_aware_admits_more;
@(posedge clk) disable iff (!rst_n)
(LENGTH_AWARE && frame_pending && (frame_octets < MTU_OCTETS) &&
(time_to_gate_close_ns >= this_frame_ns) &&
(time_to_gate_close_ns < static_guard_ns)) |=> admit;
endproperty
// P14. A held frame is counted, so a window whose band is eating it
// is visible -- section 8's collapse.
property p_held_is_counted;
@(posedge clk) disable iff (!rst_n)
(frame_pending && !admit) |=> $changed(c_held);
endpropertyGroup 4 — eligibility.
// P15. A class whose gate is shut never transmits. The schedule's
// central guarantee.
property p_shut_gate_never_transmits;
@(posedge clk) disable iff (!rst_n)
!gates[c] |-> !eligible[c];
endproperty
// P16. The three masks compose by conjunction and each one can only
// subtract -- 13.4's scheduler, 14.4's PFC gate, and this.
property p_masks_only_subtract;
@(posedge clk) disable iff (!rst_n)
eligible[c] |-> (gates[c] && q_nonempty[c] && !pfc_paused[c] && guard_admits);
endproperty
// P17. Without a valid phase nothing scheduled is eligible.
property p_no_phase_no_eligibility;
@(posedge clk) disable iff (!rst_n)
!phase_valid |-> (eligible == '0);
endproperty
// P18. Each blocking cause increments its own counter -- 14.3
// section 15's attribution argument, applied to three parties.
property p_attribution_is_separate;
@(posedge clk) disable iff (!rst_n)
(q_nonempty[c] && !gates[c]) |=> $changed(c_gate_blocked[c]);
endpropertyGroup 5 — the installer.
// P19. A swap happens at the effective instant and not before.
property p_swap_at_the_instant;
@(posedge clk) disable iff (!rst_n)
swap_now |-> (now_abs >= eff_abs);
endproperty
// P20. An instant already past is REFUSED, not swapped late. A late
// swap is a permanent phase split, not a transient.
property p_late_arm_is_refused;
@(posedge clk) disable iff (!rst_n)
(arm && (eff_abs <= now_abs)) |=> (install_refused && !armed);
endproperty
// P21. Every precondition, violated, refuses with its own reason.
property p_refusal_has_a_reason;
@(posedge clk) disable iff (!rst_n)
install_refused |-> (refuse_reason != 8'd0);
endproperty
// P22. An armed install eventually swaps or is disarmed -- it never
// waits for ever.
property p_armed_resolves;
@(posedge clk) disable iff (!rst_n)
$rose(armed) |-> ##[1:$] !armed;
endproperty
// P23. The disagreement window is twice the live budget, not a
// commissioning constant -- section 13.
property p_window_uses_live_budget;
@(posedge clk) disable iff (!rst_n)
(disagreement_window_ns == ns_t'(2 * 32'(sync_budget_ns)));
endpropertyGroup 6 — configuration and telemetry.
// P24. Standing property: every class's gate opens somewhere in the
// list. A class that never opens is permanently starved.
property p_no_class_never_opens;
@(posedge clk) disable iff (!rst_n)
!cfg_class_never_open;
endproperty
// P25. Standing property: every window exceeds the guard band.
// Otherwise the window delivers nothing -- section 8's last row.
property p_window_exceeds_guard;
@(posedge clk) disable iff (!rst_n)
!cfg_window_below_guard;
endproperty
// P26. The usable fraction is computed from the live window and band,
// so a stale budget cannot hide a collapse.
property p_usable_uses_live_values;
@(posedge clk) disable iff (!rst_n)
window_tick |=> (usable_fraction_x100 ==
(((window_ns - guard_ns) * 10000) / cycle_ns));
endproperty
// P27. guard_dominates_window fires before the collapse, not after.
property p_collapse_warned_early;
@(posedge clk) disable iff (!rst_n)
(window_ns <= (guard_ns + (guard_ns >> 2))) |=> guard_dominates_window;
endproperty
// P28. schedule_is_executing requires BOTH a advancing list and a
// valid phase -- the two failures look identical from outside.
property p_executing_needs_both;
@(posedge clk) disable iff (!rst_n)
schedule_is_executing |-> (phase_valid && ($past(c_cycles) != c_cycles));
endproperty
// P29. Standing property: the clock is locked.
property p_clock_locked;
@(posedge clk) disable iff (!rst_n)
!cfg_no_ptp_lock;
endproperty
// P30. A gate-shut transmission is the schedule's one real violation
// and is always counted.
property p_violation_counted;
@(posedge clk) disable iff (!rst_n)
transmitted_with_gate_shut |=> $changed(c_violations);
endproperty
// P31. conformant is exactly the conjunction it claims.
property p_conformant_definition;
@(posedge clk) disable iff (!rst_n)
conformant <-> (fault_vector == 16'h0000);
endpropertyP11 is the guard band's whole purpose, P15 is the schedule's central guarantee, P20 keeps a failed install from becoming a permanent split, and P25 catches the configuration that meets its bound and delivers nothing. Every one of them is about this port. None claims anything about the path — which is the property this chapter refuses.
20. Verification Scenarios
Seventy-two scenarios. Several have expected outcomes in which the schedule is correct, the bound is met, and the link delivers half a percent.
The gate-control list
| # | Scenario | Expected |
|---|---|---|
| 1 | Write the shadow bank while running | live bank unchanged |
| 2 | Single-banked list, rewritten live | a mixture of two schedules |
| 3 | Same | intervals sum to no cycle; gates drift |
| 4 | Swap | one flop toggle, index → 0 |
| 5 | Intervals sum to 999 µs on a 1000 µs cycle | cfg_sum_mismatch |
| 6 | Same, undetected | drift of 1 µs per cycle — a full window in 0.1 s |
| 7 | 64-entry list, two banks | 640 octets |
| 8 | List wraps | c_cycles + 1, wrapped |
| 9 | Compare c_cycles across hops at a PTP instant | a drifting device is visible |
The cycle timer
| # | Scenario | Expected |
|---|---|---|
| 10 | Phase from (now − base) mod cycle | every device agrees to 24.2 ns |
| 11 | Free-running local counter instead | drifts 100 µs/s at 100 ppm |
| 12 | Same | a full window apart within a second |
| 13 | PTP lock lost | phase_valid low, c_phase_lost |
| 14 | Base times differ by 500 µs, 1000 µs cycle | half a cycle out of phase |
| 15 | Same | every scheduled frame arrives outside its window |
| 16 | Base in the future | modulo is arithmetically correct, semantically meaningless |
| 17 | Cycles 1000 µs and 1024 µs on two hops | phase wraps in 41 cycles — works 41 ms at a time |
| 18 | Correct schedule, phase_error_ns | bounded by a clock period + 24.2 ns |
| 19 | Interval-sum error | phase_error_ns grows without limit |
The guard band
| # | Scenario | Expected |
|---|---|---|
| 20 | 1 Gb/s, MTU 1518, sync 24.2 ns | 12.192 µs |
| 21 | 10 Gb/s | 1.263 µs |
| 22 | 100 Gb/s | 0.170 µs |
| 23 | MTU's share at 1 Gb/s | 99.6% |
| 24 | MTU's share at 100 Gb/s | 71.5% |
| 25 | Static band, 64-octet frame at window end | refused |
| 26 | Length-aware band, same frame | admitted — it needs 0.56 µs |
| 27 | Length-aware, 1518-octet frame | refused at 12 µs remaining |
| 28 | Sync budget stale after a renegotiation | the band is wrong — Chapter 16.3 §4's trap |
| 29 | Frame held | c_held |
Cycle and window sizing
| # | Scenario | Expected |
|---|---|---|
| 30 | 2000 µs cycle, 200 µs window | wait 1800 µs, usable 9.39% |
| 31 | 1000 µs / 100 µs | wait 900 µs, usable 8.78% |
| 32 | 500 µs / 75 µs | wait 425 µs, usable 12.56% |
| 33 | 250 µs / 50 µs | wait 200 µs, usable 15.12% — the optimum |
| 34 | 125 µs / 25 µs | wait 100 µs, usable 10.25% |
| 35 | 62.5 µs / 12.5 µs | wait 50 µs, usable 0.49% |
| 36 | Same | 12.192 µs of band, 0.308 µs of transmission |
| 37 | Same | guard_dominates_window |
| 38 | Halving the cycle | wait halves, overhead doubles |
| 39 | 62.5 µs cycle, 3-entry list | 48 000 entry advances/s |
Latency
| # | Scenario | Expected |
|---|---|---|
| 40 | Frame ready at window start | schedule contributes zero |
| 41 | Frame ready 1 ns after the window ends | waits cycle − window |
| 42 | Five hops, bounded terms, 1 Gb/s | 185.6 µs |
| 43 | 1000 µs cycle, shared phase | 1085.6 µs — Chapter 17.1 §20's Run D |
| 44 | 250 µs cycle, shared phase | 385.6 µs — that section's row 14 |
| 45 | 1000 µs cycle, bases differ | wait paid at every hop: 4500 µs |
| 46 | Same, five hops total | 4685.6 µs — a factor of five |
| 47 | Both configurations' status outputs | identical |
| 48 | no_cycle_works | the deadline is unachievable, not under-searched |
Installation
| # | Scenario | Expected |
|---|---|---|
| 49 | Management says "swap now" over a bus | devices swap milliseconds apart |
| 50 | Management says "swap at T" | together, to 24.2 ns |
| 51 | Effective instant already past | refused, refuse_reason = 4 |
| 52 | Same, swapped anyway | a permanent phase split |
| 53 | PTP not locked | refused, reason 1 |
| 54 | Shadow sum wrong | refused, reason 3 |
| 55 | Disagreement window, 24.2 ns sync | 48.4 ns — 0.077% of a 62.5 µs cycle |
| 56 | Same, 1 µs sync | 2 µs — 3.2% |
| 57 | Same, NTP at 1 ms | 2 ms — thirty-two cycles |
| 58 | Interval length changed, same cycle | frames in the window; safe |
| 59 | Window position changed | one window's frames |
| 60 | Cycle length changed | phases diverge until both swap |
| 61 | Same, install fails on one device | they never reconverge |
| 62 | A hop added mid-install | on no schedule at all |
Eligibility and conformance
| # | Scenario | Expected |
|---|---|---|
| 63 | Gate shut, queue non-empty | not eligible; c_gate_blocked[c] |
| 64 | Gate open, PFC paused | not eligible — Chapter 14.4 §9 |
| 65 | Gate open, frame too long | not eligible; c_guard_blocked |
| 66 | Phase invalid, frames waiting | c_no_phase |
| 67 | Three masks, one counter | every investigation to the same place |
| 68 | Class whose gate never opens | cfg_class_never_open |
| 69 | Same, schedule otherwise complete | looks configured; permanently starved |
| 70 | A frame overruns a window | overran_the_window — the band is wrong |
| 71 | Eight priorities into four queues | a gate must be the AND of its classes' |
| 72 | Conformant port, path bases mismatched | conformant high, latency 5× |
The directed test random stimulus will not produce
A base-time mismatch is not a traffic condition and not a fault — it is a configuration in which every device is individually correct. Random stimulus varies frames; it does not vary one hop's base_ns by 500 µs. And the finding is that the two configurations are indistinguishable in every per-device output while differing by a factor of five in latency, which requires running both and comparing the end-to-end result rather than any instrument.
Setup: a five-hop path at 1 Gb/s, 100 m per hop, Chapter 17.1 §4's bounded terms at 37.12 µs per hop. A 1000 µs cycle with a 100 µs window for class 6, three-entry list, PTP locked at Chapter 16.5's 24.2 ns. A measured class-6 flow with a 1 ms deadline, one frame per cycle, offered at a uniformly random phase.
Stimulus, four runs of 10⁶ frames.
Run A — shared base: every hop's base_ns identical.
Run B — mismatched base: hop 3's base offset by 500 µs.
Run C — shared base, 250 µs cycle, 50 µs window.
Run D — shared base, 62.5 µs cycle, 12.5 µs window.
Oracle:
| # | Observable | A — shared, 1000 µs | B — mismatched | C — 250 µs | D — 62.5 µs |
|---|---|---|---|---|---|
| 1 | cfg_sum_mismatch, every hop | low | low | low | low |
| 2 | phase_valid, every hop | high | high | high | high |
| 3 | phase_error_ns, every hop | ≈0 | ≈0 | ≈0 | ≈0 |
| 4 | conformant, every hop | high | high | high | low — window < guard |
| 5 | c_cycles, every hop | equal | equal | equal | equal |
| 6 | worst end-to-end latency | ≈1085 µs | ≈4686 µs | ≈386 µs | ≈236 µs |
| 7 | deadline met | no | no | yes | yes |
| 8 | any per-device output distinguishing A from B | — | none | — | — |
| 9 | schedule wait, measured | ≈900 µs | ≈4500 µs | ≈200 µs | ≈50 µs |
| 10 | usable_fraction_x100 | 878 | 878 | 1512 | 49 |
| 11 | guard_dominates_window | low | low | low | high |
| 12 | c_held / (c_held + c_admitted) | ≈0% | ≈0% | ≈0% | ≈96% |
| 13 | class-6 throughput delivered | 8.78% | 8.78% | 15.12% | 0.49% |
| 14 | rerun D with 1518-octet frames | — | — | — | every frame held |
| 15 | rerun D with 64-octet frames | — | — | — | admitted — length-aware band |
Rows 6 and 8 together are the first finding. Run B is 4.3× worse than Run A and not one per-device output differs — every hop's sum check passes, every phase is valid, every phase error is zero, every conformance bit is high, and every cycle count matches. The failure is entirely in a configured constant that each device holds correctly and that differs between them.
Rows 10 to 13 are the second. Run D meets the deadline with the most margin of any run and delivers 0.49% of the link — 96% of ready frames held by the guard band, guard_dominates_window high, and row 4's conformance bit correctly low because Section 16's standing check caught it. A design that chased latency by halving the cycle twice arrived here.
And rows 14 and 15 are the length-aware band earning its keep: at a 12.5 µs window, maximum frames never fit and minimum frames always do — so the class's usable bandwidth depends entirely on its frame-size distribution, which is a dependency the static band does not have and which Chapter 17.3 removes.
21. Debugging a Schedule
Five questions, and the first three need no traffic.
Step 1 — is the schedule well formed? cfg_sum_mismatch, cfg_class_never_open, cfg_window_below_guard. All three are standing properties answerable from the configuration alone, and all three produce a port that looks like it is working. The sum error drifts the phase; the never-open class starves silently; and a window below the guard band delivers nothing.
Step 2 — do the hops agree? The cycle_ns and base_ns of every hop, compared. This is the check Section 20's Run B fails and no device performs. A mismatch in cycle makes the path work intermittently; a mismatch in base makes the schedule wait be paid at every hop — a factor of N in latency with every status output identical.
Step 3 — is the clock adequate? phase_valid, c_phase_lost, and disagreement_window_ns against the cycle. A device in Chapter 16.4 §9's S_HOLDOVER has a phase that is drifting, and an install window of 2 ms against a 62.5 µs cycle is a deployment that cannot run this schedule.
Step 4 — is the window delivering? usable_fraction_x100, held_pct_x100 and guard_dominates_window. A window within 25% of the guard band's length delivers almost nothing — Section 20's Run D — and held_pct_x100 near 100 means the band is refusing everything that is offered.
Step 5 — which mask is blocking? c_gate_blocked[c], c_guard_blocked, and Chapter 14.4's c_gated[c]. Three mechanisms subtract from the same eligibility vector and they name three different parties — our schedule, the frame's own length, and the neighbour's pause. A single blocked counter sends every investigation to the same place and two thirds of them are wrong.
And the finding that ends an investigation: schedule well formed, hops agreeing, clock locked, usable fraction at its optimum, and blocking attributed to the gate during other classes' windows. That is a schedule doing exactly what it was configured to do — and any remaining shortfall is the guard band, which is Chapter 17.3's subject.
22. Common Misconceptions
1 — "A shorter cycle is always better for latency."
The wrong model: the schedule wait is the only thing the cycle affects.
What it costs: Section 20's Run D — the deadline met with the most margin of any run and 0.49% of the link delivered. Halving the cycle halves the wait and doubles the guard band's share, and once the window approaches the band the usable fraction collapses rather than declining.
The corrected model: at 1 Gb/s the usable fraction peaks at about a 250 µs cycle and 15.12%, and the collapse is at roughly a 12 µs window. Choose the longest cycle that meets the deadline — Section 11's search halves until it is met and stops — because margin bought below the optimum is paid for in bandwidth.
2 — "Every device is conformant, so the path is scheduled correctly."
The wrong model: per-device conformance composes.
What it costs: a factor of five in latency with no indication anywhere. Section 20's Run B: hop 3's base_ns offset by 500 µs, every hop's sum check passing, every phase error zero, every conformance bit high — and the schedule wait paid at every hop instead of once, 4686 µs against 1086.
The corrected model: conformant is a claim about this port executing this schedule. The path's consistency — the same cycle, the same base, the same effective instant — is a configuration property no device can verify, and it is the thing commissioning must check.
3 — "The guard band is a small overhead."
The wrong model: 12.192 µs is negligible.
What it costs: 12.19% of a 100 µs window at 1 Gb/s, and 97.5% of a 12.5 µs one. The band is subtracted from every window in every cycle, so its cost is a fraction of time rather than a one-off — and it is 99.6% MTU term at 1 Gb/s, which is why Chapter 17.3 exists.
The corrected model: the band scales with the line rate and the window does not scale with anything. At 100 Gb/s it is 0.17% of a 100 µs window; at 1 Gb/s with a short cycle it can exceed the window entirely. guard_dominates_window fires before the collapse, and it is computed from two configured numbers with no traffic at all.
4 — "A schedule change is a configuration write."
The wrong model: write the new list and it takes effect.
What it costs: a device on the new schedule while its neighbours are on the old one — for milliseconds if the management bus is the distribution mechanism, and permanently if one device's effective instant had already passed and it swapped anyway.
The corrected model: a schedule change is a scheduled operation. Every device is given the same effective instant in Chapter 16.2's timescale and arms; the swap fires when each device's clock reaches it. An instant already past must be refused, not honoured late — a late swap is a permanent phase split rather than a transient.
5 — "Good synchronisation matters to the applications, not to the schedule."
The wrong model: the clock is a service the network provides.
What it costs: two things at once. The guard band contains 2 × sync_error — negligible at 1 Gb/s and 28% of the band at 100 — and the install window is 2 × sync_error too. At NTP's millisecond that window is 2 ms, or thirty-two 62.5 µs cycles, which is not a window at all.
The corrected model: Chapter 16.5's 24.2 ns is an input to this chapter in two places. A schedule cannot be installed more precisely than the devices agree about the time, and it cannot be safer than the guard band that error contributes to.
6 — "A class with no gate entry just uses the default."
The wrong model: an unmentioned class behaves as it did before.
What it costs: permanent, silent starvation. A gate vector is eight bits and every entry sets all eight — so a class absent from every entry's vector has its gate shut for the entire cycle, for ever. The schedule looks complete: every interval is accounted for and the sum matches the cycle.
The corrected model: the check is a bitwise OR across the list's gate vectors, and a zero bit in the result is a class that will never transmit. cfg_class_never_open is that OR, and it costs eight bits of logic.
23. Interview Reasoning
Q1 — What does a gate schedule actually change?
It replaces Chapter 17.1 §8's unbounded interference term with a configured one. During a class's window every other gate is shut, so a frame transmitted in its own window waits for nothing — interference is zero. A frame that misses its window waits cycle − window, which is a number an operator chose rather than a property of the traffic. The bounded terms are unchanged: Chapter 17.1 §4's 37.12 µs per hop at 1 Gb/s is still there, and a five-hop worst case is 185.6 µs + (cycle − window).
Q2 — Derive the guard band and say what it costs.
MTU/R + 2 × sync_error — Chapter 17.1 §17. A gate must stop admitting early enough that nothing is still transmitting when it shuts, because Chapter 12.6 §8 established a frame in flight cannot be aborted; and the two devices may disagree about the closing instant by twice the synchronisation budget. At 1 Gb/s that is 12.144 + 0.048 = 12.192 µs, which is 12.19% of a 100 µs window and 97.5% of a 12.5 µs one. The MTU term is 99.6% of it at 1 Gb/s and 71.5% at 100 — which is why preemption is the next chapter.
Q3 — Why does shortening the cycle stop helping?
Because the two costs move in opposite directions. The wait is cycle − window and falls linearly; the guard band's share is band / cycle and rises linearly. Their combination gives a usable fraction with a maximum — at 1 Gb/s about 15.12% at a 250 µs cycle — and below that the window approaches the band and the fraction collapses rather than declining: 0.49% at 62.5 µs. So the rule is the longest cycle that meets the deadline, not the shortest achievable.
Q4 — How is a schedule installed across a path?
By scheduling the swap rather than commanding it. Every device is given the same effective instant in PTP time, writes the new list into a shadow bank, and arms; the swap fires when each device's own clock reaches the instant. Double buffering is mandatory — a single-banked list rewritten live runs a mixture whose intervals sum to no cycle. And an instant already past must be refused, because a late swap leaves that device on the new schedule alone, permanently, rather than for a window.
Q5 — What is the installation window and why can't it be closed?
2 × sync_error — each device's clock is wrong by up to the budget and the two errors can oppose. At Chapter 16.5's 24.2 ns that is 48.4 ns, which against a 62.5 µs cycle is 0.077%; at 1 µs it is 2 µs and 3.2%; at NTP's millisecond it is 2 ms — thirty-two whole cycles. It cannot be closed by anything in this chapter because its length is Module 16's error budget, and a better installer does not shorten it. A better clock does, and only that.
Q6 — Why can't you assert that the path is running one schedule?
Because the transition is atomic at each device and not across them, and the falsity has a duration imported from another module. The property is false for 2 × sync_error around every effective instant, by construction, on a correct design — 48 ns on a good network and 2 ms on a bad one, with identical logic. The assertable form names the window: the path agrees outside an interval of disagreement_window_ns around the effective instant — plus a second property that the window is small against the cycle, which is the condition that makes the disruption tolerable and which fails by a factor of 32 on an NTP-synchronised network.
24. Understanding Check
25. What's Next
This chapter built two of Chapter 17.1 §16's three additions and consumed the third.
The schedule bounds the interference term at cycle − window, which is a configuration parameter. The guard band prevents a frame overrunning into the next window. And the clock — Module 16's, at Chapter 16.5's 24.2 ns — appears twice: inside the guard band, and as the width of Section 14's installation window.
What is left is the guard band's dominant term, and it dominates everywhere below 100 Gb/s.
| Line rate | Guard band | MTU's share |
|---|---|---|
| 1 Gb/s | 12.192 µs | 99.6% |
| 10 Gb/s | 1.263 µs | 96.2% |
| 25 Gb/s | 0.534 µs | 90.9% |
| 100 Gb/s | 0.170 µs | 71.5% |
Chapter 17.3 — Frame Preemption (802.1Qbu / 802.3br) removes it by interrupting the frame in flight. A maximum frame becomes a minimum fragment, so the band at 1 Gb/s goes from 12.192 µs to 0.560 µs — a factor of 21.8 — and Section 18's last table follows: a 12.5 µs window goes from delivering 0.49% of the link to 19.10%.
And the cost is a second MAC, a second framing, and a new kind of partial frame. A preempted frame arrives as fragments, each with its own CRC arrangement, and a receiver must distinguish a fragment from a corrupt frame — which Chapter 6.3's residue check was never designed to do. That chapter's checker gains a case it did not have.
One thread carries directly across. This chapter's guard band exists because Chapter 12.6 §8 established that a frame in flight cannot be aborted — a fact every chapter since has taken as given. Chapter 17.3 is what happens when that stops being true.
Continue learning
Related tutorials
- Related topic
What Determinism Requires
Five of a hop's seven latency terms are bounded and sum to 37 microseconds. The other two have no bound at all, and no amount of engineering gives them one.
- Related topic
Backpressure and Head-of-Line Blocking
Every queue is work-conserving, every link runs at line rate, every switch is non-blocking — and the composition delivers 58.6% of capacity. The bound has been known since 1987.
- Related topic
Frame Preemption (802.1Qbu / 802.3br)
Interrupting a frame in flight takes the guard band from 12.192 to 0.560 microseconds, and gives Chapter 6.3's FCS checker a case it was never designed for.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
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.
