Ethernet · Module 5
Frame Format Overview
Every field exists to let a receiver make one decision at one moment, and the field order is the order those decisions must be made. The check value comes last because it covers everything before it — which makes every decision taken before it provisional.
Chapter 2.5 listed the MAC's six responsibilities and explained why each exists: the medium is shared, or it is unreliable. That argument accounts for which fields a frame needs.
It does not account for the order they appear in, and the order is not arbitrary.
The usual presentation is a byte layout — destination address at offset 0, source at 6, type at 12 — read left to right like a struct definition. That presentation is accurate and it teaches almost nothing, because it invites the assumption that the ordering is a convention someone chose and could have chosen differently.
It could not have been chosen differently, and the reason is that a receiver has no choice about the order it makes decisions in.
A receiver cannot ask "is this frame for me?" before it knows where the frame begins. It cannot interpret a payload before it knows what kind of payload it is. And it cannot trust any of those answers until it has seen the check value — which, because the check value covers every octet before it, must be last.
What decision does each field drive, why must they be made in that order, and what follows from the check value coming last?
1. Scope — What This Chapter Owns
This chapter owns: the organising principle — every field grouped by the decision it drives — and the argument that the order is a dependency order rather than a convention; the complete layout as a consequence of that argument; the fact that field positions are not fixed once tagging exists; a streaming parser that emits field boundaries rather than a struct; and the provisional-decision problem the check value's position creates.
This chapter does not own and does not restate:
- Why each field exists as a MAC responsibility — Chapter 2.5 owns that, and this chapter takes its six responsibilities as given.
- The preamble's internal structure — Chapter 5.2.
- The address's internal structure — Chapter 5.3 — or what its types mean — Chapter 5.4.
- The length/type ambiguity and its resolution — Chapter 5.5. This chapter uses the field as one decision point and does not resolve it.
- Padding, maximum size and the check value's algebra — Chapters 5.6, 5.7 and 5.8.
The question this chapter answers that its neighbours do not: why is the frame in this order, and what does the order cost?
2. Why the Order Is a Dependency Order
Take the decisions a receiver must make and ask, for each, what it needs to already know.
"Where does this frame begin?" depends on nothing. It cannot depend on anything, because every other question presupposes an answer to it. So it is first.
"Is this frame for me?" depends only on knowing where the frame began. It does not need the source, the type, or the payload. So it can be second — and it should be second, because it is the question whose "no" answer saves the most work.
"Who sent this?" also depends only on the frame's start. It could have come before the destination. It does not, and that is the one genuinely discretionary choice in the layout — the standard put the destination first because abandoning early is worth more than learning early.
"What is this payload?" depends on the frame's start. It must precede the payload, because a payload you cannot classify is a payload you cannot hand to anyone.
"Is any of this trustworthy?" depends on everything before it. That is not a preference; a check value computed over the whole frame cannot be transmitted until the whole frame has been transmitted. It is last by construction.
3. The Layout, as a Consequence
The exact field sizes, since they are small enough to state completely:
| Group | Field | Octets |
|---|---|---|
| framing | preamble | 7 |
| framing | start frame delimiter | 1 |
| addressing | destination address | 6 |
| addressing | source address | 6 |
| classification | length or type | 2 |
| payload | payload | 46 to 1500 |
| verification | frame check sequence | 4 |
The preamble and delimiter are not part of the frame the MAC deals with. Chapter 4.3 §4 established that they are generated below the MAC and consumed below it, so the frame the MAC counts runs from the destination address to the check value — 64 octets minimum, 1,518 maximum in the untagged case.
Two numbers worth having exactly, because later chapters depend on them:
minimum frame = 6 + 6 + 2 + 46 + 4 = 64 octets
maximum frame = 6 + 6 + 2 + 1500 + 4 = 1518 octetsChapter 5.6 owns why the payload has a floor of 46 and Chapter 5.7 owns the ceiling of 1,500. Here they are two ends of one field.
4. The Positions Are Not Fixed
Everything above describes an untagged frame, and it is the description that gets memorised. It is also the description that produces one of the most common parser bugs in networking hardware.
A VLAN tag is 4 octets inserted between the source address and the length/type field. Its first two octets carry a tag protocol identifier of 0x8100, and the remaining two carry a priority field of 3 bits, a drop-eligible indicator of 1 bit, and a 12-bit VLAN identifier.
Which means the length/type field is not at offset 12. It is at offset 12 on an untagged frame and at offset 16 on a tagged one — and a parser that hard-codes 12 reads the tag's identifier as though it were a type.
And it nests. Double tagging places an outer service tag with identifier 0x88a8 before an inner customer tag of 0x8100, so the length/type field moves to offset 20. Deeper stacking is possible.
The maximum frame size moves with it: 1,518 octets untagged becomes 1,522 with one tag.
Conceptual — where the classification field lands
10 cycles5. RTL 1 — Field Positions Are Computed, Not Looked Up
// SYNTHESIZABLE. Field offsets derived from tag depth.
//
// A field-offset TABLE is the natural way to document a frame and it encodes
// an assumption that is false the moment a VLAN tag exists. This computes
// instead, from what has already been observed.
//
// The offsets that move, and by how much:
//
// tags destination source classification payload
// ---- ----------- ------ -------------- -------
// 0 0 6 12 14
// 1 0 6 16 18
// 2 0 6 20 22
//
// Destination and source NEVER move -- they precede the insertion point --
// which is why Section 2's early-abandon argument survives tagging intact.
package frame_pkg;
localparam int unsigned OCT_DA = 6;
localparam int unsigned OCT_SA = 6;
localparam int unsigned OCT_CLASS = 2;
localparam int unsigned OCT_FCS = 4;
localparam int unsigned OCT_TAG = 4;
// Tag protocol identifiers, as published. A parser recognises these to
// know a tag is present; it does not need to interpret the rest of it.
localparam logic [15:0] TPID_C_TAG = 16'h8100; // customer tag
localparam logic [15:0] TPID_S_TAG = 16'h88A8; // service tag, outer
// The five decision groups of Section 3, as a parser state.
typedef enum logic [2:0] {
FLD_DA,
FLD_SA,
FLD_TAG,
FLD_CLASS,
FLD_PAYLOAD,
FLD_FCS,
FLD_DONE
} field_e;
endpackage
module frame_field_positions
import frame_pkg::*;
#(
// Maximum tag depth this parser will follow. A BOUND is mandatory:
// Section 4 showed tags nest, and a walker with no bound can be made to
// walk forever by a crafted frame. Section 7 develops that.
parameter int unsigned MAX_TAGS = 2
) (
input logic [$clog2(MAX_TAGS+1)-1:0] tag_count,
output int unsigned off_da,
output int unsigned off_sa,
output int unsigned off_class,
output int unsigned off_payload,
// Frame sizes for this tag depth. Both move with tagging, and a design
// that checks size against an untagged constant rejects legal frames.
output int unsigned min_frame_octets,
output int unsigned max_frame_octets
);
// Destination and source are before the insertion point, so they are
// constant. Everything after it shifts by four octets per tag.
assign off_da = 0;
assign off_sa = OCT_DA;
assign off_class = OCT_DA + OCT_SA + (tag_count * OCT_TAG);
assign off_payload = off_class + OCT_CLASS;
// 64 and 1518 untagged; 68 and 1522 with one tag. A design that hard-codes
// 1518 drops legal tagged frames and reports them as oversized, which
// sends the investigation to the far end rather than to itself.
assign min_frame_octets = 64 + (tag_count * OCT_TAG);
assign max_frame_octets = 1518 + (tag_count * OCT_TAG);
endmoduleClassification: synthesizable.
What it teaches: that destination and source never move and everything after the insertion point does. That asymmetry is why Section 2's early-abandon argument survives tagging completely — the address filter of Chapter 2.7 §4 can still decide at octet six regardless of how many tags follow.
Deliberately simplified: it takes tag_count as an input rather than discovering it. Section 7's walker discovers it, and separating the two is deliberate: discovery has a termination problem and position arithmetic does not.
Production implication: max_frame_octets moving with tag depth is the second half of the same trap. A design that checks length against a hard-coded 1,518 rejects legal tagged frames and reports them as oversized — and an oversized-frame report points at the transmitter, so the investigation goes to the far end while the fault is local.
Later ownership: what the classification field means once located is Chapter 5.5; the size limits themselves are Chapters 5.6 and 5.7.
6. RTL 2 — A Streaming Parser That Emits Boundaries
A parser that produces a struct has already decided the frame is well-formed. A streaming parser cannot afford that assumption, because it is producing output while the frame is still arriving — and the frame may stop at any octet.
// SYNTHESIZABLE. Streaming frame parser.
//
// IT EMITS BOUNDARIES, NOT A STRUCT, and the difference is architectural.
//
// A struct is only available once the whole frame has arrived. Producing one
// therefore requires buffering the frame and defeats the early abandonment
// Section 2 showed the field ORDER exists to enable. A parser that hands the
// address filter a struct has thrown away the reason the address is second.
//
// So this emits a pulse at each field boundary with the field's identity,
// and a consumer acts when the field it cares about completes.
module frame_stream_parser
import frame_pkg::*;
#(
parameter int unsigned MAX_TAGS = 2,
parameter int unsigned MAX_FRAME = 1522 + 8,
parameter int unsigned CNT_W = $clog2(MAX_FRAME + 1)
) (
input logic clk,
input logic rst_n,
input logic in_valid,
input logic [7:0] in_octet,
input logic in_sof, // from the layer below: a frame starts here
input logic in_eof,
// Boundary events. One pulse as each field completes, naming the field.
output logic field_done,
output field_e field_id,
output logic [CNT_W-1:0] field_end_offset,
// The current field, continuously, so a consumer can capture octets as
// they pass rather than waiting for a completion pulse.
output field_e current_field,
output logic [CNT_W-1:0] octet_index,
// Discovered tag depth. An OUTPUT rather than a parameter, because it is
// a property of the frame in flight.
output logic [$clog2(MAX_TAGS+2)-1:0] tags_seen
);
field_e state_q;
logic [CNT_W-1:0] idx_q;
logic [CNT_W-1:0] field_start_q;
logic [15:0] class_shift_q;
logic [$clog2(MAX_TAGS+2)-1:0] tags_q;
// A candidate classification field is complete when two octets have been
// gathered. Whether it is a TAG identifier or the real classification is
// the decision below.
wire [15:0] candidate_c = {class_shift_q[7:0], in_octet};
wire is_tag_c = (candidate_c == TPID_C_TAG) || (candidate_c == TPID_S_TAG);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= FLD_DA;
idx_q <= '0;
field_start_q <= '0;
class_shift_q <= '0;
tags_q <= '0;
field_done <= 1'b0;
field_id <= FLD_DA;
end else begin
field_done <= 1'b0;
if (in_valid && in_sof) begin
state_q <= FLD_DA;
idx_q <= '0;
field_start_q <= '0;
tags_q <= '0;
end else if (in_valid) begin
idx_q <= idx_q + 1'b1;
unique case (state_q)
FLD_DA:
if (idx_q == CNT_W'(OCT_DA - 1)) begin
field_done <= 1'b1;
field_id <= FLD_DA;
state_q <= FLD_SA;
// The earliest abandon point. A consumer acts HERE.
end
FLD_SA:
if (idx_q == CNT_W'(OCT_DA + OCT_SA - 1)) begin
field_done <= 1'b1;
field_id <= FLD_SA;
state_q <= FLD_CLASS;
class_shift_q <= '0;
end
FLD_CLASS: begin
class_shift_q <= {class_shift_q[7:0], in_octet};
// Two octets gathered: decide whether this is a tag or the
// classification field itself.
if (idx_q[0] != field_start_q[0]) begin
if (is_tag_c && (tags_q < ($clog2(MAX_TAGS+2))'(MAX_TAGS))) begin
// A tag. Consume its remaining two octets and look again.
tags_q <= tags_q + 1'b1;
field_done <= 1'b1;
field_id <= FLD_TAG;
state_q <= FLD_TAG;
end else begin
// Not a tag, or the depth bound is reached. This is the
// classification field -- Chapter 5.5 owns what it means.
field_done <= 1'b1;
field_id <= FLD_CLASS;
state_q <= FLD_PAYLOAD;
end
end
end
FLD_TAG:
// The tag's two control octets, then back to look for another.
if (idx_q[0] == field_start_q[0]) begin
state_q <= FLD_CLASS;
class_shift_q <= '0;
end
FLD_PAYLOAD:
if (in_eof) begin
field_done <= 1'b1;
field_id <= FLD_PAYLOAD;
state_q <= FLD_DONE;
end
default: ;
endcase
end
end
end
assign current_field = state_q;
assign octet_index = idx_q;
assign field_end_offset = idx_q;
assign tags_seen = tags_q;
endmoduleClassification: synthesizable.
What it teaches: that a streaming parser must discover tag depth rather than be told it, and that discovery is a loop — read two octets, decide whether they are a tag identifier, and if so consume two more and look again. MAX_TAGS bounds that loop, and the bound is not optional.
Deliberately simplified: it assumes the layer below marks start and end of frame, which is Chapter 4.3's interface, and it does not extract field contents — only boundaries. A production parser captures the addresses as they pass.
Production implication: emitting boundary events rather than a struct is what preserves the early abandonment the field order exists for. A parser that assembles a struct has forced the receive path to buffer the whole frame before the address filter can act — which reverses the entire argument of Section 2 and costs buffering on every frame, most of which are not for this station.
Later ownership: what the classification field means is Chapter 5.5; the address filtering that consumes the destination boundary is Chapter 2.7 §4.
7. RTL 3 — Where the Parser Stopped
This module is this chapter's contribution to the track's observability discipline, and it is a small idea with a large effect.
// SYNTHESIZABLE INSTRUMENTATION.
//
// THE IDEA: a frame that fails to parse should say WHICH FIELD it was in,
// not merely that it failed.
//
// The reason this is worth silicon is that each field is produced by a
// different part of the transmitter, so the stopping field names a
// subsystem:
//
// stopped in DA/SA -> truncation very early: a PHY or link fault
// stopped in CLASS -> a tag walk that ran out of depth, or a truncation
// at exactly the wrong place
// stopped in PAYLOAD -> a MAC-side underrun (Chapter 4.3 §5's abort path)
// stopped in FCS -> the frame ended a few octets short: a length or
// padding fault, not a link fault
//
// A single "malformed frame" counter conflates four different owners.
module parse_stop_observer
import frame_pkg::*;
#(
parameter int unsigned CNT_W = 24,
parameter int unsigned OFF_W = 12
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_ended, // end of frame, from the layer below
input logic parse_complete, // the parser reached FLD_DONE cleanly
input field_e current_field,
input logic [OFF_W-1:0] octet_index,
// Per-field stop counts. The whole point of the module.
output logic [CNT_W-1:0] c_stopped_in [7],
// The field the FIRST failure stopped in, and how far into the frame.
// Held, because by the time anyone looks the counters will all have moved
// and only ordering distinguishes a cause from its consequences.
output field_e first_stop_field,
output logic [OFF_W-1:0] first_stop_offset,
output logic first_stop_valid,
// Frames that parsed cleanly. The denominator -- a stop count without it
// cannot distinguish a broken link from a busy one.
output logic [CNT_W-1:0] c_parsed_ok
);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
input logic en);
bump = (en && !(&v)) ? (v + 1'b1) : v; // saturating, never wrapping
endfunction
wire stopped_early_c = frame_ended && !parse_complete;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int unsigned f = 0; f < 7; f++) c_stopped_in[f] <= '0;
c_parsed_ok <= '0;
first_stop_field <= FLD_DA;
first_stop_offset <= '0;
first_stop_valid <= 1'b0;
end else begin
if (clear) begin
for (int unsigned f = 0; f < 7; f++) c_stopped_in[f] <= '0;
c_parsed_ok <= '0;
// first_stop deliberately survives: it is the record of what went
// wrong first, and clearing counters must not erase it.
end else begin
if (stopped_early_c)
c_stopped_in[current_field] <= bump(c_stopped_in[current_field], 1'b1);
c_parsed_ok <= bump(c_parsed_ok, frame_ended && parse_complete);
end
if (stopped_early_c && !first_stop_valid) begin
first_stop_valid <= 1'b1;
first_stop_field <= current_field;
first_stop_offset <= octet_index;
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that a per-field stop count names an owner where a total does not. Four failure classes that a single "malformed frame" counter conflates become four distinct readings — and Section 15's debugging method is built entirely on that table.
Deliberately simplified: no correlation with the check-value result, which a production design wants because a frame that stopped early and failed its check value is a different story from one that stopped early with a valid check value over what arrived.
Production implication: c_parsed_ok is the denominator and it is routinely omitted. A stop count of four hundred means nothing without knowing whether that is out of five hundred frames or five hundred million — and a rate is what separates a broken link from a busy one.
And first_stop_offset matters as much as the field. A frame that stopped at octet 4 and one that stopped at octet 5 are both "in the destination address", but the first suggests the frame barely started and the second suggests it very nearly cleared the field — different faults, and only the offset distinguishes them.
8. RTL 4 — Malformed Is Not One Thing
// SYNTHESIZABLE. Malformed-frame classification, as a vector.
//
// WHY A VECTOR AND NOT AN ENUM. Chapter 2.7 §7's forwarding disposition was
// one-hot because a frame goes to exactly one place. Malformation is not
// like that: a frame can be undersized AND have an unparseable tag stack
// AND fail its check value, and each of those points somewhere different.
//
// Collapsing them to one enum forces a priority that discards evidence, and
// the discarded evidence is usually the part that names the cause.
module malformed_classifier
import frame_pkg::*;
#(
parameter int unsigned CNT_W = 24,
parameter int unsigned OFF_W = 12
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_ended,
input logic [OFF_W-1:0] frame_octets, // excluding preamble and SFD
input logic [OFF_W-1:0] min_frame_octets, // from Section 5, tag-aware
input logic [OFF_W-1:0] max_frame_octets,
input logic parse_complete,
input logic tag_depth_exceeded,
input logic fcs_ok,
// Independent, and several may be set on one frame.
output logic m_undersized,
output logic m_oversized,
output logic m_unparseable,
output logic m_tag_overflow,
output logic m_bad_fcs,
output logic m_any,
output logic [CNT_W-1:0] c_undersized,
output logic [CNT_W-1:0] c_oversized,
output logic [CNT_W-1:0] c_unparseable,
output logic [CNT_W-1:0] c_tag_overflow,
output logic [CNT_W-1:0] c_bad_fcs,
// Frames with MORE THAN ONE fault. Rare on a healthy link, and a rising
// count means the faults share a cause rather than being independent --
// which is a different investigation from any single count rising.
output logic [CNT_W-1:0] c_multi_fault
);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
input logic en);
bump = (en && !(&v)) ? (v + 1'b1) : v;
endfunction
always_comb begin
// Size is checked against the TAG-AWARE limits of Section 5. Checking
// against a hard-coded 1518 reports legal tagged frames as oversized.
m_undersized = frame_ended && (frame_octets < min_frame_octets);
m_oversized = frame_ended && (frame_octets > max_frame_octets);
m_unparseable = frame_ended && !parse_complete;
m_tag_overflow = frame_ended && tag_depth_exceeded;
m_bad_fcs = frame_ended && !fcs_ok;
m_any = m_undersized || m_oversized || m_unparseable
|| m_tag_overflow || m_bad_fcs;
end
logic [2:0] fault_count_c;
always_comb begin
fault_count_c = 3'(m_undersized) + 3'(m_oversized) + 3'(m_unparseable)
+ 3'(m_tag_overflow) + 3'(m_bad_fcs);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
c_undersized <= '0;
c_oversized <= '0;
c_unparseable <= '0;
c_tag_overflow <= '0;
c_bad_fcs <= '0;
c_multi_fault <= '0;
end else begin
c_undersized <= bump(c_undersized, m_undersized);
c_oversized <= bump(c_oversized, m_oversized);
c_unparseable <= bump(c_unparseable, m_unparseable);
c_tag_overflow <= bump(c_tag_overflow, m_tag_overflow);
c_bad_fcs <= bump(c_bad_fcs, m_bad_fcs);
c_multi_fault <= bump(c_multi_fault, frame_ended && (fault_count_c > 3'd1));
end
end
endmoduleClassification: synthesizable.
What it teaches: that malformation is a vector, not an enum, and that this is the right departure from Chapter 2.7 §7's one-hot disposition. A frame goes to exactly one place, so a disposition is one-hot. A frame can be wrong in several ways at once, so a classification is not.
Deliberately simplified: it takes fcs_ok as an input, since Chapter 5.8 owns the check value.
Production implication: c_multi_fault is the counter that changes an investigation's shape. On a healthy link the fault classes are essentially independent and coincidence is rare. A rising multi-fault count means they share a cause — usually severe corruption that trips several checks at once, which points at the physical layer rather than at any of the individual classes. Without it, five independent counters rising together look like five independent problems.
9. Every Decision Before the Check Value Is Provisional
Section 2 established that the check value is last by construction. This is what that costs.
A receiver acts on the destination address at octet six. It learns whether that address was corrupt at octet 64 at the earliest, and up to octet 1,518 at the latest.
Every decision in between is made on unverified data:
| Decision | Made at octet | Verified at octet | Provisional for |
|---|---|---|---|
| is this frame mine? | 6 | 64 to 1518 | 58 to 1512 octets |
| which lane or queue? | 6 | 64 to 1518 | 58 to 1512 octets |
| who sent this? | 12 | 64 to 1518 | 52 to 1506 octets |
| what is the payload? | 14 or later | 64 to 1518 | 50 to 1504 octets |
This is not a defect and it cannot be designed away. A check that covers the whole frame cannot arrive before the whole frame. The alternative — a check value per field — would multiply overhead and still leave the payload unverified until its own check arrived.
What a design must do instead is decide, per decision, whether to act provisionally or wait.
10. RTL 5 — Provisional Decisions, Tracked
// SYNTHESIZABLE. Provisional-decision tracking.
//
// Section 9: every decision before the check value is made on unverified
// data. That is unavoidable. What is avoidable is not KNOWING which
// decisions were provisional when the check value fails.
//
// This records them and, on a failure, enumerates what must be undone --
// and separately counts how often an EXTERNALLY VISIBLE action was taken
// provisionally, because that is the number that decides whether
// cut-through is safe for a given deployment.
module provisional_tracker #(
parameter int unsigned CNT_W = 24
) (
input logic clk,
input logic rst_n,
input logic clear,
// Decisions, as they are taken. Each pulses once per frame at most.
input logic took_address_decision,
input logic took_queue_decision,
input logic took_learning_decision,
input logic took_forward_decision, // EXTERNALLY VISIBLE -- cannot be undone
input logic frame_ended,
input logic fcs_ok,
// What was provisional on this frame, latched until the verdict.
output logic [3:0] provisional_mask,
// On a failure: what must be undone, and whether anything cannot be.
output logic undo_required,
output logic [3:0] undo_mask,
output logic irrevocable, // a forward already happened
output logic [CNT_W-1:0] c_frames,
output logic [CNT_W-1:0] c_fcs_fail,
// Failures where an irrevocable action had already been taken. THIS is
// the number that decides whether cut-through is acceptable here.
output logic [CNT_W-1:0] c_irrevocable_on_fail,
// Longest provisional window in octets, from the first decision to the
// verdict. Sizing an undo queue from the average rather than this is the
// same trap Chapter 4.3 §10 named for burst depth.
input logic [11:0] octet_index,
output logic [11:0] longest_provisional_window
);
logic [3:0] mask_q;
logic [11:0] first_decision_at_q;
logic window_open_q;
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
input logic en);
bump = (en && !(&v)) ? (v + 1'b1) : v;
endfunction
wire any_decision_c = took_address_decision || took_queue_decision
|| took_learning_decision || took_forward_decision;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mask_q <= '0;
first_decision_at_q <= '0;
window_open_q <= 1'b0;
c_frames <= '0;
c_fcs_fail <= '0;
c_irrevocable_on_fail <= '0;
longest_provisional_window <= '0;
end else begin
if (clear) begin
c_frames <= '0;
c_fcs_fail <= '0;
c_irrevocable_on_fail <= '0;
// longest_provisional_window deliberately survives.
end
// Open the window on the first decision of a frame.
if (any_decision_c && !window_open_q) begin
window_open_q <= 1'b1;
first_decision_at_q <= octet_index;
end
if (took_address_decision) mask_q[0] <= 1'b1;
if (took_queue_decision) mask_q[1] <= 1'b1;
if (took_learning_decision) mask_q[2] <= 1'b1;
if (took_forward_decision) mask_q[3] <= 1'b1;
if (frame_ended) begin
c_frames <= bump(c_frames, 1'b1);
if (!fcs_ok) begin
c_fcs_fail <= bump(c_fcs_fail, 1'b1);
// A forward already happened and cannot be recalled.
c_irrevocable_on_fail <= bump(c_irrevocable_on_fail, mask_q[3]);
end
if (window_open_q) begin
automatic logic [11:0] w = octet_index - first_decision_at_q;
if (w > longest_provisional_window) longest_provisional_window <= w;
end
mask_q <= '0;
window_open_q <= 1'b0;
end
end
end
assign provisional_mask = mask_q;
assign undo_required = frame_ended && !fcs_ok && (mask_q != '0);
assign undo_mask = undo_required ? mask_q : 4'd0;
assign irrevocable = frame_ended && !fcs_ok && mask_q[3];
endmoduleClassification: synthesizable.
What it teaches: that the distinction between decisions is revocability, not cost. Withdrawing a queue selection is bookkeeping. Withdrawing a frame that has already left a port is impossible, and irrevocable names exactly that case.
Deliberately simplified: four decision types and no undo mechanism — this records what needs undoing, and the undoing lives with whichever block took the decision.
Production implication: c_irrevocable_on_fail is the number that decides whether cut-through is acceptable for a deployment, and it is measurable rather than estimable. A link whose check-value failure rate is negligible can forward provisionally with confidence. One where this counter moves is propagating corrupted frames onward, and Chapter 2.7 §5's error-containment argument says a store-and-forward switch would not have.
And longest_provisional_window is the sizing number, not the average. This is the same average-versus-worst-case trap Chapter 4.3 §10 named for burst depth and Chapter 4.2 §8 for escape runs: an undo queue sized from typical frames overflows on a maximum-length one.
11. Assertions
Some properties below rest on published field widths and the frame's structure, which IEEE 802.3 defines. The parser's state machine, its tag-depth bound and its telemetry are implementation choices, and each property says which it is.
// ─── FRAME PROPERTY: addresses never move ──────────────────────────────────
// Destination and source precede the tag insertion point, so their offsets
// are constant at every tag depth. This is what makes Section 2's early
// abandonment survive tagging, and a design that recomputes them is either
// wrong or doing needless work.
property p_addresses_do_not_move;
@(posedge clk) disable iff (!rst_n)
(off_da == 0) && (off_sa == 6);
endproperty
// ─── FRAME PROPERTY: classification shifts four octets per tag ─────────────
// Catches a hard-coded offset 12, which reads a tag identifier as a type on
// every tagged frame -- and 0x8100 looks like an unrecognised protocol
// rather than an error, so nothing reports a failure.
property p_class_offset_tracks_tags;
@(posedge clk) disable iff (!rst_n)
(off_class == 12 + (tag_count * 4));
endproperty
// ─── FRAME PROPERTY: the size limits move with tagging ─────────────────────
// Catches a length check against a hard-coded 1518, which rejects legal
// tagged frames and reports them as oversized -- pointing the investigation
// at the transmitter for a fault in the receiver.
property p_size_limits_track_tags;
@(posedge clk) disable iff (!rst_n)
(max_frame_octets == 1518 + (tag_count * 4))
&& (min_frame_octets == 64 + (tag_count * 4));
endproperty
// ─── Ordering: fields complete in dependency order ─────────────────────────
// Section 2's argument, asserted. Catches a parser that can report a
// classification boundary before an address boundary, which means its state
// machine has a path the frame format does not have.
property p_field_order_is_monotone;
@(posedge clk) disable iff (!rst_n)
(field_done && (field_id == FLD_CLASS)) |-> $past(current_field != FLD_DA);
endproperty
// ─── Safety: the destination boundary is at octet six ──────────────────────
// The earliest abandon point. Catches a parser that reports it late, which
// silently costs the receive path the early abandonment the field order
// exists to provide.
property p_da_boundary_at_six;
@(posedge clk) disable iff (!rst_n)
(field_done && (field_id == FLD_DA)) |-> (field_end_offset == 5);
endproperty
// ─── Bounded response: the tag walk terminates ─────────────────────────────
// Tags nest, so a walker without a bound can be made to walk forever by a
// crafted frame. Catches a missing bound, which is a denial-of-service
// surface rather than merely a bug.
property p_tag_walk_bounded;
@(posedge clk) disable iff (!rst_n)
(tags_seen <= MAX_TAGS);
endproperty
// ─── Causation: a stop is recorded against the field it stopped in ─────────
// Catches telemetry that records a stop against the wrong field, which
// names the wrong subsystem with apparent authority.
property p_stop_recorded_correctly;
@(posedge clk) disable iff (!rst_n)
(frame_ended && !parse_complete)
|=> (c_stopped_in[$past(current_field)] == $past(c_stopped_in[$past(current_field)]) + 1);
endproperty
// ─── Conservation: every ended frame is counted once ───────────────────────
// Catches a frame counted as neither parsed nor stopped, which makes the
// rate that Section 7 depends on incomputable.
property p_every_frame_classified;
@(posedge clk) disable iff (!rst_n)
frame_ended |-> (parse_complete ^ (current_field != FLD_DONE));
endproperty
// ─── Safety: malformation is a vector, not an enum ─────────────────────────
// Catches a classifier that forces one-hot, which discards evidence -- and
// the discarded evidence is usually the part that names the cause.
property p_malformation_may_be_multiple;
@(posedge clk) disable iff (!rst_n)
(m_undersized && m_bad_fcs) |-> m_any;
endproperty
// ─── Causation: size checks use the tag-aware limits ───────────────────────
// Catches a size check wired to a constant rather than to Section 5's
// output, which is the same fault as the offset table one layer up.
property p_size_check_uses_computed_limits;
@(posedge clk) disable iff (!rst_n)
m_oversized |-> (frame_octets > max_frame_octets);
endproperty
// ─── Safety: an irrevocable action is flagged on failure ───────────────────
// Catches a design that treats a forwarded frame as undoable, which it is
// not -- and the flag is what tells an operator the corruption left the box.
property p_irrevocable_flagged;
@(posedge clk) disable iff (!rst_n)
(frame_ended && !fcs_ok && provisional_mask[3]) |-> irrevocable;
endproperty
// ─── Conservation: the undo mask matches what was taken ────────────────────
// Catches an undo list that omits a decision, leaving state behind after a
// failed frame -- which then corrupts the NEXT frame's handling.
property p_undo_mask_complete;
@(posedge clk) disable iff (!rst_n)
undo_required |-> (undo_mask == provisional_mask);
endproperty
// ─── Stability: the first stop survives a clear ────────────────────────────
// Catches it folded into the clear branch, destroying the record of what
// went wrong first when everything else has moved.
property p_first_stop_survives_clear;
@(posedge clk) disable iff (!rst_n)
(clear && first_stop_valid) |=> first_stop_valid;
endproperty
// ─── Stability: the provisional window records the maximum ─────────────────
// Catches an average creeping in where a maximum belongs, which undersizes
// an undo queue for exactly the frames that need it most.
property p_window_is_maximum;
@(posedge clk) disable iff (!rst_n)
1'b1 |=> (longest_provisional_window >= $past(longest_provisional_window));
endproperty12. Verification
Scenarios
- An untagged frame, minimum size. Verify every field boundary fires at the offset Section 5 computes, and that the frame is classified as well-formed at exactly 64 octets.
- An untagged frame, maximum size. The same at 1,518 octets. Verify it is not flagged oversized.
- A frame one octet below the minimum and one above the maximum. Two runs. Verify
m_undersizedandm_oversizedrespectively, and that neither fires on the legal boundary cases above. - A singly-tagged frame. Verify the classification boundary moves to offset 16, the address boundaries do not move, and the size limits become 68 and 1,522.
- A doubly-tagged frame. Verify the classification boundary reaches offset 20 and
tags_seenreads two. - A tagged frame at exactly 1,522 octets. Verify it is not oversized. This is the case a hard-coded 1,518 limit rejects, and it is legal traffic.
- A frame with more tags than
MAX_TAGS. Verify the walk stops at the bound,tag_depth_exceededasserts, and the parser does not hang. - A payload beginning with
0x8100. Verify it is not treated as a tag — the walk has already ended by then, and a parser that re-enters tag detection inside the payload will mis-parse ordinary data. - Truncation inside each field. Six runs, one per field. Verify
first_stop_fieldnames the right field each time andfirst_stop_offsetgives the octet. - Truncation at octet 5 and at octet 6. Both stop "in the destination address"; verify the offsets distinguish them, since one barely started and the other nearly cleared the field.
- A frame that is both undersized and fails its check value. Verify both flags set and
c_multi_faultadvances — this is the vector-not-enum property. - A frame that is oversized, unparseable and fails its check value. Verify all three, and that no priority is applied that hides two of them.
- The parse-ok denominator. Run a mixed stream and verify
c_parsed_okplus the stop counts equals the frames presented. clearduring operation. Verify counters zero andfirst_stop_validandlongest_provisional_windowdo not.- A provisional address decision on a frame that then fails. Verify
undo_required,undo_masknaming exactly that decision, andirrevocablelow. - A provisional forward on a frame that then fails. Verify
irrevocablehigh andc_irrevocable_on_failadvancing — the corruption left the box. - The provisional window on a minimum and a maximum frame. Two runs. Verify
longest_provisional_windowrecords the maximum-frame case, not an average of the two. - A frame with no decisions taken before failure. Verify
undo_requiredstays low — nothing was provisional, so nothing needs undoing. - Back-to-back frames at the minimum gap. Verify the parser resets cleanly between them and no field boundary from one frame is attributed to the next.
What the checker must own
- A frame generator parameterised on tag depth, not two separate generators. Scenarios 4 through 7 are one generator with a parameter, and a testbench with a separate tagged-frame path will diverge from the untagged one over time.
- An independent offset model computing positions from tag depth, written from Section 2's argument rather than from Section 5's RTL. A scoreboard sharing the design's arithmetic verifies only self-consistency.
- A truncation harness able to cut a frame at an arbitrary octet, since Scenarios 9 and 10 need per-field and per-offset truncation.
- Coverage crosses of tag depth against truncation field against fault vector. The bin
(tagged, payload contains 0x8100, parsed correctly)must be well populated — it is Scenario 8, it happens in real traffic, and a run on synthetic payloads never reaches it.
13. Debugging — Which Field, Then Which Fault
The symptom: frames are being dropped and the counter says "malformed".
Step 1 — read the fault vector, not a total. Section 8's five flags are independent, and each names a different owner:
| Flag | What it means | Where to go |
|---|---|---|
m_bad_fcs alone | the frame arrived corrupted | the PHY — Chapter 3.3's margin method |
m_undersized alone | the frame is short and otherwise clean | the transmitter's padding — Chapter 5.6 |
m_oversized alone | check the tag depth first | very often a hard-coded 1,518 limit here, not an oversized frame there |
m_unparseable alone | the structure broke | Step 2 |
| several together | they share a cause | severe corruption — the physical layer |
Row three is the one that wastes the most time. An oversized report points at the transmitter, so the investigation goes to the far end — while the fault is a local length check that does not know about tagging. Read tags_seen before believing an oversized report.
Step 2 — if unparseable, read first_stop_field and first_stop_offset. Each field names a different subsystem:
| Stopped in | Likely cause |
|---|---|
| destination or source | truncation very early — a PHY or link fault |
| classification | a tag walk that hit its bound, or truncation at exactly that point |
| payload | a transmit-side underrun — Chapter 4.3 §5's abort path |
| check value | the frame ended a few octets short — a length or padding fault, not a link fault |
Step 3 — read c_parsed_ok as the denominator. Four hundred stops out of five hundred frames is a broken link. Four hundred out of five hundred million is background. A stop count without its denominator cannot distinguish them, and the two demand opposite responses.
Step 4 — if tagged traffic fails while untagged traffic is fine, stop looking at the network. That signature is nearly diagnostic of a parser offset assumption — Section 11's rejected property, in design form. The classification field is being read at offset 12 on frames where it is at 16, and 0x8100 looks like an unrecognised protocol rather than an error.
Step 5 — if c_irrevocable_on_fail is moving, the corruption is leaving the box. A frame was forwarded provisionally and its check value then failed. Chapter 2.7 §5's error-containment argument applies: a store-and-forward path would have caught it, and this deployment has made a cut-through trade that its error rate no longer justifies.
The method stated once: read the vector before any total, check tag depth before believing an oversized report, use the stopping field to name a subsystem, and always divide by the frames that parsed — because every one of those readings points somewhere different and a single "malformed" counter points nowhere.
14. Common Misconceptions
"The frame is a byte layout."
The wrong model: a struct definition — destination at 0, source at 6, type at 12 — read left to right.
What it costs: you index instead of walking, and every tagged frame is mis-parsed. You cannot explain why the order is what it is, so you cannot reason about a protocol whose layout you have not memorised. And you write Section 11's rejected property.
The corrected model: the frame is a decision sequence. Each field arrives when the decision it drives becomes possible and necessary, and the order is a dependency order — where, whose, what kind, the thing, was it true. Positions are derived from that order, not the other way round.
"Field offsets are constants."
The wrong model: the classification field is at offset 12.
What it costs: the batch's most consequential structural fault. A tagged frame reads 0x8100 there, which is in the type range and looks like an unrecognised protocol rather than an error — so nothing reports a failure, tagged traffic silently does not work, and the investigation goes to VLAN configuration.
The corrected model: only fields to the left of every variable-length field have fixed offsets. Destination and source do — they precede the tag insertion point, which is why early abandonment survives tagging. Everything after moves four octets per tag, and the size limits move with it.
"The check value at the end is just where it ended up."
The wrong model: the FCS is last by convention.
What it costs: you miss that every decision before it is provisional, so you build a pipeline that acts on unverified data without knowing it has. When a check value fails you have no record of what needs undoing, and no way to tell whether anything irrevocable already happened.
The corrected model: it is last by construction — a check covering the whole frame cannot be transmitted before the whole frame. That forces a per-decision choice between waiting, acting-and-withdrawing, and acting-and-accepting-error, and the deciding question is revocability: a queue selection can be undone, a forwarded frame cannot.
"A malformed frame is a malformed frame."
The wrong model: one counter, one condition.
What it costs: four failure classes with four different owners collapse into one number that names none of them. And you cannot see when several faults arrive together, which is the signature that they share a cause.
The corrected model: malformation is a vector, deliberately unlike Chapter 2.7 §7's one-hot disposition. A frame goes to exactly one place, so a disposition is one-hot; a frame can be undersized and unparseable and fail its check value, so a classification is not. And a stop should name which field it stopped in, because each field is produced by a different part of the transmitter.
"Destination before source is arbitrary."
The wrong model: two addresses, either order would do.
What it costs: you cannot explain why an address filter can decide at octet six, and you may design a receive path that buffers a frame before filtering it — paying, on every frame, a cost the field order exists to avoid.
The corrected model: it is the only genuinely discretionary choice in the layout, and it went to early abandonment over early learning. Every station pays the abandonment cost and only bridges would have gained from source-first, so the common case won. On a shared medium the common case is "this frame is not for me".
15. Interview Reasoning
"Why is the Ethernet frame in this order?"
The weak answer describes the layout. The answer that ends the topic derives it: each field arrives when the decision it drives becomes possible, the destination is second because it is the earliest abandon point, and the check value is last by construction because it covers everything before it. Naming destination-before-source as the one discretionary choice — and that it bought abandonment over learning — shows the reasoning rather than the memory.
"Where is the EtherType field?"
The trap is "offset 12". The correct answer is that it depends on tag depth — 12 untagged, 16 with one tag, 20 with two — and that the addresses are the only fields whose offsets are genuinely constant, because they precede the insertion point. The strong follow-up is what happens when you get it wrong: 0x8100 is in the type range, so a mis-parsed tagged frame looks like an unrecognised protocol rather than an error and reports nothing.
"A switch forwards a frame and then its check value fails. What now?"
Nothing, for that frame — it has left the port and cannot be recalled. That is the cut-through trade of Chapter 2.7 §5, and the deeper point is that the check value's position at the end makes every earlier decision provisional. A complete answer names the distinction that matters — revocability, not cost — and observes that whether cut-through is acceptable is a measurable property of the link's error rate rather than a philosophical one.
16. Understanding Check
Because a receiver has no choice about the order it makes decisions in, and each field arrives when the decision it drives becomes possible.
| Decision | Depends on | So it is |
|---|---|---|
| where does it begin? | nothing | first |
| is it mine? | only the start | early — the best abandon point |
| who sent it? | only the start | after the destination |
| what is inside? | only the start | before the payload it describes |
| was any of it true? | everything before it | last, by construction |
The last row is not a preference. A check value computed over the whole frame cannot be transmitted before the whole frame exists.
And the second row is the one genuinely discretionary choice. Destination and source are both independent of everything but the start, so either could have come first. Destination won because it buys early abandonment: a station that is not the target learns so six octets in and stops, with no buffering and no parsing.
The follow-up to be ready for: what would source-first have bought? Early learning — a bridge learns station locations from source addresses. The trade went the other way because every station pays the abandonment cost while only bridges gain from learning sooner. Optimise the common case, and on a shared medium the common case is "not for me".
17. What's Next
The claim this chapter defended: field order is decision order. Each field arrives when the decision it drives becomes both possible and necessary, the destination address is second because it is the earliest point a station can give up, and the check value is last by construction because it covers everything before it.
Two things follow that the layout alone does not show. Positions are not constants — only the addresses have fixed offsets, because only they precede the tag insertion point, and everything after moves four octets per tag along with the size limits. And every decision before the check value is provisional, which forces a per-decision choice between waiting, withdrawing and accepting error, decided by revocability rather than cost.
The rest of Module 5 opens the fields this chapter only located.
Chapter 5.2 — Preamble and Start Frame Delimiter takes the first group. Chapter 4.3 §4 established where the preamble lives — generated and consumed below the MAC, because a preamble failure is detectable only in the signal. 5.2 asks what it actually does, and the answer is that one field solves two different problems: recovering bit timing, and then finding the octet boundary. Those are separate jobs, and the field's structure has a distinct part for each.
Then Chapter 5.3 opens the address as a structured value, Chapter 5.4 takes its types, and Chapter 5.5 resolves the classification ambiguity this chapter deliberately left open.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
Packet Switching
A circuit allocates capacity in advance and guarantees it; a packet network allocates on demand and guarantees nothing. The exchange is measurable in RTL — idle reserved slots against buffered, delayed and occasionally dropped packets — and it is why a packet must describe its own extent and destination.
- Related topic
The MAC Layer
Framing, addressing, error detection, sizing, interframe gap and transmit access. Each exists because the medium is unreliable, shared, or both — and knowing which reason applies predicts exactly what full duplex deleted and what it left untouched.
- 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.
- Related topic
CSMA/CD, Collision Domains and Slot Time
Slot time is the parameter the whole half-duplex MAC hangs on: it bounds medium acquisition, bounds a collision fragment, and is the retransmission quantum. Deriving it from round-trip propagation plus jam is what fixes Ethernet's minimum frame size — a timing constant wearing a frame-format costume.
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.
