Skip to content
VLSI Mentor

Ethernet · Module 13

The 802.1Q Tag

Four octets inserted at offset 12 move every field after them, and four octets is half a word on any datapath wider than 32 bits. A parser cannot know the offsets without parsing.

Chapter 13.1 consumed a VID on every interface and never said where it came from. This is where it comes from: four octets, inserted into the middle of the frame.

Not appended. Not prefixed. Inserted — after the two addresses, before the EtherType — which means every field offset after octet 12 moves by four.

FieldUntagged offsetTagged offset
destination address00
source address66
TPID12
TCI — PCP, DEI, VID14
EtherType1216
payload1418

And four octets is not a whole word on any datapath a modern switch uses. On a 32-bit bus it is exactly one word and costs nothing. On the 64-, 128- and 256-bit datapaths that carry 10, 25 and 100 Gb/s, it is half a word, a quarter and an eighth — so inserting or removing it misaligns everything after it and requires a barrel shifter in the middle of the fastest path in the design.

1. Scope — What This Chapter Owns

This chapter owns the four octets and their consequences for a parser: the TPID and how a tag is recognised, the TCI's three fields, what the insertion does to every downstream offset, the length and rate arithmetic, the datapath realignment, and how deep a tag stack a design must tolerate.

It does not own insertion and stripping. Where a tag is added, where it is removed, what an access port does with an untagged frame and what a trunk does with one are Chapter 13.3's subject. This chapter parses a frame as it arrives and stops there.

It does not own the datapath. VID lookup structure, per-VLAN table organisation and the mapping from PCP into egress queues are Chapter 13.4. This chapter decodes PCP and hands it on.

It does not own why VLANs existChapter 13.1 established the requirement and priced the state. This chapter supplies the VID that chapter consumed.

And it depends heavily on Chapter 5.5. The length/type ambiguity is what makes tag detection a comparison against a value rather than a flag, and Chapter 5.5's resolution rule is the machinery Section 4 extends.

2. The Four Octets

A tag is 32 bits in two halves, and the split is between what this is and what it says.

BitsFieldWidthMeaning
31:16TPID — Tag Protocol Identifier160x8100. Occupies the position an EtherType would
15:13PCP — Priority Code Point38 priority levelsChapter 13.4's queue mapping
12DEI — Drop Eligible Indicator1this frame may be discarded first under congestion
11:0VID — VLAN Identifier124096 values, 4094 usableChapter 13.1

The lower 16 bits are collectively the TCI — the Tag Control Information — and the three fields in it are unrelated to each other.

PCP is a priority. It says nothing about which VLAN the frame is in and everything about how urgently it should be served. Chapter 13.1 §6 categorised the egress arbiter as unchanged by VLANs and said priority was a different mechanism — this is that mechanism, riding in the same four octets for reasons of frame economy rather than of logic.

DEI is a congestion hint. Under Chapter 12.1 §9's tail drop, a switch discarding from a full queue may prefer frames with DEI set. It is advisory in both directions: a sender marks it, and a switch is not obliged to honour it.

And VID is the field Chapter 13.1 spent a chapter consuming. 12 bits, 0 and 4095 reserved, 4094 usable.

3. RTL 1 — Recognising a Tag

A tag is recognised by a value in a position, and the position is one an EtherType would otherwise occupy. Chapter 5.5's ambiguity is what makes this a comparison rather than a flag.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// q_tag_pkg -- shared types for 802.1Q tag parsing.
// -----------------------------------------------------------------------
package q_tag_pkg;

  localparam logic [15:0] TPID_8100 = 16'h8100;  // 802.1Q -- customer tag
  localparam logic [15:0] TPID_88A8 = 16'h88A8;  // 802.1ad -- service tag
  localparam logic [15:0] TPID_9100 = 16'h9100;  // legacy, still in the wild

  // Chapter 5.5's boundary: a value below 1536 at the type position is a
  // LENGTH; at or above it is a TYPE. Every TPID is well above it.
  localparam logic [15:0] TYPE_MIN  = 16'd1536;

  localparam int VID_W = 12;
  localparam int PCP_W = 3;

  typedef struct packed {
    logic [PCP_W-1:0] pcp;
    logic             dei;
    logic [VID_W-1:0] vid;
  } tci_t;

  typedef enum logic [2:0] {
    PS_UNTAGGED   = 3'd0,
    PS_SINGLE     = 3'd1,
    PS_DOUBLE     = 3'd2,
    PS_TOO_DEEP   = 3'd3,   // more tags than the design will parse
    PS_TRUNCATED  = 3'd4,   // frame ended inside a tag
    PS_AMBIGUOUS  = 3'd5    // Chapter 5.5's length/type case
  } parse_state_e;

  // Offsets are OUTPUTS of the parse, never constants. Section 17's
  // rejected property is about designs that treat them as constants.
  typedef struct packed {
    logic [5:0] ethertype_off;
    logic [5:0] payload_off;
    logic [5:0] header_len;
  } offsets_t;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tpid_detector -- is there a tag at this position, and is it one we
// recognise?
//
// The detection is a VALUE COMPARISON at a position, not a flag. Nothing
// in the frame says "a tag follows"; the parser looks at octets 12-13,
// and if they hold a TPID it is a tag, and if they hold anything else it
// is Chapter 5.5's EtherType-or-length.
// -----------------------------------------------------------------------
module tpid_detector
  import q_tag_pkg::*;
#(
  parameter logic ACCEPT_88A8 = 1'b1,
  parameter logic ACCEPT_9100 = 1'b0,   // legacy, off by default
  parameter int   CNT_W       = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             probe_valid,
  input  logic [15:0]      probe_value,   // the 16 bits at the position

  output logic             is_tag,
  output logic             is_ctag,       // 0x8100
  output logic             is_stag,       // 0x88A8
  output logic             is_length,     // Chapter 5.5: below 1536
  output logic             is_ethertype,

  output logic [CNT_W-1:0] c_untagged,
  output logic [CNT_W-1:0] c_ctag,
  output logic [CNT_W-1:0] c_stag,
  output logic [CNT_W-1:0] c_unknown_tpid  // looks like a tag, is not one
);

  assign is_ctag = (probe_value == TPID_8100);
  assign is_stag = (ACCEPT_88A8 && (probe_value == TPID_88A8)) ||
                   (ACCEPT_9100 && (probe_value == TPID_9100));
  assign is_tag  = is_ctag || is_stag;

  // CHAPTER 5.5's RULE, unchanged. A value below 1536 at the type
  // position is a length. Every TPID is far above it, so a tag can never
  // be confused with a length -- which is why the TPID values were
  // chosen where they were.
  assign is_length     = probe_valid && (probe_value < TYPE_MIN);
  assign is_ethertype  = probe_valid && !is_tag && !is_length;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_untagged     <= '0;
      c_ctag         <= '0;
      c_stag         <= '0;
      c_unknown_tpid <= '0;
    end else if (probe_valid) begin
      if (is_ctag)          c_ctag     <= c_ctag + 1'b1;
      else if (is_stag)     c_stag     <= c_stag + 1'b1;
      else begin
        c_untagged <= c_untagged + 1'b1;
        // A value that is one of the OTHER standardised TPIDs but which
        // this design was not configured to accept. It will be parsed as
        // an EtherType, and the four octets after it will be read as
        // payload -- silently, and every offset downstream is wrong.
        if ((probe_value == TPID_88A8) || (probe_value == TPID_9100))
          if (!(&c_unknown_tpid)) c_unknown_tpid <= c_unknown_tpid + 1'b1;
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that nothing in a frame announces that a tag follows. There is no flag, no length field and no marker. The parser reads octets 12–13 and asks what value is there — and the answer is a tag, an EtherType, or Chapter 5.5's length, decided entirely by the number.

And it teaches why the TPID values sit where they do. 0x8100 is 33 024, far above Chapter 5.5's 1536 boundary, so a tag can never be mistaken for a length. That was not luck — every TPID was allocated in the type range precisely so the existing resolution rule would classify it without change.

Deliberately simplified: three TPID values with two of them behind parameters. Some deployments use others, and a production detector often makes the accepted set programmable, because a switch in the middle of somebody else's network may meet a TPID its designers did not choose.

Production implication: c_unknown_tpid is the counter for a failure with no other symptom. A frame carrying 0x88A8 on a design configured to accept only 0x8100 is parsed as an EtherType of 0x88A8 with the next four octets read as payload. Every offset after that is wrong by four, the destination and source addresses are still correct so the frame forwards normally, and the switch has silently misparsed a frame it will nonetheless deliver. The counter is the only evidence, and it exists only if somebody thought to compare against the TPIDs the design rejects.

4. RTL 2 — Decoding the Control Field

Sixteen bits, three fields, three consumers that have nothing to do with each other.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tci_decoder -- splits the Tag Control Information and validates the VID.
//
// The three fields go to three different places: the VID to Chapter
// 13.1's key and masks, the PCP to Chapter 13.4's egress queues, and the
// DEI to Chapter 12.1's discard policy. They share four octets and
// nothing else.
// -----------------------------------------------------------------------
module tci_decoder
  import q_tag_pkg::*;
