Skip to content
VLSI Mentor

Ethernet · Module 19

The Receive Frame Parser

A double-tagged frame's full header fits in one beat only when the frame starts within the first three octets of it — 4.7% of the time — and the parser has 1.31 cycles per frame.

Chapter 19.1 gave this chapter two constraints and they are the whole of it.

§4: at 100 Gb/s a MAC has 1.312 clock cycles per minimum-size frame. §3: above a 128-bit datapath a beat routinely carries the end of one frame and the start of the next, so a header may begin at any offset in a beat rather than at offset zero.

Put them together and the parser's problem is stated.

What a naive parser assumesWhat is true at 100 Gb/s
where a header startsoctet 0 of a beatany of 64 offsets
how long it hasthe frame1.312 cycles
how many frames it sees at onceonetwelve — Chapter 19.1 §6

And the first row's consequence is sharper than it looks, because it decides how many beats a parse spans.

A frame's header runs from Chapter 5.1's destination address at offset 0 through the L4 header Chapter 18.7 §3's checksum engine needs. How far that is depends on Chapter 13.2's tags:

TaggingL3 starts atL4 starts atHeader through TCP
untagged143454 octets
one tag183858 octets
two tags224262 octets

A 64-octet beat holds the whole of that only if the frame starts early enough in it.

TaggingFits in one beat when the start offset isFraction of the 64 offsets
untagged0 to 1017.2%
one tag0 to 610.9%
two tags0 to 24.7%

So a double-tagged frame's full parse fits in one beat 4.7% of the time, and 95.3% of the time it spans two — which at 1.312 cycles per frame is a deficit, not a budget.

This chapter builds a parser that lives with that, and the answer is Chapter 19.1 §6's: the parse is pipelined across frames and tagged, because there is no way to spend two cycles on a frame that arrives every 1.312.


1. Scope, and the Two Constraints

Everything this chapter builds is a response to one of the two numbers above.

SectionEstablishes
2where the fields are, and what moves them
4the barrel shift, priced in muxes
6how many beats a parse spans, by tag depth and offset
9Chapter 18.7 §16's eight assumptions, costed
11what can be decided in beat zero
13the declines the parser cannot make
16pipelining the parse across frames
17what the parser owes its consumers

What this chapter does not build: the transmit side is Chapter 19.3, the CRC integration is Chapter 19.4, and the FIFOs are Chapter 19.5. Those forward references are bold and unlinked because those chapters are not yet published.

And one scoping decision is worth stating because it is not obvious. The parser does not decide whether to keep a frame — Chapter 7.4's address filter does, using the destination address this block extracts. The parser's output is a set of facts about offsets, and every consumer decides for itself what to do with them.

ConsumerWants
Chapter 7.4's filterthe destination address
Chapter 13.4's VLAN logicthe VID and the PCP
Chapter 18.7's checksum enginethe L3 and L4 offsets, and a decline flag
Chapter 18.7's RSS hashthe 5-tuple
Chapter 16.3's capturewhether this is a PTP event message
Chapter 19.7's countersthe length and the frame class

Six consumers, one parse, and the parse must produce every field any of them needs whether or not that consumer is enabled — because enabling it later must not change the parser's timing.


2. Where the Fields Are, and What Moves Them

Chapter 5.1 fixed the layout: destination at offset 0, source at 6, type at 12. Nothing after offset 12 has a fixed position.

The reason is Chapter 13.2's: four octets inserted at offset 12 move every field after them, and a frame may carry none, one or two tags — so the EtherType that actually says what the payload is sits at 12, 16 or 20.

FieldUntaggedOne tagTwo tags
destination0–50–50–5
source6–116–116–11
outer TPID12–1312–13
outer TCI14–1514–15
inner TPID16–17
inner TCI18–19
EtherType12–1316–1720–21
IPv4 header14–3318–3722–41
L4 header34–5338–5742–61

Rows one and two are the only rows that do not move, which is why Chapter 5.1 §3's argument — destination first so a receiver can abandon early — still holds at 100 Gb/s: the address filter's input is at a fixed offset and everything else is not.

And the EtherType's position is decided by its own content, which is the recursion a parser has to unwind:

The field at offset 12 is an EtherType unless it is a TPID, in which case the field at 16 is an EtherType unless it is a TPID.

Chapter 13.2 §5's TPID values are 0x8100 and 0x88A8, so the test is a comparison against a small set — and each level of the recursion is one comparison and one 4-octet shift.

Two more things move offsets and neither is a tag.

IPv4 options. The IPv4 header's length is in its own first octet — the IHL field, in the low nibble — and it is 5 words (20 octets) in the common case and up to 15 words (60 octets) otherwise. So the L4 header's offset depends on a field inside the L3 header, which is a second level of the same recursion.

And a tunnel. An encapsulated frame has a second L3 and L4 header after the first, and locating them requires the whole parse to run again at a new base offset. Chapter 18.7 §16's row five listed this as detectable "partly", and Section 13 is why.

What moves the L4 offsetHow farDetectable in beat 0?
VLAN tags+4 per tagyes — the TPID chain
IPv4 options+0 to +40yes — the IHL nibble
IPv6 extension headers+8 per header, chainedNO — unbounded chain
a tunnela whole second parseNO

Rows three and four are the parser's real limits and Section 13 is about what a design does when it meets them.


At 100 gigabits per second a 512-bit datapath delivers 64 octets per beat, and the gap between frames is 17 to 23 octets — eight of preamble and start-frame delimiter plus Chapter 5.9's interframe gap of 9 to 15. Since the gap is smaller than a beat, a beat routinely carries the end of one frame and the start of the next, so a header can begin at any of 64 offsets within a beat. The parser's first block therefore holds two consecutive beats and applies a barrel shift so that octet zero of its output window is octet zero of the frame. Doing this once is cheaper than the alternative: leaving the header where it lands means every field extractor takes the offset as an input, which puts it on ten critical paths and makes each extractor its own 64-way shift. A flat 64-way byte shift over a 64-octet window is 4096 byte-muxes and about six levels of logic; pipelined as two stages — first by zero, eight, sixteen and so on up to 56 octets, then by zero to seven — it is 1024 byte-muxes and two levels. That is a quarter of the area and a third of the depth, for one cycle of latency which the cross-frame pipelining already pays for. Everything downstream then reads constant slices: the destination address at octets zero to five and the source at six to eleven, which Chapter 5.1 fixed and Chapter 13.2's tags cannot move.A 64-octet beatgap is 17 to 23Header startsanywhereany of 64 offsetsCarry the offset10 extractors x 64-wayBarrel shift oncetwo 8-way stages1024 byte-muxes2 levelsFlat: 4096 muxes6 levelsFixed slicesdownstreamdst 0-5, src 6-11Offset on 10 pathsand a new field costs ashift12
Figure 1 — a header that begins anywhere, normalised once so that everything downstream reads a fixed layout.

3. RTL 1 — The Beat Aligner

The first block, and it exists because Chapter 19.1 §3 established that a frame may begin anywhere in a beat.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// parse_pkg -- the receive frame parser.
// -----------------------------------------------------------------------
package parse_pkg;

  localparam int DP_BYTES  = 64;         // 512-bit at 100 Gb/s
  localparam int HDR_BYTES = 64;         // the parse window
  localparam int MAX_TAGS  = 2;          // 13.2's stack depth

  // 13.2 section 5's TPID values. A third exists in some deployments
  // and a design that hard-codes two will decline those frames --
  // which is safe, and is section 13's subject.
  localparam logic [15:0] TPID_8100 = 16'h8100;
  localparam logic [15:0] TPID_88A8 = 16'h88A8;

  localparam logic [15:0] ET_IPV4   = 16'h0800;
  localparam logic [15:0] ET_IPV6   = 16'h86DD;
  localparam logic [15:0] ET_PTP    = 16'h88F7;   // 16.2's layer-2 PTP

  // What the parse produces. Every field is an OFFSET or a fact
  // about one -- the parser locates, it does not decide.
  typedef struct packed {
    logic [7:0]  frame_id;               // 19.1 section 3's tag
    logic        valid;

    logic [47:0] dst_addr;
    logic [47:0] src_addr;
    logic [15:0] ethertype;
    logic [1:0]  tag_count;
    logic [11:0] outer_vid;
    logic [2:0]  outer_pcp;

    logic [7:0]  l3_offset;              // from the frame's first octet
    logic [7:0]  l4_offset;
    logic        is_ipv4;
    logic        is_ipv6;
    logic        is_tcp;
    logic        is_udp;
    logic        is_fragment;
    logic        has_ip_options;
    logic        is_ptp;

    // The three ways a parse can be incomplete. Each has a different
    // consumer response and conflating them is section 13's error.
    logic        offsets_valid;          // l3/l4 are trustworthy
    logic        declined_known;         // a case we recognised and refused
    logic        declined_unknown;       // a case we could not classify
  } parse_t;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// beat_aligner -- presents a 64-octet window whose octet 0 is the
// frame's first octet, whatever offset the frame began at.
//
// 19.1 section 3: a beat can hold the end of one frame and the start
// of the next, so the header begins at an arbitrary offset. Every
// downstream extraction would otherwise need that offset in its own
// addressing, which is 64 variants of every field mux.
// -----------------------------------------------------------------------
module beat_aligner
  import parse_pkg::*;
