Ethernet · Module 12
Store-and-Forward against Cut-Through
A cut-through switch commits at octet 14 and the check sequence arrives at octet 1518. One of the six forwarding gates cannot be run at all — which is the discipline's defining property, not a defect.
Chapter 12.1 §10 introduced this choice and could only half-state it, because Chapter 12.3 had not yet established what a forwarding decision contains.
With the six gates in hand the statement becomes exact, and it is sharper than a latency trade-off.
A cut-through switch commits the frame to an egress port after 14 octets. Chapter 12.3's gate 1 — is this frame eligible to be forwarded at all — depends on the frame check sequence, which arrives in the last four octets of a frame up to 1518 octets long.
So the gate is not slow, not expensive, and not hard to pipeline. It cannot be run. Its input does not exist yet, and it will not exist until 12 032 bits after the decision has already been made.
That is not a defect to engineer away. It is what cut-through is — and every property, every counter and every constructive mechanism in this chapter follows from it.
1. Scope — What This Chapter Owns
This chapter owns the commit point and its consequences: where in the frame each discipline decides, which of Chapter 12.3's gates survive an early commit, what happens to a frame found corrupt after it has begun leaving, the rate rule that makes cut-through unavailable on a speed step upward, and why the latency benefit vanishes under load.
It does not own the decision itself — Chapter 12.3 owns the six gates and this chapter asks only when each one's input is available.
It does not own the table — Chapter 12.5 built the structure that answers gate 3 in four cycles, and that four-cycle answer is what makes an early commit arithmetically possible at all.
It does not own the cost of flooding — Chapter 12.4 priced it, and Section 9 spends that price when a corrupt frame is forwarded rather than contained.
And it does not own frame validity — Chapter 7.3 established what makes a frame valid and Chapter 5.8 established what the check sequence covers. This chapter takes both as given and asks only when they can be evaluated.
2. A Discipline Is a Commit Point
Write the frame out as a timeline and the two disciplines become one number: the octet at which the switch stops being able to change its mind.
| Octet offset | What has arrived | What can be decided |
|---|---|---|
| 0–5 | destination address | gate 3's lookup can be issued |
| 6–11 | source address | Chapter 12.2's learning has its evidence |
| 12–13 | EtherType or length — Chapter 5.5 | classification complete |
| 14 | — | cut-through commits here |
| 14 … N−5 | payload | nothing new for forwarding |
| N−4 … N−1 | frame check sequence | gate 1 can finally run |
| N | — | store-and-forward commits here |
The gap between the two commit points is the entire payload, and on a maximum-length frame it is 1518 − 14 = 1504 octets — 12 032 bits.
At 1 Gb/s that is 12.032 µs of frame that a cut-through switch has already committed to forwarding without having seen.
And the asymmetry in what the two disciplines give up is not symmetric at all. Store-and-forward gives up time, proportionally to frame length. Cut-through gives up one gate, permanently and unconditionally — and the gate it gives up is the one that decides whether the frame is worth forwarding.
3. RTL 1 — Tracking the Commit Point
The commit point is a position in the frame, and it is worth making it an explicit signal rather than an emergent property of the pipeline.
// -----------------------------------------------------------------------
// cutthru_pkg -- shared types for forwarding-discipline selection.
// -----------------------------------------------------------------------
package cutthru_pkg;
typedef enum logic [1:0] {
FM_STORE_FORWARD = 2'd0, // commit at the last octet
FM_CUT_THROUGH = 2'd1, // commit at octet 14
FM_FRAGMENT_FREE = 2'd2, // commit at octet 64 -- runts excluded
FM_ADAPTIVE = 2'd3 // per-frame, Section 12
} fwd_mode_e;
// Why a frame could not be cut through. Each has a different remedy and
// only one of them is a defect.
typedef enum logic [2:0] {
CR_OK = 3'd0,
CR_EGRESS_FASTER = 3'd1, // Section 7's rate rule -- would underrun
CR_EGRESS_BUSY = 3'd2, // queued, so it has been stored anyway
CR_MODE_FORCED = 3'd3, // configured store-and-forward
CR_ERROR_RATE = 3'd4, // Section 12 fell back after errors
CR_UNKNOWN_LENGTH = 3'd5 // Chapter 5.5's length/type ambiguity
} cutthru_refusal_e;
// What happened to a frame already in flight when it turned out bad.
typedef enum logic [1:0] {
LF_NONE = 2'd0,
LF_STOMPED = 2'd1, // FCS deliberately corrupted -- Section 11
LF_TRUNCATED= 2'd2, // transmission aborted mid-frame
LF_PASSED = 2'd3 // emitted intact -- the failure this chapter warns of
} late_fault_e;
localparam int COMMIT_CUT = 14; // through EtherType
localparam int COMMIT_FRAGFREE = 64; // Chapter 5.6's minimum
localparam int FCS_OCTETS = 4;
endpackage// -----------------------------------------------------------------------
// commit_point_tracker -- says, for each octet of an arriving frame, what
// is known and whether the switch has committed.
//
// The value of making this explicit is that "committed" becomes a signal
// other modules can be checked against, rather than something implied by
// where a valid happens to assert in a pipeline.
// -----------------------------------------------------------------------
module commit_point_tracker
import cutthru_pkg::*;
#(
parameter int LEN_W = 14,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic octet_valid,
input logic frame_end,
input fwd_mode_e mode,
output logic [LEN_W-1:0] octets_received,
output logic have_destination, // >= 6
output logic have_source, // >= 12
output logic have_ethertype, // >= 14
output logic have_min_frame, // >= 64
output logic have_fcs, // frame_end
output logic committed,
output logic [LEN_W-1:0] commit_octet,
output logic [LEN_W-1:0] octets_after_commit,
output logic [CNT_W-1:0] c_commits,
output logic [CNT_W-1:0] c_octets_uninspected
);
logic [LEN_W-1:0] cnt_q;
logic committed_q;
assign octets_received = cnt_q;
assign have_destination = (cnt_q >= LEN_W'(6));
assign have_source = (cnt_q >= LEN_W'(12));
assign have_ethertype = (cnt_q >= LEN_W'(COMMIT_CUT));
assign have_min_frame = (cnt_q >= LEN_W'(COMMIT_FRAGFREE));
assign have_fcs = frame_end;
always_comb begin
unique case (mode)
FM_CUT_THROUGH: commit_octet = LEN_W'(COMMIT_CUT);
FM_FRAGMENT_FREE: commit_octet = LEN_W'(COMMIT_FRAGFREE);
default: commit_octet = '1; // the last octet
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt_q <= '0;
committed_q <= 1'b0;
c_commits <= '0;
c_octets_uninspected <= '0;
octets_after_commit <= '0;
end else begin
if (frame_start) begin
cnt_q <= '0;
committed_q <= 1'b0;
octets_after_commit <= '0;
end else if (octet_valid) begin
cnt_q <= cnt_q + 1'b1;
// THE COMMIT. After this edge the frame is leaving and no later
// information can prevent it -- only Section 11's stomping can
// mark it.
if (!committed_q && (cnt_q + 1'b1 >= commit_octet) &&
(mode != FM_STORE_FORWARD)) begin
committed_q <= 1'b1;
if (!(&c_commits)) c_commits <= c_commits + 1'b1;
end
// Every octet arriving after the commit is an octet the switch
// forwarded without inspecting. On a maximum frame under cut-
// through that is 1504 octets -- 12032 bits.
if (committed_q) begin
octets_after_commit <= octets_after_commit + 1'b1;
c_octets_uninspected <= c_octets_uninspected + 1'b1;
end
end
if (frame_end && (mode == FM_STORE_FORWARD)) begin
committed_q <= 1'b1;
if (!(&c_commits)) c_commits <= c_commits + 1'b1;
end
end
end
assign committed = committed_q;
endmoduleClassification: synthesizable.
What it teaches: that committed is the signal every other module in this chapter is checked against. A design where the commit point is implicit — an emergent consequence of where a valid happens to assert three pipeline stages down — cannot be asserted about, because no property can name the moment after which a decision is irrevocable.
And c_octets_uninspected is the honest measure of what the discipline gave up. Under store-and-forward it is zero, always. Under cut-through on maximum-length frames it is 1504 octets per frame — and at 1.4881 Mpps of maximum frames that is 1504 × 8 × 812 k = a substantial fraction of a link's worth of content forwarded sight unseen.
Deliberately simplified: an octet counter, where a real datapath is 8 or 64 octets wide and the commit lands mid-word. Production designs commit on a word boundary at or after octet 14, which makes the true commit point implementation-dependent and is exactly why it deserves an explicit signal.
Production implication: have_min_frame exists because of a discipline this chapter has not introduced yet. Fragment-free forwarding commits at octet 64 — Chapter 5.6's minimum frame size — which excludes runts and collision fragments while still committing 1454 octets early on a maximum frame. Section 12's selector uses all three commit points, and the tracker is what makes them interchangeable.
4. RTL 2 — Which of the Six Gates Survive an Early Commit
Chapter 12.3 established six gates. Schedule each one against the commit point and the answer is not "some are harder" — it is that one of them is impossible.
// -----------------------------------------------------------------------
// gate_schedule_checker -- for each of Chapter 12.3's six gates, is its
// input available before the commit point?
//
// This module computes nothing the datapath needs. It exists so that the
// answer is a signal rather than a claim in a design document, and so
// that a design which quietly runs a gate on unavailable data is caught.
// -----------------------------------------------------------------------
module gate_schedule_checker
import cutthru_pkg::*;
#(
parameter int LEN_W = 14,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_active,
input logic [LEN_W-1:0] octets_received,
input logic committed,
input fwd_mode_e mode,
// The six gates asserting that they have evaluated.
input logic g1_eligibility_done, // needs the FCS
input logic g2_ingress_state_done,
input logic g3_lookup_done,
input logic g4_filter_done,
input logic g5_egress_state_done,
input logic g6_admission_done,
output logic [5:0] gates_available, // input has arrived
output logic [5:0] gates_evaluated,
output logic g1_impossible, // the chapter's thesis
output logic [CNT_W-1:0] v_gate_ran_early, // evaluated without input
output logic [CNT_W-1:0] c_frames_uninspected
);
// GATE 1 needs the frame check sequence, which is the LAST four octets.
// Under cut-through the commit is at octet 14 and the FCS arrives at
// octet N-4 for N up to 1518. The input is not late; it is absent.
logic g1_input_available;
assign g1_input_available = 1'b0; // never, before a cut-through commit
// GATES 2 and 5 read a port-state register. Available at octet 0.
// GATE 3 needs the destination address -- octet 6 -- plus Chapter
// 12.5's four-cycle lookup, which at 500 MHz is 8 ns, comfortably
// inside the 48 ns the next 6 octets take at 1 Gb/s.
// GATE 4 compares the lookup's answer against the ingress port.
// GATE 6 reads queue occupancy, available at octet 0 -- but see the
// note below on why its answer can be invalidated.
always_comb begin
gates_available[0] = (mode == FM_STORE_FORWARD); // g1
gates_available[1] = 1'b1; // g2
gates_available[2] = (octets_received >= LEN_W'(6)); // g3
gates_available[3] = (octets_received >= LEN_W'(6)); // g4
gates_available[4] = 1'b1; // g5
gates_available[5] = 1'b1; // g6
end
assign gates_evaluated = {g6_admission_done, g5_egress_state_done,
g4_filter_done, g3_lookup_done,
g2_ingress_state_done, g1_eligibility_done};
// THE THESIS, AS A SIGNAL. Under any early-commit mode, gate 1 cannot
// be evaluated before the commit, because its input arrives up to
// 12032 bits afterwards.
assign g1_impossible = frame_active && (mode != FM_STORE_FORWARD);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
v_gate_ran_early <= '0;
c_frames_uninspected <= '0;
end else begin
// A gate that claims to have evaluated before its input arrived is
// evaluating on reset values, on the previous frame's data, or on
// an X. All three produce a decision that looks valid.
for (int g = 0; g < 6; g++)
if (gates_evaluated[g] && !gates_available[g])
if (!(&v_gate_ran_early)) v_gate_ran_early <= v_gate_ran_early + 1'b1;
if (committed && g1_impossible && !$past(committed))
if (!(&c_frames_uninspected))
c_frames_uninspected <= c_frames_uninspected + 1'b1;
end
end
endmoduleClassification: synthesizable, and intended to stay in silicon as a standing check.
What it teaches: the chapter's central fact, expressed as a truth table rather than a claim.
| Gate | Input | Available at octet | Survives a commit at 14 |
|---|---|---|---|
| 1 — eligibility | the FCS | N−4, up to 1514 | no — never |
| 2 — ingress port state | a register | 0 | yes |
| 3 — the lookup | destination address | 6 | yes — Chapter 12.5 answers in 8 ns |
| 4 — ingress filter | the lookup's answer | 6 + 8 ns | yes |
| 5 — egress port state | a register | 0 | yes |
| 6 — admission | queue occupancy | 0 | yes, but revocably |
Five of six survive. One cannot be run at all. And it is worth being precise about why gate 3 comfortably survives: the destination address completes at octet 6, and Chapter 12.5 §16 established a four-cycle lookup — 8 ns at 500 MHz — while octets 6 to 14 take 8 × 8 = 64 ns to arrive at 1 Gb/s. The lookup finishes with 56 ns to spare.
And it teaches why gate 6 is marked revocably. Queue occupancy is available immediately and can change while the frame is still arriving. A cut-through frame admitted at octet 14 may find, 12 µs later, that the egress queue filled behind it — and by then the frame's head is already on the wire. Section 13 is about that case.
Deliberately simplified: g1_input_available is hard-wired to zero under early commit rather than being computed from a frame-length model. That is deliberate — the point is that no computation makes it true, and writing it as a constant says so more clearly than an expression that happens to evaluate false.
Production implication: v_gate_ran_early catches a specific and plausible bug. A pipeline that structurally expects all six gates to report will, under cut-through, receive nothing from gate 1 — and the natural repair is to tie its done high and its result to pass. That converts "this gate could not run" into "this gate ran and approved", silently, and every corrupt frame is then forwarded with a positive eligibility result recorded against it.
5. RTL 3 — A Forwarding Engine That Commits Early
Put the five available gates in front of the commit and the sixth nowhere, and the engine writes itself — which is exactly the danger.
// -----------------------------------------------------------------------
// cutthrough_forwarder -- runs the five available gates and commits at
// octet 14.
//
// Note what is NOT in this module: any evaluation of frame validity. It
// is absent because it is impossible, and the absence is marked with an
// explicit output rather than left as a gap somebody later fills with a
// tied-off constant.
// -----------------------------------------------------------------------
module cutthrough_forwarder
import cutthru_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int LEN_W = 14,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic [LEN_W-1:0] octets_received,
input fwd_mode_e mode,
// The five gates whose inputs exist by octet 14.
input logic ingress_forwarding, // gate 2
input logic lookup_valid, // gate 3
input logic lookup_hit,
input logic [PORT_BITS-1:0] lookup_port,
input logic [PORT_BITS-1:0] ingress_port, // gate 4
input logic [N_PORTS-1:0] forwarding_mask, // gate 5
input logic egress_has_room, // gate 6
output logic commit,
output logic [N_PORTS-1:0] egress_mask,
output logic eligibility_unknown, // ALWAYS high here
output logic [CNT_W-1:0] c_committed_unchecked,
output logic [2:0] deciding_gate
);
logic committed_q;
logic [N_PORTS-1:0] all_but_ingress;
assign all_but_ingress = ~(N_PORTS'(1) << ingress_port);
// THE ADMISSION. Chapter 12.3's composer, minus gate 1, at octet 14.
always_comb begin
egress_mask = '0;
deciding_gate = 3'd0;
if (!ingress_forwarding) begin
deciding_gate = 3'd2;
end else if (!lookup_valid) begin
// The lookup has not answered by the commit point. Chapter 12.3
// Section 4's deadline applies here too, and it is much tighter:
// the answer must exist by octet 14, not by 28 ns.
deciding_gate = 3'd3;
egress_mask = forwarding_mask & all_but_ingress;
end else if (lookup_hit && (lookup_port == ingress_port)) begin
deciding_gate = 3'd4; // filter -- emit nothing
end else if (lookup_hit && !forwarding_mask[lookup_port]) begin
deciding_gate = 3'd5;
end else if (lookup_hit && !egress_has_room) begin
// Gate 6 at the commit point. NOTE: this answer can be invalidated
// by the queue filling while the remaining 1504 octets arrive --
// Section 13.
deciding_gate = 3'd6;
end else if (lookup_hit) begin
egress_mask = (N_PORTS'(1) << lookup_port);
end else begin
egress_mask = forwarding_mask & all_but_ingress;
end
end
// GATE 1 IS NOT HERE AND CANNOT BE. This output states that in the
// interface rather than leaving a hole for somebody to tie off.
assign eligibility_unknown = (mode != FM_STORE_FORWARD);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
committed_q <= 1'b0;
commit <= 1'b0;
c_committed_unchecked <= '0;
end else begin
commit <= 1'b0;
if (frame_start) committed_q <= 1'b0;
if (!committed_q && (mode != FM_STORE_FORWARD) &&
(octets_received >= LEN_W'(COMMIT_CUT))) begin
committed_q <= 1'b1;
commit <= 1'b1;
// Every commit under an early-commit mode is a frame forwarded
// without its validity established. The count is not an error --
// it is the discipline's operating volume, and it belongs on a
// report next to the error counters it can explain.
if (!(&c_committed_unchecked))
c_committed_unchecked <= c_committed_unchecked + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that eligibility_unknown is an output rather than an omission, and the distinction is the difference between a design that is honest about its discipline and one that quietly lies. A module that simply has no eligibility input looks complete, and the next engineer integrating it will connect a frame_ok signal from somewhere plausible — and whatever that signal is, it is not the FCS result, because the FCS result does not exist yet.
And it teaches that gate 3's deadline is tighter here than Chapter 12.3 stated. That chapter's budget was 28 ns, derived from the frame rate. Under cut-through the lookup must answer by octet 14 — 112 ns at 1 Gb/s, but only 11.2 ns at 10 Gb/s and 4.5 ns at 25 Gb/s. At high line rates the commit point arrives faster than the table can be searched, which is why cut-through above 10 Gb/s requires either a faster lookup or a later commit point.
Deliberately simplified: an octet-granular commit and a combinational gate chain. A production engine pipelines the gates across the octets as they arrive, which is what makes the 11.2 ns budget at 10 Gb/s achievable at all.
Production implication: c_committed_unchecked is not an error counter and should not sit among the error counters — it is the discipline's operating volume, and its value is that it explains the error counters elsewhere. A downstream switch reporting FCS errors and this switch reporting a large c_committed_unchecked on the same path is a complete diagnosis: the corrupt frames are being relayed, not generated, and Section 9 shows why the distinction is otherwise very hard to make.
6. The Latency Difference, Measured
Store-and-forward's latency floor is the whole frame's serialisation time. Cut-through's is a constant. Put both on the same table.
| Frame | Store-and-forward at 1 Gb/s | at 10 Gb/s | at 100 Mb/s |
|---|---|---|---|
| 64 octets | 512 ns | 51.2 ns | 5.12 µs |
| 128 octets | 1.024 µs | 102.4 ns | 10.24 µs |
| 512 octets | 4.096 µs | 409.6 ns | 40.96 µs |
| 1518 octets | 12.144 µs | 1.214 µs | 121.44 µs |
| 9000 octets — jumbo | 72 µs | 7.2 µs | 720 µs |
Cut-through's wait depends only on how far into the frame the commit point sits:
| Commit point | at 1 Gb/s | at 10 Gb/s | at 100 Mb/s |
|---|---|---|---|
| destination address, 6 octets | 48 ns | 4.8 ns | 480 ns |
| through EtherType, 14 octets | 112 ns | 11.2 ns | 1.12 µs |
| fragment-free, 64 octets | 512 ns | 51.2 ns | 5.12 µs |
The ratio, at 1 Gb/s with a 14-octet commit:
| Frame | Store-and-forward | Cut-through | Speed-up |
|---|---|---|---|
| 64 octets | 512 ns | 112 ns | 4.6× |
| 512 octets | 4.096 µs | 112 ns | 36.6× |
| 1518 octets | 12.144 µs | 112 ns | 108.4× |
| 9000 octets | 72 µs | 112 ns | 642.9× |
The advantage is real, large, and largest exactly where store-and-forward is worst — long frames on slow links.
And the fragment-free row is worth reading against the 64-octet row of the first table. Committing at octet 64 costs 512 ns at 1 Gb/s, which is exactly store-and-forward's latency on a minimum-length frame — so fragment-free forwarding is store-and-forward for small frames and cut-through for large ones, without needing to decide which it is.
7. RTL 4 — The Rate Rule
Cut-through requires that the egress port drain no faster than the ingress port fills. Violate it and the transmitter runs out of frame mid-transmission, and Ethernet has no way to pause inside a frame.
// -----------------------------------------------------------------------
// rate_compatibility_gate -- decides whether cut-through is even available
// for this ingress/egress pair.
//
// The arithmetic: with a head start of H bits, an egress at rate Re and
// an ingress at rate Ri, the egress has sent more than has been received
// after t = H / (Re - Ri). For a 1 Gb/s ingress feeding a 10 Gb/s egress
// with a 14-octet head start, t = 12.4 ns -- the transmitter runs dry
// having emitted 15.6 octets.
// -----------------------------------------------------------------------
module rate_compatibility_gate
import cutthru_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
// Rates in units of 100 Mb/s, so 1 G is 10 and 10 G is 100.
input logic [11:0] port_rate [N_PORTS],
input logic req_valid,
input logic [PORT_BITS-1:0] ingress_port,
input logic [PORT_BITS-1:0] egress_port,
input fwd_mode_e requested_mode,
input logic [7:0] head_start_octets,
output logic cutthru_available,
output fwd_mode_e granted_mode,
output cutthru_refusal_e refusal,
output logic [15:0] underrun_ns, // 0 when compatible
output logic [CNT_W-1:0] c_refused_rate,
output logic [CNT_W-1:0] c_granted
);
logic [11:0] ri, re;
assign ri = port_rate[ingress_port];
assign re = port_rate[egress_port];
// THE RULE. Egress must be no faster than ingress. Equal is fine --
// the transmitter consumes exactly as fast as the receiver supplies,
// and the head start is never spent. SLOWER egress is also fine: the
// buffer grows, which is what a queue is for.
logic rate_ok;
assign rate_ok = (re <= ri);
// How long the frame survives if the rule is violated, in nanoseconds.
// t = H_bits / (Re - Ri), with rates in 100 Mb/s units:
// bits/ns at 100 Mb/s = 0.1, so (re - ri) * 0.1 bits per ns.
always_comb begin
if (rate_ok || (re == ri)) underrun_ns = 16'd0;
else underrun_ns =
16'((16'(head_start_octets) * 16'd8 * 16'd10) / 16'(re - ri));
end
always_comb begin
cutthru_available = 1'b0;
granted_mode = FM_STORE_FORWARD;
refusal = CR_OK;
if (req_valid) begin
if (requested_mode == FM_STORE_FORWARD) begin
granted_mode = FM_STORE_FORWARD;
end else if (!rate_ok) begin
// A speed step UPWARD -- access to aggregation, the direction of
// almost every real network -- forces store-and-forward. This is
// not a policy choice; the alternative emits a truncated frame.
refusal = CR_EGRESS_FASTER;
granted_mode = FM_STORE_FORWARD;
end else begin
cutthru_available = 1'b1;
granted_mode = requested_mode;
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_refused_rate <= '0;
c_granted <= '0;
end else if (req_valid) begin
if (cutthru_available) begin
if (!(&c_granted)) c_granted <= c_granted + 1'b1;
end else if (refusal == CR_EGRESS_FASTER) begin
if (!(&c_refused_rate)) c_refused_rate <= c_refused_rate + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the rule is egress ≤ ingress and that equality is the interesting case. With equal rates the transmitter consumes exactly as fast as the receiver supplies, so the 14-octet head start is never spent and the frame streams indefinitely. With a slower egress the head start grows — which is simply a queue forming, and is fine. With a faster egress the head start shrinks at Re − Ri and runs out.
And the arithmetic is brutal. t = H ÷ (Re − Ri):
| Ingress → egress | Head start 6 oct | 14 oct | 64 oct |
|---|---|---|---|
| 1 Gb/s → 10 Gb/s | 5.3 ns | 12.4 ns | 56.9 ns |
| 100 Mb/s → 1 Gb/s | 53.3 ns | 124.4 ns | 568.9 ns |
| 10 Gb/s → 25 Gb/s | 3.2 ns | 7.5 ns | 34.1 ns |
A 1 Gb/s frame cut through to a 10 Gb/s port survives 12.4 nanoseconds, having emitted 15.6 octets. The rest of the frame is not there yet and never will be in time. Ethernet has no mechanism for pausing inside a frame — the transmitter must produce a continuous stream from the start delimiter to the FCS — so the result is a truncated frame on the wire, which the receiver discards as a runt or an FCS error.
Deliberately simplified: rates as integers in 100 Mb/s units and a combinational divide. A production gate uses a small lookup indexed by the two ports' rate codes, because the rates are known at link-up and change only on a renegotiation — Chapter 11.3's bring-up sequence.
Production implication: c_refused_rate will be the dominant refusal reason in almost every real deployment, and that is worth knowing before enabling the feature. Access-to-aggregation traffic is a speed step upward by definition — a 1 Gb/s host port feeding a 10 Gb/s uplink — so every frame taking that path is store-and-forward regardless of configuration. A network whose value from cut-through was assumed to be network-wide typically gets it only on the small fraction of traffic that stays within one speed tier.
8. RTL 5 — Detecting the Underrun When the Rule Is Broken
The rate gate prevents this. It is worth building the detector anyway, because the condition also arises from causes the gate does not see.
// -----------------------------------------------------------------------
// underrun_detector -- catches a transmitter that has run out of frame.
//
// The rate gate in Section 7 prevents the configured case. This detector
// exists for the cases it cannot see: an ingress link that slows
// mid-frame (Chapter 11.4's renegotiation), a receive FIFO that stalls,
// or a rate table that disagrees with the link's actual negotiated speed.
// -----------------------------------------------------------------------
module underrun_detector
import cutthru_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int LEN_W = 14,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic tx_active, // mid-frame on egress
input logic tx_octet_ready, // egress wants an octet
input logic rx_octet_available, // ingress has supplied one
input logic frame_complete, // whole frame received
input logic [PORT_BITS-1:0] egress_port,
input logic [LEN_W-1:0] octets_sent,
output logic underrun,
output logic abort_transmission,
output late_fault_e fault,
output logic [CNT_W-1:0] c_underruns,
output logic [LEN_W-1:0] shortest_underrun_at,
output logic [PORT_BITS-1:0] last_underrun_port
);
// THE CONDITION. The egress wants an octet, the ingress has not
// supplied one, and the frame is not finished. There is no way to
// pause: Ethernet's transmitter must emit a continuous stream from the
// start delimiter to the FCS.
assign underrun = tx_active && tx_octet_ready &&
!rx_octet_available && !frame_complete;
// Abort rather than emit filler. Emitting idle or padding produces a
// frame that is the WRONG LENGTH with a VALID-looking body, which a
// receiver may accept. Aborting produces something a receiver will
// certainly reject.
assign abort_transmission = underrun;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
fault <= LF_NONE;
c_underruns <= '0;
shortest_underrun_at <= '1;
last_underrun_port <= '0;
end else begin
fault <= LF_NONE;
if (underrun) begin
fault <= LF_TRUNCATED;
if (!(&c_underruns)) c_underruns <= c_underruns + 1'b1;
// How far in the frame died. Section 7 predicts 15.6 octets for
// a 1 G to 10 G step with a 14-octet head start; a measurement
// far from the prediction means the rate table is wrong.
if (octets_sent < shortest_underrun_at)
shortest_underrun_at <= octets_sent;
last_underrun_port <= egress_port;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that aborting is better than filling, and the reasoning is the same one Section 11 will apply to stomping. A transmitter that runs dry could emit idle octets or padding to keep the stream continuous — and the result is a frame of the wrong length whose body looks structurally valid. Chapter 7.3's checks may or may not catch it depending on where the filler landed. Aborting produces something the receiver will certainly reject, which is the correct outcome for a frame that cannot be completed.
And shortest_underrun_at is a cross-check on the configuration rather than on the datapath. Section 7 predicts 15.6 octets for a 1 Gb/s ingress feeding a 10 Gb/s egress with a 14-octet head start. A measured underrun at 15 or 16 octets confirms the rate table matches the links. An underrun at 400 octets means the rates are not what the table says — a link that renegotiated down without the table being updated, which Chapter 11.4 showed produces no error anywhere.
Deliberately simplified: an octet-granular ready/available handshake. A real datapath runs a word-wide FIFO with a programmable low-water mark, and the design question becomes how many octets of head start to accumulate before starting the transmitter — which is precisely the H in Section 7's t = H ÷ (Re − Ri).
Production implication: c_underruns should be exactly zero, always, because Section 7's gate prevents the configured case entirely. A non-zero value means the rate table disagrees with reality, and the frames it produced are truncated frames on the wire that the downstream device will report as FCS errors on its receive port. The error appears one hop away from its cause, which is Section 9's general problem in miniature.
9. Where the Error Appears Is Not Where It Was Made
A corrupt frame forwarded by a cut-through switch is discarded by the destination. The error counter that increments is at the destination, and nothing anywhere points back at the link that corrupted it.
Trace a single bit error through a chain of cut-through switches:
| Hop | What happens | What increments |
|---|---|---|
| link A→S1 corrupts a bit | — | nothing yet — S1 has not seen the FCS |
| S1 commits at octet 14, forwards | 1518 octets sent onward | S1's c_committed_unchecked |
| S1 receives the bad FCS | too late — the frame has left | S1's ingress FCS error counter |
| S2 commits, forwards | another 1518 octets spent | S2's c_committed_unchecked |
| S3, S4 … | the same, per hop | — |
| destination receives it | discards it | the destination's FCS error counter |
The bandwidth cost is linear in the hop count:
| Cut-through hops | Link capacity spent on one corrupt 1518-octet frame |
|---|---|
| 1 — store-and-forward at the first hop | 12 144 bits |
| 3 | 36 432 bits |
| 5 | 60 720 bits |
| 8 | 97 152 bits |
Store-and-forward at the first hop contains the frame there. The bad link's own switch discards it, its ingress FCS counter increments, and the counter is on the port attached to the faulty cable.
Under cut-through the frame travels the whole path and is discarded at the end — and the destination's error counter says only a frame arrived corrupt, with no indication of where it was corrupted. On a network of cut-through switches, every host downstream of one bad cable reports FCS errors, and none of them is near the cable.
10. Why the Latency Benefit Cancels Under Load
Cut-through's saving is available only when the egress port is idle at the moment the commit point arrives. Chapter 12.1 §10 asserted this; here is the arithmetic.
End-to-end switch latency is serialisation + lookup + queueing, and cut-through attacks only the first term.
| Load | Serialisation | Lookup | Queueing | Cut-through saves |
|---|---|---|---|---|
| idle egress | 12.144 µs | 8 ns | 0 | 12.03 µs — 99% |
| 50% utilisation | 12.144 µs | 8 ns | ~12 µs | 12.03 µs of 24 µs — 50% |
| 90% utilisation | 12.144 µs | 8 ns | ~109 µs | 12.03 µs of 121 µs — 10% |
| congested | 12.144 µs | 8 ns | unbounded | → 0% |
And the deeper point is not that the saving shrinks proportionally — it is that it disappears entirely.
A frame whose egress port is busy must be queued. A queued frame has been stored. The switch has performed store-and-forward whether or not it was configured to, and the commit point is irrelevant because the frame is not going anywhere until the port is free.
So the benefit is not merely diluted under load. The mechanism does not engage.
| Egress state at the commit point | What happens | Cut-through engaged |
|---|---|---|
| idle | frame streams straight through | yes — full benefit |
| transmitting another frame | frame is queued | no — it has been stored |
| queue non-empty | frame joins the queue | no |
Which produces the shape that makes cut-through a niche rather than a default: the benefit is inversely proportional to the load, and latency is a problem only under load.
At low utilisation, where the egress is usually idle, cut-through saves 12 µs on an end-to-end path that was already fast. At high utilisation, where a millisecond of queueing delay is the actual complaint, the egress is usually busy and the mechanism does not fire.
The networks where it earns its correctness cost are therefore the ones that engineer the third term to zero — storage fabrics and trading networks run deliberately underloaded so the queue is empty by construction. Everywhere else the load profile cancels the benefit and keeps the cost, which is Section 9's relocated evidence and Section 4's missing gate.
11. RTL 6 — FCS Stomping: The Constructive Answer
The frame has left and cannot be recalled. What is still available is to make certain nobody downstream mistakes it for a good one — and to make certain the next switch does not relay it too.
// -----------------------------------------------------------------------
// fcs_stomper -- deliberately corrupts the outgoing check sequence when
// the incoming one turns out to be bad.
//
// THE CONSTRUCTIVE ANSWER TO SECTION 4. Gate 1 cannot run before the
// commit. What CAN be done is to ensure the frame that was committed is
// unambiguously marked as bad by the time it finishes leaving -- so that
// every downstream device discards it, and so that no downstream
// cut-through switch relays it further.
// -----------------------------------------------------------------------
module fcs_stomper
import cutthru_pkg::*;
#(
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic tx_active,
input logic tx_in_fcs, // emitting the last 4 octets
input logic rx_fcs_bad, // ingress check failed
input logic rx_frame_end,
input logic committed,
input logic [31:0] computed_fcs, // over what was forwarded
output logic [31:0] tx_fcs,
output logic stomped,
output late_fault_e fault,
output logic [CNT_W-1:0] c_stomped,
output logic [CNT_W-1:0] c_bad_after_commit,
output logic [CNT_W-1:0] c_bad_before_commit
);
logic stomp_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
stomp_q <= 1'b0;
stomped <= 1'b0;
fault <= LF_NONE;
c_stomped <= '0;
c_bad_after_commit <= '0;
c_bad_before_commit <= '0;
end else begin
stomped <= 1'b0;
fault <= LF_NONE;
if (rx_frame_end && rx_fcs_bad) begin
if (committed) begin
// THE CASE THIS MODULE EXISTS FOR. The frame is already
// leaving. Nothing can stop it; the outgoing FCS can be made
// wrong on purpose.
stomp_q <= 1'b1;
if (!(&c_bad_after_commit))
c_bad_after_commit <= c_bad_after_commit + 1'b1;
end else begin
// Not committed yet -- a store-and-forward path, or a frame
// shorter than the commit point. Ordinary gate 1 rejection.
if (!(&c_bad_before_commit))
c_bad_before_commit <= c_bad_before_commit + 1'b1;
end
end
if (stomp_q && tx_in_fcs) begin
stomped <= 1'b1;
fault <= LF_STOMPED;
stomp_q <= 1'b0;
if (!(&c_stomped)) c_stomped <= c_stomped + 1'b1;
end
end
end
// INVERT the computed value rather than emitting a constant. A constant
// could, on some frame, coincidentally BE the correct FCS -- and then
// the corrupt frame is forwarded with a valid check sequence, which is
// strictly the worst outcome available.
assign tx_fcs = stomp_q ? ~computed_fcs : computed_fcs;
endmoduleClassification: synthesizable.
What it teaches: that the stomp inverts the computed value rather than writing a constant, and the reason is a genuine hazard rather than fastidiousness. A fixed "bad FCS" constant will, on some frame, happen to equal that frame's correct check sequence — one frame in 2³², which at 1.4881 Mpps is once every 48 minutes on a fully loaded gigabit link. On that frame the corrupt payload is forwarded with a valid FCS, and every device downstream accepts it. Inverting the computed value cannot coincide, because a value and its complement differ in all 32 bits.
And it teaches why stomping is the right answer rather than a consolation prize. The frame is already gone; the choice is only between a corrupt frame that looks valid and a corrupt frame that is unmistakably marked. The marked version is discarded at the very next hop — including by a downstream cut-through switch, whose own gate 1 sees the stomped FCS and stomps in turn — so Section 9's linear bandwidth cost is bounded at one extra hop instead of the whole path.
Deliberately simplified: a single stomp flag with no per-frame identity. A pipelined design carries the stomp decision with the frame's descriptor, since the ingress FCS result for frame n may arrive while frame n+1 is already being transmitted.
Production implication: c_bad_after_commit against c_bad_before_commit is the diagnostic pair. Both count frames that failed their check sequence; the split says whether the switch was able to contain them. A path where c_bad_after_commit dominates is relaying corruption — Section 9's condition — and the remedy is to force store-and-forward on that ingress port, which contains the damage at the cost of that port's latency and nothing else.
12. RTL 7 — Choosing the Discipline Per Frame
The discipline does not have to be a configuration. Every input the choice depends on is available at the commit point, so it can be made per frame.
// -----------------------------------------------------------------------
// latency_mode_selector -- picks store-and-forward, fragment-free or
// cut-through for THIS frame.
//
// Every input is available by octet 14, which is what makes a per-frame
// choice possible rather than a per-port configuration. The selector is
// also where an error-rate fallback lives: a port that has recently
// delivered corrupt frames should not have its frames relayed onward.
// -----------------------------------------------------------------------
module latency_mode_selector
import cutthru_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int ERR_WINDOW = 1_000_000,
parameter int ERR_THRESH = 10, // bad frames per window
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic sel_valid,
input logic [PORT_BITS-1:0] ingress_port,
input logic rate_ok, // Section 7's gate
input logic egress_idle, // Section 10
input fwd_mode_e configured_mode,
input logic length_ambiguous, // Chapter 5.5
input logic rx_frame_done,
input logic rx_frame_bad,
output fwd_mode_e selected_mode,
output cutthru_refusal_e refusal,
output logic fallback_active [N_PORTS],
output logic [CNT_W-1:0] c_sf,
output logic [CNT_W-1:0] c_ff,
output logic [CNT_W-1:0] c_ct,
output logic [PORT_BITS-1:0] worst_error_port
);
logic [CNT_W-1:0] err_win [N_PORTS];
logic [CNT_W-1:0] frames_win [N_PORTS];
always_comb begin
selected_mode = FM_STORE_FORWARD;
refusal = CR_OK;
if (sel_valid) begin
if (configured_mode == FM_STORE_FORWARD) begin
refusal = CR_MODE_FORCED;
end else if (!rate_ok) begin
// Section 7. Not a policy -- the alternative truncates the frame.
refusal = CR_EGRESS_FASTER;
end else if (fallback_active[ingress_port]) begin
// This port has recently delivered corrupt frames. Relaying them
// spends Section 9's bandwidth on every downstream hop, so the
// switch contains them at the cost of this port's latency.
refusal = CR_ERROR_RATE;
end else if (length_ambiguous) begin
// Chapter 5.5's length/type ambiguity: a value at octets 12-13
// below 1536 is a LENGTH, and a frame whose length is not yet
// decidable cannot have its end predicted.
refusal = CR_UNKNOWN_LENGTH;
end else if (!egress_idle) begin
// Section 10. The frame will be queued, so it has been stored
// anyway -- and saying so keeps the counters honest about how
// often the mechanism actually engages.
refusal = CR_EGRESS_BUSY;
selected_mode = FM_STORE_FORWARD;
end else begin
selected_mode = configured_mode;
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int p = 0; p < N_PORTS; p++) begin
err_win[p] <= '0; frames_win[p] <= '0; fallback_active[p] <= 1'b0;
end
c_sf <= '0; c_ff <= '0; c_ct <= '0;
worst_error_port <= '0;
end else begin
if (sel_valid) begin
unique case (selected_mode)
FM_STORE_FORWARD: c_sf <= c_sf + 1'b1;
FM_FRAGMENT_FREE: c_ff <= c_ff + 1'b1;
FM_CUT_THROUGH: c_ct <= c_ct + 1'b1;
default: ;
endcase
end
if (rx_frame_done) begin
frames_win[ingress_port] <= frames_win[ingress_port] + 1'b1;
if (rx_frame_bad) err_win[ingress_port] <= err_win[ingress_port] + 1'b1;
if (frames_win[ingress_port] >= CNT_W'(ERR_WINDOW)) begin
// Latch the fallback and keep it until the window is clean.
// Oscillating between disciplines on a marginal link produces
// a latency that varies by two orders of magnitude frame to
// frame, which is worse for a real-time application than
// uniformly slow.
fallback_active[ingress_port] <=
(err_win[ingress_port] > CNT_W'(ERR_THRESH));
if (err_win[ingress_port] > CNT_W'(ERR_THRESH))
worst_error_port <= ingress_port;
err_win[ingress_port] <= '0;
frames_win[ingress_port] <= '0;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the CR_EGRESS_BUSY branch keeps the counters honest. A design that "selects cut-through" and then queues the frame anyway has performed store-and-forward while reporting cut-through, and c_ct becomes a count of intentions rather than events. Reporting the refusal makes c_ct a count of frames that actually streamed through, which is the number Section 10's argument needs and the one an operator would use to decide whether the feature is doing anything.
And it teaches the error-rate fallback, which is the direct answer to Section 9. A port delivering corrupt frames is a port whose frames should be contained rather than relayed, because relaying them spends k × 12 144 bits across k downstream hops and scatters the evidence. Falling back to store-and-forward on that one port costs that port's latency and nothing else — and it puts the FCS error counter back next to the failing cable.
Deliberately simplified: a per-port error window with a latching fallback. Production designs add hysteresis and a minimum dwell time, because a link that is marginal rather than broken will otherwise toggle the discipline continuously.
Production implication: the latch matters more than the threshold. A selector that re-evaluates every frame produces a latency that alternates between 112 ns and 12.144 µs unpredictably — a 108× variation, frame to frame, on the same path. For the real-time applications that are the reason cut-through was enabled, a consistently slow path is far better than an unpredictable one, because jitter is what their buffers are sized against, not mean latency.
13. When the Admission Decision Is Revoked
Gate 6 was marked revocably in Section 4, and it is the one gate whose input exists early and whose answer can become wrong afterwards.
A cut-through frame is admitted at octet 14 because the egress queue had room. On a 1518-octet frame at 1 Gb/s the remaining 1504 octets take 12.032 µs to arrive — and in that time the queue can fill from other ports.
| Moment | Queue state | What the switch can still do |
|---|---|---|
| octet 14 — commit | room | admit, begin transmitting |
| octet 500 | queue filling | nothing — the head is on the wire |
| octet 1200 | full | nothing |
| octet 1514 | full | nothing |
And this is where cut-through's failure differs qualitatively from store-and-forward's.
A store-and-forward switch that finds its egress queue full discards the whole frame — Chapter 12.1 §9's tail drop, a complete frame, cleanly gone, counted.
A cut-through switch has already emitted the first 500 octets. It cannot discard what has left. The only options are to truncate — abort the transmission, producing a runt the receiver discards — or to have reserved the buffer at admission time.
So a cut-through egress must reserve worst-case buffer at the commit point, which means reserving MAX_FRAME octets for a frame whose length is not yet known. On a 24-port switch with jumbo frames enabled that is 24 × 9000 = 216 KB of reservation for frames that will mostly be 64 octets — and the reservation is what a store-and-forward design does not need, because it knows the length before it commits.
14. The Three Commit Points, Compared
Store-and-forward, fragment-free and cut-through are the same mechanism with three different values of one parameter.
| store-and-forward | fragment-free | cut-through | |
|---|---|---|---|
| commits at octet | N | 64 | 14 |
| latency, 64-octet frame at 1 Gb/s | 512 ns | 512 ns | 112 ns |
| latency, 1518-octet frame at 1 Gb/s | 12.144 µs | 512 ns | 112 ns |
| octets forwarded uninspected, max frame | 0 | 1454 | 1504 |
| gate 1 — eligibility | runs | cannot run | cannot run |
| rejects runts — Chapter 7.3 | yes | yes | no |
| rejects collision fragments | yes | yes | no |
| buffer reserved per in-flight frame | actual length | max | max |
requires egress ≤ ingress rate | no | yes | yes |
Fragment-free is the row worth studying, because it is nearly free.
Committing at octet 64 — Chapter 5.6's minimum frame size — costs 512 ns at 1 Gb/s, which is exactly store-and-forward's latency on a minimum-length frame. So on small frames fragment-free is no slower than store-and-forward, and on a 1518-octet frame it is 23.7× faster.
And it buys back one of the two things cut-through gave up. A frame shorter than 64 octets is a runt or a collision fragment, and fragment-free has seen the whole thing before committing, so it rejects them. It still cannot run gate 1 — the FCS is still at the end — but it no longer relays fragments from a legacy segment or a failing transceiver.
What it does not buy back is the FCS check, and that is the irreducible part. Any commit point before the last octet gives up gate 1, and the only commit point that does not give it up is the last one, which is store-and-forward by definition.
15. RTL 8 — Conformance for an Irrevocable Decision
The monitor's difficulty here is that the thing it must check happens after the thing it constrains, which is the same structure as Section 16's rejected property — and the resolution is to check the response rather than the decision.
// -----------------------------------------------------------------------
// cutthrough_conformance_monitor -- checks a discipline whose defining
// property is that it decides before it knows.
//
// What it CAN check: that the commit happened at the configured octet;
// that gate 1 was never claimed to have run; that a frame found bad after
// commit was STOMPED; that the rate rule was honoured; that no frame was
// silently truncated.
//
// What it CANNOT check: that the committed frame was worth forwarding.
// That is Section 16's rejected property, and its evidence arrives 12032
// bits after the decision.
// -----------------------------------------------------------------------
module cutthrough_conformance_monitor
import cutthru_pkg::*;
#(
parameter int LEN_W = 14,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic committed,
input logic [LEN_W-1:0] commit_octet_actual,
input logic [LEN_W-1:0] commit_octet_expected,
input fwd_mode_e mode,
input logic g1_claimed_done,
input logic rate_ok,
input logic rx_frame_end,
input logic rx_fcs_bad,
input logic tx_frame_end,
input late_fault_e fault,
output logic [CNT_W-1:0] v_commit_wrong_octet,
output logic [CNT_W-1:0] v_gate1_claimed, // claimed the impossible
output logic [CNT_W-1:0] v_bad_passed_intact, // THE critical failure
output logic [CNT_W-1:0] v_rate_violated,
output logic [CNT_W-1:0] v_silent_truncation,
output logic conformant
);
logic committed_q, bad_pending_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
committed_q <= 1'b0;
bad_pending_q <= 1'b0;
v_commit_wrong_octet <= '0;
v_gate1_claimed <= '0;
v_bad_passed_intact <= '0;
v_rate_violated <= '0;
v_silent_truncation <= '0;
end else begin
if (frame_start) begin
committed_q <= 1'b0;
bad_pending_q <= 1'b0;
end
if (committed && !committed_q) begin
committed_q <= 1'b1;
// The commit landed where the configured discipline says it
// should. A datapath wider than one octet will commit on a word
// boundary, and this catches a design whose real commit point
// drifted from its documented one.
if (commit_octet_actual != commit_octet_expected)
if (!(&v_commit_wrong_octet))
v_commit_wrong_octet <= v_commit_wrong_octet + 1'b1;
// THE STRUCTURAL CHECK. Under any early-commit mode, a gate 1
// that claims to have completed is claiming to have read the FCS
// before it arrived -- which means it read reset values, the
// previous frame's result, or a tie-off.
if ((mode != FM_STORE_FORWARD) && g1_claimed_done)
if (!(&v_gate1_claimed))
v_gate1_claimed <= v_gate1_claimed + 1'b1;
if (!rate_ok && (mode != FM_STORE_FORWARD))
if (!(&v_rate_violated))
v_rate_violated <= v_rate_violated + 1'b1;
end
if (rx_frame_end && rx_fcs_bad && committed_q) bad_pending_q <= 1'b1;
if (tx_frame_end) begin
// A frame that was known bad and left with an intact check
// sequence is the failure this whole chapter is organised
// around: a corrupt frame wearing a valid FCS, which every
// device downstream will accept.
if (bad_pending_q && (fault != LF_STOMPED) && (fault != LF_TRUNCATED))
if (!(&v_bad_passed_intact))
v_bad_passed_intact <= v_bad_passed_intact + 1'b1;
// A truncation that was not reported. Chapter 12.4's missing
// copy, in a different guise: the frame simply stopped, and
// nothing said so.
if ((fault == LF_TRUNCATED) && !bad_pending_q && rate_ok)
if (!(&v_silent_truncation))
v_silent_truncation <= v_silent_truncation + 1'b1;
bad_pending_q <= 1'b0;
end
end
end
assign conformant = (v_commit_wrong_octet == '0) &&
(v_gate1_claimed == '0) &&
(v_bad_passed_intact == '0) &&
(v_rate_violated == '0) &&
(v_silent_truncation == '0);
endmoduleClassification: synthesizable, and intended to stay in silicon.
What it teaches: the resolution to the chapter's central difficulty. The monitor cannot check that a committed frame was worth forwarding — the evidence arrives after the decision, which is Section 16's rejected property. What it can check is the response to the evidence when it finally arrives: was the frame that turned out bad stomped, truncated, or passed intact?
v_bad_passed_intact is the failure the whole chapter is organised around. A frame whose ingress FCS check failed, emitted with a valid outgoing check sequence, is a corrupt frame that every device downstream will accept — and it will be accepted as data, by an application, silently. That is strictly worse than any latency, and it is what Section 11's stomper exists to prevent.
And v_gate1_claimed catches Section 4's tie-off directly. A pipeline that structurally expects six gate results and receives five will have its sixth input tied to pass, and this counter fires on the first frame.
Deliberately simplified: one frame tracked at a time, with bad_pending_q assuming the ingress FCS result arrives before the egress frame ends. At high line rates with deep egress queues that is not guaranteed, and a production monitor tags the stomp decision to a frame identifier.
Production implication: conformant here means the discipline behaved as specified: it committed where it said, it never claimed a gate it could not run, it honoured the rate rule, and every frame that turned out bad was marked. It does not mean the forwarded frames were good — Chapter 12.4 §17 established that claims about a frame's downstream fate are not assertable, and this chapter adds a second reason: for a cut-through switch, the claim is not even evaluable at the moment it would need to be made.
16. Properties Worth Asserting, and One Worth Refusing
Every property here is about the commit, the response to late evidence, or the rate rule. None is about whether the committed frame was good, and Section 16's rejected class is why.
The commit point
// P1. The commit lands at the octet the configured discipline names.
property p_commit_at_configured_octet;
@(posedge clk) disable iff (!rst_n)
$rose(committed) |-> (octets_received >= commit_octet);
endproperty
a_commit_octet: assert property (p_commit_at_configured_octet);
// P2. Store-and-forward commits ONLY at the last octet.
property p_sf_commits_at_end;
@(posedge clk) disable iff (!rst_n)
((mode == FM_STORE_FORWARD) && $rose(committed)) |-> frame_end;
endproperty
a_sf_late_commit: assert property (p_sf_commits_at_end);
// P3. The commit happens at most once per frame.
property p_commit_once;
@(posedge clk) disable iff (!rst_n)
$rose(committed) |=> committed until_with frame_start;
endproperty
a_commit_once: assert property (p_commit_once);
// P4. Every octet after the commit is counted as uninspected -- the
// honest measure of what the discipline gave up.
property p_uninspected_counted;
@(posedge clk) disable iff (!rst_n)
(committed && octet_valid) |=> (octets_after_commit > $past(octets_after_commit));
endproperty
a_uninspected: assert property (p_uninspected_counted);
// P5. Fragment-free commits no earlier than the minimum frame size, so a
// runt is always fully received before any decision is irrevocable.
property p_fragfree_after_min_frame;
@(posedge clk) disable iff (!rst_n)
((mode == FM_FRAGMENT_FREE) && $rose(committed))
|-> (octets_received >= LEN_W'(COMMIT_FRAGFREE));
endproperty
a_fragfree_min: assert property (p_fragfree_after_min_frame);The gates that can and cannot run
// P6. THE STRUCTURAL PROPERTY. Under any early-commit mode, gate 1 never
// claims to have completed. Its input does not exist yet.
property p_gate1_never_claimed_early;
@(posedge clk) disable iff (!rst_n)
((mode != FM_STORE_FORWARD) && committed) |-> !g1_eligibility_done;
endproperty
a_gate1_impossible: assert property (p_gate1_never_claimed_early);
// P7. eligibility_unknown is asserted whenever the discipline cannot run
// gate 1 -- the interface states the gap rather than leaving one.
property p_eligibility_unknown_declared;
@(posedge clk) disable iff (!rst_n)
(mode != FM_STORE_FORWARD) |-> eligibility_unknown;
endproperty
a_unknown_declared: assert property (p_eligibility_unknown_declared);
// P8. No gate evaluates before its input has arrived.
property p_no_gate_runs_early;
@(posedge clk) disable iff (!rst_n)
frame_active |-> ((gates_evaluated & ~gates_available) == '0);
endproperty
a_no_early_gate: assert property (p_no_gate_runs_early);
// P9. Gate 3's answer exists by the commit point. Under cut-through at
// 10 Gb/s that is 11.2 ns, which is tighter than Chapter 12.3's 28 ns.
property p_lookup_answers_before_commit;
@(posedge clk) disable iff (!rst_n)
($rose(committed) && (mode != FM_STORE_FORWARD)) |-> lookup_valid;
endproperty
a_lookup_in_time: assert property (p_lookup_answers_before_commit);
// P10. Gates 2, 4 and 5 are evaluated before the commit -- they are
// available and skipping them would be a choice, not a necessity.
property p_available_gates_all_run;
@(posedge clk) disable iff (!rst_n)
$rose(committed) |-> (g2_ingress_state_done && g4_filter_done &&
g5_egress_state_done);
endproperty
a_available_gates_run: assert property (p_available_gates_all_run);The rate rule
// P11. Cut-through is granted only when the egress is no faster than the
// ingress. Violating this truncates the frame; it is not a policy.
property p_rate_rule_enforced;
@(posedge clk) disable iff (!rst_n)
(req_valid && cutthru_available) |-> (port_rate[egress_port] <=
port_rate[ingress_port]);
endproperty
a_rate_rule: assert property (p_rate_rule_enforced);
// P12. Equal rates are permitted -- the head start is never spent.
property p_equal_rates_allowed;
@(posedge clk) disable iff (!rst_n)
(req_valid && (requested_mode == FM_CUT_THROUGH) &&
(port_rate[egress_port] == port_rate[ingress_port]))
|-> cutthru_available;
endproperty
a_equal_ok: assert property (p_equal_rates_allowed);
// P13. A rate refusal names its reason, so it is not confused with a
// configuration or an error-rate fallback.
property p_rate_refusal_classified;
@(posedge clk) disable iff (!rst_n)
(req_valid && !cutthru_available &&
(port_rate[egress_port] > port_rate[ingress_port]))
|-> (refusal == CR_EGRESS_FASTER);
endproperty
a_refusal_named: assert property (p_rate_refusal_classified);
// P14. An underrun ABORTS rather than emitting filler. Filler produces a
// wrong-length frame with a structurally valid body.
property p_underrun_aborts;
@(posedge clk) disable iff (!rst_n)
underrun |-> abort_transmission;
endproperty
a_underrun_aborts: assert property (p_underrun_aborts);
// P15. Under the rate rule, no underrun occurs at all.
property p_no_underrun_when_rate_ok;
@(posedge clk) disable iff (!rst_n)
(tx_active && rate_ok) |-> !underrun;
endproperty
a_no_underrun: assert property (p_no_underrun_when_rate_ok);The response to late evidence
// P16. THE CENTRAL PROPERTY. A frame found bad AFTER the commit leaves
// with a deliberately wrong check sequence.
property p_bad_after_commit_is_stomped;
@(posedge clk) disable iff (!rst_n)
(rx_frame_end && rx_fcs_bad && committed) |-> ##[1:$] (stomped or abort_transmission);
endproperty
a_stomp_or_abort: assert property (p_bad_after_commit_is_stomped);
// P17. A frame known bad NEVER leaves with a valid check sequence. This
// is the failure the whole chapter is organised around.
property p_bad_never_passes_intact;
@(posedge clk) disable iff (!rst_n)
(tx_frame_end && bad_pending_q) |-> (fault inside {LF_STOMPED, LF_TRUNCATED});
endproperty
a_never_pass_bad: assert property (p_bad_never_passes_intact);
// P18. The stomp INVERTS the computed value. A constant could, once in
// 2^32 frames, coincide with the correct FCS -- once every 48 minutes on
// a loaded gigabit link.
property p_stomp_inverts;
@(posedge clk) disable iff (!rst_n)
stomped |-> (tx_fcs == ~computed_fcs);
endproperty
a_stomp_inverts: assert property (p_stomp_inverts);
// P19. A good frame is never stomped.
property p_good_never_stomped;
@(posedge clk) disable iff (!rst_n)
(tx_in_fcs && !rx_fcs_bad) |-> !stomped;
endproperty
a_good_intact: assert property (p_good_never_stomped);
// P20. Bad frames are counted by WHETHER they were containable, because
// the split is the diagnosis.
property p_bad_split_by_commit;
@(posedge clk) disable iff (!rst_n)
(rx_frame_end && rx_fcs_bad)
|=> ($changed(c_bad_after_commit) ^ $changed(c_bad_before_commit));
endproperty
a_bad_split: assert property (p_bad_split_by_commit);
// P21. A truncation is never silent.
property p_truncation_reported;
@(posedge clk) disable iff (!rst_n)
abort_transmission |-> ##[1:4] (fault == LF_TRUNCATED);
endproperty
a_truncation_reported: assert property (p_truncation_reported);Discipline selection
// P22. A frame whose egress is busy is reported as CR_EGRESS_BUSY, so
// c_ct counts frames that actually streamed rather than intentions.
property p_busy_egress_reported;
@(posedge clk) disable iff (!rst_n)
(sel_valid && !egress_idle && (configured_mode != FM_STORE_FORWARD))
|-> (refusal == CR_EGRESS_BUSY);
endproperty
a_busy_reported: assert property (p_busy_egress_reported);
// P23. A port with a recent error history falls back to store-and-forward
// -- containing corruption at the cost of one port's latency.
property p_error_fallback_engages;
@(posedge clk) disable iff (!rst_n)
(sel_valid && fallback_active[ingress_port])
|-> (selected_mode == FM_STORE_FORWARD);
endproperty
a_error_fallback: assert property (p_error_fallback_engages);
// P24. The fallback LATCHES for a whole window. Oscillating between
// disciplines produces a latency that varies 108x frame to frame.
property p_fallback_latches;
@(posedge clk) disable iff (!rst_n)
($rose(fallback_active[ingress_port]) && !window_end)
|=> fallback_active[ingress_port];
endproperty
a_fallback_stable: assert property (p_fallback_latches);
// P25. Chapter 5.5's length/type ambiguity forces store-and-forward -- a
// frame whose end cannot be predicted cannot be streamed safely.
property p_ambiguous_length_forces_sf;
@(posedge clk) disable iff (!rst_n)
(sel_valid && length_ambiguous) |-> (selected_mode == FM_STORE_FORWARD);
endproperty
a_ambiguous_sf: assert property (p_ambiguous_length_forces_sf);
// P26. Exactly one discipline counter moves per selection.
property p_one_mode_counted;
@(posedge clk) disable iff (!rst_n)
sel_valid |=> (($changed(c_sf) + $changed(c_ff) + $changed(c_ct)) == 1);
endproperty
a_one_mode: assert property (p_one_mode_counted);Conformance
// P27. Conformance means the discipline behaved as specified -- never
// that the forwarded frames were good.
property p_conformant_definition;
@(posedge clk) disable iff (!rst_n)
conformant |-> ((v_commit_wrong_octet == '0) && (v_gate1_claimed == '0) &&
(v_bad_passed_intact == '0) && (v_rate_violated == '0) &&
(v_silent_truncation == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);
// P28. Store-and-forward forwards ZERO uninspected octets. The chapter's
// baseline, asserted rather than assumed.
property p_sf_inspects_everything;
@(posedge clk) disable iff (!rst_n)
((mode == FM_STORE_FORWARD) && frame_end) |-> (octets_after_commit == '0);
endproperty
a_sf_full_inspection: assert property (p_sf_inspects_everything);17. Verification Scenarios
Sixty scenarios. The commit and response scenarios have no acceptable failure; the latency scenarios have expected values that include giving the benefit up entirely.
The commit point
| # | Scenario | Expected |
|---|---|---|
| 1 | Cut-through, 1518-octet frame | commit at octet 14, octets_after_commit = 1504 |
| 2 | Cut-through, 64-octet frame | commit at 14, octets_after_commit = 50 |
| 3 | Fragment-free, 1518-octet frame | commit at 64, uninspected = 1454 |
| 4 | Fragment-free, 40-octet runt | never commits — the frame ends first |
| 5 | Store-and-forward, any frame | commit at the last octet, uninspected = 0 |
| 6 | Any frame, any mode | commit asserts at most once |
| 7 | Cut-through, frame shorter than 14 octets | no commit; the frame is rejected outright |
| 8 | Datapath 8 octets wide, cut-through | commit on the word containing octet 14 — P1 must still hold |
Gate scheduling
| # | Scenario | Expected |
|---|---|---|
| 9 | Cut-through, any frame | g1_impossible high, g1_eligibility_done never asserts |
| 10 | Cut-through, gate 1 tied to done | v_gate1_claimed increments on the first frame |
| 11 | Cut-through, any frame | eligibility_unknown high |
| 12 | Store-and-forward | eligibility_unknown low, gate 1 runs |
| 13 | Cut-through at 1 Gb/s | lookup answers by octet 14 = 112 ns; Chapter 12.5's 8 ns fits |
| 14 | Cut-through at 10 Gb/s | budget is 11.2 ns — tighter than Chapter 12.3's 28 ns |
| 15 | Cut-through at 25 Gb/s | budget 4.5 ns — the lookup must be pipelined into the octets |
| 16 | Gates 2, 4, 5 at the commit | all three evaluated — they are available |
| 17 | Any gate evaluated before its input | v_gate_ran_early |
The rate rule
| # | Scenario | Expected |
|---|---|---|
| 18 | 1 Gb/s ingress → 1 Gb/s egress | cut-through granted |
| 19 | 1 Gb/s ingress → 100 Mb/s egress | granted — the head start grows |
| 20 | 1 Gb/s ingress → 10 Gb/s egress | refused, CR_EGRESS_FASTER |
| 21 | Same, rule bypassed | underrun after 12.4 ns, having sent 15.6 octets |
| 22 | 100 Mb/s → 1 Gb/s, rule bypassed | underrun after 124.4 ns, 15.6 octets |
| 23 | 10 Gb/s → 25 Gb/s, rule bypassed | underrun after 7.5 ns |
| 24 | Underrun with a 64-octet head start, 1 G → 10 G | underrun after 56.9 ns, 71.1 octets |
| 25 | Any underrun | abort, never filler; LF_TRUNCATED |
| 26 | Rate rule honoured, sustained line rate | c_underruns = 0 |
| 27 | Link renegotiates down mid-run, table stale | underrun at an octet far from the prediction |
| 28 | Typical access-to-aggregation traffic | c_refused_rate dominates c_granted |
Late evidence and stomping
| # | Scenario | Expected |
|---|---|---|
| 29 | Bad FCS, store-and-forward | frame never leaves; c_bad_before_commit |
| 30 | Bad FCS discovered after a cut-through commit | stomped; c_bad_after_commit and c_stomped |
| 31 | Stomped frame's outgoing FCS | ~computed_fcs — inverted, never a constant |
| 32 | Good frame | never stomped; FCS intact |
| 33 | Bad frame emitted with a valid FCS | v_bad_passed_intact — the critical failure |
| 34 | Stomped frame at the next switch | discarded, and stomped again if that switch is cut-through |
| 35 | Corrupt frame across 5 cut-through hops with stomping | contained at hop 2, not hop 5 |
| 36 | Corrupt frame across 5 cut-through hops without stomping | 60 720 bits of link capacity spent |
| 37 | A constant "bad FCS" value | coincides with a correct FCS once per 2³² frames — 48 min at line rate |
| 38 | Truncation with no fault reported | v_silent_truncation |
Latency
| # | Scenario | Expected |
|---|---|---|
| 39 | Store-and-forward, 1518 octets at 1 Gb/s | 12.144 µs |
| 40 | Cut-through, 1518 octets at 1 Gb/s | 112 ns — 108.4× |
| 41 | Store-and-forward, 64 octets at 1 Gb/s | 512 ns |
| 42 | Cut-through, 64 octets at 1 Gb/s | 112 ns — 4.6× |
| 43 | Fragment-free, 64 octets | 512 ns — identical to store-and-forward |
| 44 | Fragment-free, 1518 octets | 512 ns — 23.7× faster than store-and-forward |
| 45 | Cut-through, 9000-octet jumbo at 1 Gb/s | 112 ns against 72 µs — 642.9× |
| 46 | Cut-through with an idle egress | full benefit, c_ct increments |
| 47 | Cut-through with a busy egress | CR_EGRESS_BUSY, store-and-forward performed |
| 48 | 90% egress utilisation | queueing dominates; the saving is ~10% of total latency |
| 49 | Congested egress | the mechanism does not engage at all |
Selection, buffering and conformance
| # | Scenario | Expected |
|---|---|---|
| 50 | Port exceeding the error threshold | fallback_active, CR_ERROR_RATE, store-and-forward |
| 51 | Fallback engaged mid-window | latches — no per-frame oscillation |
| 52 | Chapter 5.5 length/type ambiguity | forced to store-and-forward, CR_UNKNOWN_LENGTH |
| 53 | Any selection | exactly one of c_sf, c_ff, c_ct moves |
| 54 | Cut-through egress with jumbo enabled | reserves 9000 octets per in-flight frame |
| 55 | Egress queue fills after a cut-through commit | cannot discard — the head is on the wire |
| 56 | Healthy run, one million frames | conformant high throughout |
| 57 | Cut-through, mixed frame sizes | latency constant at 112 ns — zero spread |
| 58 | Store-and-forward, mixed frame sizes | latency spread 23.7× — 512 ns to 12.144 µs |
| 59 | Stomped frame received by a store-and-forward switch | discarded; its ingress FCS counter increments |
| 60 | c_stomped on the emitting switch | the only record that a downstream error was manufactured |
18. Debugging a Cut-Through Path
Every row produces a switch forwarding correctly with healthy links. The middle column is the hypothesis and the right column settles it — and several of these appear on a device other than the one at fault.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| FCS errors on many hosts across a building | one bad cable, relayed by cut-through | the first-hop switch's ingress FCS counter plus c_committed_unchecked |
| FCS errors at a destination, none in between | corruption relayed and never contained | c_bad_after_commit on the ingress switch |
| Corrupt data accepted by an application | a bad frame left with a valid FCS | v_bad_passed_intact — the critical failure |
| Runts arriving from a switched path | cut-through relayed a collision fragment | mode = cut-through; fragment-free would reject it |
| Truncated frames on one egress port | ingress slower than egress, rule bypassed | c_underruns and shortest_underrun_at |
| Truncated frames at an unexpected octet | the rate table disagrees with the negotiated link | shortest_underrun_at far from Section 7's prediction |
| Cut-through enabled, latency unchanged | the egress is never idle | CR_EGRESS_BUSY dominating; Section 10 |
Cut-through enabled, c_ct near zero | the rate rule refuses the path | c_refused_rate — access-to-aggregation is a speed step up |
| Latency alternating between 112 ns and 12 µs | the discipline is being re-selected per frame | fallback_active toggling — the latch is missing |
| Burst tolerance fell after enabling cut-through | worst-case buffer reservation | Section 13 — reservation is MAX_FRAME, not actual |
| Every frame reports eligibility pass under cut-through | gate 1 tied off | v_gate1_claimed — it cannot have run |
| Commit at a different octet than documented | a wide datapath commits on a word boundary | v_commit_wrong_octet |
| Latency spread across frame sizes on a cut-through path | the discipline is silently falling back | c_sf non-zero where c_ct was expected |
| An FCS error that cannot be attributed to a cable | it may be a stomped frame from upstream | the upstream switch's c_stomped — the only record |
19. Common Misconceptions
1 — "Cut-through is faster, so it is better."
The wrong model: latency is the metric, cut-through wins on it, therefore it wins.
What it costs: every one of this chapter's trades is invisible. Corrupt frames propagate hop by hop instead of being contained (Section 9). Runts and collision fragments are relayed. A speed step upward forces store-and-forward anyway (Section 7), and buffer must be reserved at the maximum frame size (Section 13), reducing the burst tolerance that absorbs congestion.
The corrected model: cut-through trades error containment and buffer efficiency for a saving of the serialisation term only — and end-to-end latency is serialisation + lookup + queueing. It earns its cost only where the queueing term is engineered to zero, which is a deliberately underloaded storage or trading fabric and nowhere else.
2 — "A cut-through switch checks the FCS, it just checks it later."
The wrong model: the check still happens, so validity is still enforced — merely after a delay.
What it costs: the belief that the frame can still be stopped. It cannot. By the time the FCS arrives, up to 1504 octets have already been transmitted and the head of the frame reached the destination microseconds ago. A check whose result cannot change the outcome is not enforcement.
The corrected model: the check happens and its only remaining use is to mark the frame — Section 11's stomp — so that downstream devices discard it and no downstream cut-through switch relays it further. The switch's ingress FCS counter still increments, which is diagnostically valuable and operationally powerless.
3 — "Enabling cut-through will reduce my network's latency."
The wrong model: the feature applies to traffic generally.
What it costs: disappointment, and a feature enabled for nothing. The rate rule eliminates every upward speed step — host to uplink, access to aggregation, aggregation to core — which is the direction most traffic travels. And Section 10's load argument eliminates the rest: a frame whose egress is busy is queued, and a queued frame has been stored.
The corrected model: measure before enabling. c_refused_rate against c_granted gives the fraction of paths where the rule permits it, and CR_EGRESS_BUSY against c_ct gives the fraction where the egress was actually idle. On a typical hierarchical network both are small, and their product is the fraction of frames that see any benefit at all.
4 — "Fragment-free is a compromise that gets neither benefit."
The wrong model: committing at octet 64 is halfway between the two and therefore half as good as each.
What it costs: dismissing the option that is nearly free. Committing at octet 64 costs 512 ns at 1 Gb/s — which is exactly store-and-forward's latency on a minimum-length frame — and on a 1518-octet frame it is 23.7× faster than store-and-forward.
The corrected model: fragment-free is store-and-forward for small frames and cut-through for large ones, without needing to know which is which in advance. It buys back runt and fragment rejection for a cost that is zero on the frames where store-and-forward would have been cheap anyway. What it cannot buy back is gate 1, because the FCS is at the end regardless of where the commit sits.
5 — "Stomping is a hack."
The wrong model: deliberately corrupting a check sequence is crude, and a better design would avoid needing it.
What it costs: the alternative, which is worse in every respect. Once the frame has committed, the choice is between a corrupt frame that looks valid and a corrupt frame that is unmistakably marked. A design that "avoids needing" stomping is a design that forwards the first kind.
The corrected model: stomping is the correct response to information that arrives after a decision has become irrevocable. It is the mechanism that bounds Section 9's error propagation at one extra hop instead of the whole path, and P18's inversion rather than a constant is what stops it failing once every 48 minutes.
6 — "The FCS could have been put at the front of the frame."
The wrong model: the trailing checksum is a legacy choice, and a modern format would place it where a streaming switch could use it.
What it costs: a misunderstanding of why the constraint exists, which then shows up as an expectation that some future standard will fix it. A checksum covers everything before it. It cannot precede what it protects — a transmitter cannot compute a checksum over data it has not generated yet, and the whole frame must be buffered before transmission if it did.
The corrected model: the trailing position is the only position available in a streamed format, and it is what makes cut-through's trade permanent rather than incidental. Chapter 5.1's field order simultaneously enables cut-through — routing information first — and bounds it, by putting the integrity information last. Both fall out of the same constraint: a field can only be used after it has arrived.
20. Interview Reasoning
Q1 — "Why can a cut-through switch not check the frame check sequence before forwarding?"
Reason through it. Because the check sequence is the last four octets and the commit is at octet 14. On a maximum-length frame the gap is 1518 − 14 = 1504 octets — 12 032 bits, 12.032 µs at 1 Gb/s — and the decision was made at the start of it. The strong answer states that this is not an implementation shortfall: the FCS covers everything before it, so Chapter 5.8's trailing position is the only one a checksum can occupy in a streamed format. No pipeline depth, no faster logic and no additional buffering makes the input arrive sooner. The only way to satisfy the check before committing is to move the commit to the last octet — which is choosing store-and-forward, not fixing cut-through.
Q2 — "A 1 Gb/s port cuts through to a 10 Gb/s port. What happens?"
Reason through it. The transmitter runs out of frame. With a 14-octet head start, t = H ÷ (Re − Ri) = 112 bits ÷ 9 Gb/s = 12.4 ns, by which point the egress has emitted 15.6 octets and the ingress has supplied nothing more. Ethernet has no mechanism for pausing inside a frame — the transmitter must produce a continuous stream from the start delimiter to the FCS — so the frame is truncated on the wire and the receiver discards it as a runt or an FCS error. The strong answer generalises the rule as egress ≤ ingress and names its operational consequence: every upward speed step is excluded, which is host to uplink, access to aggregation and aggregation to core — the direction most traffic in a hierarchical network travels.
Q3 — "Your cut-through switch discovers a bad FCS after 1500 octets have left. What should it do, and what must it not do?"
Reason through it. It cannot recall the frame, so it must mark it: deliberately corrupt the outgoing check sequence so that every downstream device discards it and no downstream cut-through switch relays it further. What it must not do is emit the correct FCS, which would deliver a corrupt payload wearing a valid integrity check — accepted as data, by an application, silently. The strong answer adds the implementation detail that matters: the stomp must invert the computed value rather than write a constant, because a constant will coincide with some frame's correct FCS once in 2³² frames — once every 48 minutes on a fully loaded gigabit link — and on that frame the mechanism silently does the opposite of its purpose.
Q4 — "Cut-through was enabled and end-to-end latency did not improve. Explain."
Reason through it. Two independent reasons, and both are usually present. The rate rule eliminates every path whose egress is faster than its ingress, which in a hierarchical network is most of them — c_refused_rate against c_granted measures it. And Section 10's load argument eliminates the rest: cut-through's saving is available only when the egress is idle at the commit point, and a frame whose egress is busy is queued — a queued frame has been stored, so the switch performed store-and-forward regardless of configuration. The strong answer closes with the shape that makes this a niche feature: end-to-end latency is serialisation + lookup + queueing, cut-through attacks only the first term, and the third term dominates exactly under the load where latency is the complaint.
Q5 — "What does cut-through cost in buffer, and why is that counter-intuitive?"
Reason through it. A store-and-forward egress buffers what it received, which is the frame's actual length. A cut-through egress must reserve MAX_FRAME at the commit point, because the length is not known — the frame is still arriving — and the transmission cannot be stopped once started. With jumbo frames enabled that is 9000 octets per in-flight frame, against a real distribution in which most frames are minimum-length. The strong answer names why this is counter-intuitive: the feature was enabled to reduce latency, and the reservation comes directly out of the buffer that Chapter 12.1 §6 showed absorbs oversubscription bursts — so a switch configured for minimum latency has quietly reduced its tolerance for the congestion that causes latency.
Q6 — "A building's hosts all report FCS errors. The switches report healthy links. Where do you look?"
Reason through it. At the first-hop switch's ingress counters, not at the hosts. If the path is cut-through, a single bad cable's corrupt frames are relayed rather than contained — the first switch commits at octet 14, discovers the bad FCS 12 µs later, and the frame is already gone. Every downstream device discards it and increments its own FCS counter, so the symptom appears everywhere and the cause appears nowhere. The strong answer names the two numbers that close it: the first-hop switch's ingress FCS error counter — which increments even though the frame was forwarded — and c_committed_unchecked, which says frames from that port are being relayed without validation. Together they distinguish a relay from a source, and they sit on the one switch nobody is looking at because it is not complaining.
21. Understanding Check
22. What's Next
Module 12 is complete. Six chapters built a switch: the per-frame decision, learning, the six gates, flooding, the table, and now the commit point that decides which gates can run at all.
And the module ends on a limit it cannot pass. Chapter 12.4 §10 derived that a broadcast domain tops out around 200 stations — not for any reason inside the switch, but because every station must process every broadcast and no hardware filter can discard one. Section 12 of that chapter showed storm control cannot hold a domain inside that budget: the setting that would is 43.5 frames per second per port, 0.00292% of line rate, finer than the hardware expresses.
The only variable left is N — how many stations can hear one another.
Chapter 13.1 — Why VLANs Exist takes that up. It is not a chapter about a tag; it is about what happens to everything Module 12 built when one physical switch must behave as several independent ones — and what a switch that kept one table, one flood mask and one port state must now keep several of.
Then Chapter 13.2 — The 802.1Q Tag examines the four octets that carry the distinction, and what inserting them in the middle of a frame does to every field offset after it — including Chapter 5.5's length/type resolution, which this chapter's Section 12 already needed.
Continue learning
Related tutorials
- Related topic
Serialization Delay
A frame's size divided by the line rate — trivial arithmetic whose significance changes by four orders of magnitude, inverting latency budgets so that at 100 Gb/s one metre of cable outweighs an entire minimum-size frame.
- Related topic
One Frame, End to End
A frame's journey down the stack and back up the other side, stage by stage. The transmit path decides and the receive path must discover — at four layers, not one — and that asymmetry is why the receive half of every Ethernet design is the larger, later and buggier one.
- Related topic
Forward Error Correction
FEC converts a gradual degradation into a cliff and hides the gradient behind it. The pre-correction error rate is the link's health metric and gives months of warning; the corrected output reads zero until the moment it collapses, and can be silently wrong when a decoder miscorrects.
- Related topic
The Receive Path
A transmitter assembles from settled quantities; a receiver discovers, and every decision stays provisional until the check sequence at the very end — so the pipeline either waits for the verdict or acquires the ability to withdraw what it has already delivered.
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.