#(
  parameter int CNT_W = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             tci_valid,
  input  logic [15:0]      tci_raw,

  output tci_t             tci,
  output logic             vid_usable,      // 1..4094
  output logic             vid_priority_only,  // VID 0 -- Chapter 13.3
  output logic             vid_reserved,       // VID 4095
  output logic [2:0]       priority_level,
  output logic             drop_eligible,

  output logic [CNT_W-1:0] c_by_pcp [8],
  output logic [CNT_W-1:0] c_dei_set,
  output logic [CNT_W-1:0] c_vid_zero,
  output logic [CNT_W-1:0] c_vid_4095
);

  // The split is fixed and positional -- 3, 1, 12, most significant
  // first. There is no version field and no options, which is why the
  // decode is wires rather than logic.
  assign tci.pcp = tci_raw[15:13];
  assign tci.dei = tci_raw[12];
  assign tci.vid = tci_raw[11:0];

  assign priority_level = tci.pcp;
  assign drop_eligible  = tci.dei;

  // VID 0 does NOT mean "no VLAN" in the sense of an untagged frame. It
  // means "this tag carries PRIORITY only" -- the frame is tagged, has a
  // PCP worth honouring, and its VLAN is whatever the receiving port's
  // default is. Chapter 13.3 owns that rule; this module only classifies.
  assign vid_priority_only = tci_valid && (tci.vid == 12'd0);
  assign vid_reserved      = tci_valid && (tci.vid == 12'd4095);
  assign vid_usable        = tci_valid && !vid_priority_only && !vid_reserved;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < 8; i++) c_by_pcp[i] <= '0;
      c_dei_set  <= '0;
      c_vid_zero <= '0;
      c_vid_4095 <= '0;
    end else if (tci_valid) begin
      c_by_pcp[tci.pcp] <= c_by_pcp[tci.pcp] + 1'b1;
      if (tci.dei)          c_dei_set  <= c_dei_set + 1'b1;
      if (vid_priority_only) c_vid_zero <= c_vid_zero + 1'b1;
      if (vid_reserved)      c_vid_4095 <= c_vid_4095 + 1'b1;
    end
  end

endmodule

Classification: synthesizable, and almost entirely wires — the decode is positional with no options and no version field.

What it teaches: that VID 0 is not "untagged". A frame with VID 0 is tagged — it has a TPID, it has four extra octets, every offset after it has moved, and its PCP is meaningful. What it does not have is a VLAN assignment, and the receiving port supplies one. A design that treats VID 0 as untagged will parse the frame at the wrong offsets, because the tag is physically there.

And it teaches that the three fields have three unrelated consumers. VID goes to Chapter 13.1's key and masks. PCP goes to Chapter 13.4's egress queue mapping. DEI goes to Chapter 12.1 §9's discard policy — a hint that this frame is a preferred victim when the queue is full. None of them constrains the others, and a design that routes all three through one path has coupled things the format merely co-located.

Deliberately simplified: a per-PCP frame counter and nothing else. Production designs also count bytes per PCP, because the queue-mapping decision Chapter 13.4 makes is about bandwidth rather than frame count, and a priority level carrying many small frames and one carrying few large ones need different treatment.

Production implication: c_by_pcp is the histogram that tells an operator whether priority marking is being used at all, and the common finding is that it is not. A distribution with everything in PCP 0 means no sender is marking, so Chapter 13.4's queue mapping is configured, consuming egress queues, and doing nothing. A distribution with everything in PCP 7 means somebody marked all their traffic as highest priority, which has the same effect by a different route — and both are invisible without the histogram.

5. RTL 3 — Resolving the Offsets

This is the module the chapter is really about. Every field position after octet 12 is an output of the parse, and a design that treats any of them as a constant has assumed the answer.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// offset_resolver -- walks the tag stack and produces the offsets of
// everything after it.
//
// THE CENTRAL POINT: the offsets are OUTPUTS. A parser cannot know where
// the EtherType is until it has determined how many tags precede it, and
// it cannot determine that without reading each candidate position in
// turn. The resolution is inherently sequential.
// -----------------------------------------------------------------------
module offset_resolver
  import q_tag_pkg::*;
#(
  parameter int MAX_TAGS = 2,        // Section 9 -- the stack must be bounded
  parameter int CNT_W    = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             start,
  input  logic             probe_ready,     // a 16-bit probe is available
  input  logic [15:0]      probe_value,
  input  logic [5:0]       probe_offset,
  input  logic [13:0]      frame_len,

  output logic [5:0]       next_probe_offset,
  output logic             need_probe,

  output logic             done,
  output parse_state_e     state,
  output offsets_t         offsets,
  output logic [2:0]       tag_count,
  output tci_t             outer_tci,
  output tci_t             inner_tci,

  output logic [CNT_W-1:0] c_parsed,
  output logic [CNT_W-1:0] c_too_deep,
  output logic [CNT_W-1:0] c_truncated
);

  logic [2:0] tags_q;
  logic [5:0] off_q;
  logic       busy_q;

  logic is_tag;
  assign is_tag = (probe_value == TPID_8100) || (probe_value == TPID_88A8);

  assign need_probe        = busy_q && !done;
  assign next_probe_offset = off_q;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      tags_q      <= '0;
      off_q       <= 6'd12;
      busy_q      <= 1'b0;
      done        <= 1'b0;
      state       <= PS_UNTAGGED;
      offsets     <= '0;
      tag_count   <= '0;
      outer_tci   <= '0;
      inner_tci   <= '0;
      c_parsed    <= '0;
      c_too_deep  <= '0;
      c_truncated <= '0;
    end else begin
      done <= 1'b0;

      if (start) begin
        // Every parse begins at octet 12 -- the one position that is
        // fixed, because the two addresses before it are fixed-width.
        tags_q <= '0;
        off_q  <= 6'd12;
        busy_q <= 1'b1;
      end else if (busy_q && probe_ready) begin
        // A frame that ends inside a tag cannot be parsed at all, and
        // must not be parsed as though the missing octets were zero.
        if ({8'd0, probe_offset} + 14'd4 > frame_len) begin
          busy_q <= 1'b0;
          done   <= 1'b1;
          state  <= PS_TRUNCATED;
          if (!(&c_truncated)) c_truncated <= c_truncated + 1'b1;

        end else if (is_tag) begin
          if (tags_q >= 3'(MAX_TAGS)) begin
            // Section 9: the stack must be bounded. An unbounded walk is
            // an unbounded parse time on the fastest path in the design.
            busy_q <= 1'b0;
            done   <= 1'b1;
            state  <= PS_TOO_DEEP;
            if (!(&c_too_deep)) c_too_deep <= c_too_deep + 1'b1;
          end else begin
            tags_q <= tags_q + 3'd1;
            off_q  <= off_q + 6'd4;
          end

        end else begin
          // Not a tag. This position holds Chapter 5.5's EtherType or
          // length, and the walk is complete.
          busy_q    <= 1'b0;
          done      <= 1'b1;
          tag_count <= tags_q;
          state     <= (tags_q == 3'd0) ? PS_UNTAGGED
                     : (tags_q == 3'd1) ? PS_SINGLE : PS_DOUBLE;

          // THE OUTPUTS. Every one of these is a function of how many
          // tags were found, which was not knowable before the walk.
          offsets.ethertype_off <= probe_offset;
          offsets.payload_off   <= probe_offset + 6'd2;
          offsets.header_len    <= probe_offset + 6'd2;

          if (!(&c_parsed)) c_parsed <= c_parsed + 1'b1;
        end
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the walk is sequential and its length is data-dependent. Octet 12 is the only fixed position in the whole structure — it is fixed because the two addresses before it are fixed-width, and for no other reason. Everything after it depends on what was found.

And it teaches that a truncated frame must not be parsed optimistically. A frame ending inside a tag has four octets that do not exist, and reading them as zeros produces TPID = 0x0000, which is below Chapter 5.5's 1536 boundary and therefore parses as a length of zero — a structurally plausible result from missing data. PS_TRUNCATED refuses instead.

Deliberately simplified: a one-probe-per-cycle walk against a probe interface. A production parser speculatively reads octets 12–13, 16–17 and 20–21 in parallel and selects among the results, because Chapter 12.6 §5 established that a cut-through commit at 10 Gb/s leaves 11.2 ns — and a three-cycle sequential walk at 500 MHz is 6 ns of it.

Production implication: offsets being a bus rather than a set of parameters is the design decision this module exists to force. Every downstream consumer — the EtherType comparison, the payload start, the header length used for the realignment in Section 8 — takes its position from this output. A design where any of them uses a constant works perfectly on untagged traffic and misparses every tagged frame, and the addresses are before the tag so the frame still forwards correctly, which is why Section 17's rejected property survives review.

A parser begins at octet twelve, which is the only fixed position in an Ethernet frame after the two fixed-width addresses. It reads the sixteen bits there and compares them against the known tag protocol identifiers. If the value is a tag identifier, four octets have been consumed and the parser advances to octet sixteen and repeats. If the value is not a tag identifier, the walk is complete and this position holds Chapter 5.5's EtherType or length field. The offsets of the EtherType, the payload and the header length are therefore outputs of the walk rather than constants, and their values depend on how many tags the frame happened to carry: an untagged frame puts the EtherType at octet twelve, a single tagged frame at sixteen, and a double tagged frame at twenty. A frame that ends inside a tag must be refused rather than parsed with the missing octets read as zeros, because zeros parse as a length of zero and produce a structurally plausible result from absent data.Probe octet 12the only fixed positionIs it a TPID?a value comparisonAdvance 4, probeagainoctet 16, then 20Bounded at MAX_TAGSPS_TOO_DEEPNot a TPID — doneEtherType or length hereOffsets are outputs12, 16 or 20Ends inside a tagrefuse — zeros parse as alength12
Figure 1 — octet 12 is the only fixed position; everything after it is an output of a sequential walk whose length the frame itself decides.

6. RTL 4 — Length, Padding and the Rate Cost

Four octets change the maximum frame, leave the minimum alone, and cost 4.55% of the frame rate. Each of those is worth deriving rather than remembering.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tagged_length_checker -- validates a frame's length against the limits
// its own tag count implies.
//
// Chapter 7.3's validity rules take a tag-dependent maximum: 1518
// untagged, 1522 with one tag, 1526 with two. The MINIMUM does not move
// -- the tag consumes padding, not headroom.
// -----------------------------------------------------------------------
module tagged_length_checker
  import q_tag_pkg::*;
#(
  parameter int MIN_FRAME  = 64,
  parameter int MAX_UNTAG  = 1518,
  parameter int CNT_W      = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             chk_valid,
  input  logic [13:0]      frame_len,       // octets, DA through FCS
  input  logic [2:0]       tag_count,

  output logic [13:0]      max_allowed,
  output logic [13:0]      payload_floor,   // minimum payload after padding
  output logic             len_ok,
  output logic             oversize,
  output logic             undersize,

  output logic [CNT_W-1:0] c_oversize,
  output logic [CNT_W-1:0] c_undersize,
  output logic [CNT_W-1:0] c_by_tags [3]
);

  // THE MAXIMUM MOVES. Each tag adds 4 octets that the original limit did
  // not contemplate, and a switch that checks against 1518 discards every
  // maximum-length tagged frame -- as an oversize error, on frames that
  // are perfectly legal.
  assign max_allowed = 14'(MAX_UNTAG) + (14'(tag_count) * 14'd4);

  // THE MINIMUM DOES NOT. A 64-octet frame stays 64 octets; what changes
  // is how much of it is header. Untagged: 14 header + 46 payload + 4
  // FCS. Single-tagged: 18 + 42 + 4. The tag eats padding.
  assign payload_floor = 14'(MIN_FRAME) - 14'd4 - 14'd14 -
                         (14'(tag_count) * 14'd4);

  assign oversize  = chk_valid && (frame_len > max_allowed);
  assign undersize = chk_valid && (frame_len < 14'(MIN_FRAME));
  assign len_ok    = chk_valid && !oversize && !undersize;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_oversize  <= '0;
      c_undersize <= '0;
      for (int i = 0; i < 3; i++) c_by_tags[i] <= '0;
    end else if (chk_valid) begin
      if (oversize)  if (!(&c_oversize))  c_oversize  <= c_oversize + 1'b1;
      if (undersize) if (!(&c_undersize)) c_undersize <= c_undersize + 1'b1;
      if (tag_count < 3'd3) c_by_tags[tag_count] <= c_by_tags[tag_count] + 1'b1;
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the maximum moves and the minimum does not, and the asymmetry follows from where the tag sits. The maximum is a limit on the whole frame, so four more octets of header means four more octets of frame — 1518 → 1522 → 1526. The minimum is also a limit on the whole frame, and Chapter 5.6's padding already fills it — so the tag consumes padding rather than extending the frame, and the payload floor drops from 46 octets to 42.

And it teaches the failure a fixed 1518 produces. A switch checking every frame against 1518 discards every maximum-length tagged frame as oversize — legal frames, rejected, counted as errors. The symptom is that large transfers fail while small ones work, on a link with no errors at the physical layer, and it points at an MTU problem several layers up.

Deliberately simplified: limits derived arithmetically from the tag count. Production designs often make the maximum programmable because jumbo support and provider tagging both move it, and a hard-wired 1522 fails the same way 1518 does one tag later.

Production implication: c_by_tags is the distribution that says what the port is actually carrying, and it is the first thing to check when a link between two administrative domains misbehaves. A port expecting untagged traffic and receiving single-tagged frames will parse every one of them at the wrong offsets — and because the addresses precede the tag, the frames still forward. The histogram distinguishes this port receives tagged traffic from this port is configured for it in one look.

7. The Rate Cost, Derived

A tag adds four octets to every frame that carries one, and on minimum-length frames that is a measurable fraction of a link's capacity.

FrameOverhead of 4 octets
64 octets6.25%
128 octets3.12%
512 octets0.78%
1518 octets0.26%

And in frames per second, which is the number Chapter 12.1 §12's forwarding engine is sized against:

On-wire octetsBitsRate at 1 Gb/s
untagged minimum, 6464 + 20 = 846721.4881 Mpps
tagged minimum, 6868 + 20 = 887041.4205 Mpps
change−4.55%

The 20 octets are Chapter 5.2's preamble and start delimiter plus the interframe gap, unchanged by tagging.

Which produces a small but real relaxation nobody plans for. Chapter 12.1 §12 sized a 24-port gigabit switch's forwarding engine at 35.71 Mpps against untagged minimum frames. On fully tagged traffic that becomes 24 × 1.4205 M = 34.09 Mpps — a 4.55% reduction in the worst-case demand, and the per-lookup budget rises from 28 ns to 29.3 ns.

It is not a design margin worth spending, because a switch must handle untagged traffic too and is therefore sized against the harder case. What it is worth is knowing which number a measurement was taken against: a throughput test run with tagged frames measures a 4.55% easier problem than one run without, and the two results are not comparable.

Why two throughput measurements are not comparable

Section 7's 4.55% is small enough to ignore as a design margin and large enough to invalidate a comparison, and the distinction is worth stating because throughput numbers get quoted without their conditions.

A minimum-length frame test run with untagged frames offers 24 × 1.4881 M = 35.71 Mpps to the forwarding engine. The same test with tagged frames offers 24 × 1.4205 M = 34.09 Mpps — because each frame is four octets longer on the wire and therefore fewer of them fit in a second.

Test conditionOffered loadPer-lookup budget
untagged 64-octet frames35.71 Mpps28.0 ns
tagged 68-octet frames34.09 Mpps29.3 ns
difference−4.55%+4.7% more time

So a switch that passes a tagged line-rate test has passed a measurably easier one, and two vendors' figures taken under different conditions differ by 4.55% before any hardware is compared.

It is not a margin worth spending in a design — a switch must handle untagged traffic and is therefore sized against 28 ns regardless. It is a number worth recording next to a measurement, because "line rate, 64-octet frames" and "line rate, 64-octet tagged frames" are different tests and only one of them stresses the worst case.

8. RTL 5 — Realigning a Datapath

Four octets is exactly one word on a 32-bit bus and half a word on a 64-bit one. That single fact decides how expensive tagging is in the datapath, and it is not monotonic in the number of tags.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// datapath_realigner -- shifts a frame's octets after a tag insertion or
// removal so that the payload stays word-aligned.
//
// On a 32-bit datapath this module is empty: 4 octets is one word, and
// inserting a whole word shifts nothing. On 64 bits and above every octet
// after the tag lands in a different lane, and a barrel shifter sits in
// the middle of the fastest path in the design.
// -----------------------------------------------------------------------
module datapath_realigner
  import q_tag_pkg::*;
#(
  parameter int W_OCT = 8,          // datapath width in octets
  parameter int CNT_W = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 in_valid,
  input  logic [W_OCT*8-1:0]   in_data,
  input  logic [$clog2(W_OCT+1)-1:0] shift_octets,   // 0 or 4, typically

  output logic                 out_valid,
  output logic [W_OCT*8-1:0]   out_data,

  output logic                 realign_needed,
  output logic [7:0]           shifter_muxes,   // cost, in 8:1 mux-equivalents
  output logic [CNT_W-1:0]     c_realigned,
  output logic [CNT_W-1:0]     c_aligned
);

  // THE COST TEST. A shift that is a whole number of datapath words moves
  // no octet between lanes and needs no shifter at all.
  localparam int TAG_OCT = 4;
  localparam logic ALIGNED_BY_CONSTRUCTION = ((TAG_OCT % W_OCT) == 0);

  assign realign_needed = !ALIGNED_BY_CONSTRUCTION && (shift_octets != '0);

  // A barrel shifter over W_OCT lanes, each 8 bits, with W_OCT+1
  // positions. The cost grows with the datapath width -- which is the
  // opposite of the direction a designer moving to 100 Gb/s wants.
  assign shifter_muxes = 8'(W_OCT);

  logic [2*W_OCT*8-1:0] window_q;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      window_q    <= '0;
      out_valid   <= 1'b0;
      out_data    <= '0;
      c_realigned <= '0;
      c_aligned   <= '0;
    end else begin
      out_valid <= in_valid;
      if (in_valid) begin
        // Two words held so that a shift can draw octets from both --
        // any shift that is not a whole word needs octets that arrived
        // in the previous cycle.
        window_q <= {window_q[W_OCT*8-1:0], in_data};

        if (realign_needed) begin
          out_data <= window_q[(W_OCT*8 + 8*shift_octets) -: (W_OCT*8)];
          if (!(&c_realigned)) c_realigned <= c_realigned + 1'b1;
        end else begin
          out_data <= in_data;
          if (!(&c_aligned)) c_aligned <= c_aligned + 1'b1;
        end
      end
    end
  end

endmodule

Classification: synthesizable, and the one module in this chapter whose area depends strongly on a parameter.

What it teaches: that the alignment cost is a property of the datapath width and the tag width together, and neither alone.

DatapathOctets per word4-octet insertRealigner needed
32-bit4exactly 1 wordno
64-bit8half a wordyes
128-bit16quarter of a wordyes
256-bit32eighth of a wordyes

And the counter-intuitive consequence: a double tag is 8 octets, which realigns a 64-bit datapath. So on a 64-bit design, an untagged frame and a double-tagged frame are both aligned and a single-tagged frame is not — the cost is not monotonic in the number of tags, and a design that tested with untagged and Q-in-Q traffic has exercised only the easy cases.

Deliberately simplified: a two-word sliding window with a single shift amount. Production designs pipeline the shifter and often avoid it entirely on the receive path by starting the frame at a deliberate offset in the buffer, so that the payload lands aligned regardless of tag count — trading a few octets of buffer for the shifter.

Production implication: shifter_muxes grows with W_OCT, which is the wrong direction. Moving from 10 Gb/s to 100 Gb/s widens the datapath from 64 bits to 256 or more, and the realigner grows with it — the shifter is 32 lanes of 8-bit mux on a 256-bit path, sitting in the middle of a datapath whose timing is already the hardest in the design. The buffer-offset trick is not an optimisation at those widths; it is how it is done.

9. RTL 6 — Bounding the Tag Stack

Section 5's walk advances four octets per tag. Nothing in a frame limits how many tags it carries, so the design must.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tag_stack_limiter -- caps the parse depth and says what it did.
//
// A frame may carry any number of tags -- nothing in the format bounds it,
// and 355 nested tags fit inside a 1518-octet frame. An unbounded walk is
// an unbounded parse time on the critical path, so the depth must be a
// design constant and a frame exceeding it must be handled explicitly
// rather than parsed partially.
// -----------------------------------------------------------------------
module tag_stack_limiter
  import q_tag_pkg::*;
#(
  parameter int MAX_TAGS   = 2,
  parameter int CNT_W      = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             probe_valid,
  input  logic             probe_is_tag,
  input  logic [2:0]       depth_so_far,

  output logic             continue_walk,
  output logic             depth_exceeded,
  output logic [5:0]       max_parse_cycles,
  output logic [13:0]      max_header_octets,

  output logic [CNT_W-1:0] c_depth [4],
  output logic [CNT_W-1:0] c_exceeded,
  output logic [2:0]       deepest_seen
);

  // The walk continues only while the depth budget allows it. A frame
  // beyond the budget is NOT parsed further, and it is not parsed
  // partially either -- a partial parse produces offsets that are wrong
  // by a multiple of four, which is exactly the failure Section 8 warns
  // about.
  assign continue_walk  = probe_valid && probe_is_tag &&
                          (depth_so_far < 3'(MAX_TAGS));
  assign depth_exceeded = probe_valid && probe_is_tag &&
                          (depth_so_far >= 3'(MAX_TAGS));

  // The parse's worst case, as a design constant. This is what a
  // cut-through commit budget must accommodate -- Chapter 12.6 Section 5.
  assign max_parse_cycles  = 6'(MAX_TAGS + 1);
  assign max_header_octets = 14'd14 + (14'(MAX_TAGS) * 14'd4);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < 4; i++) c_depth[i] <= '0;
      c_exceeded   <= '0;
      deepest_seen <= '0;
    end else begin
      if (probe_valid && !probe_is_tag)
        if (depth_so_far < 3'd4)
          c_depth[depth_so_far] <= c_depth[depth_so_far] + 1'b1;

      if (depth_exceeded) begin
        if (!(&c_exceeded)) c_exceeded <= c_exceeded + 1'b1;
      end

      if (probe_is_tag && (depth_so_far + 3'd1 > deepest_seen))
        deepest_seen <= depth_so_far + 3'd1;
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that nothing in the frame format bounds the tag stack. A tag is recognised by the value at a position, that value can appear again four octets later, and 355 nested tags fit inside a 1518-octet frame. A parser that walks until it finds something that is not a TPID has an unbounded loop on the critical path of the fastest datapath in the design.

And it teaches that exceeding the depth must be handled, not truncated. A parser that stops walking at MAX_TAGS and treats the next position as the EtherType produces offsets that are wrong by four for every extra tag — and the frame still forwards, because the addresses precede the whole stack. PS_TOO_DEEP from Section 5 refuses; a partial parse would be Section 8's failure with a different cause.

Deliberately simplified: a depth counter with a fixed maximum. Production designs often accept a configurable depth up to some hard ceiling, because a provider network's frames carry more tags than an enterprise's, and the ceiling exists in the silicon regardless of what the configuration allows.

Production implication: max_parse_cycles and max_header_octets are the numbers a cut-through design must accommodate, and they are where this chapter meets Chapter 12.6. That chapter's commit point was octet 14 — which is the untagged header length. With MAX_TAGS = 2 the header may be 22 octets, so a cut-through switch on tagged traffic cannot commit at 14: it must wait for the parse to complete, which at 1 Gb/s is 176 ns instead of 112. A design that commits at a fixed octet 14 on tagged traffic commits before it knows the frame's VLAN, which is Chapter 13.1's key and mask decided on data that has not arrived.

10. Where This Chapter Meets Cut-Through

Chapter 12.6 established that five of six forwarding gates have their inputs by octet 14. Tagging moves that boundary, and the movement is data-dependent.

untaggedone tagtwo tags
header length141822
VID available at octet1616 — the outer tag
EtherType available at121620
earliest safe commit141822
at 1 Gb/s112 ns144 ns176 ns
at 10 Gb/s11.2 ns14.4 ns17.6 ns

The commit point is no longer a constant, and a design must choose between two honest options.

Commit at the maximum — 22 octets for MAX_TAGS = 2 — which costs 176 ns at 1 Gb/s regardless of whether the frame is tagged, and is simple, predictable and always correct.

Or commit when the parse completes, which is 112 ns on untagged frames and 176 ns on double-tagged ones — faster on average and variable, which reintroduces exactly the jitter Chapter 12.6 §6 argued cut-through exists to remove.

And there is a third option that is not honest: commit at 14 regardless. On a tagged frame that commits before the VID has been read — the VID is at octets 14–15, inside the tag — so Chapter 13.1's key and flood mask are decided on a VLAN the switch does not yet know. The frame is forwarded into whatever VLAN the default happens to be. Isolation is violated, silently, by a timing decision.

11. RTL 7 — Accounting for What the Tag Costs

Overhead, rate and alignment are three separate costs with three separate consumers, and a switch should compute all three rather than quoting one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// tagged_rate_accountant -- what tagging costs this port, measured.
//
// Section 7 derived the numbers for minimum frames. Real traffic is a
// distribution, so the overhead a port actually pays depends on its frame
// sizes and its tagged fraction -- both of which the port can measure.
// -----------------------------------------------------------------------
module tagged_rate_accountant
  import q_tag_pkg::*;
#(
  parameter int CNT_W  = 40,
  parameter int WINDOW = 1_000_000
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             frame_valid,
  input  logic [13:0]      frame_len,
  input  logic [2:0]       tag_count,

  output logic [CNT_W-1:0] octets_total,
  output logic [CNT_W-1:0] octets_tags,       // spent on tags alone
  output logic [CNT_W-1:0] c_frames,
  output logic [CNT_W-1:0] c_tagged,

  output logic             window_valid,
  output logic [15:0]      tag_overhead_pct_x100,
  output logic [15:0]      tagged_fraction_pct,
  output logic [15:0]      mean_frame_len,
  output logic             overhead_significant   // above 2%
);

  logic [CNT_W-1:0] win_frames;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      octets_total          <= '0;
      octets_tags           <= '0;
      c_frames              <= '0;
      c_tagged              <= '0;
      win_frames            <= '0;
      window_valid          <= 1'b0;
      tag_overhead_pct_x100 <= '0;
      tagged_fraction_pct   <= '0;
      mean_frame_len        <= '0;
      overhead_significant  <= 1'b0;
    end else begin
      window_valid <= 1'b0;

      if (frame_valid) begin
        // On-wire octets: the frame plus Chapter 5.2's preamble and
        // delimiter plus the interframe gap, which tagging does not
        // change.
        octets_total <= octets_total + CNT_W'(frame_len) + CNT_W'(20);
        octets_tags  <= octets_tags  + (CNT_W'(tag_count) * CNT_W'(4));
        c_frames     <= c_frames + 1'b1;
        win_frames   <= win_frames + 1'b1;
        if (tag_count != 3'd0) c_tagged <= c_tagged + 1'b1;
      end

      if (win_frames >= CNT_W'(WINDOW)) begin
        // Overhead as a share of everything on the wire, x100 so that
        // sub-percent values -- which is what large frames produce -- are
        // still readable.
        tag_overhead_pct_x100 <= (octets_total == '0) ? 16'd0
                               : 16'((octets_tags * CNT_W'(10_000)) / octets_total);
        tagged_fraction_pct   <= 16'((c_tagged * CNT_W'(100)) / c_frames);
        mean_frame_len        <= 16'(octets_total / c_frames) - 16'd20;
        // Above 2% is worth knowing about: it means the port carries
        // mostly small tagged frames, and Section 7's 6.25% floor is
        // being approached.
        overhead_significant  <= ((octets_tags * CNT_W'(50)) > octets_total);
        win_frames            <= '0;
        window_valid          <= 1'b1;
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the overhead a port actually pays is a property of its frame-size distribution, not a constant. Section 7's table gives the bounds — 6.25% on minimum frames, 0.26% on maximum — and a port's real figure sits somewhere between, decided by traffic nobody controls. tag_overhead_pct_x100 measures it rather than assuming.

And it teaches why the scaling is by 10 000 rather than 100. A port carrying mostly large frames pays 0.26%, which as an integer percentage is zero — and a metric that reads zero for the common case is a metric nobody looks at. Two extra decimal digits make it a number instead of a rounding artefact.

Deliberately simplified: a single window over all traffic. Production accounting keeps the distribution per VLAN, because a VLAN carrying voice — all minimum-length frames — pays close to the 6.25% floor while a VLAN carrying bulk transfer pays close to 0.26%, and the aggregate is a mean over two populations that behave differently.

Production implication: tagged_fraction_pct against the port's configuration is the check worth running at commissioning. A port configured as an access port and receiving 100% tagged frames is misconfigured — every frame is being parsed at offsets the configuration does not expect. A port configured as a trunk and receiving 0% tagged frames is also misconfigured, and the frames are landing in the native VLAN by default rather than by intent. Both are silent, both forward traffic, and the single number separates them from a correctly configured port.

What the tagged fraction says about a port

Section 11's tagged_fraction_pct against the port's configuration is a two-line check that catches the two commonest VLAN misconfigurations, and both of them forward traffic.

Configurationtagged_fraction_pctVerdict
access port0%correct
access port100%misconfigured — every frame parsed at the wrong offsets
access portbetweena mixture, which is worse than either
trunk port100%correct
trunk port0%misconfigured — frames land in the native VLAN by default
trunk portbetweenexpected only if a native VLAN is configured

The two error rows are the ones worth dwelling on, because their symptoms differ completely.

An access port receiving tagged frames parses each one at untagged offsets — the EtherType read from octets 12–13 is actually the TPID, and everything after is four octets out. The frame still forwards, and the damage is Section 13's: an application at the far end reading shifted fields.

A trunk port receiving untagged frames has no VID to work with. Chapter 13.3's native-VLAN rule assigns one, so the frames are placed in a VLAN by default rather than by intent — and Chapter 13.1's isolation is applied faithfully to a VLAN assignment nobody chose.

Neither produces an error, and the single ratio separates them from a correct port — which is why it belongs on a commissioning checklist rather than in a debugging session.

12. RTL 8 — Conformance for a Variable-Layout Parse

The monitor's difficulty is that there is no fixed layout to check against, so it must check the parse's internal consistency instead.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// parse_conformance_monitor -- checks that the resolved offsets are
// consistent with the tag count that produced them.
//
// There is no golden layout to compare against, because the layout is a
// function of the frame. What CAN be checked is that the offsets the
// parser produced follow from the tags it found, that no downstream
// consumer used a different offset, and that a refused parse produced no
// offsets at all.
// -----------------------------------------------------------------------
module parse_conformance_monitor
  import q_tag_pkg::*;
#(
  parameter int MAX_TAGS = 2,
  parameter int CNT_W    = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             parse_done,
  input  parse_state_e     state,
  input  offsets_t         offsets,
  input  logic [2:0]       tag_count,
  input  logic [13:0]      frame_len,

  // What downstream consumers actually used.
  input  logic             consumer_valid,
  input  logic [5:0]       consumer_ethertype_off,
  input  logic [5:0]       consumer_payload_off,

  output logic [CNT_W-1:0] v_offset_mismatch,   // offsets vs tag count
  output logic [CNT_W-1:0] v_consumer_disagrees,// a hard-coded constant
  output logic [CNT_W-1:0] v_offsets_on_refusal,
  output logic [CNT_W-1:0] v_depth_exceeded_parsed,
  output logic [CNT_W-1:0] c_checked,
  output logic             conformant
);

  logic [5:0] expected_et;
  assign expected_et = 6'd12 + (6'(tag_count) * 6'd4);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_offset_mismatch       <= '0;
      v_consumer_disagrees    <= '0;
      v_offsets_on_refusal    <= '0;
      v_depth_exceeded_parsed <= '0;
      c_checked               <= '0;
    end else begin
      if (parse_done) begin
        c_checked <= c_checked + 1'b1;

        // INTERNAL CONSISTENCY. The offsets must follow arithmetically
        // from the tag count. This catches a parser whose walk and whose
        // offset computation have drifted apart -- for instance one that
        // advances the walk by 4 and the offset by 2.
        if ((state inside {PS_UNTAGGED, PS_SINGLE, PS_DOUBLE}) &&
            (offsets.ethertype_off != expected_et))
          if (!(&v_offset_mismatch))
            v_offset_mismatch <= v_offset_mismatch + 1'b1;

        // A REFUSED parse must produce no offsets. Offsets on a
        // truncated or over-deep frame are offsets into data that was
        // never validated.
        if ((state inside {PS_TRUNCATED, PS_TOO_DEEP}) &&
            (offsets.ethertype_off != 6'd0))
          if (!(&v_offsets_on_refusal))
            v_offsets_on_refusal <= v_offsets_on_refusal + 1'b1;

        if ((state == PS_TOO_DEEP) && (tag_count <= 3'(MAX_TAGS)))
          if (!(&v_depth_exceeded_parsed))
            v_depth_exceeded_parsed <= v_depth_exceeded_parsed + 1'b1;
      end

      // THE SECTION 17 CHECK. A downstream consumer using an offset that
      // differs from the resolved one has a constant where it should
      // have a wire -- and it will be right on untagged traffic forever.
      if (consumer_valid) begin
        if ((consumer_ethertype_off != offsets.ethertype_off) ||
            (consumer_payload_off   != offsets.payload_off))
          if (!(&v_consumer_disagrees))
            v_consumer_disagrees <= v_consumer_disagrees + 1'b1;
      end
    end
  end

  assign conformant = (v_offset_mismatch       == '0) &&
                      (v_consumer_disagrees    == '0) &&
                      (v_offsets_on_refusal    == '0) &&
                      (v_depth_exceeded_parsed == '0);

endmodule

Classification: synthesizable, and intended to stay in silicon.

What it teaches: that with no golden layout available, the check becomes internal consistency plus consumer agreement. expected_et = 12 + 4 × tag_count is not a layout — it is the relationship the parse must satisfy, and it holds for every tag count including ones the design has not seen.

And v_consumer_disagrees is the check this chapter exists for. A downstream block using a constant 12 for the EtherType offset agrees with the resolver on every untagged frame — which is most frames on most benches — and disagrees on every tagged one. The counter fires on the first tagged frame, and it fires with no other symptom, because the addresses precede the tag and the frame forwards correctly regardless.

Deliberately simplified: one consumer interface. A real design has several — the EtherType comparator, the payload extractor, the length checker, the header-strip logic — and each needs its own comparison, because each could have its own constant.

Production implication: conformant here means the parse is internally consistent and its consumers used the offsets it produced. It does not mean the frame was tagged correctly, or that the VID is meaningful, or that the tag was intended — those are Chapter 13.3's questions about port configuration. The narrow claim is the parser and everything downstream of it agree about where the fields are, and on a variable-layout format that is the strongest internal claim available.

A four octet tag inserted at offset twelve shifts every octet after it. On a thirty-two bit datapath four octets is exactly one word, so no octet changes lane and no shifter is required. On a sixty-four bit datapath four octets is half a word, so every octet after the tag lands in a different lane and a barrel shifter is needed in the middle of the fastest path in the design. On a one hundred and twenty-eight bit datapath it is a quarter of a word and on two hundred and fifty-six bits an eighth, so the shifter grows with the datapath width, which is the opposite of the direction a design moving to higher line rates wants. The cost is not monotonic in the number of tags: two tags are eight octets, which realigns a sixty-four bit datapath, so an untagged frame and a double tagged frame are both aligned while the single tagged frame, which is the most common frame on an enterprise network, is the one that needs the shifter.4-octet insertat offset 1232-bit pathexactly 1 word — noshifter64-bit pathhalf a word — shifter256-bit patheighth of a word — 32lanesTwo tags = 8 octetsrealigns a 64-bit pathUntagged + Q-in-Qboth aligned — misses thecaseOne tag is the hardcaseand the commonest frame12
Figure 2 — four octets is one word at 32 bits and half a word at 64; a single tag misaligns, and a double tag realigns.

13. Everything the Switch Needs Is Before the Insertion

One observation explains why almost every failure in this chapter is silent, and it is worth stating on its own.

The two addresses occupy octets 0 through 11. The tag is inserted at octet 12.

So every field a switch needs for Chapter 12.3's forwarding decision — the destination address for the lookup, the source address for Chapter 12.2's learning, the ingress port which is not in the frame at all — is entirely before the insertion point and is unaffected by it.

ConsumerWhat it readsMoved by a tag?
the lookup — Chapter 12.3 gate 3destination, octets 0–5no
learning — Chapter 12.2source, octets 6–11no
the ingress filter — gate 4the ingress portno
Chapter 12.1's classificationthe I/G bit, octet 0no
the VIDoctets 14–15yes — it did not exist
the EtherTypeoctet 12 → 16yes
the payloadoctet 14 → 18yes
everything a receiving host parsesall of ityes

Read the split. Everything above the line belongs to the switch and does not move. Everything below it belongs to somebody else — the VLAN machinery this module adds, and the protocol stack at the far end.

Which is why a misparse forwards perfectly. A switch that gets every offset after octet 12 wrong still reads the right destination, still looks it up correctly, still learns the right source, still applies the right filter, and still delivers the frame to the right port. Chapter 12.6 §15's conformance monitor is satisfied. Chapter 13.1 §12's isolation monitor is satisfied — unless the VID itself was misread, which is the one case that crosses the line.

And the damage lands in an application at the far end, reading fields that are four octets out of place, with no counter having moved anywhere in the network.

An Ethernet frame's first twelve octets hold the destination and source addresses, and the 802.1Q tag is inserted immediately after them at octet twelve. Everything a switch consumes for its forwarding decision lies before the insertion point: the destination address for the table lookup, the source address for learning, and the individual group bit for classification, while the ingress port is not in the frame at all. Everything that moves lies after it: the EtherType shifts from octet twelve to sixteen, the payload from fourteen to eighteen, and every field a receiving protocol stack parses moves with them. A design that resolves every offset after octet twelve incorrectly therefore still reads the correct destination, looks it up correctly, learns the correct source, applies the correct ingress filter, and delivers the frame to the correct port with a valid recomputed check sequence, so every conformance monitor in the switch is satisfied and the damage appears only in an application at the far end. The single exception is the VLAN identifier itself, which sits inside the tag at octets fourteen and fifteen and is therefore the one moved field the switch does consume.Octets 0–11both addresses — nevermoveThe switch readstheselookup, learning, filterForwards correctlyvalid FCS, right portEvery monitorsatisfied12.6, 13.1, and thischapterTag at octet 12the insertion pointEtherType 12 → 16payload 14 → 18Damage at the farendfields 4 octets out ofplaceExcept the VIDoctets 14–15 — the switchdoes read it12
Figure 3 — the tag sits after everything the switch reads and before everything anyone else reads, which is why a misparse forwards perfectly.

The one field the switch does consume

Section 13's split has a single exception, and it is worth isolating because it is the only case where a parse error becomes a forwarding error rather than a payload error.

The VID sits at octets 14–15, inside the tag. So unlike the EtherType and the payload — which move but which the switch never reads — the VID is a moved field that the switch does consume, and it consumes it for Chapter 13.1's lookup key and flood mask.

FailureWhere the damage lands
EtherType offset wrongan application at the far end
payload offset wrongan application at the far end
realignment not appliedan application at the far end
VID read from the wrong offsetChapter 13.1's isolation, immediately
commit at a fixed octet 14the VLAN is chosen before the VID is read

The bottom two rows are the ones that cross the line, and they are the reason this chapter's properties include P28 — a cut-through commit waits for the parse.

Everything else in this chapter is a payload-integrity problem visible only end to end. The VID is a forwarding problem visible in Chapter 13.1 §12's isolation monitor — and that is the one place where a switch-level check does catch a parse error, which makes it worth knowing that the check exists and what it can and cannot see.

14. What a Second Tag Buys

The 12-bit VID exists because that is what fit — Section 2's callout read the constraint backwards. Stacking a second tag is the response, and it is the reason 0x88A8 exists alongside 0x8100.

Two tags give two independent 12-bit identifiers, and the combination space is their product:

identifiers
one tag4094
two tags4094 × 4094 = 16 760 836

The outer tag is a service tag — TPID 0x88A8, 802.1ad — and it names a customer. The inner tag is the customer's own 0x8100 tag, and it names a VLAN inside that customer's network.

Which is the operational point: the two tags are read by different parties. A provider's switches consume the outer tag and never look at the inner one — it is opaque payload to them. The customer's switches see only the inner tag, because the provider strips the outer one on the way out.

provider seescustomer sees
outer tag, 0x88A8its own VLAN spacenever — stripped at the edge
inner tag, 0x8100opaque — never parsedits own VLAN space
result4094 customers4094 VLANs each, unrestricted

And the reason this needs a second TPID rather than a second 0x8100 is that a parser must know which stack level it is at. A provider switch parsing a frame with two 0x8100 tags cannot tell its own tag from the customer's — it would strip the wrong one. Distinct TPIDs make the stack self-describing, which is exactly what Section 5's walk relies on.

Two consequences for the datapath, and they pull in opposite directions.

The header grows to 22 octets, so Section 10's earliest safe commit moves to 176 ns at 1 Gb/s and the maximum frame to 1526.

And the alignment improves. Eight octets is a whole word on a 64-bit datapath, so a double-tagged frame needs no realignment at all — Section 8's counter-intuitive result, and the reason a provider network's frames are cheaper to move through a wide datapath than an enterprise's.

15. Priority Is Not VLAN

PCP shares four octets with the VID and shares nothing else. The datapath must keep them apart, and a design that routes both through one path has coupled things the format merely co-located.

Chapter 13.1 §6 categorised the egress arbiter as unchanged by VLANs and said priority was a different mechanism. This is that mechanism, and the separation is worth making concrete:

VIDPCP
width12 bits3 bits
values4094 usable8
what it selectsthe broadcast domainthe egress queue
consumed byChapter 13.1's key and masksChapter 13.4's queue mapping
affects which portsyesno
affects when a frame leavesnoyes
changes the forwarding decisionyes — the lookup keyno

Read the last three rows together. VID decides where a frame may go; PCP decides how soon. Neither constrains the other, and a frame in VLAN 10 at PCP 7 and a frame in VLAN 20 at PCP 7 contend for the same egress queue on a shared port — which is Chapter 13.1's point that two VLANs sharing a physical port share its bandwidth entirely.

And the DEI bit belongs to a third mechanism again. It is a hint to Chapter 12.1 §9's discard policy — prefer this frame as a victim when the queue is full — and it is advisory in both directions. A sender marks it and a switch is not obliged to honour it.

Three fields, three consumers, one insertion. The economy is in the frame; the separation must be in the datapath.

16. The Parse Budget at Line Rate

Section 5's walk is one probe per cycle. Section 10 gave the commit points in nanoseconds. Put them together and the sequential walk stops fitting somewhere between 10 and 25 Gb/s.

Line rate14 octets (untagged)22 octets (two tags)
1 Gb/s112 ns176 ns
10 Gb/s11.2 ns17.6 ns
25 Gb/s4.48 ns7.04 ns
100 Gb/s1.12 ns1.76 ns

A sequential walk of up to three probes at 500 MHz is 6 ns.

BudgetSequential walk fits?
1 Gb/s, 112 nsyes, with 106 ns spare
10 Gb/s, 11.2 nsyes, with 5.2 ns spare
25 Gb/s, 4.48 nsno
100 Gb/s, 1.12 nsno, by 5×

So above 10 Gb/s the walk must be speculative. With MAX_TAGS = 2 there are exactly three candidate positions — octets 12, 16 and 20 — and a parser reads all three in parallel and selects among the results once the TPID comparisons resolve.

Which turns a three-cycle sequence into one cycle plus a select, at the cost of three 16-bit reads instead of one. The reads are free — the octets are arriving on a wide datapath anyway and are already in the pipeline register — and the select is a small mux.

And it explains a design constraint that looks arbitrary from outside: MAX_TAGS bounds the number of parallel probes, not just the walk length. A design accepting four tags needs five speculative positions and a five-way select inside the tightest timing path in the switch — which is why production parsers cap the stack at two or three and why Section 9's PS_TOO_DEEP is a design decision rather than a limitation.

17. Properties Worth Asserting, and One Worth Refusing

Every property here is about the relationship between the tag count and the offsets. Not one names a numeric offset, and Section 17's rejected class is why.

Tag detection

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. A TPID is recognised by value, not by a flag -- nothing in a frame
// announces that a tag follows.
property p_tag_is_a_value_match;
  @(posedge clk) disable iff (!rst_n)
  (probe_valid && is_tag) |-> ((probe_value == TPID_8100) ||
                               (probe_value == TPID_88A8));
endproperty
a_tag_by_value: assert property (p_tag_is_a_value_match);

// P2. Every TPID is above Chapter 5.5's 1536 boundary, so a tag can
// never be confused with a length.
property p_tpid_above_length_boundary;
  @(posedge clk) disable iff (!rst_n)
  is_tag |-> (probe_value >= TYPE_MIN);
endproperty
a_tpid_is_a_type: assert property (p_tpid_above_length_boundary);

// P3. A value below 1536 is a length and never a tag -- Chapter 5.5's
// rule, unchanged.
property p_length_is_never_a_tag;
  @(posedge clk) disable iff (!rst_n)
  (probe_valid && (probe_value < TYPE_MIN)) |-> (!is_tag && is_length);
endproperty
a_length_not_tag: assert property (p_length_is_never_a_tag);

// P4. A TPID the design does not accept is COUNTED, not silently parsed
// as an EtherType with four octets of payload swallowed.
property p_unknown_tpid_counted;
  @(posedge clk) disable iff (!rst_n)
  (probe_valid && !is_tag &&
   ((probe_value == TPID_88A8) || (probe_value == TPID_9100)))
    |=> $changed(c_unknown_tpid);
endproperty
a_unknown_tpid: assert property (p_unknown_tpid_counted);

The control field

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P5. The TCI split is positional and fixed: 3, 1, 12.
property p_tci_split;
  @(posedge clk) disable iff (!rst_n)
  tci_valid |-> ((tci.pcp == tci_raw[15:13]) && (tci.dei == tci_raw[12]) &&
                 (tci.vid == tci_raw[11:0]));
endproperty
a_tci_fields: assert property (p_tci_split);

// P6. VID 0 means PRIORITY ONLY, not untagged. The frame IS tagged, the
// offsets HAVE moved, and only the VLAN assignment is absent.
property p_vid_zero_is_still_tagged;
  @(posedge clk) disable iff (!rst_n)
  (tci_valid && (tci.vid == 12'd0)) |-> (vid_priority_only && !vid_usable);
endproperty
a_vid_zero: assert property (p_vid_zero_is_still_tagged);

// P7. VID 4095 is reserved and unusable.
property p_vid_4095_reserved;
  @(posedge clk) disable iff (!rst_n)
  (tci_valid && (tci.vid == 12'd4095)) |-> (vid_reserved && !vid_usable);
endproperty
a_vid_reserved: assert property (p_vid_4095_reserved);

// P8. The usable range is 1..4094 -- Chapter 13.1's 4094.
property p_usable_range;
  @(posedge clk) disable iff (!rst_n)
  vid_usable |-> ((tci.vid >= 12'd1) && (tci.vid <= 12'd4094));
endproperty
a_usable_range: assert property (p_usable_range);

// P9. The three fields are independent -- none constrains another.
property p_fields_independent;
  @(posedge clk) disable iff (!rst_n)
  tci_valid |-> ((priority_level == tci.pcp) && (drop_eligible == tci.dei));
endproperty
a_independent_fields: assert property (p_fields_independent);

The offsets

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P10. THE RELATIONSHIP, not a constant. The EtherType offset follows
// arithmetically from the tag count, for every tag count.
property p_ethertype_offset_follows_tags;
  @(posedge clk) disable iff (!rst_n)
  (parse_done && (state inside {PS_UNTAGGED, PS_SINGLE, PS_DOUBLE}))
    |-> (offsets.ethertype_off == (6'd12 + (6'(tag_count) * 6'd4)));
endproperty
a_offset_relation: assert property (p_ethertype_offset_follows_tags);

// P11. The payload begins two octets after the EtherType, always.
property p_payload_follows_ethertype;
  @(posedge clk) disable iff (!rst_n)
  parse_done |-> (offsets.payload_off == offsets.ethertype_off + 6'd2);
endproperty
a_payload_offset: assert property (p_payload_follows_ethertype);

// P12. Octet 12 is the only fixed position, and it is fixed because the
// two addresses before it are fixed-width.
property p_walk_starts_at_twelve;
  @(posedge clk) disable iff (!rst_n)
  start |=> (next_probe_offset == 6'd12);
endproperty
a_walk_start: assert property (p_walk_starts_at_twelve);

// P13. THE CONSUMER CHECK. Every downstream block uses the RESOLVED
// offset. A constant agrees on untagged traffic forever.
property p_consumers_use_resolved_offsets;
  @(posedge clk) disable iff (!rst_n)
  consumer_valid |-> ((consumer_ethertype_off == offsets.ethertype_off) &&
                      (consumer_payload_off   == offsets.payload_off));
endproperty
a_consumers_agree: assert property (p_consumers_use_resolved_offsets);

// P14. A refused parse produces NO offsets. Offsets into unvalidated
// data are worse than none.
property p_no_offsets_on_refusal;
  @(posedge clk) disable iff (!rst_n)
  (parse_done && (state inside {PS_TRUNCATED, PS_TOO_DEEP}))
    |-> (offsets.ethertype_off == 6'd0);
endproperty
a_refusal_no_offsets: assert property (p_no_offsets_on_refusal);

// P15. A frame ending inside a tag is REFUSED, never parsed with the
// missing octets read as zeros -- zeros parse as a length of zero.
property p_truncated_refused;
  @(posedge clk) disable iff (!rst_n)
  (probe_ready && (({8'd0, probe_offset} + 14'd4) > frame_len))
    |=> (state == PS_TRUNCATED);
endproperty
a_truncated: assert property (p_truncated_refused);

Depth, length and alignment

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P16. The tag stack is BOUNDED. Nothing in the format bounds it -- 355
// nested tags fit in a 1518-octet frame.
property p_depth_bounded;
  @(posedge clk) disable iff (!rst_n)
  parse_done |-> (tag_count <= 3'(MAX_TAGS));
endproperty
a_depth_bounded: assert property (p_depth_bounded);

// P17. Exceeding the depth is REFUSED, not truncated into a partial
// parse whose offsets are wrong by a multiple of four.
property p_too_deep_refused;
  @(posedge clk) disable iff (!rst_n)
  depth_exceeded |=> (state == PS_TOO_DEEP);
endproperty
a_too_deep: assert property (p_too_deep_refused);

// P18. The parse's worst case is a design constant, because a
// cut-through commit budget must accommodate it.
property p_parse_time_bounded;
  @(posedge clk) disable iff (!rst_n)
  start |-> ##[1:MAX_TAGS+1] parse_done;
endproperty
a_parse_bounded: assert property (p_parse_time_bounded);

// P19. The maximum frame length MOVES with the tag count: 1518, 1522,
// 1526. A fixed 1518 discards legal frames as oversize.
property p_max_length_follows_tags;
  @(posedge clk) disable iff (!rst_n)
  chk_valid |-> (max_allowed == (14'd1518 + (14'(tag_count) * 14'd4)));
endproperty
a_max_moves: assert property (p_max_length_follows_tags);

// P20. The MINIMUM does not move. The tag consumes padding.
property p_min_length_fixed;
  @(posedge clk) disable iff (!rst_n)
  chk_valid |-> (undersize == (frame_len < 14'd64));
endproperty
a_min_fixed: assert property (p_min_length_fixed);

// P21. The payload floor shrinks by 4 per tag: 46, 42, 38.
property p_payload_floor_shrinks;
  @(posedge clk) disable iff (!rst_n)
  chk_valid |-> (payload_floor == (14'd46 - (14'(tag_count) * 14'd4)));
endproperty
a_floor_shrinks: assert property (p_payload_floor_shrinks);

// P22. Realignment is needed exactly when the shift is not a whole
// number of datapath words.
property p_realign_iff_partial_word;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> (realign_needed == (((4 % W_OCT) != 0) && (shift_octets != '0)));
endproperty
a_realign_condition: assert property (p_realign_iff_partial_word);

// P23. A realignment never loses or duplicates an octet.
property p_realign_preserves_octets;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && realign_needed) |-> (out_data != 'x);
endproperty
a_realign_lossless: assert property (p_realign_preserves_octets);

Accounting and conformance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P24. The overhead is scaled x100, because 0.26% as an integer
// percentage is zero and a metric that reads zero is not read.
property p_overhead_has_resolution;
  @(posedge clk) disable iff (!rst_n)
  (window_valid && (octets_tags != '0)) |-> (tag_overhead_pct_x100 != 16'd0);
endproperty
a_overhead_resolution: assert property (p_overhead_has_resolution);

// P25. Tag octets never exceed total octets.
property p_overhead_bounded;
  @(posedge clk) disable iff (!rst_n)
  window_valid |-> (octets_tags <= octets_total);
endproperty
a_overhead_bounded: assert property (p_overhead_bounded);

// P26. The offsets are internally consistent with the tag count that
// produced them -- the check that works without a golden layout.
property p_offsets_consistent;
  @(posedge clk) disable iff (!rst_n)
  parse_done |-> (v_offset_mismatch == $past(v_offset_mismatch));
endproperty
a_internally_consistent: assert property (p_offsets_consistent);

// P27. Conformance means the parser and its consumers agree about where
// the fields are -- never that the tagging was intended.
property p_conformant_definition;
  @(posedge clk) disable iff (!rst_n)
  conformant |-> ((v_offset_mismatch == '0) && (v_consumer_disagrees == '0) &&
                  (v_offsets_on_refusal == '0) &&
                  (v_depth_exceeded_parsed == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);

// P28. A cut-through commit waits for the parse. Committing at a fixed
// octet 14 on a tagged frame decides the VLAN before reading the VID,
// which sits at octets 14-15.
property p_commit_after_parse;
  @(posedge clk) disable iff (!rst_n)
  commit |-> parse_done;
endproperty
a_commit_after_parse: assert property (p_commit_after_parse);

// P29. A second tag uses a DISTINCT TPID, so a parser can tell which
// stack level it is at. Two 0x8100 tags leave a provider unable to
// distinguish its own tag from the customer's.
property p_stack_is_self_describing;
  @(posedge clk) disable iff (!rst_n)
  (parse_done && (tag_count == 3'd2)) |-> (outer_tpid != inner_tpid);
endproperty
a_distinct_tpids: assert property (p_stack_is_self_describing);

// P30. PCP never affects the egress PORT SET -- it decides when a frame
// leaves, not where it may go.
property p_pcp_does_not_select_ports;
  @(posedge clk) disable iff (!rst_n)
  (tci_valid && (tci.pcp != $past(tci.pcp)) && (tci.vid == $past(tci.vid)))
    |-> (egress_mask == $past(egress_mask));
endproperty
a_pcp_not_ports: assert property (p_pcp_does_not_select_ports);

// P31. The DEI bit is ADVISORY -- a switch is permitted to ignore it,
// and asserting that it is honoured would be a claim about policy.
property p_dei_never_forces_a_drop;
  @(posedge clk) disable iff (!rst_n)
  (drop_eligible && queue_has_room) |-> !forced_discard;
endproperty
a_dei_advisory: assert property (p_dei_never_forces_a_drop);

18. Verification Scenarios

Seventy-four scenarios. The parse scenarios have no acceptable failure; the alignment ones have expected outcomes that depend on a datapath width parameter.

Tag detection

#ScenarioExpected
1Octets 12–13 = 0x8100is_ctag, tag found
2Octets 12–13 = 0x88A8, acceptedis_stag, tag found
3Octets 12–13 = 0x88A8, not acceptedc_unknown_tpid — parsed as an EtherType
4Octets 12–13 = 0x0800not a tag; is_ethertype
5Octets 12–13 = 0x0026lengthChapter 5.5's rule, below 1536
6Octets 12–13 = 0x0600type — the boundary belongs to type
7Every TPID valueabove 1536 — never confusable with a length
8Untagged framec_untagged increments, no tag

The control field

#ScenarioExpected
9TCI 0xA064PCP = 5, DEI = 0, VID = 100
10TCI 0x1000PCP = 0, DEI = 1, VID = 0
11VID 0vid_priority_only — tagged, no VLAN assignment
12VID 4095vid_reserved, unusable
13VID 1 and VID 4094both usable — the range
14PCP 0 through 7c_by_pcp histogram covers all 8
15All frames at PCP 0nobody is marking — queue mapping is idle
16All frames at PCP 7somebody marked everything — same effect
17DEI setc_dei_set, and Chapter 12.1 §9 may prefer it as a victim

The offset walk

#ScenarioExpected
18Untagged frameEtherType at 12, payload at 14
19Single-taggedEtherType at 16, payload at 18
20Double-taggedEtherType at 20, payload at 22
21Any parseethertype_off == 12 + 4 × tag_count
22Any parsepayload_off == ethertype_off + 2
23Walk startalways octet 12 — the only fixed position
24Triple-tagged, MAX_TAGS = 2PS_TOO_DEEP, refused
25Frame ending at octet 14 with a tag at 12PS_TRUNCATED
26Truncated frameno offsets produced
27Over-deep frameno offsets produced
28Missing octets read as zeros0x0000 parses as length 0 — the failure P15 prevents
29A consumer using a constant 12v_consumer_disagrees on the first tagged frame
30Same consumer on untagged trafficagrees — forever

Length and rate

#ScenarioExpected
31Untagged, 1518 octetsaccepted
32Untagged, 1519 octetsoversize
33Single-tagged, 1522 octetsaccepted — the maximum moved
34Same, checked against a fixed 1518discarded as oversize — a legal frame
35Double-tagged, 1526 octetsaccepted
36Any tag count, 64 octetsaccepted — the minimum does not move
37Any tag count, 63 octetsundersize
38Payload floor, untagged46 octets
39Payload floor, single-tagged42 octets — the tag ate padding
40Payload floor, double-tagged38 octets
414 octets on a 64-octet frame6.25% overhead
424 octets on a 1518-octet frame0.26%
43Tagged minimum frame rate at 1 Gb/s1.4205 Mpps against 1.4881 — −4.55%
4424-port switch, fully tagged traffic34.09 Mpps; budget 28 ns → 29.3 ns

Alignment

#ScenarioExpected
4532-bit datapath, one tagno realignment — 4 octets is one word
4664-bit datapath, one tagrealignment required — half a word
4764-bit datapath, two tagsno realignment — 8 octets is one word
48128-bit datapath, one tagrealignment, quarter of a word
49256-bit datapath, one tagrealignment, 32 lanes of mux
50Test suite of untagged + double-tagged onlyboth aligned — the failure is missed
51Realignment, maximum-length single-tagged framepayload byte-identical at the egress
52Failed realignmentpayload shifted 4 octets; frame still forwards

Cut-through interaction and conformance

#ScenarioExpected
53Untagged, earliest safe commitoctet 14 — 112 ns at 1 Gb/s
54Single-tagged, earliest safe commitoctet 18 — 144 ns
55Double-tagged, earliest safe commitoctet 22 — 176 ns
56Commit at a fixed octet 14 on a tagged framethe VLAN is decided before the VID is read
57SameChapter 13.1 §12's isolation violated by a timing choice
58Commit at MAX_TAGS header length alwaysconstant 176 ns, still 69× better than store-and-forward
59Offsets inconsistent with the tag countv_offset_mismatch
60Healthy run, mixed tagging, one million framesconformant high throughout
61Q-in-Q, outer 0x88A8 + inner 0x8100PS_DOUBLE, EtherType at 20
62Two tags, identifier space4094 × 4094 = 16 760 836
63Two 0x8100 tags in sequenceparses, and a provider cannot tell its tag from the customer's
64Accidental double tagging by two switchesc_by_tags shows a population at depth 2
65Sequential 3-probe walk at 500 MHz6 ns — fits 10 Gb/s, fails 25 Gb/s
66Speculative parse, MAX_TAGS = 2three parallel probes at octets 12, 16, 20
67Buffer-offset realignment, 1 tagstart at offset 6, payload aligned, no shifter
68Access port, tagged_fraction_pct = 100misconfigured — every frame at the wrong offsets
69Trunk port, tagged_fraction_pct = 0misconfigured — native-VLAN assignment by default
70Untagged throughput test35.71 Mpps offered, 28.0 ns budget
71Tagged throughput test34.09 Mpps offered, 29.3 ns budget — an easier test
72Buffer-offset realignment, 0 and 2 tagsstart at offset 2; payload aligned
73VID read from an untagged offset on a tagged framewrong VLAN — a forwarding error, not a payload one
74Any other offset read wronglya payload error — the frame still forwards correctly

19. Debugging a Tagged Path

Every row produces a switch that forwards to the correct port with a valid check sequence. That is not a coincidence — Section 13 explains it — and it is why the third column matters more here than anywhere else in the track.

SymptomLikely causeThe observable that decides it
An application receives malformed packets from a healthy hosta fixed offset downstream of the tagv_consumer_disagrees — fires on the first tagged frame
Same, only on one datapath widthrealignment not applied — Section 8realign_needed against c_realigned
Large transfers fail, small ones workthe maximum checked against a fixed 1518c_oversize rising on 1522-octet frames
Frames land in the wrong VLAN under cut-throughcommit at a fixed octet 14the VID is at octets 14–15 — Section 10
A whole class of frames misparsed after a peering changean unaccepted TPID0x88A8c_unknown_tpid
The VLAN is right but the payload is wrongthe parse succeeded, the datapath did notconformant high and payload mismatch at the egress
Parse time occasionally exceeds the budgetan unbounded tag walkdeepest_seen, c_exceeded, max_parse_cycles
Priority mapping configured and doing nothingnobody is markingc_by_pcp — everything in PCP 0
Priority mapping saturatedeverybody marked PCP 7c_by_pcp — everything in one bucket
A port drops frames it should acceptaccess port receiving tagged traffictagged_fraction_pct = 100 on an access port
Frames landing in the native VLAN unexpectedlytrunk receiving untagged traffictagged_fraction_pct = 0 on a trunk
VID 0 frames handled as untaggedthe tag is physically presentc_vid_zero — the offsets have moved
Two vendors' throughput figures differ by ~5%one test used tagged frames35.71 vs 34.09 Mpps — Section 7
Parse fails above 10 Gb/s onlya sequential walk against a shrinking budget6 ns walk against 4.48 ns at 25 Gb/s
A barrel shifter failing timing at 100 Gb/srealignment by shifting instead of by offsetthe buffer-offset technique removes it entirely
An isolation violation with a clean parse logthe VID was read at an untagged offsetChapter 13.1 §12's v_leak — the one switch-level catch
Everything correct except one application's payloada moved field the switch never readsend-to-end octet comparison; no counter will show it
The sixteen bits of tag control information carry three fields that share four octets and share nothing else. The three bit priority code point selects an egress queue and decides how soon a frame leaves, consumed by Chapter 13.4's queue mapping, and it does not affect which ports the frame may reach. The single drop eligible indicator bit is a hint to Chapter 12.1's discard policy that this frame is a preferred victim when a queue is full, and it is advisory in both directions since a sender marks it and a switch is not obliged to honour it. The twelve bit VLAN identifier selects the broadcast domain, is consumed by Chapter 13.1's lookup key and flood masks, and decides which ports the frame may reach but not when it leaves. None of the three constrains the others, so a datapath that routes all three through one path has coupled things that the frame format merely placed next to each other for economy.TCI — 16 bitsone insertion, threefieldsPCP — 3 bits8 levels, when it leavesEgress queueChapter 13.4DEI — 1 bita discard hint, advisoryDiscard policyChapter 12.1 §9VID — 12 bits4094 domains, where itmay goKey and flood masksChapter 13.112
Figure 4 — three fields, three consumers, one insertion: the frame's economy is not the datapath's, and the separation has to be built.

20. Common Misconceptions

1 — "A VLAN tag is added to the frame."

The wrong model: the tag is extra data attached to a frame, like a header or a trailer.

What it costs: every offset assumption in the design. A tag is inserted at octet 12, in the middle, after the two addresses and before the EtherType — so the EtherType moves from 12 to 16, the payload from 14 to 18, and every field a receiver parses moves with them. A design thinking in terms of "added" data will place the tag at the front or the back and be wrong about everything.

The corrected model: the tag is an insertion, and an insertion moves everything after it. On a 64-bit datapath four octets is half a word, so the insertion also moves every octet into a different lane — which is Section 8's realigner, sitting in the middle of the fastest path in the design.

2 — "The EtherType is at octet 12."

The wrong model: the frame layout is fixed, so field positions are constants.

What it costs: Section 17's rejected property, and the hard-coded offsets that go with it. The EtherType is at 12, or 16, or 20, decided by content. And the failure is invisible in the switch: the addresses precede the tag, so the frame forwards correctly and the damage lands in an application at the far end.

The corrected model: octet 12 is the only fixed position, and it is fixed only because the two addresses before it are fixed-width. Everything after it is an output of a parse — Section 5's offsets bus — and every consumer must take its position from that output.

3 — "VID 0 means the frame is untagged."

The wrong model: no VLAN number, no VLAN, therefore no tag.

What it costs: the frame is parsed at untagged offsets while the tag is physically present — a TPID at octet 12, a TCI at 14, and four octets that have moved everything after them. Every subsequent field is read four octets early.

The corrected model: VID 0 means priority-only. The frame is tagged, its PCP is meaningful, its offsets have moved, and only the VLAN assignment is absent — the receiving port supplies one, which is Chapter 13.3's rule.

4 — "The tag changes the minimum frame size."

The wrong model: four more octets of header means four more octets of minimum frame.

What it costs: an undersize check at 68 octets that discards legal 64-octet tagged frames, or a padding generator that emits frames four octets too long.

The corrected model: the minimum does not move and the maximum does. Chapter 5.6's padding already fills a short frame to 64 octets, so the tag consumes padding — the payload floor drops from 46 to 42 — while the maximum grows 1518 → 1522 → 1526 because it bounds the whole frame.

5 — "Two tags cost twice as much as one in the datapath."

The wrong model: the cost is proportional to the number of tags.

What it costs: a verification plan that tests untagged and double-tagged frames as the extremes, and misses the only misaligned case. On a 64-bit datapath 4 mod 8 = 4 and 8 mod 8 = 0one tag misaligns and two realign.

The corrected model: the alignment cost is 4 × tags mod datapath_width, which is not monotonic. On a 64-bit path the single-tagged frame — by far the commonest frame on an enterprise network — is the hard case, and the two cases a test plan naturally picks are both free.

6 — "A tagged frame's parse time is a constant."

The wrong model: parsing a header is fixed work.

What it costs: Chapter 12.6's cut-through commit point, which that chapter could treat as the constant octet 14. With tagging the header is 14, 18 or 22 octets, and a design committing at a fixed 14 on a tagged frame decides the VLAN before reading the VID, which sits at octets 14–15.

The corrected model: the parse walks the stack, so its length is data-dependent and must be bounded by the design — nothing in the format bounds it, and 355 nested tags fit in a 1518-octet frame. The honest choices are to commit at the maximum header length (constant, 176 ns) or when the parse completes (variable) — and the fixed 14 is not among them.

7 — "The tag's four octets are a rounding error."

The wrong model: four octets on a frame of hundreds is negligible.

What it costs: it hides a 4.55% change in the frame rate a switch must sustain — 1.4881 → 1.4205 Mpps at 1 Gb/s on minimum-length frames — which is enough to make two throughput measurements incomparable. And on a 64-octet frame the four octets are 6.25%, not a rounding error at all.

The corrected model: the overhead is a function of the frame-size distribution, from 6.25% on minimum frames to 0.26% on maximum ones, and a port carrying voice or control traffic sits near the top of that range. Section 11's accountant measures it per port rather than assuming a figure, and it scales the result by 10 000 because 0.26% as an integer percentage reads as zero.

21. Interview Reasoning

Q1 — "Where is the EtherType in an Ethernet frame?"

Reason through it. At octet 12, or 16, or 20 — it depends on how many 802.1Q tags the frame carries, and the frame does not say. A parser reads octets 12–13, compares the value against the known TPIDs, and if it matches, advances four octets and repeats. The offset is an output of that walk. The strong answer names the circularity: to know where the EtherType is you must know how many tags precede it, and to know that you must read each candidate position — so the resolution is inherently sequential, and any constant in the design is a bet on the answer. It then names the one genuinely fixed position: octet 12, fixed only because the two addresses before it are fixed-width.

Q2 — "What are the four octets of an 802.1Q tag, and why are three unrelated fields in one of them?"

Reason through it. 16 bits of TPID (0x8100) and 16 bits of TCI, split 3 + 1 + 12: PCP, DEI, VID. Priority, drop eligibility and VLAN identity have nothing to do with one another — they travel together because a second insertion would move every downstream offset a second time, need its own TPID, and misalign the datapath again. The strong answer reads the constraint backwards: the 12-bit VID is not a number anybody derived from a requirement about networks. It is what fits in 16 bits after 3 bits of priority and 1 of drop eligibility — and the field is undersized for large multi-tenant deployments as a direct result, which is why tag stacking exists.

Q3 — "Why is a single tag harder for a 64-bit datapath than a double tag?"

Reason through it. Alignment. Four octets is half a 64-bit word, so every octet after the tag lands in a different lane and a barrel shifter is required. Eight octets is exactly one word, so a double tag moves no octet between lanes and needs no shifter at all. The cost is 4 × tags mod 8, which is 0, 4, 0 — not monotonic. The strong answer draws the verification consequence: a test plan covering untagged and Q-in-Q frames has tested only the two aligned cases, and the single-tagged frame it skipped is the commonest frame on an enterprise network. And the failure is silent — the addresses precede the tag, so the frame forwards to the right port with a valid FCS and the payload is four octets out of place.

Q4 — "How does tagging change a cut-through switch's commit point?"

Reason through it. It makes it data-dependent. Chapter 12.6's analysis rested on the commit being a constant octet 14, fixed by the format. With tagging the header is 14, 18 or 22 octets, and the switch cannot know which until it has read the frame. The honest options are to commit at the maximum — 22 octets, 176 ns at 1 Gb/s, constant, still 69× better than store-and-forward's 12.144 µs — or when the parse completes, which is faster on average and reintroduces the jitter cut-through exists to remove. The strong answer names the option that must be refused: committing at a fixed octet 14. The VID sits at octets 14–15, so that commit decides the frame's VLAN before reading it, and Chapter 13.1 §12's isolation invariant is violated by a timing choice rather than a logic error.

Q5 — "Your switch discards some large frames as oversize and the link has no errors. What is happening?"

Reason through it. The maximum is being checked against a fixed 1518, and the frames are tagged. A single-tagged maximum-length frame is 1522 octets and a double-tagged one is 1526 — both legal, both discarded. The strong answer states the rule that generates the numbers: the maximum bounds the whole frame, so each tag adds four to it, while the minimum does not move at all because Chapter 5.6's padding already fills a short frame — the tag consumes padding and the payload floor drops from 46 to 42. The symptom is diagnostic: large transfers fail while small ones work, which points at an MTU problem several layers up and almost never at a length constant in a switch.

Q6 — "Why do this chapter's failures not show up in any switch counter?"

Reason through it. Because of where the tag is inserted. Both addresses occupy octets 0 through 11, and the tag goes in at 12 — so everything a switch consumes precedes the insertion point. The destination for the lookup, the source for learning, the I/G bit for classification: none of them moves. A design that gets every offset after octet 12 wrong still forwards the frame to the correct port with a valid check sequence, and Chapter 12.6's and Chapter 13.1's conformance monitors are both satisfied. The strong answer names the one exception and the remedy: the exception is the VID itself, which is inside the tag and therefore crosses the line into what the switch consumes — and the remedy is an end-to-end octet-for-octet payload comparison against a patterned payload, because no counter in the network is positioned to see it.

Q7 — "Why does a design cap its tag stack at two or three rather than parsing whatever arrives?"

Reason through it. Because nothing in the format bounds the stack — a tag is a value at a position, that value can recur four octets later, and 355 nested tags fit in a 1518-octet frame. An unbounded walk is unbounded parse time on the tightest timing path in the switch. The strong answer adds the constraint most people miss: above 10 Gb/s the walk must be speculative rather than sequential — a three-probe sequential walk at 500 MHz is 6 ns against a 4.48 ns budget at 25 Gb/s — so MAX_TAGS bounds the number of parallel probes and the width of the select mux, not merely the loop count. Four tags means five speculative positions and a five-way select inside the critical path. And exceeding the cap must be refused rather than partially parsed, because a partial parse produces offsets wrong by a multiple of four on a frame that still forwards correctly.

22. Understanding Check

23. What's Next

This chapter parsed a tag that had already arrived and never asked how it got there, or what a port should do with a frame that has none.

Chapter 13.3 — Access Ports, Trunk Ports and Tag Handling owns that: where a tag is inserted, where it is stripped, what an access port does with a frame that arrives tagged, what a trunk does with one that arrives untagged, and the native-VLAN rule that decides it. Section 4's vid_priority_only and Section 11's tagged_fraction_pct are both signals that chapter's rules explain.

Then Chapter 13.4 — VLAN Processing in the Switch Datapath builds what Chapter 13.1 §10 priced and this chapter decoded: the VID-to-index map, the shared table with its 60-bit key, and the mapping from this chapter's PCP field into egress queues — the one piece of per-VLAN behaviour Chapter 13.1 §6 deliberately categorised as belonging to a different mechanism.

And Module 14 — Flow Control takes up a question every chapter since Chapter 12.1 has deferred: what happens when a receiver cannot keep up. Chapter 12.1 §6 established that discarding is the switch's specified response to congestion and rejected backpressure as a cure. Module 14 asks what a mechanism that does apply backpressure has to look like — and this chapter's PCP field is how it is made per-priority rather than per-link.

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.