Ethernet · Module 5
EtherType and Length
The same two octets carry either a payload length or a protocol identifier, and nothing in the frame says which. The two meanings occupy disjoint numeric ranges so the resolution is exact — and the harder half is that a tag moves the field, and a length-form frame names its protocol somewhere else entirely.
Chapter 5.1 §6 placed this field two octets after the addresses and then deferred it, with a warning: the same two octets carry two entirely different things, and nothing anywhere in the frame says which.
One frame uses them as a length — how many octets of client data follow. Another uses them as a type — which protocol the client data belongs to. They arrive at the same offset, in the same two octets, from the same sender, with no flag, no version, and no negotiation.
A receiver must decide, on every frame, with no information beyond the sixteen bits themselves.
That sounds like a protocol design failure and it is genuinely a historical accident. What makes it worth a chapter is that it was resolved without breaking either meaning, and the resolution is exact — not a heuristic, not a probability, not a best guess that usually works.
And then the harder half, which is the part parsers actually get wrong: having decided what the field means, a parser still has to be right about where it was and where the protocol identity actually lives — because tags can push the field further into the frame, and when the field is a length, the identity is not in this field at all.
1. Scope — What This Chapter Owns
This chapter has two close neighbours, and the boundaries were drawn deliberately when those chapters deferred to it.
Chapter 2.4 owns the field as an opaque key. Its ethertype_demux showed the MAC carrying a value it never interprets — clients register values, the hardware compares, and adding a protocol changes nothing in the MAC. Its type_field_locator showed the position problem: the field is at a fixed offset only in an untagged frame, and each tag moves it. Both were explicit that the disambiguation rule belongs here.
Chapter 5.1 owns the field's place in the frame walk — two octets, immediately after the addresses in an untagged frame, and the point at which every downstream decision about the payload becomes possible.
This chapter owns the value's meaning, and everything that follows from the meaning being ambiguous: why two interpretations exist in one field, why they cannot collide, what the undefined band between them is for, what a parser must do with each of the three cases, and where the protocol identity actually lives when the field turns out to be a length.
It does not own: the tag format itself — 802.1Q's structure and semantics belong to the switching module, and this chapter treats a tag only as something that displaces the field. Nor the payload's own contents, which Chapter 2.3 established a MAC must not inspect.
The question this chapter answers that its neighbours do not: given sixteen bits that could mean either of two unrelated things, how does a receiver decide — and what does it still have to get right afterwards?
2. Why One Field Carries Two Meanings
The ambiguity is a historical join, and knowing which side came first explains why the resolution took the shape it did.
The type interpretation came first. The original commercial Ethernet specification — the DIX standard, from the vendors who built the first products — put a protocol identifier in these two octets. A frame said what it was carrying, and a receiver used that to hand the payload to the right client. Length was not carried, because the frame's extent was already known from the physical layer's framing.
The length interpretation came from the standardisation effort. IEEE 802.3 defined a frame whose two octets after the addresses carried the number of client data octets, with protocol identification handled above the MAC by a separate sublayer — the logical link control header at the start of the payload. That is architecturally cleaner: the MAC describes its own frame, and protocol identification is somebody else's layer.
Both were deployed, at scale, at the same time. Products existed that sent one and expected the other. Neither could be withdrawn.
The reconciliation did not pick a winner. It observed that the two interpretations cannot overlap numerically — the maximum client data length is 1500, so a length is never more than 0x05DC, and every assigned protocol identifier is 0x0600 or above — and standardised the rule that a receiver uses the value itself to decide. Both frame formats remain legal, both are still generated, and the field is formally named Length/Type rather than either one.
3. Three Bands, Not Two
Read the left column as arithmetic rather than convention. The boundary is not a number somebody chose to separate two ranges — it is the payload ceiling, which was fixed for entirely unrelated reasons, and the type space simply starts above it. Change the ceiling and the boundary would have to move with it, which is one reason the larger frames of Chapter 5.7 cannot express their length in this field at all.
Read the middle row as the part designs omit. Two bands are documented everywhere; the gap between them is documented rarely, and a parser written from a two-way description has no branch for it. What that parser does with 0x05F0 depends on how the comparison happened to be written — value <= 1500 sends it to the type path, value >= 1536 sends it to the length path, and both are defensible readings of a rule that was only ever stated as two cases.
So the first thing this chapter's RTL does is make the third case explicit, and the second thing it does is count it — because a frame in the undefined band is either a corrupted frame that survived its check, or a transmitter doing something wrong, and both are worth knowing about.
4. RTL 1 — Resolving the Field, With All Three Outcomes Named
// SYNTHESIZABLE.
//
// Resolves the length/type field into exactly one of THREE outcomes.
//
// The three-way split is the whole point. A two-way resolver -- "length or
// type" -- has to put the undefined band somewhere, and wherever it puts
// it, a frame that means nothing gets processed as though it meant
// something. The band is small (35 values) and it is not empty in
// practice: a corrupted field that survives its check lands there as
// often as anywhere else in the space.
package ltype_pkg;
// NORMATIVE. Maximum client data octets, and therefore the largest value
// the field can legitimately carry as a length.
localparam logic [15:0] MAX_LENGTH = 16'h05DC; // 1500
// NORMATIVE. Smallest assigned protocol identifier. The gap between this
// and MAX_LENGTH+1 is the undefined band.
localparam logic [15:0] MIN_ETHERTYPE = 16'h0600; // 1536
typedef enum logic [1:0] {
LT_LENGTH = 2'd0, // client data length; identity is further in
LT_TYPE = 2'd1, // protocol identifier; identity is right here
LT_UNDEFINED = 2'd2 // neither -- discard, and count
} lt_class_e;
endpackage
module length_type_resolver
import ltype_pkg::*;
(
input logic clk,
input logic rst_n,
input logic field_valid,
input logic [15:0] field_value,
output logic class_valid,
output lt_class_e field_class,
// Only meaningful when field_class == LT_LENGTH.
output logic [10:0] declared_length,
// Only meaningful when field_class == LT_TYPE.
output logic [15:0] protocol_id
);
// The comparisons are written as two INDEPENDENT range tests rather than
// as an if/else chain, so that the undefined band falls out of both
// rather than being swept into whichever branch happens to be last.
wire is_length = (field_value <= MAX_LENGTH);
wire is_type = (field_value >= MIN_ETHERTYPE);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
class_valid <= 1'b0;
field_class <= LT_UNDEFINED;
declared_length <= '0;
protocol_id <= '0;
end else begin
class_valid <= field_valid;
if (field_valid) begin
unique case ({is_type, is_length})
2'b01: field_class <= LT_LENGTH;
2'b10: field_class <= LT_TYPE;
default: field_class <= LT_UNDEFINED; // 2'b00 -- the band
endcase
declared_length <= field_value[10:0];
protocol_id <= field_value;
end
end
end
// 2'b11 is UNREACHABLE by construction: MAX_LENGTH < MIN_ETHERTYPE, so
// no value satisfies both tests. Stating it as an assertion rather than
// trusting it means a future edit to either constant is caught here
// instead of producing a resolver that silently prefers one meaning.
// synopsys translate_off
a_bands_disjoint: assert final (MAX_LENGTH < MIN_ETHERTYPE)
else $fatal(1, "length and type bands overlap -- resolution is ambiguous");
// synopsys translate_on
endmoduleClassification: synthesizable.
What it teaches: that the two range tests must be independent. Written as if (v <= 1500) ... else ..., the undefined band disappears into the else and becomes a type. Written as if (v >= 1536) ... else ..., it becomes a length. Both compile, both pass every test built from a two-case description of the rule, and both are wrong in a way that only shows up on a value nobody thought to generate.
Deliberately simplified: it resolves a field that has already been located. Finding the field is Chapter 2.4's type_field_locator, and Section 8 explains why the two jobs are kept apart rather than merged.
Production implication: declared_length is 11 bits, not 16. That is not compression — it is a statement that the value has been range-checked. A length is at most 1500, which fits in 11 bits, so a downstream module receiving 11 bits cannot be handed a value that failed the check. The width carries the guarantee, and a 16-bit declared_length would silently permit an unresolved field to flow onward.
5. When the Field Is a Length, the Identity Is Somewhere Else
Resolving the field as a length answers how much and leaves what completely open. The frame carries client data whose protocol is not named anywhere in the MAC header.
It is named at the start of the payload instead, by a link-layer control header the MAC does not interpret and the client does.
Case two — a control header at the start of the payload. Three octets: a destination service access point, a source service access point, and a control octet. The two access-point values identify the protocols at each end, from a space that is one octet wide rather than two — and that narrowness is the reason case three exists.
Case three — the escape hatch. When both access points hold 0xAA and the control octet holds 0x03, the header is followed by five more octets: a three-octet organisation identifier and a two-octet protocol identifier scoped to it. This is a protocol identity with an owner, and the organisation identifier 00-00-00 is defined to mean "the protocol identifier that follows is an EtherType" — which is how a length-form frame carries a type-form identity.
Read that last sentence again, because it is the joke at the centre of this chapter. The length interpretation exists so that protocol identification can be somebody else's layer. That layer's identifier space turned out to be too small, so it acquired an extension whose most common use is to carry the identifier from the interpretation it replaced. The two formats converged, through five extra octets of header.
What a MAC does with all of this: nothing. Chapter 2.3's rule holds — the payload is opaque, and every octet described above is payload. A MAC resolves the band, reports a length, and hands over. The classifier in Section 6 is a receive-path helper, offered to a client that wants the work done in hardware, and it is architecturally on the client's side of the line even when it is physically in the same chip.
6. RTL 2 — Finding the Identity in a Length-Form Frame
// SYNTHESIZABLE RECEIVE-PATH HELPER, on the CLIENT's side of the line.
//
// Consumes the first octets of the payload of a frame the resolver
// classified as LT_LENGTH, and produces the protocol identity the MAC
// header did not carry.
//
// Three outcomes, and the third one matters as much as the other two:
//
// LLC -- the access point values ARE the identity
// SNAP -- the access points were an escape; the identity is
// five octets further in, scoped by an organisation
// MALFORMED -- the frame declared a length too short to contain the
// header it implies
//
// That last case is why this module tracks the declared length rather than
// simply counting octets: a frame can promise less data than the header
// it is required to start with, and consuming the header anyway means
// reading octets belonging to the pad or to nothing at all.
module llc_snap_classifier
import ltype_pkg::*;
#(
parameter logic [7:0] SNAP_SAP = 8'hAA,
parameter logic [7:0] SNAP_CTRL = 8'h03,
// NORMATIVE. This organisation identifier means "the two octets that
// follow are an EtherType" -- the length form carrying a type identity.
parameter logic [23:0] OUI_ETHERTYPE = 24'h00_0000
) (
input logic clk,
input logic rst_n,
input logic frame_start,
input logic [10:0] declared_length, // 11 bits: already range-checked
input logic pay_valid,
input logic [7:0] pay_data,
output logic result_valid,
output logic is_snap,
output logic [7:0] dsap,
output logic [7:0] ssap,
output logic [23:0] snap_oui,
output logic [15:0] snap_protocol,
// True when snap_oui == OUI_ETHERTYPE: snap_protocol is an EtherType and
// may be handed to the same demultiplexer a type-form frame uses.
output logic snap_is_ethertype,
output logic malformed
);
typedef enum logic [2:0] {
S_IDLE, S_DSAP, S_SSAP, S_CTRL, S_OUI, S_PID, S_DONE
} s_e;
s_e state_q;
logic [2:0] byte_q; // position within the multi-octet fields
logic [7:0] ctrl_q;
logic [10:0] consumed_q;
// The header this frame is COMMITTED to, given what has been seen so
// far. Three octets for LLC; eight once the SNAP escape is recognised.
wire [10:0] required = (state_q == S_OUI || state_q == S_PID ||
(state_q == S_DONE && is_snap)) ? 11'd8 : 11'd3;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_IDLE;
byte_q <= '0;
consumed_q <= '0;
ctrl_q <= '0;
result_valid <= 1'b0;
is_snap <= 1'b0;
dsap <= '0;
ssap <= '0;
snap_oui <= '0;
snap_protocol <= '0;
snap_is_ethertype <= 1'b0;
malformed <= 1'b0;
end else begin
result_valid <= 1'b0;
if (frame_start) begin
state_q <= S_DSAP;
byte_q <= '0;
consumed_q <= '0;
is_snap <= 1'b0;
snap_is_ethertype <= 1'b0;
// Checked BEFORE any octet is consumed. A frame declaring fewer
// than three octets cannot contain the header it is required to
// start with, and no amount of reading will make it appear.
malformed <= (declared_length < 11'd3);
end else if (pay_valid && !malformed) begin
consumed_q <= consumed_q + 1'b1;
// Re-checked on every octet, because the SNAP escape RAISES the
// requirement mid-parse: a frame long enough for LLC may be too
// short for SNAP, and that is only discoverable after the escape
// has been recognised.
if ((consumed_q + 1'b1) > required && state_q != S_DONE) begin
malformed <= 1'b1;
result_valid <= 1'b1;
end else begin
case (state_q)
S_DSAP: begin dsap <= pay_data; state_q <= S_SSAP; end
S_SSAP: begin ssap <= pay_data; state_q <= S_CTRL; end
S_CTRL: begin
ctrl_q <= pay_data;
if (dsap == SNAP_SAP && ssap == SNAP_SAP && pay_data == SNAP_CTRL) begin
is_snap <= 1'b1;
state_q <= S_OUI;
byte_q <= '0;
end else begin
// Plain LLC: the access points are the identity, and the
// result is complete after three octets.
state_q <= S_DONE;
result_valid <= 1'b1;
end
end
S_OUI: begin
snap_oui <= {snap_oui[15:0], pay_data};
if (byte_q == 3'd2) begin state_q <= S_PID; byte_q <= '0; end
else byte_q <= byte_q + 1'b1;
end
S_PID: begin
snap_protocol <= {snap_protocol[7:0], pay_data};
if (byte_q == 3'd1) begin
state_q <= S_DONE;
result_valid <= 1'b1;
snap_is_ethertype <=
(snap_oui == OUI_ETHERTYPE) &&
({snap_protocol[7:0], pay_data} >= MIN_ETHERTYPE);
end else begin
byte_q <= byte_q + 1'b1;
end
end
default: ;
endcase
end
end
end
end
endmoduleClassification: synthesizable receive-path helper, architecturally above the MAC.
What it teaches: that a declared length is a promise the frame can fail to keep, and the check has to be repeated when the requirement changes. The obvious implementation checks declared_length once at frame start against three octets. That is correct until the SNAP escape is recognised, at which point the frame owes eight — and a frame that declared four octets passed the first check and cannot satisfy the second. Re-evaluating required on every octet is what catches it.
Deliberately simplified: it classifies and reports; it does not gate delivery. A production receive path would use malformed to stop the frame, and the separation is deliberate — Section 12 shows why a classifier that also disposes of frames is harder to verify than one that only classifies.
Production implication: snap_is_ethertype checks both the organisation identifier and that the protocol value is in the type band. The second check looks redundant and is not: an organisation identifier of 00-00-00 with a protocol value of, say, 0x0040 is a frame claiming an EtherType that is not one. Handing it to the type demultiplexer would search the type space for a value that could never have been registered there — and the demultiplexer would report it as an unknown type, which is a true statement that points at the wrong problem.
7. RTL 3 — Reconciling a Declared Length Against What Arrived
The length interpretation makes a claim about the frame, and the frame either honours it or does not. Checking that is a different job from parsing the header, and it is the one place a length-form frame can be caught lying.
// SYNTHESIZABLE.
//
// A declared length and a received octet count are NOT expected to be
// equal, and a design that asserts equality will fire on ordinary traffic
// (Section 13 states why formally). Padding is the reason:
//
// received_octets == declared_length -- unpadded frame
// received_octets > declared_length -- PADDED; the excess is
// pad, which is normal
// received_octets < declared_length -- the frame promised
// more than it sent:
// ALWAYS an error
//
// So the checkable relation is an INEQUALITY, and the one-sidedness is
// the point: only the third case is a fault.
module payload_extent_checker
import ltype_pkg::*;
#(
// Client data octets in a minimum-size frame. A frame shorter than this
// is padded, and the pad is what makes the excess legitimate.
parameter int unsigned MIN_CLIENT_OCTETS = 46,
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_start,
input logic is_length_form,
input logic [10:0] declared_length,
input logic pay_valid,
input logic pay_last,
output logic check_valid,
output logic short_frame, // promised more than it sent
output logic unexpected_excess, // excess that padding cannot explain
output logic [10:0] pad_octets,
output logic [CNT_W-1:0] c_short,
output logic [CNT_W-1:0] c_excess,
// First cause: the declared length of the first frame that failed.
output logic first_fault_seen,
output logic [10:0] first_fault_declared
);
logic [10:0] received_q;
logic active_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
received_q <= '0;
active_q <= 1'b0;
check_valid <= 1'b0;
short_frame <= 1'b0;
unexpected_excess <= 1'b0;
pad_octets <= '0;
c_short <= '0;
c_excess <= '0;
first_fault_seen <= 1'b0;
first_fault_declared <= '0;
end else begin
check_valid <= 1'b0;
short_frame <= 1'b0;
unexpected_excess <= 1'b0;
if (clear) begin
c_short <= '0;
c_excess <= '0;
// first_fault_* deliberately survives: the first frame that broke
// the relation is the one worth keeping, and a counter clear must
// not erase it.
end
if (frame_start) begin
received_q <= '0;
active_q <= is_length_form; // type-form frames make no claim
end else if (pay_valid && active_q) begin
received_q <= received_q + 1'b1;
if (pay_last) begin
logic [10:0] rx;
rx = received_q + 1'b1;
check_valid <= 1'b1;
active_q <= 1'b0;
if (rx < declared_length) begin
short_frame <= 1'b1;
if (!(&c_short)) c_short <= c_short + 1'b1;
if (!first_fault_seen) begin
first_fault_seen <= 1'b1;
first_fault_declared <= declared_length;
end
end else begin
pad_octets <= rx - declared_length;
// Excess is explained by padding only while the CLIENT DATA
// region is at its floor. Beyond that the frame did not need
// padding, so excess octets are unaccounted for.
if ((rx > declared_length) && (rx > 11'(MIN_CLIENT_OCTETS))) begin
unexpected_excess <= 1'b1;
if (!(&c_excess)) c_excess <= c_excess + 1'b1;
if (!first_fault_seen) begin
first_fault_seen <= 1'b1;
first_fault_declared <= declared_length;
end
end
end
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the relation between a declared length and a received count is an inequality with one legitimate direction of slack, and the slack has a bound. Excess octets are pad, and pad exists only to lift a short frame to the minimum — so excess is explicable up to the client-data floor and unexplained above it. Checking rx >= declared_length alone accepts a frame with two hundred octets of unexplained trailer; checking rx == declared_length rejects every padded frame.
Deliberately simplified: it uses a fixed client-data floor. The real floor moves when tags are present, because a tag occupies frame octets that the minimum frame size counts — which is a case Section 8's position problem produces and Chapter 5.6 owns in full.
Production implication: active_q is set from is_length_form at frame start and nowhere else. A type-form frame makes no claim about its own extent, so there is nothing to reconcile, and running this check on one would compare a received count against a protocol identifier — producing a "short frame" report on every IPv4 frame, since 0x0800 as a length is 2048 and no frame carries that much. A check must be disabled on the frames it does not apply to, and disabled by the classification rather than by a counter's behaviour.
8. The Field Moves, and Resolving the Wrong Two Octets Still Succeeds
Everything so far assumed the field had already been found. In an untagged frame it is at a fixed offset — twelve octets in, immediately after the two addresses. In a tagged frame it is not, and the difference is the most common parser bug in this area.
A tag is inserted after the addresses and before the length/type field. It occupies four octets: two that sit exactly where the length/type field would have been, and two more of tag control information. The real length/type field is then four octets further in. A second tag pushes it four further still.
Read the middle column, because it is where the bug lives. A tag protocol identifier is itself a value in the type band — 0x8100 for the common case, 0x88A8 for the outer tag of a stacked pair. So a parser that reads offset twelve on a tagged frame feeds a type-band value into Section 4's resolver, which classifies it as LT_TYPE, correctly, and reports a protocol identifier.
Nothing detects this. The resolver did its job on the octets it was given. The band test passed. No counter increments, no error asserts, and the frame is delivered to whichever client registered that value — or reported as an unknown type, which is the good outcome because at least something is visible.
So the field's meaning and the field's position are two separate problems, and only one of them is solved by this chapter's rule. Chapter 2.4's type_field_locator solves the other, and this chapter deliberately does not rebuild it — the two modules stay separate because merging them produces a single block that can fail in either dimension while reporting success in both.
9. A Depth Bound Is a Decision, Not a Limitation
Tags stack. Two is routine, more is possible, and a frame arriving from outside this design's control can carry as many as fit.
A parser that walks tags until it stops seeing them has no bound on its own work — and that is not a theoretical concern, because the walk consumes octets from a frame whose contents are supplied by whoever sent it. A frame filled with tag identifiers keeps a naive walker in its skip loop until the frame ends, at which point it has produced no result and consumed a whole frame time.
So the walker carries a declared maximum, and a frame exceeding it is rejected rather than parsed. Chapter 2.4's type_field_locator had MAX_TAGS and a too_many_tags output for exactly this. What is worth adding here is why that is the correct shape rather than a compromise.
Because the alternative is not "parse deeper" — it is "parse for an unknown time". A bounded parser gives a fixed answer in a fixed number of cycles for every possible input, including inputs constructed to be difficult. An unbounded one gives a correct answer for inputs it was designed for and an unspecified one otherwise, and receive paths do not get to choose their inputs.
And the bound is a published property of the design, not a hidden limit. A device that supports two levels of tagging says so; a network built on it does not stack three. The failure, when it happens, is a declared refusal with a counter behind it — which an operator can find — rather than a parse that silently produced the wrong offset.
The general rule, and the track has now met it three times: Chapter 3.8 bounded its search for alignment, Chapter 5.4 bounded its filter rebuild, and this bounds a parse. Any loop whose trip count comes from data supplied by the outside world needs a bound supplied by the design — and the bound should be a parameter with a counter, so that hitting it is an observation rather than a mystery.
10. RTL 4 — Accounting for What the Parser Could Not Place
// SYNTHESIZABLE INSTRUMENTATION.
//
// Three populations that all mean "this frame was not delivered to a
// client", and that need to be told apart because they have three
// different causes:
//
// UNDEFINED BAND -- the field meant nothing. Corruption that survived
// its check, or a transmitter doing something wrong.
// UNKNOWN TYPE -- the field meant something and nothing claimed it.
// Usually a protocol this station does not run --
// but ALSO the signature of the Section 8 offset bug,
// because a tag identifier arrives here.
// OVER-TAGGED -- the field was never reached.
//
// A single "dropped" counter merges a corruption problem, a configuration
// problem and a topology problem into one number that cannot be acted on.
module unknown_type_accounting
import ltype_pkg::*;
#(
parameter int unsigned TOP_N = 4,
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic result_valid,
input lt_class_e field_class,
input logic [15:0] protocol_id,
input logic claimed, // some client registered this value
input logic over_tagged,
output logic [CNT_W-1:0] c_undefined,
output logic [CNT_W-1:0] c_unknown_type,
output logic [CNT_W-1:0] c_over_tagged,
// The most frequent unclaimed types, kept because "unknown types are
// high" is not actionable and "0x8100 dominates the unknown types" names
// the Section 8 bug outright.
output logic [15:0] top_type [TOP_N],
output logic [CNT_W-1:0] top_count [TOP_N],
// Sticky: an undefined-band value is never normal, and the first one is
// worth keeping across a counter clear.
output logic undefined_seen,
output logic [15:0] first_undefined
);
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
bump = (&v) ? v : (v + 1'b1);
endfunction
// A small saturating leaderboard. Deliberately NOT a general histogram:
// the type space is 16 bits and only its busiest few entries carry
// diagnostic weight.
logic hit;
int hit_idx;
int min_idx;
always_comb begin
hit = 1'b0;
hit_idx = 0;
min_idx = 0;
for (int i = 0; i < TOP_N; i++) begin
if (top_type[i] == protocol_id && top_count[i] != '0) begin
hit = 1'b1;
hit_idx = i;
end
if (top_count[i] < top_count[min_idx]) min_idx = i;
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_undefined <= '0;
c_unknown_type <= '0;
c_over_tagged <= '0;
undefined_seen <= 1'b0;
first_undefined <= '0;
for (int i = 0; i < TOP_N; i++) begin
top_type[i] <= '0;
top_count[i] <= '0;
end
end else begin
if (clear) begin
c_undefined <= '0;
c_unknown_type <= '0;
c_over_tagged <= '0;
for (int i = 0; i < TOP_N; i++) top_count[i] <= '0;
// undefined_seen / first_undefined deliberately survive.
end
if (over_tagged) c_over_tagged <= bump(c_over_tagged);
if (result_valid) begin
case (field_class)
LT_UNDEFINED: begin
c_undefined <= bump(c_undefined);
if (!undefined_seen) begin
undefined_seen <= 1'b1;
first_undefined <= protocol_id;
end
end
LT_TYPE: if (!claimed) begin
c_unknown_type <= bump(c_unknown_type);
if (hit) begin
top_count[hit_idx] <= bump(top_count[hit_idx]);
end else begin
// Displace the least frequent entry. A newcomer starts at
// one, so a genuinely busy type climbs back within a few
// frames while a one-off does not hold a slot.
top_type[min_idx] <= protocol_id;
top_count[min_idx] <= 32'd1;
end
end
default: ;
endcase
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that an unknown-type counter is only useful if it says which types. "Unknown types rising" has at least three causes with unrelated fixes — a protocol the station legitimately does not run, a client that failed to register, and Section 8's offset bug. The leaderboard separates them in one read: a spread of values is normal traffic, one dominant value is an unregistered client, and 0x8100 or 0x88A8 dominating is the offset bug naming itself.
Deliberately simplified: the leaderboard has no ageing, so a burst of one type holds a slot after it stops. Ageing matters when the counters are read continuously and matters little when they are read after a fault.
Production implication: undefined_seen is sticky and survives clear because an undefined-band value is never a normal event. A type this station does not run is routine and its counter should be clearable; a value in 1501–1535 means either that a corrupted field passed its check — which Chapter 6.1 will quantify as a residual probability rather than an impossibility — or that something upstream is emitting frames it should not. Neither is something to lose to a housekeeping clear.
11. RTL 5 — The Transmit Side, Where the Undefined Band Must Never Be Produced
Everything above is a receiver defending itself. The transmitter's obligation is the mirror image and it is narrower: produce a field that resolves to what was intended, and refuse a request that cannot.
// SYNTHESIZABLE.
//
// A client asks for one of two things:
//
// "carry this protocol type" -> the field IS the type
// "carry this many octets" -> the field IS the length
//
// The module's real work is the refusals, because each one corresponds to
// a frame that would have been misparsed by every receiver:
//
// a type below 0x0600 -> would resolve as a LENGTH at the far end
// a length above 0x05DC -> would resolve as a TYPE at the far end
// either, in 1501..1535 -> would resolve as NOTHING
//
// Note that the first two are the SAME failure seen from opposite sides,
// and neither is detectable by the transmitter's own receive path. A
// design that only tests against itself finds none of them.
module length_type_encoder
import ltype_pkg::*;
#(
parameter int unsigned CNT_W = 16
) (
input logic clk,
input logic rst_n,
input logic req_valid,
input logic req_is_type, // else a length request
input logic [15:0] req_value,
// Octets the client will actually supply. A length request that does not
// match this is a frame that lies about itself.
input logic [10:0] req_client_octets,
output logic rsp_valid,
output logic rsp_accept,
output logic [15:0] rsp_field,
output logic [2:0] rsp_reason, // why it was refused
output logic [CNT_W-1:0] c_refused
);
localparam logic [2:0] R_OK = 3'd0;
localparam logic [2:0] R_TYPE_LOW = 3'd1; // type below the type band
localparam logic [2:0] R_LEN_HIGH = 3'd2; // length above the length band
localparam logic [2:0] R_UNDEFINED = 3'd3; // lands in 1501..1535
localparam logic [2:0] R_LEN_MISMATCH= 3'd4; // declares what it will not send
wire in_band_gap = (req_value > MAX_LENGTH) && (req_value < MIN_ETHERTYPE);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rsp_valid <= 1'b0;
rsp_accept <= 1'b0;
rsp_field <= '0;
rsp_reason <= R_OK;
c_refused <= '0;
end else begin
rsp_valid <= req_valid;
if (req_valid) begin
rsp_field <= req_value;
if (in_band_gap) begin
rsp_accept <= 1'b0;
rsp_reason <= R_UNDEFINED;
end else if (req_is_type && (req_value < MIN_ETHERTYPE)) begin
rsp_accept <= 1'b0;
rsp_reason <= R_TYPE_LOW;
end else if (!req_is_type && (req_value > MAX_LENGTH)) begin
rsp_accept <= 1'b0;
rsp_reason <= R_LEN_HIGH;
end else if (!req_is_type && (req_value[10:0] != req_client_octets)) begin
// Caught HERE rather than at the far end, where it appears as a
// short frame with no attribution to this transmitter.
rsp_accept <= 1'b0;
rsp_reason <= R_LEN_MISMATCH;
end else begin
rsp_accept <= 1'b1;
rsp_reason <= R_OK;
end
if (!rsp_accept && !(&c_refused)) c_refused <= c_refused + 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the transmit side's checks are for failures its own receive path cannot find. A type value below the type band is misparsed by the receiver, and this station's receiver never sees its own frames. Loopback testing does not find it either, because a loopback path that uses the same resolver would misparse it identically and consistently — two wrong implementations agreeing is the classic false pass, and the same trap Chapter 4.3 named when a receive reference model shares code with the design under test.
Deliberately simplified: a single-cycle request/response. A real interface would carry a client handle and queue.
Production implication: R_LEN_MISMATCH is the check most often left out, and it converts an un-attributable failure into an attributable one. A frame declaring more octets than it sends arrives at the far end as Section 7's short_frame — a receiver-side counter, on a different device, that says some transmitter somewhere is lying. Checking it at the source names which client, on which station, at the moment it happened. The general principle: a check placed where the fault originates costs one comparison; the same fault found where its symptom appears costs a cross-device investigation.
12. Classification and Disposition Are Different Jobs
Section 6's classifier reports malformed and does not drop the frame. Section 4's resolver reports LT_UNDEFINED and does not drop the frame. Neither has a delivery enable, and that separation is deliberate rather than incomplete.
A block that classifies and disposes has two failure modes that look identical from outside. If a frame does not arrive, either the classification was wrong or the disposition was wrong, and there is no output that distinguishes them — the frame is gone either way. Splitting them means the classification is observable whether or not the disposition acted on it, so a verification run can check the two independently and a debug session can read what the parser thought before asking what the pipeline did.
It also lets the disposition policy be a property of the design rather than of the parser. Whether an undefined-band frame is dropped, delivered to a diagnostic client, or counted and dropped is a system decision that differs between a switch, a NIC and a test instrument. A parser that hard-codes one of those cannot be reused by the others, and the version that gets reused anyway acquires a parameter that turns the drop off — at which point the classification and the disposition are separate again, just worse.
And it matters for the specific bug this chapter is about. Section 8's offset error produces a frame that classifies successfully and is delivered to the wrong client. No disposition logic would have caught it. What catches it is Section 10's leaderboard — an observation of what the classifier reported, which only exists because the classifier reports rather than merely acts.
13. Assertions — Three Bands, Two Header Forms, One Inequality
// ---------------------------------------------------------------------
// P1 -- The three classifications are exhaustive and mutually exclusive.
// A resolver that can produce no class, or two, has a hole in its case.
// ---------------------------------------------------------------------
property p_class_is_total;
@(posedge clk) disable iff (!rst_n)
class_valid |-> (field_class inside {LT_LENGTH, LT_TYPE, LT_UNDEFINED});
endproperty
a_class_is_total: assert property (p_class_is_total);
// ---------------------------------------------------------------------
// P2 -- The length band maps to LT_LENGTH, exactly.
// ---------------------------------------------------------------------
property p_length_band;
@(posedge clk) disable iff (!rst_n)
(class_valid && $past(field_value) <= MAX_LENGTH) |-> (field_class == LT_LENGTH);
endproperty
a_length_band: assert property (p_length_band);
// ---------------------------------------------------------------------
// P3 -- The type band maps to LT_TYPE, exactly.
// ---------------------------------------------------------------------
property p_type_band;
@(posedge clk) disable iff (!rst_n)
(class_valid && $past(field_value) >= MIN_ETHERTYPE) |-> (field_class == LT_TYPE);
endproperty
a_type_band: assert property (p_type_band);
// ---------------------------------------------------------------------
// P4 -- THE ONE THAT CATCHES THE REAL BUG. The gap maps to LT_UNDEFINED
// and to neither of the other two. An if/else resolver passes P2 and P3
// and fails only here.
// ---------------------------------------------------------------------
property p_gap_is_undefined;
@(posedge clk) disable iff (!rst_n)
(class_valid &&
$past(field_value) > MAX_LENGTH &&
$past(field_value) < MIN_ETHERTYPE) |-> (field_class == LT_UNDEFINED);
endproperty
a_gap_is_undefined: assert property (p_gap_is_undefined)
else $error("undefined band swept into a real classification");
// ---------------------------------------------------------------------
// P5 -- A declared length is never presented wider than its band allows.
// The 11-bit port carries the range check; this proves it holds.
// ---------------------------------------------------------------------
property p_declared_length_in_range;
@(posedge clk) disable iff (!rst_n)
(class_valid && field_class == LT_LENGTH) |-> (declared_length <= 11'd1500);
endproperty
a_declared_length_in_range: assert property (p_declared_length_in_range);
// ---------------------------------------------------------------------
// P6 -- The SNAP escape requires ALL THREE octets. Two out of three is a
// plain LLC frame that happens to share two values.
// ---------------------------------------------------------------------
property p_snap_needs_all_three;
@(posedge clk) disable iff (!rst_n)
(result_valid && is_snap) |-> (dsap == SNAP_SAP && ssap == SNAP_SAP);
endproperty
a_snap_needs_all_three: assert property (p_snap_needs_all_three);
// ---------------------------------------------------------------------
// P7 -- A SNAP frame claiming to carry an EtherType must carry a value
// that IS one. Section 6 explains why this is not redundant.
// ---------------------------------------------------------------------
property p_snap_ethertype_in_band;
@(posedge clk) disable iff (!rst_n)
(result_valid && snap_is_ethertype) |-> (snap_protocol >= MIN_ETHERTYPE);
endproperty
a_snap_ethertype_in_band: assert property (p_snap_ethertype_in_band);
// ---------------------------------------------------------------------
// P8 -- The header requirement only ever RISES during a parse. A design
// in which it can fall has a state that un-recognises the escape.
// ---------------------------------------------------------------------
property p_required_monotonic;
@(posedge clk) disable iff (!rst_n)
(!frame_start) |=> (required >= $past(required));
endproperty
a_required_monotonic: assert property (p_required_monotonic);
// ---------------------------------------------------------------------
// P9 -- A frame too short for the header it implies is malformed, and is
// reported rather than parsed from whatever followed.
// ---------------------------------------------------------------------
property p_short_header_is_malformed;
@(posedge clk) disable iff (!rst_n)
(frame_start && declared_length < 11'd3) |=> malformed;
endproperty
a_short_header_is_malformed: assert property (p_short_header_is_malformed);
// ---------------------------------------------------------------------
// P10 -- Malformed and a valid identity are mutually exclusive. A parse
// that reports both has produced an identity from octets it rejected.
// ---------------------------------------------------------------------
property p_malformed_yields_no_identity;
@(posedge clk) disable iff (!rst_n)
(result_valid && malformed) |-> !snap_is_ethertype;
endproperty
a_malformed_yields_no_identity: assert property (p_malformed_yields_no_identity);
// ---------------------------------------------------------------------
// P11 -- THE EXTENT RELATION, and note that it is an INEQUALITY. A frame
// may send MORE than it declared -- that excess is padding. It may never
// send less.
// ---------------------------------------------------------------------
property p_never_shorter_than_declared;
@(posedge clk) disable iff (!rst_n)
(check_valid && !short_frame) |-> (received_octets >= declared_length);
endproperty
a_never_shorter_than_declared: assert property (p_never_shorter_than_declared);
// ---------------------------------------------------------------------
// P12 -- Excess above the client-data floor is not explained by padding,
// so it must be reported rather than folded into pad_octets.
// ---------------------------------------------------------------------
property p_excess_bounded_by_pad;
@(posedge clk) disable iff (!rst_n)
(check_valid && !unexpected_excess && pad_octets != '0)
|-> (received_octets <= 11'(MIN_CLIENT_OCTETS));
endproperty
a_excess_bounded_by_pad: assert property (p_excess_bounded_by_pad);
// ---------------------------------------------------------------------
// P13 -- The extent check does not run on type-form frames, which make no
// claim about their own length.
// ---------------------------------------------------------------------
property p_no_extent_check_on_type_form;
@(posedge clk) disable iff (!rst_n)
(frame_start && !is_length_form) |-> ##[1:$] !check_valid until frame_start;
endproperty
a_no_extent_check_on_type_form: assert property (p_no_extent_check_on_type_form);
// ---------------------------------------------------------------------
// P14 -- The transmitter never emits a field in the undefined band. This
// is a property of THIS DESIGN's output, which is what makes it a real
// assertion -- compare the rejected property below.
// ---------------------------------------------------------------------
property p_never_emit_undefined;
@(posedge clk) disable iff (!rst_n)
(rsp_valid && rsp_accept)
|-> !((rsp_field > MAX_LENGTH) && (rsp_field < MIN_ETHERTYPE));
endproperty
a_never_emit_undefined: assert property (p_never_emit_undefined);
// ---------------------------------------------------------------------
// P15 -- An accepted length request declares exactly what the client will
// send. Refusing here is what stops a far-end short-frame report that
// cannot be traced back.
// ---------------------------------------------------------------------
property p_accepted_length_matches_client;
@(posedge clk) disable iff (!rst_n)
(rsp_valid && rsp_accept && !$past(req_is_type))
|-> (rsp_field[10:0] == $past(req_client_octets));
endproperty
a_accepted_length_matches_client: assert property (p_accepted_length_matches_client);
// ---------------------------------------------------------------------
// P16 -- Every refusal carries a reason. A refusal with R_OK is a refusal
// nobody can act on.
// ---------------------------------------------------------------------
property p_refusal_has_reason;
@(posedge clk) disable iff (!rst_n)
(rsp_valid && !rsp_accept) |-> (rsp_reason != R_OK);
endproperty
a_refusal_has_reason: assert property (p_refusal_has_reason);
// ---------------------------------------------------------------------
// P17 -- COVERAGE. The undefined band must actually be exercised. A run
// that never produced one proves nothing about P4.
// ---------------------------------------------------------------------
c_undefined_band_seen: cover property (
@(posedge clk) disable iff (!rst_n) (class_valid && field_class == LT_UNDEFINED)
);
// ---------------------------------------------------------------------
// P18 -- COVERAGE. Both boundary values, which is where an off-by-one in
// either comparison shows and nowhere else.
// ---------------------------------------------------------------------
c_boundaries_seen: cover property (
@(posedge clk) disable iff (!rst_n)
(class_valid && $past(field_value) == MAX_LENGTH)
##[1:$] (class_valid && $past(field_value) == MIN_ETHERTYPE)
);14. Verification — Twenty-Two Scenarios and the Four Adjacent Values
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Ordinary type | field 0x0800 | LT_TYPE; protocol_id = 0x0800; no extent check runs |
| 2 | Ordinary length | field 0x002E (46) | LT_LENGTH; declared_length = 46; LLC parse begins |
| 3 | Zero length | field 0x0000 | LT_LENGTH with a length of zero — legal band, and malformed by P9 |
| 4 | Maximum length | field 0x05DC (1500) | LT_LENGTH — the last value that is one |
| 5 | First undefined | field 0x05DD (1501) | LT_UNDEFINED; c_undefined increments; first_undefined captures |
| 6 | Last undefined | field 0x05FF (1535) | LT_UNDEFINED — the other end of the band |
| 7 | First type | field 0x0600 (1536) | LT_TYPE — the first value that is one |
| 8 | All-ones field | field 0xFFFF | LT_TYPE; unclaimed; enters the leaderboard |
| 9 | Plain LLC | length form, DSAP/SSAP not 0xAA | is_snap low; identity is the two access points |
| 10 | SNAP, one octet wrong | DSAP 0xAA, SSAP 0xAA, control 0x00 | is_snap low (P6) — parsed as plain LLC |
| 11 | SNAP carrying an EtherType | OUI 00-00-00, protocol 0x0800 | snap_is_ethertype high; value routable to the type demux |
| 12 | SNAP, vendor OUI | OUI non-zero | is_snap high, snap_is_ethertype low |
| 13 | SNAP claiming a bad EtherType | OUI 00-00-00, protocol 0x0040 | snap_is_ethertype low (P7) — not handed to the demux |
| 14 | Length too short for LLC | declared length 2 | malformed before any octet is consumed (P9) |
| 15 | Length long enough for LLC, short for SNAP | declared length 4, SNAP escape present | malformed mid-parse, after the requirement rises (P8) |
| 16 | Padded frame | declared 20, 46 octets received | pad_octets = 26; no fault |
| 17 | Unpadded frame | declared 400, 400 received | pad_octets = 0; no fault |
| 18 | Short frame | declared 400, 380 received | short_frame; c_short increments; first cause captures 400 |
| 19 | Unexplained excess | declared 100, 400 received | unexpected_excess — beyond the client-data floor (P12) |
| 20 | Type-form frame with a long payload | field 0x0800, 300 octets | no check_valid at all (P13) |
| 21 | Transmit refusals | request type 0x0100; length 0x0700; either 0x05F0 | three refusals, three distinct reasons (P16) |
| 22 | Transmit length mismatch | length request 100, client supplies 90 | R_LEN_MISMATCH at the source, not a short frame at the far end |
15. Debugging — What Each Symptom Narrows To
Symptom — a specific protocol's frames are not reaching their client, and c_unknown_type is climbing at the same rate.
The parse resolved and nothing claimed the value. Read Section 10's leaderboard first, because the dominant value distinguishes three unrelated causes in one look. If the dominant unknown type is the protocol's own value, the client never registered it — a software problem, and the hardware is behaving. If it is 0x8100 or 0x88A8, the parser is reading offset twelve on tagged frames and Section 8 is the whole diagnosis. If the counts are spread thinly across many values, this is ordinary traffic for protocols the station does not run, and the real problem is elsewhere.
Symptom — c_undefined is non-zero on a link with no errors reported anywhere else.
Two candidates, and they are separated by rate rather than by the value. A steady trickle proportional to traffic points at a transmitter emitting fields it should not — check the far end's c_refused from Section 11, which will be zero if that station has no encoder check at all. Occasional and uncorrelated with load points at corruption that survived its integrity check, which is a physical-layer problem showing up in a parser; first_undefined gives the value, and a value that is one or two bit-flips away from a common type or a plausible length supports that reading.
Symptom — every frame from one peer is reported as short_frame.
The peer is declaring more than it sends, and Section 11's R_LEN_MISMATCH is the check it does not have. Before concluding that, rule out the mirror-image bug on this side: if the receive path is counting payload octets after stripping something — a tag, an LLC header — the count is legitimately smaller than the declared length and the check is comparing two different quantities. Confirm by checking whether short_frame appears on tagged frames only, which points at this station, or on all frames from that peer, which points at the peer.
Symptom — malformed fires on frames that a software parser handles without complaint.
Almost certainly the mid-parse requirement rise of Section 6. A software parser typically reads the whole payload into a buffer and indexes it, so a frame declaring four octets but containing more — because it was padded to the minimum — parses fine: the bytes are there. The hardware parser is honouring the declared length, which is smaller. Both are defensible and they disagree, and the resolution is a policy decision about whether the declared length or the received extent governs the header parse. Whichever is chosen, it must be the same on both sides of the design, and the disagreement between hardware and software is itself the finding.
Symptom — the parser works in simulation and mis-delivers in the lab, on the same frames.
Check whether the testbench generates tags at all. A stimulus set built from the frame format of Chapter 5.1 contains no tags, so offset twelve is always the real field and Section 8's bug is unreachable by construction. The design is not failing in the lab; it was never tested against the case the lab supplies, and the fix to the testbench matters more than the fix to the design because it is what stops the next one.
16. Common Misconceptions
"It's the EtherType field."
The wrong model: two octets that identify a protocol, with the length interpretation a legacy curiosity that no longer occurs.
What it costs: you write a two-way parser, or a one-way one. A length-form frame then resolves as a type whose value happens to be small, gets handed to a demultiplexer that finds no client, and is counted as an unknown type — a true report that points nowhere. And the undefined band has no branch at all.
The corrected model: the field is formally Length/Type, both interpretations are legal and current, and the value itself selects between them. Length-form frames are less common on general traffic and are not rare in the places they appear — spanning-tree and other link-layer protocols use them routinely.
"The gap between 1500 and 1536 is just slack."
The wrong model: an arbitrary margin between two ranges, with nothing in it.
What it costs: Section 13's rejected property, and then the deleted handler that follows from it. When a value from the band does arrive — from corruption that survived its check, or a misbehaving transmitter — it is swept into whichever comparison was written last, and the frame is parsed under an interpretation the sender never intended.
The corrected model: it is a third case with its own disposition. Nothing legitimate lands there, which is exactly why something landing there is worth a sticky counter and a captured first value.
"If the field says 400, the frame carries 400 octets of client data."
The wrong model: the declared length is the received length.
What it costs: an equality check that fires on every padded frame — which is most short frames — so it gets relaxed to "greater than or equal", which then accepts a frame with an arbitrary unaccounted trailer.
The corrected model: the relation is an inequality with a bounded slack. Received is never less than declared; the excess is padding; and padding exists only to lift a frame to the minimum, so the excess is explicable up to the client-data floor and unexplained above it.
"A parser that resolves the field correctly has parsed the frame correctly."
The wrong model: getting the band right is the hard part.
What it costs: the single most common bug in this area. On a tagged frame, offset twelve holds a tag identifier, which is a type-band value, which resolves successfully as a protocol type. No assertion fires, no counter moves, and the frame is delivered somewhere confident and wrong.
The corrected model: where the field is and what it means are independent problems. This chapter solves the second; Chapter 2.4's locator solves the first; and only Section 10's leaderboard detects the case where the first was answered wrongly.
"Once the field is a type, the identity is settled."
The wrong model: symmetrically, once it is a length, the frame is unidentifiable at this layer.
What it costs: you stop parsing at the MAC header and treat every length-form frame as opaque, losing the identity that is present four to eight octets further in — and you miss that a SNAP header with a zero organisation identifier is carrying the same EtherType space the type form uses.
The corrected model: the identity for a length-form frame lives at the start of the payload, and the two formats converge: a length-form frame with a SNAP header and a zero OUI names its protocol with the identical value a type-form frame would have carried in the MAC header.
17. Interview Reasoning
"How does a receiver know whether those two octets are a length or a type?"
The weak answer is "values under 1500 are lengths". The answer that ends the topic gives the boundary in both forms — 1500 is 0x05DC and the type space starts at 1536, 0x0600 — and then says why they cannot collide: the payload ceiling is 1500, so no length can reach the type space, and the disjointness is a consequence of the frame size rather than a convention. The payoff is the third case: 1501 to 1535 is neither, and a parser needs a branch for it.
"What happens to a frame whose length/type field is 1520?"
The trap is to answer with one of the two interpretations. Nothing legitimate produces that value, so the correct answer is that the frame is undefined and must be discarded and counted — and then the interesting half: how it got there. Corruption that survived its integrity check lands anywhere in the space, including here, which is why the count is worth keeping sticky rather than clearing it with the housekeeping counters.
"Your parser reports a lot of unknown EtherTypes. Where do you look?"
The strong answer asks which values before proposing a cause, because the distribution separates three unrelated problems in one read: a spread means ordinary traffic for protocols this station does not run; one dominant application value means a client that never registered; and 0x8100 dominating means the parser is reading a fixed offset on tagged frames and resolving the tag identifier as a protocol type. Naming that third case unprompted is what distinguishes someone who has debugged this from someone who has read about it.
"Why can't you assert that the field is never in the reserved range?"
Because both sides of that property are primary inputs — it constrains the sender, not the design. The complete answer adds the consequence, which is the part that bites: the assertion makes the undefined-band handler unreachable, so the handler is removed as dead logic, and the design is then correct against its own assertions and silently mis-parses the value when it eventually arrives. Then the constructive half: assume it in formal if the proof is deliberately about well-formed traffic, constrain it in random stimulus, and assert what the design does with the value instead.
18. Understanding Check
Because the maximum payload is 1500 octets, so the length interpretation cannot produce a value above 0x05DC.
That ceiling was fixed for its own reasons — Chapter 1.2 and the frame-size arguments around it — and it has a side effect: the entire sixteen-bit space above 1500 is unreachable by any legitimate length. Protocol identifiers were then assigned from 0x0600 upward, into space that no length could occupy.
So the disjointness is derived, not agreed. Nobody had to promise not to use overlapping values; the overlap is arithmetically impossible while the ceiling holds.
Which also says what would break it. Raise the payload ceiling and lengths could reach into the type space — which is why frames larger than the conventional maximum do not express their size in this field at all, and why Section 4 asserts the ordering of its two constants rather than trusting it.
And it explains the shape of the fix historically. A rule based on a value's range works on frames sent before the rule existed, which is the only kind of compatibility that was available to an installed base that could not be updated.
19. What's Next
The claim this chapter defended: the ambiguity in these two octets is real, the resolution is exact, and resolving them correctly is not the same as parsing the frame correctly.
The two meanings cannot collide because the payload ceiling of 1500 puts every legitimate length below 0x05DC and every protocol identifier at 0x0600 or above — arithmetic, not agreement, which is why the rule works on frames sent before it existed. Between them sits a band of 35 values that is neither, and a parser without a branch for it acquires one by accident from whichever comparison it happened to write last.
And then the two things a correct resolution still leaves open: where the field was, because a tag displaces it and a tag identifier is itself a type-band value that resolves successfully; and where the identity lives, because a length-form frame names its protocol at the start of its payload, through a header whose escape hatch most often carries the very identifier space the length form was meant to replace.
Chapter 5.6 — Payload, Padding and Minimum Frame Size takes the quantity this chapter kept comparing against. Section 7 reconciled a declared length with a received count and found the difference explicable only up to a floor — a floor it took as a parameter and did not justify.
5.6 justifies it, and the derivation is not a frame-format argument at all: the minimum frame size comes from Chapter 1.2's slot time, a quantity computed from the speed of propagation across a maximum-length shared segment. A field in every frame sent today is sized by a collision-detection requirement that full-duplex links removed decades ago — and 5.6 explains why it stayed, what padding costs, and what a receiver can and cannot conclude from a frame that arrived at exactly the minimum.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
Where Ethernet Stops
The payload is opaque to a MAC, and every capability that follows — one silicon design for every protocol above, including protocols invented after it shipped — depends on it staying opaque. Checksum offload is the deliberate exception, and it costs exactly what the boundary was buying.
- 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.
- 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.
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.