(
  input  logic                        clk,
  input  logic                        rst_n,

  input  logic                        beat_valid,
  input  logic [DP_BYTES*8-1:0]       beat_data,
  input  logic                        beat_sof,
  input  logic [$clog2(DP_BYTES)-1:0] beat_sof_offset,
  input  logic [7:0]                  beat_frame_id,

  // A 2-beat window, aligned so that octet 0 is the frame's octet 0.
  output logic                        win_valid,
  output logic [HDR_BYTES*8-1:0]      win_data,
  output logic [7:0]                  win_frame_id,
  output logic                        win_complete,      // 64 octets present
  output logic [6:0]                  win_bytes,

  output logic [31:0]                 c_aligned,
  output logic [31:0]                 c_offset_zero,
  output logic [31:0]                 c_spans_two_beats,
  output logic [6:0]                  worst_offset
);

  // Two beats are held so that a header beginning late in beat n can
  // be completed from beat n+1. Section 6: at a two-tag frame the
  // header needs 62 octets, so an offset above 2 requires both.
  logic [DP_BYTES*8-1:0]       prev_data;
  logic [$clog2(DP_BYTES)-1:0] sof_off_q;
  logic [7:0]                  id_q;
  logic                        armed;

  wire [2*DP_BYTES*8-1:0] pair = {beat_data, prev_data};

  // The barrel shift. Section 4 prices it: 64 positions x 64 octets.
  // Written as a variable part-select, which synthesises to exactly
  // that and is the honest way to express it.
  assign win_data = pair[{1'b0, sof_off_q} * 8 +: HDR_BYTES*8];

  assign win_valid    = armed && beat_valid;
  assign win_frame_id = id_q;
  assign win_bytes    = 7'(DP_BYTES) - {1'b0, sof_off_q} + 7'(DP_BYTES);
  assign win_complete = (win_bytes >= 7'(HDR_BYTES));

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      prev_data <= '0; sof_off_q <= '0; id_q <= '0; armed <= 1'b0;
      c_aligned <= '0; c_offset_zero <= '0;
      c_spans_two_beats <= '0; worst_offset <= '0;
    end else if (beat_valid) begin
      prev_data <= beat_data;

      if (beat_sof) begin
        sof_off_q <= beat_sof_offset;
        id_q      <= beat_frame_id;
        armed     <= 1'b1;
        c_aligned <= c_aligned + 1;

        if (beat_sof_offset == '0) c_offset_zero <= c_offset_zero + 1;
        // Section 6: a two-tag header is 62 octets, so any offset
        // above 2 needs the following beat as well.
        if (beat_sof_offset > 8'd2)
          c_spans_two_beats <= c_spans_two_beats + 1;
        if ({1'b0, beat_sof_offset} > worst_offset)
          worst_offset <= {1'b0, beat_sof_offset};
      end
    end
  end

endmodule

Classification: a two-beat sliding window with a variable barrel shift, producing an offset-normalised header.

What it teaches: that normalising the offset once is far cheaper than carrying it into every extraction. A design that leaves the header where it lands needs every field mux to take the offset as an input — the destination address mux, the source mux, the EtherType mux, the IHL mux — and each becomes 64 variants of itself. One barrel shift at the front costs Section 4's figure once; not having it costs it per field.

And it teaches why two beats are held rather than one. Section 6: a double-tagged frame's header through the L4 is 62 octets, so a frame starting at offset 3 or beyond does not have its header in the beat it started in. The window is therefore 64 octets taken from a 128-octet pair, and win_complete says whether the second beat has arrived yet.

Deliberately simplified: the variable part-select across a 1024-bit vector is one enormous mux and is written as a part-select for clarity — a production design pipelines it into two stages of 8-way shift, which is Section 4's argument. win_bytes is computed as though both beats are always present, which is wrong on the first beat of a frame and is why win_complete exists rather than being derived. And there is one aligner, so a beat containing two frame starts — impossible at 512 bits, possible at 1024 — is not handled.

Production implication: c_offset_zero against c_aligned is the fraction of frames that began at a beat boundary, and it should be small. A port where it is near 1.0 has been tested with frames spaced far enough apart to realignChapter 19.1 §14's dual_frame_pct argument in the parser's own terms — and the barrel shift's 64 positions have been exercised at one of them.


4. The Barrel Shift, Priced

Section 3 asserted that normalising the offset is cheaper than carrying it. This section is the arithmetic, because the barrel shift is the parser's largest single structure and is worth seeing costed.

A 64-way byte-granular shift on a 64-octet output selects each output octet from 64 candidates.

What is shiftedOctets64-way byte muxes2:1 bit-muxes
destination alone63843 072
destination and source127686 144
through the EtherType221 40811 264
the full 54-octet header543 45627 648
the whole 64-octet window644 09632 768

Row five is what Section 3's aligner builds and 32 768 two-input muxes is a real structurecomparable to Chapter 19.1 §9's 512-bit CRC matrix at 16 384 XOR terms, and about twice it.

Now the alternative, and why it is worse.

A design that leaves the header in place gives each field extractor the offset and lets it address the pair directly. Each extractor is then its own 64-way shift over its own width:

ExtractorOctets64-way muxes
destination6384
source6384
outer TPID2128
outer TCI2128
inner TPID2128
inner TCI2128
EtherType — three possible positions2 × 3384
IHL nibble — three possible positions1 × 3192
IP protocol — three positions1 × 3192
fragment flags — three positions2 × 3384
total2 432

Which is less than the aligner's 4 096, and it is still the wrong answerbecause every one of those extractors now takes the offset as a timing-critical input, and the offset is produced by the same logic that detects the start of frame. The aligner's shift happens once, in its own pipeline stage, and everything downstream addresses a fixed layout.

Align onceOffset per extractor
muxes4 0962 432
stages the offset is on the critical path of110
adding a field later costsnothinganother 64-way shift
the downstream layout isfixedoffset-dependent

Row three is the one that decides it. Chapter 18.7 added the L3 and L4 offsets to the parser's outputs after the parser existed, and a design with a fixed post-alignment layout absorbed that as two more constant-offset reads. A design with per-extractor shifts absorbed it as two more 64-way shifts.

And the shift itself is pipelined rather than flat, which is how the 32 768 muxes close timing:

StageShiftMuxes
1by 0, 8, 16, 24, 32, 40, 48 or 56 octets8-way, 64 octets — 512
2by 0 to 7 octets8-way, 64 octets — 512

Two stages of 8-way shift is 1 024 byte-muxes and two levels of logic, against one stage of 64-way at 4 096 muxes and six levels. The pipelined version is a quarter of the area and a third of the depthand costs one cycle of latency, which Chapter 19.1 §6's cross-frame pipelining already pays for.


5. RTL 2 — The Field Extractor

With the window aligned, every fixed-offset field is a constant slice. The interesting part is the fields whose offset the content decides.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// field_extractor -- constant slices from an aligned window, plus the
// three content-dependent reads.
//
// Section 2: destination and source never move. Everything from
// offset 12 onward moves by 0, 4 or 8 octets depending on the tag
// stack, so those reads are three-way muxes rather than slices.
// -----------------------------------------------------------------------
module field_extractor
  import parse_pkg::*;
(
  input  logic                   clk,
  input  logic                   rst_n,

  input  logic                   win_valid,
  input  logic [HDR_BYTES*8-1:0] win_data,
  input  logic [1:0]             tag_count,        // from section 7
  input  logic                   tag_count_valid,

  output logic [47:0]            dst_addr,
  output logic [47:0]            src_addr,
  output logic [15:0]            ethertype,
  output logic [11:0]            outer_vid,
  output logic [2:0]             outer_pcp,
  output logic [7:0]             l3_offset,
  output logic                   fields_valid,

  output logic [31:0]            c_extracted,
  output logic [31:0]            c_untagged,
  output logic [31:0]            c_one_tag,
  output logic [31:0]            c_two_tags
);

  // Fixed slices. 5.1 fixed these and 13.2 cannot move them, which
  // is why 5.1 section 3's early-abandonment argument survives at
  // 100 Gb/s -- the address filter's input is a constant slice.
  assign dst_addr = win_data[0   +: 48];
  assign src_addr = win_data[48  +: 48];

  // The outer tag, if any. Read unconditionally: reading it when it
  // is not a tag costs nothing and removes it from the critical path
  // of the tag decision.
  wire [15:0] at12 = win_data[96  +: 16];
  wire [15:0] at14 = win_data[112 +: 16];
  wire [15:0] at16 = win_data[128 +: 16];
  wire [15:0] at20 = win_data[160 +: 16];

  assign outer_pcp = at14[15:13];
  assign outer_vid = at14[11:0];

  // The EtherType sits at 12, 16 or 20. A three-way mux, not a
  // 64-way one -- which is what section 3's alignment bought.
  always_comb begin
    unique case (tag_count)
      2'd0:    ethertype = at12;
      2'd1:    ethertype = at16;
      default: ethertype = at20;
    endcase
    l3_offset = 8'd14 + (8'd4 * {6'b0, tag_count});
  end

  assign fields_valid = win_valid && tag_count_valid;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_extracted <= '0; c_untagged <= '0;
      c_one_tag <= '0; c_two_tags <= '0;
    end else if (fields_valid) begin
      c_extracted <= c_extracted + 1;
      unique case (tag_count)
        2'd0: c_untagged  <= c_untagged + 1;
        2'd1: c_one_tag   <= c_one_tag + 1;
        default: c_two_tags <= c_two_tags + 1;
      endcase
    end
  end

endmodule

Classification: constant slices plus three-way muxes on the fields the tag stack moves.

What it teaches: that reading a field unconditionally and deciding later is faster than deciding first. at12, at14, at16 and at20 are all read every cycle, whether or not the frame is taggedso the tag decision is not on the read's critical path, only on the mux that follows it. A design that gates the reads on the tag count serialises a comparison in front of a slice, which costs a level of logic on the parser's tightest path for no benefit: the reads are free.

And it teaches that Section 3's alignment turned a 64-way problem into a 3-way one. The EtherType sits at 12, 16 or 20 — three positions, one 3:1 mux of 16 bits. Without alignment it would sit at offset + 12, offset + 16 or offset + 20 with offset spanning 64 values: 192 positions, and Section 4's table shows what that costs.

Deliberately simplified: tag_count arrives from Section 7 as a combinational input, so the two blocks are one logic cone in this listing and a real design registers between them. Only the outer tag's VID and PCP are extracted, where a design supporting Chapter 13.2's double tagging needs the inner one too — and the inner tag's fields are at 18–19, which is another constant slice. The IPv6 case is absent entirely; its L3 header is 40 octets rather than 20, so the L4 offset arithmetic differs.

Production implication: c_untagged, c_one_tag and c_two_tags together tell an integrator what the parser is actually seeing, and the ratio is frequently not what the network diagram says. A port believed to be on an access link — untagged — that reports a substantial c_one_tag is receiving tagged frames, which either means the switch port is configured as a trunk or something upstream is not stripping. Neither is visible from the frames' contents once the parser has stripped them, so the counters are the only evidence.


Chapter 5.1 fixed the destination address at offset zero and the source at six, and nothing after offset twelve has a fixed position. Chapter 13.2's four-octet VLAN tag moves every field after it, so the EtherType sits at twelve, sixteen or twenty depending on whether there are zero, one or two tags, and the IPv4 header follows at fourteen, eighteen or twenty-two. The layer-4 header is a further twenty octets on, at thirty-four, thirty-eight or forty-two. So the header through the layer-4 field is 54 octets untagged, 58 with one tag and 62 with two. A 64-octet beat holds all of that only if the frame started early enough in it: one beat suffices when the start offset is at most ten, six or two respectively, which is 17.2 percent, 10.9 percent and 4.7 percent of the sixty-four possible offsets. A double-tagged frame therefore spans two beats 95.3 percent of the time, and at 1.312 cycles per frame two beats is a latency deficit rather than a throughput one — which is why the parse is pipelined across frames and tagged rather than made faster.Fixed: dst 0, src65.1, and tags cannotmove themUntaggedL4 at 34; header 54One tagL4 at 38; header 58Two tagsL4 at 42; header 62The beat holds 64- kk is the start offsetk <= 1017.2% one-beatk <= 610.9%k <= 24.7% -- so 95.3% spantwoLatency, notthroughputpipeline and tag12
Figure 2 — how far into the frame the L4 header sits, and how often that fits in one beat.

6. How Many Beats a Parse Spans

Section 1 gave the fractions. This section derives them, because the derivation shows which of the three inputs a design controls.

A parse spans one beat if the frame's header fits inside the beat the frame started in. The header's length through the L4 is 14 + 4T + 20 + 20 for IPv4 with T tags — and the beat has 64 − k octets left after a frame starting at offset k.

One beat suffices when k + 54 + 4T ≤ 64, that is k ≤ 10 − 4T.

TagsHeader through L4k must beOffsets that workFraction
054 octets≤ 1011 of 6417.2%
158≤ 67 of 6410.9%
262≤ 23 of 644.7%

And k is not uniformly distributed, which is worth being careful about.

Chapter 5.9 establishes that the gap between frames is 9 to 15 octets with a bounded deficit, plus Chapter 5.2's 8 octets of preamble and SFDso consecutive frames are 17 to 23 octets apart. The next frame's start offset is therefore the previous frame's end offset plus 17 to 23, modulo 64which over a long run of same-size frames cycles through a subset of the 64 offsets rather than covering them uniformly.

Traffick behaves
identical frame sizes, back to backcycles through a fixed orbit
mixed sizesapproximately uniform
frames spaced far apartk is whatever the idle alignment gives

Row one is the case a regression must construct deliberately, and Section 20's directed test does: a run of identical minimum-size frames visits the same handful of offsets for ever, so a barrel shift that is broken at offset 37 is never exercised.

Now the consequence for the cycle budget, which is the section's point.

One-beat parseTwo-beat parse
cycles needed12
cycles available at 100 Gb/s, 64-octet frames1.3121.312
verdictfitsDEFICIT of 0.688 cycles

And 95.3% of double-tagged frames are in the right-hand column.

Which is not a problem to be solved by making the parse faster. The parse takes two beats because the header is in two beats; no amount of logic changes that. The answer is Chapter 19.1 §6's: the parse is pipelined across frames, so the parser works on frame n's second beat while frame n+1's first beat arrives, and its throughput is one frame per 1.312 cycles even though its latency is two.

LatencyThroughput
a two-beat parse2 cyclesone frame per cycle, pipelined
what the budget requiresanythingone frame per 1.312 cycles

Throughput is what the budget constrains and latency is what the structure costsand confusing them is how a design concludes the parse is impossible.


7. RTL 3 — The Tag Stack Detector

The recursion from Section 2, unrolled to a fixed depth — because a recursion cannot be unrolled at run time in 1.312 cycles.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tag_stack_detector -- how many 13.2 tags precede the EtherType.
//
// The recursion is "the field at 12 is an EtherType unless it is a
// TPID, in which case the field at 16 is...". It is unrolled to
// MAX_TAGS levels, in parallel, because a sequential unroll costs a
// cycle per level and section 6's budget has 1.312 in total.
// -----------------------------------------------------------------------
module tag_stack_detector
  import parse_pkg::*;
(
  input  logic                   clk,
  input  logic                   rst_n,

  input  logic                   win_valid,
  input  logic [HDR_BYTES*8-1:0] win_data,
  input  logic                   cfg_accept_88a8,

  output logic [1:0]             tag_count,
  output logic                   tag_count_valid,
  output logic                   stack_too_deep,     // more than MAX_TAGS

  output logic [31:0]            c_depth [3],
  output logic [31:0]            c_too_deep,
  output logic [31:0]            c_88a8_seen
);

  wire [15:0] at12 = win_data[96  +: 16];
  wire [15:0] at16 = win_data[128 +: 16];
  wire [15:0] at20 = win_data[160 +: 16];

  // One comparator per level, all three in parallel. The comparisons
  // are independent; only the PRIORITY between them is sequential,
  // and priority is a mux rather than a cycle.
  function automatic logic is_tpid(input logic [15:0] v,
                                   input logic accept_88a8);
    return (v == TPID_8100) || (accept_88a8 && (v == TPID_88A8));
  endfunction

  wire t0 = is_tpid(at12, cfg_accept_88a8);
  wire t1 = t0 && is_tpid(at16, cfg_accept_88a8);
  wire t2 = t1 && is_tpid(at20, cfg_accept_88a8);

  // MAX_TAGS is 2, so a third tag is a stack this parser refuses.
  // Refusing is correct and is section 13's point: the parse is
  // wrong beyond this depth, and saying so is better than guessing.
  assign stack_too_deep = win_valid && t2;

  always_comb begin
    if      (t1) tag_count = 2'd2;
    else if (t0) tag_count = 2'd1;
    else         tag_count = 2'd0;
  end

  assign tag_count_valid = win_valid && !stack_too_deep;

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < 3; i++) c_depth[i] <= '0;
      c_too_deep <= '0; c_88a8_seen <= '0;
    end else if (win_valid) begin
      if (stack_too_deep) c_too_deep <= c_too_deep + 1;
      else                c_depth[tag_count] <= c_depth[tag_count] + 1;

      if ((at12 == TPID_88A8) || (at16 == TPID_88A8))
        c_88a8_seen <= c_88a8_seen + 1;
    end
  end

endmodule

Classification: a fixed-depth parallel unroll of a content-dependent recursion, with an explicit refusal beyond the depth.

What it teaches: that a recursion whose depth is unbounded in the protocol must be unrolled to a fixed depth in hardware, and the design must say what it does beyond that depth. Chapter 13.2 permits a tag stack; this parser handles two and refuses three. The refusal is a correct outcome — the offsets would be wrong, and a wrong offset is Section 13's undetectable corruption — so stack_too_deep is a feature and not a limitation apologised for.

And it teaches that the three comparisons are parallel and only the priority is ordered. is_tpid(at12), is_tpid(at16) and is_tpid(at20) do not depend on each other — each reads a different constant slice — so all three fire in one level of logic, and the t1 = t0 && ... chain is an AND tree rather than a sequence. A sequential unroll would cost a cycle per level, and Section 6's budget has 1.312 in total.

Deliberately simplified: cfg_accept_88a8 is a single bit where a real design has a configurable TPID set — some deployments use 0x9100 or a provider-chosen value, and a parser that hard-codes two will decline those frames. The counters are indexed by tag_count as an array subscript on a 2-bit value into a 3-entry array, which is legal and would be a case statement. And stack_too_deep is computed from t2 alone, so a frame with three tags and a fourth is reported identically — the parser cannot count beyond what it unrolled.

Production implication: c_too_deep should be zero on almost every network, and any non-zero value is worth investigating rather than tuning away. A frame carrying three tags is provider-bridged traffic reaching an access port, or a misconfigured trunk, or a device that is stacking tags it should be swapping. The parser declines it and Chapter 18.7's offload declines with it — so the frame is delivered, unoffloaded, and works. The counter is the only sign anything unusual happened.


8. RTL 4 — The L3/L4 Locator

Section 7 found where the EtherType is. This block is the second level of the same recursion: where the L4 header is, given an L3 header whose length is inside itself.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// l3_l4_locator -- the L3 and L4 offsets 18.7 section 16 needs.
//
// The IPv4 header's length is the IHL nibble in its own first octet,
// so the L4 offset depends on a field whose position depends on the
// tag count. Two levels of content-dependence, both resolved in the
// same cycle because section 6's budget allows one.
// -----------------------------------------------------------------------
module l3_l4_locator
  import parse_pkg::*;
(
  input  logic                   clk,
  input  logic                   rst_n,

  input  logic                   win_valid,
  input  logic [HDR_BYTES*8-1:0] win_data,
  input  logic [15:0]            ethertype,
  input  logic [7:0]             l3_offset,
  input  logic                   win_complete,

  output logic [7:0]             l4_offset,
  output logic                   is_ipv4,
  output logic                   is_ipv6,
  output logic                   is_tcp,
  output logic                   is_udp,
  output logic                   is_fragment,
  output logic                   has_ip_options,
  output logic                   offsets_valid,

  output logic [31:0]            c_ipv4,
  output logic [31:0]            c_ipv6,
  output logic [31:0]            c_options,
  output logic [31:0]            c_fragments,
  output logic [31:0]            c_incomplete_window
);

  assign is_ipv4 = (ethertype == ET_IPV4);
  assign is_ipv6 = (ethertype == ET_IPV6);

  // Read the IPv4 header's first octets at the tag-dependent offset.
  // Three possible positions -- 14, 18 or 22 -- so a three-way mux,
  // which is what section 3's alignment and section 7's count bought.
  logic [7:0] ip_b0, ip_proto;
  logic [15:0] ip_flags_frag;
  always_comb begin
    unique case (l3_offset)
      8'd14: begin
        ip_b0         = win_data[112 +: 8];
        ip_flags_frag = win_data[160 +: 16];
        ip_proto      = win_data[184 +: 8];
      end
      8'd18: begin
        ip_b0         = win_data[144 +: 8];
        ip_flags_frag = win_data[192 +: 16];
        ip_proto      = win_data[216 +: 8];
      end
      default: begin
        ip_b0         = win_data[176 +: 8];
        ip_flags_frag = win_data[224 +: 16];
        ip_proto      = win_data[248 +: 8];
      end
    endcase
  end

  // IHL is in words. 5 means a 20-octet header; anything else means
  // 18.7 section 16's row four -- options, and a moved L4 offset.
  wire [3:0] ihl = ip_b0[3:0];
  assign has_ip_options = is_ipv4 && (ihl != 4'd5);

  // 18.7 section 16's row three. The fragment bit and a non-zero
  // offset both mean there is no L4 header here at all.
  assign is_fragment = is_ipv4 &&
                       (ip_flags_frag[13] || (ip_flags_frag[12:0] != 13'd0));

  assign is_tcp = is_ipv4 && !is_fragment && (ip_proto == 8'd6);
  assign is_udp = is_ipv4 && !is_fragment && (ip_proto == 8'd17);

  assign l4_offset = l3_offset + ({4'b0, ihl} << 2);

  // The offsets are only trustworthy if the whole header was in the
  // window. Section 6: at a start offset above 2 with two tags, it
  // was not -- and the second beat has to arrive first.
  assign offsets_valid = win_valid && win_complete && is_ipv4 &&
                         !is_fragment;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_ipv4 <= '0; c_ipv6 <= '0; c_options <= '0;
      c_fragments <= '0; c_incomplete_window <= '0;
    end else if (win_valid) begin
      if (is_ipv4)         c_ipv4      <= c_ipv4 + 1;
      if (is_ipv6)         c_ipv6      <= c_ipv6 + 1;
      if (has_ip_options)  c_options   <= c_options + 1;
      if (is_fragment)     c_fragments <= c_fragments + 1;
      if (!win_complete)   c_incomplete_window <= c_incomplete_window + 1;
    end
  end

endmodule

Classification: a second content-dependent read, muxed over the three positions the first level can produce.

What it teaches: that two levels of content-dependence compose into a 3-way mux rather than a 9-way one, because the second level's offset is fixed once the first is resolved. The IHL nibble is at l3_offset + 0 and l3_offset has three values — so three positions, not three times the sixty-four the un-aligned case would give. Section 3's alignment pays for itself twice, once at the EtherType and once here.

And it teaches that l4_offset is arithmetic on a field the frame supplied, which is the parser's most dangerous output. An IHL of 15 gives an L4 offset of l3_offset + 60 — 82 octets into a frame that may be 64 octets long. The offset is computed correctly from a value the frame contained, and Chapter 18.7 §16's row seven is the consumer-side version of the same exposure. Section 17 is what the parser owes about it.

Deliberately simplified: IPv6 is detected and not located — its L4 offset requires walking an extension-header chain, which Section 13 establishes is unbounded and is why offsets_valid requires is_ipv4. The fragment test reads the flags and fragment-offset field as one 16-bit word, which is correct for IPv4's layout and is written without naming the bits. And l4_offset is computed without checking it against the frame's length, which a production design must do and Section 12 does.

Production implication: c_incomplete_window against c_ipv4 is the measured version of Section 6's fraction, and comparing it against the prediction is how a design confirms its alignment logic is exercised. Section 6 predicts 95.3% for double-tagged traffic at uniformly distributed offsets; a measured 0% means frames are arriving at offset zero, which means they are spaced far enough apart to realign — and the two-beat path has not been tested.


9. Chapter 18.7's Eight Assumptions, Costed

Chapter 18.7 §16 listed eight assumptions offload makes about the traffic and said the parser must detect each one. This section is what each costs, in the two currencies the parser has: cycles and mux width.

#AssumptionWhat must be readMux widthCycles
1IPv4 or IPv6the EtherType, at 12/16/203-way × 16 bits0 — in beat 0
2TCP or UDPthe IP protocol, at L3+93-way × 8 bits0
3not a fragmentflags and offset, at L3+63-way × 16 bits0
4no IPv4 optionsIHL, at L3+0 low nibble3-way × 4 bits0
5no tunnela SECOND L3/L4 parsethe whole parser again1+ — Section 13
6tag depththe TPID chain3 × 16-bit comparators0 for depth ≤ 2
7the checksum offsetnothing — software supplied itnot the parser's
8a sane MSSnothing — transmit onlynot the parser's

Six of the eight are free, and that is the section's first finding.

Rows 1 to 4 and 6 are all reads from the aligned window at positions Section 7's tag count already resolvedthree-way muxes of between 4 and 16 bits, all in parallel, all in beat 0. Their total cost:

Bits muxed
EtherType3 × 16 = 48
protocol3 × 8 = 24
fragment flags3 × 16 = 48
IHL3 × 4 = 12
TPID comparators3 × 16 = 48
total180 bits of 3:1 mux

One hundred and eighty bits of three-way mux buys five of Chapter 18.7's eight assumptionsagainst Section 4's 32 768 two-input muxes for the barrel shift. The assumptions are 0.5% of the parser.

Row five is the expensive one and it is expensive in a structural way rather than a numerical one.

A tunnel is a second L3 and L4 header after the first. Detecting it requires knowing the tunnel protocol — VXLAN, GRE, GENEVE, IP-in-IP — and then running the whole parse again at a new base offset.

The outer parseThe inner parse
base offset0the outer L4's payload
beats it needs1 or 21 or 2 more
cycles11 more
against a 1.312-cycle budgetfitsdoes not

So a parser that handles one level of tunnelling costs roughly twice the logic and twice the latencyand the latency is affordable, because Section 6 established that latency is not what the budget constrains. The logic is the cost: a second aligner window, a second locator, a second set of muxes.

Which is why most MACs decline tunnelled traffic and Chapter 18.7 §16 marked row five "partly". The parser detects that the outer L4 is UDP on a known port and stops there, reporting the outer offsets and a decline for the inner.

Rows seven and eight are not the parser's at all, and they are in the table because Chapter 18.7 §16 listed them. Row seven — a software-supplied checksum offset — is unverifiable by anything, which that chapter established; row eight is a transmit parameter. Neither costs this block a gate.

And the section's second finding is the distribution. Five assumptions cost 0.5% of the parser; one costs 100% of it again; two cost nothing. A design deciding how much offload to support is really deciding about row five, and every other row comes along for free.


10. RTL 5 — The Parse Result Store

Chapter 19.1 §6's tagged side channel, instantiated for the parser.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// parse_result_store -- holds parse results by frame_id until their
// consumers ask.
//
// 19.1 section 6: at 1.312 cycles per frame a 16-stage pipeline
// holds twelve frames, so a result produced in this stage belongs to
// a frame that left several stages ago. The tag is the only way to
// say which.
// -----------------------------------------------------------------------
module parse_result_store
  import parse_pkg::*;
#(
  parameter int SLOTS = 16
)(
  input  logic                   clk,
  input  logic                   rst_n,

  input  logic                   res_valid,
  input  parse_t                 res,

  // Consumers read by tag, not by position.
  input  logic                   rd_valid,
  input  logic [7:0]             rd_frame_id,
  output logic                   rd_hit,
  output parse_t                 rd_res,

  input  logic                   retire_valid,
  input  logic [7:0]             retire_frame_id,

  output logic [$clog2(SLOTS+1)-1:0] occupancy,
  output logic [31:0]            c_stored,
  output logic [31:0]            c_hits,
  output logic [31:0]            c_misses,
  output logic                   store_full,
  output logic                   tag_collision      // must never assert
);

  parse_t     slot     [SLOTS];
  logic       slot_busy[SLOTS];

  // Find the slot holding this tag, and a free slot for a new one.
  logic                    hit;
  logic [$clog2(SLOTS)-1:0] hit_i, free_i;
  logic                    have_free;
  always_comb begin
    int i;
    hit = 1'b0; hit_i = '0; have_free = 1'b0; free_i = '0;
    for (i = SLOTS-1; i >= 0; i--) begin
      if (slot_busy[i] && (slot[i].frame_id == rd_frame_id)) begin
        hit   = 1'b1;
        hit_i = i[$clog2(SLOTS)-1:0];
      end
      if (!slot_busy[i]) begin
        have_free = 1'b1;
        free_i    = i[$clog2(SLOTS)-1:0];
      end
    end
  end

  assign rd_hit     = rd_valid && hit;
  assign rd_res     = slot[hit_i];
  assign store_full = !have_free;

  // Two live results with the same tag means the tag space is
  // smaller than the pipeline's occupancy -- 19.1 section 14's
  // c_results_unmatched, caught at the store instead.
  logic coll;
  always_comb begin
    int i;
    coll = 1'b0;
    for (i = 0; i < SLOTS; i++)
      if (res_valid && slot_busy[i] && (slot[i].frame_id == res.frame_id))
        coll = 1'b1;
  end
  assign tag_collision = coll;

  always_comb begin
    int i;
    occupancy = '0;
    for (i = 0; i < SLOTS; i++) if (slot_busy[i]) occupancy = occupancy + 1'b1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      for (i = 0; i < SLOTS; i++) begin
        slot[i] <= '0; slot_busy[i] <= 1'b0;
      end
      c_stored <= '0; c_hits <= '0; c_misses <= '0;
    end else begin
      if (res_valid && have_free && !tag_collision) begin
        slot[free_i]      <= res;
        slot_busy[free_i] <= 1'b1;
        c_stored          <= c_stored + 1;
      end

      if (rd_valid) begin
        if (hit) c_hits   <= c_hits + 1;
        else     c_misses <= c_misses + 1;
      end

      for (i = 0; i < SLOTS; i++)
        if (retire_valid && slot_busy[i] &&
            (slot[i].frame_id == retire_frame_id))
          slot_busy[i] <= 1'b0;
    end
  end

endmodule

Classification: a small content-addressed store keyed on a pipeline tag, with an explicit collision check.

What it teaches: that the store's depth must exceed the pipeline's occupancy and the tag space must exceed the store's depth, and the two are different requirements. Chapter 19.1 §6: twelve frames in flight at 100 Gb/s with a 16-stage pipeline — so sixteen slots is adequate. The tag is eight bits, giving 256 values, which is far more than sixteen — and that surplus is what makes tag_collision a genuine invariant rather than an expected condition.

And it teaches that consumers read by tag rather than by position, which is what lets Chapter 7.4's filter, Chapter 13.4's VLAN logic and Chapter 18.7's checksum engine consume the same parse at different pipeline depths. A store indexed by position would require every consumer to be the same number of stages away, which they are not and cannot be made to be.

Deliberately simplified: the hit and free searches are combinational over sixteen entries every cycle, which is two 16-way comparisons of an 8-bit tag in the read path; a production store uses a small CAM. rd_res is driven from slot[hit_i] unconditionally, so a miss returns slot zero's contents with rd_hit low — which is correct and will trip up a consumer that ignores rd_hit. And a result arriving when the store is full is dropped silently; a real design stalls the parse, which it may do because the parse is not the datapath.

Production implication: c_misses is the counter that catches a consumer reading a result that has already been retired, which is a pipeline-depth mismatch rather than a parser bug. A consumer eighteen stages downstream of the parser reading a store that retires at sixteen misses every time — and the symptom is that consumer behaving as though every frame were unparseable, which for Chapter 18.7's checksum engine means offload silently doing nothing.


11. What Can Be Decided in Beat Zero

Section 9 said six of eight assumptions cost no cycles. This section is the more precise statement, because "in beat zero" depends on where in beat zero the frame started.

A decision can be made in the frame's first beat if the field it reads is inside that beat. With a frame starting at offset k, the beat holds octets 0 to 63 − k of the frame.

DecisionReads octetAvailable when
is this mine?0–5k ≤ 58
who sent it?6–11k ≤ 52
how many tags?12–21k ≤ 42
IPv4 or IPv6?12/16/20k ≤ 42
IPv4 options?L3+0 — up to 22k ≤ 41
a fragment?L3+6 — up to 28k ≤ 35
TCP or UDP?L3+9 — up to 31k ≤ 32
where is L4?computed from IHLk ≤ 32
the 5-tuple for RSSthrough L4+4 — up to 46k ≤ 17
the full L4 headerthrough 61k ≤ 2

Read the right-hand column down: the decisions degrade gracefully rather than failing together.

A frame starting at offset 50 has its destination and source addresses in beat zero and nothing elseso Chapter 7.4's address filter can run and Chapter 18.7's checksum engine must wait. That is exactly the behaviour Chapter 5.1 §3's layout was designed to produce: the earliest decision is available earliest, at every alignment.

Start offset kDecisions available in beat zero
0–2all of them
3–17everything except the full L4 header
18–32through the L4 offset; not the 5-tuple
33–35through the fragment test
36–42through the tag count and EtherType
43–52destination and source only
53–58destination only
59–63none — the address is not complete

And the distribution of k decides how often each row happens. Section 6 established that consecutive frames are 17 to 23 octets apartChapter 5.9's gap plus Chapter 5.2's preamble — so on back-to-back minimum-size frames k advances by (64 + 20) mod 64 = 20 each frame and cycles through a short orbit.

FramekRow reached
10all
220through the L4 offset
340tag count and EtherType
460none
516all but the full L4 header
636tag count and EtherType
756destination only
812all but the full L4 header

A cycle of sixteen offsets0, 20, 40, 60, 16, 36, 56, 12, 32, 52, 8, 28, 48, 4, 24, 44 — and then it repeats, because 20 and 64 have a greatest common divisor of 4. So a run of identical minimum-size frames visits sixteen of the sixty-four offsets and never the other forty-eight.

Which is Section 20's directed test in one sentence, and it is why a regression of back-to-back minimum-size frames does not exercise the barrel shift.

And the practical consequence for a design is a rule about ordering the consumers. A parser that produces all its outputs together makes every consumer wait for the slowest decision — the full L4 header, available at k ≤ 2. A parser that produces them as they become available lets the address filter abandon the frame at offset 6 even when the L4 parse will not complete until the next beat, which is Chapter 5.1 §3's early-abandonment argument surviving intact at 100 Gb/s.


A parser has three distinct ways of not producing trustworthy offsets and conflating them is expensive. A known decline means the parser recognised the case and refused: an IP fragment, which has no layer-4 header at all; IPv4 options, which move the layer-4 offset; a tag stack deeper than the parser unrolled; a protocol that is neither TCP nor UDP; or a computed layer-4 offset that falls past the end of the frame, which is the parser's only defence against a hostile header-length field. An unknown decline means the parser could not classify the frame at all — a new EtherType, or a tunnel whose inner headers need a second parse. And incomplete means the header did not fit in the beat the frame started in, so the answer will be available one beat later. The third is not a decline and must not be reported as one: at 95.3 percent for double-tagged traffic, a consumer that treats incomplete as declined throws away offload on almost every frame and pays Chapter 18.7's 102.81 percent of a core in software checksums for a condition that lasted one cycle.A parse attemptone frame, one windowDeclined: knownfragment, options, tags,protoDeclined: unknowna new EtherType, a tunnelIncompletethe header spans two beatsSoftware does it18.7's 102.81% of a coreWait one beatand it parsesTreating incompleteas declined95.3% of two-tagframesoffload thrown awayoffsets_trustworthythe fourth outcome12
Figure 3 — three ways a parse can fail to produce offsets, and only two of them are declines.

12. RTL 6 — The Decline Logic

The block that turns Section 11's degradation into a statement a consumer can act on.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// parse_decline_logic -- three kinds of "I did not parse this", kept
// apart.
//
// 18.7 section 3 established that a checksum engine must be able to
// say "not computed". The parser's version has three cases and they
// have different consumer responses:
//   declined_known   -- we recognised the case and refused (a fragment)
//   declined_unknown -- we could not classify it at all (a new EtherType)
//   incomplete       -- we ran out of window; ask again next beat
// -----------------------------------------------------------------------
module parse_decline_logic
  import parse_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        win_valid,
  input  logic        win_complete,
  input  logic [15:0] ethertype,
  input  logic        is_ipv4,
  input  logic        is_ipv6,
  input  logic        is_tcp,
  input  logic        is_udp,
  input  logic        is_fragment,
  input  logic        has_ip_options,
  input  logic        stack_too_deep,
  input  logic [7:0]  l4_offset,
  input  logic [15:0] frame_bytes,

  output logic        declined_known,
  output logic        declined_unknown,
  output logic        incomplete,
  output logic        offsets_trustworthy,

  output logic [31:0] c_decline_fragment,
  output logic [31:0] c_decline_options,
  output logic [31:0] c_decline_tags,
  output logic [31:0] c_decline_proto,
  output logic [31:0] c_decline_l4_past_end,
  output logic [31:0] c_incomplete,
  output logic [31:0] c_clean
);

  // Cases we RECOGNISE and refuse. Each is a real frame type and
  // each has a counter, because 18.7 section 21's complaint 1 is
  // diagnosed by which of them dominates.
  wire d_fragment = is_ipv4 && is_fragment;
  wire d_options  = is_ipv4 && has_ip_options;
  wire d_tags     = stack_too_deep;
  wire d_proto    = (is_ipv4 || is_ipv6) && !(is_tcp || is_udp) && !is_fragment;

  // The L4 offset is computed from a field the FRAME supplied --
  // section 8. An IHL of 15 puts it 60 octets past the L3 header,
  // which on a 64-octet frame is past the end. Computing it
  // correctly from a hostile value is not the same as it being
  // usable.
  wire d_past_end = is_ipv4 && ({8'b0, l4_offset} >= frame_bytes);

  assign declined_known = win_valid &&
                          (d_fragment | d_options | d_tags | d_proto |
                           d_past_end);

  // A case we could not classify: not IPv4, not IPv6, not a tag we
  // know. The frame is fine; we simply have nothing to say about it.
  assign declined_unknown = win_valid && !is_ipv4 && !is_ipv6 &&
                            !stack_too_deep;

  // Not a decline at all: the window did not hold the whole header
  // and the answer is available next beat -- section 11.
  assign incomplete = win_valid && !win_complete &&
                      !declined_known && !declined_unknown;

  assign offsets_trustworthy = win_valid && win_complete &&
                               !declined_known && !declined_unknown;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_decline_fragment <= '0; c_decline_options <= '0;
      c_decline_tags <= '0; c_decline_proto <= '0;
      c_decline_l4_past_end <= '0; c_incomplete <= '0; c_clean <= '0;
    end else if (win_valid) begin
      if (d_fragment)  c_decline_fragment    <= c_decline_fragment + 1;
      if (d_options)   c_decline_options     <= c_decline_options + 1;
      if (d_tags)      c_decline_tags        <= c_decline_tags + 1;
      if (d_proto)     c_decline_proto       <= c_decline_proto + 1;
      if (d_past_end)  c_decline_l4_past_end <= c_decline_l4_past_end + 1;
      if (incomplete)  c_incomplete          <= c_incomplete + 1;
      if (offsets_trustworthy) c_clean       <= c_clean + 1;
    end
  end

endmodule

Classification: a three-way classification of parse failure, with per-cause accounting on one of the three.

What it teaches: that "incomplete" is not a decline and must not be reported as one. A frame whose header did not fit in beat zero will parse perfectly next beat — Section 11 — so a consumer told "declined" discards an offload opportunity that was available one cycle later. The three outcomes have three responses: a known decline means fall back to software; an unknown decline means the same; an incomplete means wait.

And it teaches that d_past_end is the parser's only defence against a hostile header. Section 8 computes l4_offset from the frame's own IHL nibble — correctly, from whatever value was there — and a value of 15 puts the L4 header 60 octets past the L3. On a 64-octet frame that is past the end, and a consumer that reads there reads the FCS or the next frame. The check is one comparison against the frame length and it is the difference between a computed offset and a usable one.

Deliberately simplified: frame_bytes arrives as an input and is not known when the parse runs on a frame still arriving — a real design either delays the check or uses a conservative bound. declined_unknown fires on every non-IP EtherType including ARP and Chapter 16.2's layer-2 PTP, which are perfectly parseable by a design that wants them; the listing treats "not IP" as "unclassified" for brevity. And IPv6 is neither declined nor located, which Section 13 addresses.

Production implication: the five decline counters are Chapter 18.7 §21's complaint-1 table with the causes separated at the source. c_decline_proto dominating means a protocol the parser does not know — on a modern network usually a tunnel. c_decline_fragment dominating means the path is fragmenting, an MTU problem elsewhere. c_decline_l4_past_end non-zero at all is worth investigating, because it means frames are arriving whose IHL and length disagree — malformed, or hostile.


13. The Declines the Parser Cannot Make

Section 12 classified three kinds of failure. This section is about a fourth, which the parser cannot report because it does not know it has happened.

Chapter 18.7 §16 marked two of its eight assumptions "NO" in the detectable column. Row six is the parser's.

The VLAN tagging matches the parser's expectation — if it does not, every offset is wrong, and there is no detection.

Section 7's detector compares against 0x8100 and optionally 0x88A8. A frame tagged with a third TPID — 0x9100, or a provider-chosen value — is read as untagged, and every offset after 12 is four octets early.

What the parser concludesWhat is actually there
EtherType at 12a TPID
IPv4 header at 14the TCI and the real EtherType
IHL at 14, low nibblethe TCI's low nibble
L4 offset = 14 + 4 × thatarithmetic on a priority code point

And the failure is silent in the specific way that matters: the parse succeeds. A TCI whose low nibble happens to be 5 produces an IHL of 5, a plausible header length, and a plausible L4 offset — and offsets_trustworthy asserts.

The probability is computable and it is not small. The parser concludes IPv4 if the 16 bits at offset 12 equal 0x0800; with an unrecognised TPID at 12, those 16 bits are the TPID, which is not 0x0800so the frame is declined_unknown, which is safe.

The dangerous case is one tag deeper. A frame with one recognised outer tag and one unrecognised inner one: the parser sees the outer TPID, shifts by 4, reads the inner TPID at 16, does not recognise it, and treats it as the EtherType. Again not 0x0800, again a safe decline.

So the actual exposure is narrower than Chapter 18.7 §16 suggests, and worth stating precisely:

CaseOutcomeSafe?
an unrecognised TPID where an EtherType is expectednot IPv4 — declinedYES
a parser expecting NO tags on a tagged frameTPID read as EtherType — declinedYES
a parser expecting a tag on an untagged framereads 4 octets into the IP headerNO — may match

Row three is the real hazard and it is a configuration error rather than a traffic one. A parser configured to strip a tag that is not present reads the IPv4 header's first four octets as a TCI and the next two as an EtherType — and the IPv4 header at offset 12 begins 45 00 followed by the total length. The "EtherType" it then reads is the total length, which for a 2 048-octet frame is 0x0800. Exactly 0x0800.

So a mis-configured parser on a 2 048-octet untagged IPv4 frame concludes it is looking at IPv4, at an offset four octets wrong, and every subsequent field is garbage that happens to parse.

Value
frame sizes whose total-length field equals 0x0800exactly one: total length 2 048
as a fraction of 1 500-octet-MTU trafficzero — 2 048 exceeds the MTU
with jumbo frames enabledone size in 9 000

Which is reassuring and is not a proof. The general form — a field read at the wrong offset landing on a value that passes the testhas no bound, and Chapter 18.7 §16's general rule stands: over-estimate a header's length rather than under-estimate it, because reading too far lands in a constrained field and reading too little lands in data.

And there is a second undetectable case that Section 9's row five introduced: the unbounded chain.

IPv6 extension headers form a linked list, each carrying the type of the next. A parser that walks them needs a loop whose depth the frame decides, which Section 6's budget does not have — so a design either unrolls to a fixed depth and declines beyond it, or declines IPv6's L4 entirely.

DesignIPv6 L4 offsetCost
decline all IPv6nevernothing; offload lost on IPv6
unroll 2 headerswhen the chain is shorttwo more levels of mux
walk the chainalwaysunbounded cycles — impossible

Row one is what Section 8's offsets_valid implements and it is the honest choice at 100 Gb/s: a parser that declines IPv6's L4 is correct, and one that guesses is Section 12's d_past_end waiting to happen.


14. RTL 7 — Parser Telemetry

The counters that make Sections 6, 9 and 11's arithmetic checkable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rxparse_telemetry -- the parser's observable state.
// -----------------------------------------------------------------------
module rxparse_telemetry
  import parse_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic        frame_seen,
  input  logic [6:0]  start_offset,
  input  logic        spans_two_beats,
  input  logic [1:0]  tag_count,
  input  logic        offsets_trustworthy,
  input  logic        declined_known,
  input  logic        declined_unknown,
  input  logic        incomplete,
  input  logic        stack_too_deep,
  input  logic        tag_collision,
  input  logic [7:0]  l3_offset,
  input  logic [7:0]  l4_offset,

  output logic [31:0] c_frames,
  output logic [31:0] c_two_beat,
  output logic [31:0] c_offset_hist [8],     // 8-octet buckets
  output logic [31:0] c_trust,
  output logic [31:0] c_decline_known,
  output logic [31:0] c_decline_unknown,
  output logic [31:0] c_incomplete,
  output logic [31:0] c_collisions,
  output logic [7:0]  worst_l4_offset,
  output logic [15:0] two_beat_pct,
  output logic [15:0] trust_pct,
  output logic [15:0] offsets_covered      // how many buckets are non-zero
);

  wire [2:0] bucket = start_offset[5:3];

  always_comb begin
    int i;
    two_beat_pct = (c_frames == '0) ? 16'd0
                   : 16'((c_two_beat * 32'd100) / c_frames);
    trust_pct    = (c_frames == '0) ? 16'd0
                   : 16'((c_trust * 32'd100) / c_frames);
    offsets_covered = '0;
    for (i = 0; i < 8; i++)
      if (c_offset_hist[i] != '0) offsets_covered = offsets_covered + 16'd1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    int i;
    if (!rst_n) begin
      c_frames <= '0; c_two_beat <= '0; c_trust <= '0;
      c_decline_known <= '0; c_decline_unknown <= '0;
      c_incomplete <= '0; c_collisions <= '0; worst_l4_offset <= '0;
      for (i = 0; i < 8; i++) c_offset_hist[i] <= '0;
    end else if (frame_seen) begin
      c_frames <= c_frames + 1;
      c_offset_hist[bucket] <= c_offset_hist[bucket] + 1;
      if (spans_two_beats)     c_two_beat        <= c_two_beat + 1;
      if (offsets_trustworthy) c_trust           <= c_trust + 1;
      if (declined_known)      c_decline_known   <= c_decline_known + 1;
      if (declined_unknown)    c_decline_unknown <= c_decline_unknown + 1;
      if (incomplete)          c_incomplete      <= c_incomplete + 1;
      if (tag_collision)       c_collisions      <= c_collisions + 1;
      if (l4_offset > worst_l4_offset) worst_l4_offset <= l4_offset;
    end
  end

endmodule

Classification: a parse-outcome accountant with a start-offset histogram.

What it teaches: that offsets_covered is a coverage measurement the design makes about its own testing, which is Chapter 19.1 §14's dual_frame_pct argument applied to the barrel shift. Section 11 showed that back-to-back identical frames visit sixteen of sixty-four offsets — a quarter — so eight buckets of eight offsets each should all be non-zero after any realistic run, and a value below eight is evidence the shift has been tested at a subset.

And it teaches that worst_l4_offset is the parser's hostile-input watermark. Section 12's d_past_end catches an offset past the frame's end; this records how far the computed offset ever got. A value above about 62 — the largest legitimate offset, two tags plus a 20-octet IPv4 headermeans an IHL above 5 has been seen, which is Section 9's row four and is legitimate; a value near 82 means an IHL of 15, which is legal IPv4 and vanishingly rare on real traffic.

Deliberately simplified: three combinational divides and an 8-way scan. The histogram is 8 buckets of 8 offsets, which is enough to see a subset and not enough to see which sixteen of Section 11's orbit are being hit — a 64-bucket version is 64 counters and would be worth it during bring-up. And the counters never reset, so offsets_covered describes the port's lifetime rather than the last run.

Production implication: trust_pct is the number that decides whether Chapter 18.7's offload is worth having on this port, and it is the parser-side version of that chapter's offload_coverage_pct. The two should agree; where they do not, the difference is consumers declining for reasons the parser did not — a checksum engine refusing a frame the parser located perfectly, which means the decline is in the consumer's own configuration rather than in the traffic.


15. RTL 8 — The Parser Conformance Monitor

The verdicts, and they divide into a fault the parser can have and three properties of the traffic it cannot fix.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// rxparse_conformance_monitor -- parse verdicts.
// -----------------------------------------------------------------------
module rxparse_conformance_monitor
  import parse_pkg::*;
(
  input  logic        clk,
  input  logic        rst_n,

  input  logic [31:0] c_frames,
  input  logic [31:0] c_trust,
  input  logic [31:0] c_decline_known,
  input  logic [31:0] c_decline_unknown,
  input  logic [31:0] c_incomplete,
  input  logic [31:0] c_collisions,
  input  logic [31:0] c_decline_l4_past_end,
  input  logic [31:0] c_decline_tags,
  input  logic [15:0] two_beat_pct,
  input  logic [15:0] trust_pct,
  input  logic [15:0] offsets_covered,
  input  logic [7:0]  worst_l4_offset,

  input  logic        tag_collision,
  input  logic        store_full,

  output logic        parser_ok,
  output logic        tag_space_fault,
  output logic        offsets_untested,
  output logic        malformed_traffic,
  output logic        tunnelled_traffic,
  output logic        provider_tags_seen,
  output logic        none_of_the_above
);

  // The only fault the parser itself can have: a tag reused while a
  // result is still live -- 19.1 section 14's class, at the store.
  assign tag_space_fault = (c_collisions != 32'd0) || store_full;

  // A coverage verdict, not a fault -- section 14. Fewer than all
  // eight buckets means the barrel shift has been exercised at a
  // subset of its 64 positions.
  assign offsets_untested = (c_frames > 32'd1_000_000) &&
                            (offsets_covered < 16'd8);

  // Frames whose IHL and length disagree. Legal IPv4 permits an IHL
  // of 15; a frame that is too short to hold it is malformed.
  assign malformed_traffic = (c_decline_l4_past_end != 32'd0);

  // Section 9 row five: a protocol the parser does not classify. On
  // a modern network this is usually an overlay.
  assign tunnelled_traffic = (c_frames > 32'd100_000) &&
                             (c_decline_unknown > (c_frames >> 4));

  // Section 13: a tag stack deeper than this parser unrolls.
  assign provider_tags_seen = (c_decline_tags != 32'd0);

  assign parser_ok = !tag_space_fault && !malformed_traffic;

  assign none_of_the_above = parser_ok && !offsets_untested &&
                             !tunnelled_traffic && !provider_tags_seen;

  // ---- properties -------------------------------------------------

  p_no_tag_collision:
    assert property (@(posedge clk) disable iff (!rst_n)
      !tag_collision)
    else $error("two live parse results shared a frame tag");

  p_outcomes_partition:
    assert property (@(posedge clk) disable iff (!rst_n)
      (c_frames > 32'd0) |->
        ((c_trust + c_decline_known + c_decline_unknown + c_incomplete)
          <= c_frames))
    else $error("the parse outcomes do not partition the frames");

  p_malformed_excludes_ok:
    assert property (@(posedge clk) disable iff (!rst_n)
      malformed_traffic |-> !parser_ok)
    else $error("parser_ok asserted with malformed traffic recorded");

  p_trust_bounded:
    assert property (@(posedge clk) disable iff (!rst_n)
      trust_pct <= 16'd100)
    else $error("trust percentage exceeded 100");

  p_l4_offset_sane:
    assert property (@(posedge clk) disable iff (!rst_n)
      worst_l4_offset <= 8'd82)
    else $error("an L4 offset exceeded the largest legal value");

endmodule

Classification: a verdict generator with one design fault and three traffic observations.

What it teaches: that only one of the five verdicts is something the parser can be blamed for. tag_space_fault is a design error — the tag space is smaller than the pipeline's occupancy, Chapter 19.1 §14's class. The other four describe the traffic: malformed frames, tunnels, provider tags, and a testbench that has not covered the offsets. A monitor that reported them with equal urgency would have an operator chasing the network for a coverage gap.

And it teaches that p_l4_offset_sane's bound of 82 is derived rather than chosen. The largest legitimate L4 offset is two tags (8) plus the 14-octet Ethernet header plus a 60-octet IPv4 header with maximum options82. Anything above that is arithmetic on a field that was not an IHL, which is Section 13's mis-tagging case detected from its consequence.

Deliberately simplified: the thresholds are literals — a sixteenth for tunnelled traffic, a million frames for the coverage verdict — where production takes them from registers. p_outcomes_partition uses rather than = because a frame can be counted in two categories in this listing's accounting; a real monitor makes the four mutually exclusive at the source. And store_full is folded into the tag verdict, which conflates a depth problem with a tag-width one.

Production implication: none_of_the_above for the eighth time across Modules 18 and 19, and this is the first one that is partly about the testbench. A port asserting it has no parse fault, no malformed traffic, no unexpected tunnelling or tagging, and a barrel shift that has been exercised across its rangewhich is a different kind of claim from the seven before it, because the last clause is about how the design was verified rather than about how it is behaving.


The start offset of each frame within a beat is the previous frame's end offset plus the gap, modulo the beat width. With 64-octet frames and a nominal 20-octet gap on a 64-octet beat, the offset advances by 20 each frame, and because the greatest common divisor of 20 and 64 is four, the sequence is a cycle of sixteen values: zero, twenty, forty, sixty, sixteen, thirty-six, fifty-six, twelve, thirty-two, fifty-two, eight, twenty-eight, forty-eight, four, twenty-four and forty-four. It then repeats for ever. So a regression of back-to-back minimum-size frames — the obvious line-rate stress test — exercises the barrel shift at sixteen of its sixty-four positions and never at the other forty-eight, and a shift broken at one position is caught with probability one in four. Worse, a bench generator that spaces frames with generous gaps realigns before every frame, so the offset is zero every time and the whole shift is exercised at a single position. The fix is cheap: Chapter 5.9's deficit mechanism permits gaps between nine and fifteen octets, so a generator that varies the gap within that legal range steers the offset and sweeps all sixty-four positions in at most sixty-four frames.k advances by thegapmodulo 64gcd(20, 64) = 4orbit of 16 offsetsLab: generous gapsk = 0 every frame1 of 64 positionsthe whole shift untested16 of 64 positionsa broken one found 1 timein 4Vary the gap 9 to 155.9 permits itAll 64 in <= 64framesthe coverage runWorks in the labfails in the field12
Figure 4 — a run of identical frames visits sixteen of the sixty-four shift positions and never the rest.

16. Pipelining the Parse Across Frames

Section 6 established that a two-beat parse cannot fit in a 1.312-cycle budget and that the answer is throughput rather than latency. This section is the structure.

The parse is four stages and none of them can be removed.

StageDoesLatency
1align — Section 3's barrel shift, pipelined in two2 cycles
2detect the tag stack — Section 71 cycle
3extract fields and locate L3/L4 — Sections 5, 81 cycle
4classify and decline — Section 121 cycle
total5 cycles

Five cycles of latency against a 1.312-cycle frame interval means 3.8 frames are in the parser at once.

Line rateCycles per frameFrames in a 5-stage parser
1 Gb/s84.0000.06
10 Gb/s10.5000.48
25 Gb/s10.5000.48
100 Gb/s1.3123.81

And the 1 Gb/s row is why a parser written for that rate has no tags: 0.06 frames in flight means the parse completes long before the next frame arrives, so "the current frame" is unambiguous and the structure works without a name for it.

At 100 Gb/s four frames are in the parser simultaneously, so every stage carries frame_id and Section 10's store is keyed on it. This is Chapter 19.1 §6's technique with a concrete depth.

Two details make it work and both are easy to get wrong.

First, the window spans two beats and therefore two frames' worth of data. Section 3's aligner holds prev_data and beat_data; at 100 Gb/s those two beats may contain parts of three frames — the end of one, the whole of a minimum-size second, and the start of a third. The aligner tracks one start-of-frame offset, so a beat with two starts is not representable — which at 512 bits cannot happen, because 64 octets holds at most one 84-octet frame slot, and is Section 17's note for wider datapaths.

Second, the stages have different latencies for different frames. A frame whose header fits in one beat completes stage 1 in two cycles; one that spans two completes in three, because the second beat has to arrive. So results emerge out of order relative to frame arrival, and Section 10's store — content-addressed by tag rather than a FIFO — is what makes that harmless.

A FIFO of resultsA tag-addressed store
in-order resultsworksworks
out-of-order resultsBREAKSworks
a consumer at a different depthbreaksworks

Row two is the one that decides it, and the out-of-order case is not rare: Section 6's fractions say 95.3% of double-tagged frames take the longer path, so a run of mixed tagging produces results out of order constantly.


17. What the Parser Owes Its Consumers

Section 1 listed six consumers. This section is the contract, because several of its clauses are not obvious and one of them is the whole of Section 13.

#The parser guaranteesOr says it cannot
1the destination address is the frame's octets 0–5always — a fixed slice
2the tag count is the number of TPIDs it recognisedstack_too_deep if deeper
3the EtherType is the field after the last tagsubject to clause 2
4l3_offset is where an IP header would startsubject to clause 3
5l4_offset is computed from the frame's own IHLd_past_end if it is past the end
6offsets_trustworthy means clauses 3 to 5 holdexplicitly false otherwise
7nothing about IPv6's L4offsets_valid requires IPv4
8nothing about a tunnel's inner headersdeclined_unknown

Clause 5 is the one worth reading twice. The parser does not guarantee that l4_offset points at an L4 header. It guarantees that l4_offset is what the frame's IHL field says, which is a different and weaker claim — and it is the strongest claim available, because the IHL is the only information about the header's length that exists.

Which means a consumer reading at l4_offset is trusting the frame, and Chapter 18.7 §16's row seven is the same exposure on the transmit side. The parser's contribution is d_past_end, which bounds the damage to the frame's own extent, and that is all hardware can do.

And clause 6 is the contract's load-bearing clause. A consumer that ignores offsets_trustworthy and reads l4_offset anyway gets a number — always a number, never an errorand computes a checksum over the wrong span. Chapter 18.7 §3's checksum_computed bit exists because of this clause, and a consumer that does not check it is the reason that bit exists.

There is one more thing the parser owes and it is about timing rather than content.

Section 11 established that decisions become available at different offsets. A parser that emits all its outputs together makes Chapter 7.4's address filter wait for the L4 parse, which it does not need — and the filter's whole purpose, Chapter 5.1 §3, is to abandon a frame six octets in.

Output groupAvailable atConsumer
destinationk ≤ 58Chapter 7.4's filter
tag count, VID, PCPk ≤ 42Chapter 13.4
L3/L4 offsetsk ≤ 32Chapter 18.7's checksum
the 5-tuplek ≤ 17Chapter 18.7's RSS

Four output groups with four availability points, and emitting them as they become ready costs four valid bits and preserves the early-abandonment property Chapter 5.1 designed the frame layout for.


18. The Cost, Accounted

Eight blocks, and one of them is 80% of the chapter.

BlockApproximate costDominated by
beat_aligner~1 200 flops + 1 024 byte-muxesthe two-stage barrel shift
field_extractor~200 flops + 180 bits of 3:1 muxthe three-way reads
tag_stack_detector~80 flops + 3 comparatorstrivial
l3_l4_locator~150 flops + a 4-bit shift-addthe IHL arithmetic
parse_result_store~2 400 flops + a 16-way CAM16 slots × ~140 bits
parse_decline_logic~200 flopscounters
rxparse_telemetry~450 flopsthe histogram
rxparse_conformance_monitor~130 flopscomparators

About 4 800 flops plus the mux structures, and the two large entries are the aligner and the store — neither of which does any parsing. The blocks that actually extract fields are 430 flops and 180 bits of mux.

Which is the chapter's cost summary in one sentence: the parse is almost free and the alignment and the bookkeeping are the design.

CostCaused by
alignment~1 200 flops + 1 024 byte-muxesChapter 19.1 §3's dual-frame beat
tagging and storage~2 400 flopsChapter 19.1 §6's 1.312 cycles
the actual parse~430 flops + 180 mux bitsChapter 5.1's layout

Both causes are Chapter 19.1's two numbers, and neither would exist at 1 Gb/s: a GMII parser has 84 cycles per frame and a frame that always starts at offset zero, so it needs no aligner and no tags. The 3 600 flops of alignment and bookkeeping are what 100 Gb/s costs a parser that would otherwise be 430.

The mux structures, placed against Chapter 19.1 §18's:

StructureSize
the CRC matrix, 512 bits16 384 XOR terms
the barrel shift, two stages1 024 byte-muxes = 8 192 2:1 bit-muxes
the barrel shift, flat4 096 byte-muxes = 32 768

The pipelined shift is half the CRC matrix; the flat one is twice itwhich is Section 4's argument placed against the MAC's other large structure.

And no memory at all. The store is flops; the window is flops; the histogram is flops. This chapter adds zero bytes of SRAM, which leaves Chapter 19.1 §18's total unchanged at about 48 KiB for a 100 Gb/s port.


19. Properties Worth Asserting, and One Worth Refusing

The parser's properties are about offsets, classification and tags. The rejected one is about classification, and it is the one a verification plan opens with.

Alignment.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The window's octet 0 is the frame's octet 0.
p_window_is_aligned:
  assert property (@(posedge clk) disable iff (!rst_n)
    win_valid |-> (win_data[7:0] == frame_octet_0))
  else $error("the aligned window did not begin at the frame's first octet");

// The shift amount is the recorded start offset.
p_shift_matches_offset:
  assert property (@(posedge clk) disable iff (!rst_n)
    (beat_valid && beat_sof) |=> (sof_off_q == $past(beat_sof_offset)))
  else $error("the aligner used a different offset from the one it was given");

// A window is only complete when both beats have arrived.
p_complete_needs_two_beats:
  assert property (@(posedge clk) disable iff (!rst_n)
    win_complete |-> armed)
  else $error("a window was declared complete before the aligner was armed");

// The offset never exceeds the datapath.
p_offset_within_beat:
  assert property (@(posedge clk) disable iff (!rst_n)
    beat_sof |-> (beat_sof_offset < DP_BYTES))
  else $error("a start-of-frame offset fell outside the beat");

// A frame starting at offset 0 needs only one beat for any tagging.
p_offset_zero_is_complete:
  assert property (@(posedge clk) disable iff (!rst_n)
    (win_valid && (sof_off_q == '0)) |-> win_complete)
  else $error("a frame at offset zero did not produce a complete window");

Field extraction.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The fixed slices are fixed. 5.1 says so and 13.2 cannot move them.
p_dst_is_octets_0_to_5:
  assert property (@(posedge clk) disable iff (!rst_n)
    fields_valid |-> (dst_addr == win_data[0 +: 48]))
  else $error("the destination address was not the window's first six octets");

p_src_is_octets_6_to_11:
  assert property (@(posedge clk) disable iff (!rst_n)
    fields_valid |-> (src_addr == win_data[48 +: 48]))
  else $error("the source address was not the window's octets 6 to 11");

// The EtherType's position follows the tag count, and only that.
p_ethertype_position:
  assert property (@(posedge clk) disable iff (!rst_n)
    (fields_valid && (tag_count == 2'd0)) |->
      (ethertype == win_data[96 +: 16]))
  else $error("an untagged frame's EtherType was not at octet 12");

// l3_offset is 14 plus four per tag, always.
p_l3_offset_arithmetic:
  assert property (@(posedge clk) disable iff (!rst_n)
    fields_valid |-> (l3_offset == (8'd14 + (8'd4 * {6'b0, tag_count}))))
  else $error("the L3 offset did not follow the tag count");

// The tag count never exceeds what the detector unrolled.
p_tag_count_bounded:
  assert property (@(posedge clk) disable iff (!rst_n)
    tag_count_valid |-> (tag_count <= 2'(MAX_TAGS)))
  else $error("a tag count exceeded the unrolled depth");

Tag detection.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// A TPID at 12 means at least one tag.
p_tpid_implies_tag:
  assert property (@(posedge clk) disable iff (!rst_n)
    (win_valid && (win_data[96 +: 16] == TPID_8100)) |-> (tag_count != 2'd0))
  else $error("a TPID at octet 12 did not produce a tag");

// No TPID at 12 means no tags.
p_no_tpid_no_tag:
  assert property (@(posedge clk) disable iff (!rst_n)
    (win_valid && (win_data[96 +: 16] != TPID_8100) &&
     (win_data[96 +: 16] != TPID_88A8)) |-> (tag_count == 2'd0))
  else $error("a frame with no TPID was reported as tagged");

// A third tag is refused rather than counted.
p_third_tag_refused:
  assert property (@(posedge clk) disable iff (!rst_n)
    stack_too_deep |-> !tag_count_valid)
  else $error("a tag count was offered for a stack that is too deep");

// 0x88A8 is only a tag when configured.
p_88a8_gated:
  assert property (@(posedge clk) disable iff (!rst_n)
    (win_valid && !cfg_accept_88a8 &&
     (win_data[96 +: 16] == TPID_88A8)) |-> (tag_count == 2'd0))
  else $error("0x88A8 was treated as a tag with acceptance disabled");

L3 and L4 location.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The L4 offset is the L3 offset plus four times the IHL.
p_l4_offset_arithmetic:
  assert property (@(posedge clk) disable iff (!rst_n)
    (win_valid && is_ipv4) |->
      (l4_offset == (l3_offset + ({4'b0, ihl} << 2))))
  else $error("the L4 offset did not follow the IHL");

// An IHL of 5 means no options.
p_ihl5_no_options:
  assert property (@(posedge clk) disable iff (!rst_n)
    (win_valid && is_ipv4 && (ihl == 4'd5)) |-> !has_ip_options)
  else $error("an IHL of 5 was reported as carrying options");

// A fragment is never reported as TCP or UDP.
p_fragment_has_no_l4:
  assert property (@(posedge clk) disable iff (!rst_n)
    is_fragment |-> (!is_tcp && !is_udp))
  else $error("a fragment was classified as TCP or UDP");

// IPv4 and IPv6 are mutually exclusive.
p_l3_exclusive:
  assert property (@(posedge clk) disable iff (!rst_n)
    !(is_ipv4 && is_ipv6))
  else $error("a frame was classified as both IPv4 and IPv6");

// Offsets are only trustworthy on a complete window.
p_offsets_need_complete:
  assert property (@(posedge clk) disable iff (!rst_n)
    offsets_valid |-> win_complete)
  else $error("offsets were declared valid on an incomplete window");

// IPv6 never produces a trustworthy L4 offset -- section 13.
p_ipv6_l4_declined:
  assert property (@(posedge clk) disable iff (!rst_n)
    is_ipv6 |-> !offsets_valid)
  else $error("an IPv6 frame produced a trustworthy L4 offset");

Decline classification and the store.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The three outcomes are mutually exclusive.
p_outcomes_exclusive:
  assert property (@(posedge clk) disable iff (!rst_n)
    $onehot0({declined_known, declined_unknown, incomplete}))
  else $error("two parse outcomes asserted together");

// Trustworthy excludes all three.
p_trust_excludes_declines:
  assert property (@(posedge clk) disable iff (!rst_n)
    offsets_trustworthy |->
      (!declined_known && !declined_unknown && !incomplete))
  else $error("offsets were trustworthy alongside a decline");

// An incomplete window is not a decline.
p_incomplete_is_not_decline:
  assert property (@(posedge clk) disable iff (!rst_n)
    incomplete |-> (!declined_known && !declined_unknown))
  else $error("an incomplete window was reported as a decline");

// An L4 offset past the frame's end is always refused.
p_past_end_declined:
  assert property (@(posedge clk) disable iff (!rst_n)
    ({8'b0, l4_offset} >= frame_bytes) |-> !offsets_trustworthy)
  else $error("an L4 offset past the frame's end was offered as trustworthy");

// A result is only stored under a tag not already live.
p_store_tag_unique:
  assert property (@(posedge clk) disable iff (!rst_n)
    !tag_collision)
  else $error("two live results shared a tag");

// A read hit returns the result stored under that tag.
p_read_returns_its_tag:
  assert property (@(posedge clk) disable iff (!rst_n)
    rd_hit |-> (rd_res.frame_id == rd_frame_id))
  else $error("a tagged read returned another frame's result");

// A retired tag is no longer readable.
p_retire_removes:
  assert property (@(posedge clk) disable iff (!rst_n)
    retire_valid |=> !(rd_valid && (rd_frame_id == $past(retire_frame_id))
                       && rd_hit))
  else $error("a retired result was still readable");

20. Verification Scenarios

Fifty-eight scenarios, plus a six-run directed test that requires a frame generator which controls the start offset.

Alignment — 11 scenarios.

#ScenarioExpected
1a frame at offset 0window complete in one beat
2a frame at offset 2, two tagscomplete — the boundary case
3a frame at offset 3, two tagsspans two beats
4a frame at offset 10, untaggedcomplete — the boundary case
5a frame at offset 11, untaggedspans two beats
6a frame at offset 63one octet in the beat; nothing decidable
7every offset 0 to 63the aligned window is identical in all 64
8back-to-back frames sharing a beattwo starts, one per beat — handled
9the shift's two stages8-way then 8-way = the 64-way result
10a frame preceded by a long idleoffset is whatever alignment gives
11win_bytes on the first beat of a framewin_complete low

Field extraction — 10 scenarios.

#ScenarioExpected
12untagged IPv4EtherType at 12, L3 at 14
13one tagEtherType at 16, L3 at 18
14two tagsEtherType at 20, L3 at 22
150x88A8 outer, acceptance oncounted as a tag
160x88A8 outer, acceptance offNOT a tag; EtherType is 0x88A8
17three tagsstack_too_deep, count invalid
18the VID and PCP of a single tagfrom octets 14–15
19a broadcast destinationextracted as a fixed slice
20the address at offset 58the last offset where it is complete
21ARP (0x0806)declined_unknown

L3 and L4 location — 10 scenarios.

#ScenarioExpected
22IPv4, IHL 5, TCPL4 at L3+20; is_tcp
23IPv4, IHL 6has_ip_options; L4 at L3+24; declined
24IPv4, IHL 15L4 at L3+60; d_past_end on a short frame
25IPv4, UDPis_udp
26IPv4, ICMPd_proto, declined
27IPv4 with the more-fragments bitis_fragment, declined
28IPv4 with a non-zero fragment offsetis_fragment, declined
29IPv6is_ipv6; offsets_valid LOW
30IPv6 with extension headersdeclined — Section 13
31two tags plus IHL 15 on a 64-octet framed_past_end

Decline classification — 9 scenarios.

#ScenarioExpected
32a clean untagged IPv4 TCP frameoffsets_trustworthy
33an incomplete windowincomplete, NOT a decline
34the same frame one beat laternow trustworthy
35a fragmentdeclined_known, c_decline_fragment
36an unknown EtherTypedeclined_unknown
37three tagsdeclined_known, c_decline_tags
38the three outcomes togetherimpossible — $onehot0
39trustworthy with a declineimpossible
40an L4 offset equal to the frame lengthd_past_end

The result store — 9 scenarios.

#ScenarioExpected
41store one, read it back by taghit, same contents
42read a tag never storedmiss; rd_hit low
43four frames in flight, read out of orderall four hit
44sixteen slots filledstore_full
45a seventeenth resultdropped; the parse stalls
46two live results with one tagtag_collision
47retire a tag, then read itmiss
48a consumer 18 stages downstream, store of 16c_misses rises
49occupancy against 19.1's 12 framespeaks near 12

Telemetry and verdicts — 9 scenarios.

#ScenarioExpected
50back-to-back 64-octet frames16 of 64 offsets visited; offsets_covered = 8
51mixed frame sizesall 8 buckets
52frames spaced far apartoffset 0 every time; offsets_covered = 1
53a million frames, one bucketoffsets_untested
54an L4 offset above 82property fires
55a tag collisiontag_space_fault, not parser_ok
56d_past_end non-zeromalformed_traffic, not parser_ok
57more than a sixteenth unknowntunnelled_traffic
58everything clean and coverednone_of_the_above

The directed test — six runs random stimulus will not produce.

Section 11 established the problem: a run of identical minimum-size frames advances the start offset by 20 each frame and visits sixteen of the sixty-four offsets0, 20, 40, 60, 16, 36, 56, 12, 32, 52, 8, 28, 48, 4, 24, 44and never the other forty-eight. A random frame-size generator does better and still does not cover: it visits offsets in proportion to the size distribution, and a barrel shift broken at one position is found only by chance.

So the test needs a generator that controls k directly, which means controlling the interframe gap frame by frame — and Chapter 5.9's deficit mechanism means the gap is 9 to 15 octets, so k can be steered by ±3 per frame and swept.

Construct it. Six runs, one variable.

RunStimulusOffsets visitedExpected
A64-octet frames, minimum gap, sustained16 of 64 — the orbitpasses; covers a quarter
Bmixed sizes, random gaps~all, unevenlypasses; no guarantee
Cgap steered to sweep k = 0…63all 64, once eachthe coverage run
Dk swept, two tags, IPv4 TCPall 6495.3% span two beats
Ek swept, shift broken at 37all 64FAILS at k = 37 only
Fk = 0 only, shift broken at 371 of 64passes — the false pass

Run F is the point. A barrel shift with one broken position passes every test that does not visit that position — and run A, the obvious "line rate, minimum frames" stress, visits sixteen. The probability that a single broken position is among them is 16 in 64, one in four; for a shift broken at two positions it is better, and for one broken position it is a coin flip weighted against the tester.

Run C is the fix and it is cheap: a generator that varies the gap within Chapter 5.9's legal 9-to-15 range sweeps k across all 64 positions in at most 64 frames.

And run D is the one that exercises both paths deliberately. Section 6: a two-tagged frame parses in one beat only at k ≤ 2, and the orbit contains exactly one such offset — k = 0. So run A exercises the one-beat path once in sixteen frames and the two-beat path fifteen times in sixteen. That ratio is the right way round and it is luck: a different frame size gives a different orbit, and an orbit containing several small offsets would exercise the two-beat path hardly at all.

The oracle, in four parts:

CheckRuns A–DRun ERun F
aligned window against the generator's framematches at every kdiffers at k = 37matches — k = 0 only
offsets_covered8 in C and D, ≤ 4 in A81
p_classification_matches_injectionpassesFAILSpasses
p_unparseable_declinespassesPASSESpasses

Row four is Section 19's rejected class demonstrated. At k = 37 the window is shifted wrongly, so the parser reads garbage and classifies it as whatever the garbage saysand if it says "not IPv4", the frame is declined_unknown and the rejected property is satisfied. Only row three, which compares against what the generator injected, fails.


21. Debugging a Parser

Three complaints, and the first is the one this chapter exists for.

Complaint 1 — "offload is disabled on most frames and the traffic looks ordinary."

CheckIf yesMeaning
c_incomplete large?the window is not completeSection 6 — a two-beat parse
is the consumer treating incomplete as a decline?it must notSection 12's three outcomes
c_decline_unknown dominant?a protocol the parser does not classifyusually a tunnel
c_decline_options dominant?IPv4 optionsrare, and real
trust_pct against 18.7's coverage?they should agreea gap means the consumer declines

Row two is the bug this chapter predicts and it is a consumer-side error. A checksum engine told "incomplete" and treating it as "declined" throws away an offload that would have been available one cycle later — and on a port where 95.3% of double-tagged frames span two beats, that is almost all of them.

Complaint 2 — "some frames are parsed wrongly and there is no error."

CheckIf yesMeaning
is the parser configured to strip a tag?Section 13's row threean untagged frame is read four octets early
c_decline_l4_past_end non-zero?an IHL that does not fitmalformed, or the above
worst_l4_offset above 82?arithmetic on a non-IHLconfirmed misparse
does disabling tag stripping fix it?confirmsa configuration fault

Row four is the test to run first because it is a register write rather than an investigation, and Section 13 establishes that this is the one mis-tagging case with no detection.

Complaint 3 — "it works in the lab and fails in the field."

CheckIf yesMeaning
offsets_covered below 8?the barrel shift is under-testedSection 20's run F
were frames spaced apart in the lab?offset 0 every timeone of 64 positions
does the field traffic have mixed sizes?it visits more offsetsincluding a broken one
c_offset_hist concentrated?a short orbitSection 11

Row two is the lab's fault and it is the most common cause of this complaint in this chapter. A bench generator sending frames with generous gaps realigns before every frame, so k is zero always — and the whole barrel shift is exercised at one position.

And the two symptoms this chapter is systematically blamed for:

SymptomBlamed onUsually is
offload disabled on most framesthe checksum enginea consumer treating "incomplete" as "declined"
occasional misparsed frames in the field onlythe traffica barrel-shift position the lab never visited

22. Misconceptions

Misconception 1 — "a frame starts at the beginning of a beat."

The wrong model: the datapath delivers frames aligned; the header is at octet 0 of the first beat.

What it costs: a parser that reads the destination address from the wrong six octets on every frame that shares a beat with its predecessor — which at 100 Gb/s with minimum-size frames is essentially all of them, because Chapter 19.1 §3's beat is 64 octets and the gap is 17 to 23.

The corrected model: a header begins at any of 64 offsets, and normalising that once with a barrel shift is cheaper than carrying the offset into every extraction. 1 024 byte-muxes in two pipelined stages against ten extractors each needing their own 64-way shift. Sections 3, 4.

Misconception 2 — "the parse has 1.31 cycles, so it must fit in one."

The wrong model: the budget is 1.312 cycles per frame, so the parse must complete in one.

What it costs: a design that concludes the parse is impossible and either narrows the datapath — quadrupling the clock, which Chapter 19.1 §16 shows the CRC cannot meet — or drops the L4 offsets and with them Chapter 18.7's offload.

The corrected model: the budget constrains throughput, not latency. A five-stage parse with five cycles of latency produces one result per cycle once pipelined, and holds 3.81 frames at 100 Gb/s — each carrying Chapter 19.1 §3's frame_id. Sections 6, 16.

Misconception 3 — "incomplete means declined."

The wrong model: the parser could not produce the offsets, so the frame is not offloadable.

What it costs: offload thrown away on 95.3% of double-tagged frames, because their headers span two beats and the answer arrives one cycle later. The port then does its checksums in software — Chapter 18.7 §2's 102.81% of a core — for a reason that lasts one cycle.

The corrected model: there are three outcomes, not two: a known decline, an unknown decline, and incomplete. The first two mean fall back; the third means wait. Section 12.

Misconception 4 — "a parser that cannot classify a frame is a limitation."

The wrong model: declining frames is a shortfall to be engineered away.

What it costs: a design that guesses. A tag stack deeper than the unroll, an IPv6 extension chain, a tunnel — each has an offset the parser cannot determine, and computing one anyway produces Section 12's d_past_end at best and a silently wrong checksum at worst.

The corrected model: a decline is a correct outcome and the parser's most important output is the bit that says so. Chapter 18.7 §3 needed "not computed" as a third value for exactly this reason, and a parser that cannot decline forces its consumers to trust offsets that were guessed. Sections 12, 13.

Misconception 5 — "the tag detection is the expensive part."

The wrong model: unwinding Chapter 13.2's recursion is the parser's hard problem.

What it costs: attention on three 16-bit comparators — 80 flops — while the aligner and the result store, 3 600 flops between them, get none.

The corrected model: the recursion unrolls to a fixed depth in parallel and costs almost nothing. The parser's cost is 80% alignment and bookkeeping: 1 200 flops plus 1 024 byte-muxes for the barrel shift, 2 400 for the tagged result store, and 430 for every field extraction in the chapter. Section 18.

Misconception 6 — "assert that every unparseable frame is declined."

The wrong model: the property states the requirement, it passes, the requirement is covered.

What it costs: a vacuous pass on the exact frames that matter. The antecedent is computed from the parser's own classification, so a frame the parser misclassifies as IPv4 is not "unparseable" by the property's definition — and the property is satisfied while the offsets are wrong.

The corrected model: a property about a classifier must draw its cases from outside the classifier. The antecedent is what the generator built, not what the parser concluded, and the two differ exactly on the misclassifications. Section 19.


23. Interview Questions

Q1 — "Where does a frame's header start, on a 100 Gb/s MAC?"

Anywhere in the beat. Chapter 19.1 §3: a 512-bit beat is 64 octets and the gap between frames is 17 to 23Chapter 5.2's 8 octets of preamble plus Chapter 5.9's 9-to-15 gap — so a beat routinely carries the end of one frame and the start of the next. The parser therefore begins with a 64-way barrel shift, pipelined as two 8-way stages: 1 024 byte-muxes and two levels of logic, against 4 096 and six for a flat shift.

Q2 — "How many beats does a parse span?"

One or two, and which depends on the tagging and the start offset. A frame's header through the L4 is 54 octets untagged, 58 with one tag, 62 with two — so one beat suffices when the start offset k satisfies k ≤ 10 − 4T. That is 17.2%, 10.9% and 4.7% of the 64 offsets respectivelyso 95.3% of double-tagged frames span two beats. At 1.312 cycles per frame that is a deficit in latency and not in throughput, which is the distinction that matters.

Q3 — "The budget is 1.31 cycles and your parse takes five. Explain."

The budget constrains throughput, not latency. A five-stage parse pipelined across frames produces one result per cycle and holds 3.81 frames at 100 Gb/s — each tagged with Chapter 19.1 §3's frame_id, because "the current frame" names four things. The results emerge out of order — a one-beat parse finishes before a two-beat one that started earlier — so the result store is content-addressed by tag rather than a FIFO.

Q4 — "What does Chapter 18.7's offload cost the parser?"

Six of its eight assumptions are free and one costs the parser again. The EtherType, the IP protocol, the fragment flags, the IHL and the TPID chain are all reads from the aligned window at positions the tag count already resolved — 180 bits of three-way mux, 0.5% of the parser. The eighth, a tunnel, needs a second L3 and L4 parse at a new base offset: a second aligner window, a second locator, roughly double the logic. Which is why most MACs decline tunnelled traffic.

Q5 — "A parser configured to strip a tag meets an untagged IPv4 frame. What happens?"

It reads four octets into the IP header and may conclude IPv4 anyway. The IPv4 header begins 45 00 followed by the total length, so the "EtherType" it reads is the total length — and for a 2 048-octet frame that is exactly 0x0800. The parse then succeeds at an offset four octets wrong, with offsets_trustworthy asserted. Chapter 18.7 §16 marked this undetectable and it is; the general defence is to over-estimate a header's length rather than under-estimate it, because reading too far lands in a constrained field and reading too little lands in data.

Q6 — "You assert that every unparseable frame is declined. It passes. Is the parser verified?"

No — the property cannot fail. Its antecedent is computed from the parser's own classification, so a frame the parser misclassifies as IPv4 is not "unparseable" by the property's definition and the antecedent never fires on it. The property is exercised thousands of times on genuinely unparseable frames and never on the case that matters. The fix is to move the universe outside the design: the antecedent must be what the generator built, not what the parser concluded — and the two differ exactly on the misclassifications.


24. Understanding Check


25. What's Next

Module 19 has five chapters left and this one has handed each of them something.

ChapterBuildsTakes from here
Chapter 19.3 — the transmit assemblerpadding, CRC append, the gapthe mirror of the alignment problem
Chapter 19.4 — the CRC enginethe matrix, integratedthe dual-frame case, shared
Chapter 19.5 — the FIFOsthe clock crossingthe depths
Chapter 19.6 — the memory interfacerequest shaping
Chapter 19.7 — the countersRMON, saturatingthe frame classification

Chapter 19.3 is the mirror and its alignment problem runs the other way. This chapter received frames at arbitrary offsets and normalised them. The assembler must place frames at offsets that satisfy Chapter 5.9's gap — which is 9 to 15 octets with a bounded deficit, so the transmitter chooses k rather than discovering it, and the choice is constrained by a deficit accumulator rather than by a barrel shift.

And it inherits a harder version of one constraint. This parser may take five cycles because its output is advisory — a consumer waits, and nothing has left the building. The assembler's output is on a cable, so Chapter 19.1 §5's rule applies: a transmit stage may stall before commit and not after, and the commit point is where the assembler's latency stops being free.

Chapter 19.4 takes the dual-frame case this chapter met in the aligner and meets it again in the accumulator. A beat holding the end of one frame and the start of another needs two CRC accumulators — one finalising, one initialising, in the same cycle — which is the same structural consequence as this chapter's two-beat window, arriving at a different block from the same 64-octet beat and 17-to-23-octet gap.

Continue learning

Standards & specifications

Governing standard
IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)

Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the Ethernet curriculum.