Skip to content
VLSI Mentor

Ethernet · Module 13

Access Ports, Trunk Ports and Tag Handling

A frame's VLAN is assigned by the receiving port, not carried by the frame. Across an untagged link it is destroyed and re-invented — which is why a native-VLAN mismatch leaks both ways with every check clean.

Chapter 13.2 parsed a tag that had already arrived and deferred, twice, the question of how it got there. Chapter 13.1 consumed a VID on every interface and never said who assigned it.

This is the answer, and it is not a configuration table. It is a transformation applied to the frame itself, at two places, asymmetrically.

At ingress the port assigns a VLAN. A tagged frame brings one; an untagged frame has none, and the receiving port supplies its own — the PVID. At egress the port represents that VLAN, by inserting a tag, stripping one, or leaving the frame alone.

And the consequence that organises the whole chapter: across an untagged link, a frame's VLAN does not exist. It is not in the frame, not on the wire, and not recoverable from anything transmitted. It exists only inside the switches at each end, as a number each of them chose independently — which is why Chapter 13.1 §12's isolation invariant can hold perfectly at both ends of a link that is leaking traffic between VLANs in both directions.

1. Scope — What This Chapter Owns

This chapter owns the transformation: the per-port configuration that drives it, the six ingress cases, the assignment rule, ingress filtering, the three egress actions, what stripping a tag does to a minimum-length frame, and the native-VLAN rule.

It does not own the tag's fieldsChapter 13.2 owns TPID, PCP, DEI and VID, and the parser that finds them. This chapter moves tags; that chapter reads them.

It does not own why VLANs existChapter 13.1 established the requirement, the isolation invariant and what must become per-VLAN.

And it does not own the lookup datapath. The VID-to-index map, the shared table with its widened key, and PCP into egress queues are Chapter 13.4. This chapter delivers a VID to that chapter's input and represents its output on the wire.

One dependency is load-bearing throughout. Chapter 13.2 §13 established that everything a switch consumes precedes the insertion point at octet 12 — so every failure in this chapter forwards correctly, and the third column of Section 20's table matters more than the first two.

2. A Frame's VLAN Is Assigned, Not Carried

Start with the claim that sounds wrong and is the foundation of everything else.

An untagged Ethernet frame contains no VLAN information of any kind. There is no field, no flag, no implicit encoding. Chapter 5.1's layout is destination, source, EtherType, payload, FCS — and none of them says anything about a VLAN.

So when an untagged frame arrives at a port, the switch must invent one. The rule is that the port's configured PVID — its port VLAN identifier, sometimes called the default or native VLAN — becomes the frame's VLAN for as long as it is inside this switch.

Which produces a sequence worth following carefully:

StepWhere the VLAN lives
a station transmitsnowhere — the station has no VLAN concept
the frame crosses the access linknowhere — the wire carries no VID
it arrives at switch A's port 3assigned: port 3's PVID, say 10
it crosses switch A's fabrica descriptor field
it leaves on a trunkoctets 14–15 — a tag is inserted
it arrives at switch Bthe tag's VID, 10 — carried, not invented
it leaves on switch B's access portstripped — nowhere again
it arrives at the destination stationnowhere

The VLAN exists for exactly the middle of that journey. At both ends it does not, and the two switches agree only because a tag carried it between them.

Remove the tag from that middle hop — make it an untagged link — and the two switches agree only because two operators configured the same number twice.

A station transmits an ordinary untagged frame, which carries no VLAN information of any kind because an untagged Ethernet frame has no field for it. The access link therefore carries no VLAN identifier. On arrival the receiving port assigns its configured port VLAN identifier, so the VLAN comes into existence inside the switch as a descriptor field. If the frame leaves on a trunk, a tag is inserted and the VLAN identifier travels in octets fourteen and fifteen, so the next switch reads it rather than inventing it. When the frame finally leaves on an access port the tag is stripped and the VLAN identifier ceases to exist again before the frame reaches the destination station. Across any untagged link the identifier is destroyed at transmit and recreated at receive from a number the receiving port's operator configured independently, so continuity across such a link is a property of two configurations agreeing rather than a property of the frame.Station transmitsno VLAN concept at allAccess linkthe wire carries no VIDPort assigns itsPVIDthe VLAN begins to existTrunk — tag insertedVID in octets 14–15Switch B reads itcarried, not inventedAccess port —strippedthe VLAN endsDestination stationno VLAN concept either12
Figure 1 — the VLAN exists only in the middle of the journey; at both ends it is nowhere, and the tag is what carries it between switches that would otherwise have to agree by configuration.

3. RTL 1 — The Per-Port Configuration

Five fields per port drive every transformation in this chapter, and three of them are one bit or a handful.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlanport_pkg -- shared types for VLAN port modes.
//
// This chapter owns the TRANSFORMATION. Chapter 13.2 owns the tag's
// fields, Chapter 13.1 owns why VLANs exist, and Chapter 13.4 owns the
// lookup datapath this chapter feeds.
// -----------------------------------------------------------------------
package vlanport_pkg;

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

  // What a port will accept. The third value exists for a port facing a
  // device that always tags -- a virtualisation host or a router -- and
  // is the only place a station is expected to know what a VLAN is.
  typedef enum logic [1:0] {
    AFT_ADMIT_ALL       = 2'd0,
    AFT_TAGGED_ONLY     = 2'd1,
    AFT_UNTAGGED_ONLY   = 2'd2
  } accept_types_e;

  // What the egress does to represent a VLAN on this port.
  typedef enum logic [1:0] {
    EG_STRIP  = 2'd0,   // VLAN is in this port's UNTAGGED set
    EG_TAG    = 2'd1,   // VLAN is in this port's TAGGED set
    EG_PASS   = 2'd2,   // already correct -- no transformation
    EG_DROP   = 2'd3    // port is not a member of this VLAN
  } egress_action_e;

  // Why a frame was refused. Each has a different remedy and only two of
  // them are faults.
  typedef enum logic [2:0] {
    VR_OK              = 3'd0,
    VR_WRONG_TYPE      = 3'd1,  // acceptable-frame-types said no
    VR_INGRESS_FILTER  = 3'd2,  // port is not a member of the assigned VID
    VR_RESERVED_VID    = 3'd3,  // 0 handled separately, 4095 refused
    VR_EGRESS_NOT_MEM  = 3'd4,  // Chapter 13.1's isolation, at the egress
    VR_LENGTH          = 3'd5   // transformation produced an illegal length
  } vlan_reject_e;

  // Per-port configuration. Section 4's matrix is entirely a function of
  // these five fields plus Chapter 13.1's membership array.
  typedef struct packed {
    logic [VID_W-1:0] pvid;            // assigned to untagged arrivals
    accept_types_e    accept;
    logic             ingress_filter;  // Section 8 -- optional, and it matters
    logic             is_trunk;        // annotation; behaviour comes from the sets
  } port_cfg_t;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// port_mode_config -- the per-port state that drives every transformation.
//
// "Access port" and "trunk port" are NOT modes in the hardware. They are
// names for particular settings of these fields, and a design that
// implements them as an enumerated mode has hard-coded two points of a
// continuous space and made the third case -- a hybrid port -- impossible.
// -----------------------------------------------------------------------
module port_mode_config
  import vlanport_pkg::*;
#(
  parameter int N_PORTS = 24,
  parameter int N_VLANS = 64,
  parameter int VIDX_W  = 6,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 cfg_valid,
  input  logic [4:0]           cfg_port,
  input  port_cfg_t            cfg_data,
  input  logic                 cfg_untagged_set_we,
  input  logic [VIDX_W-1:0]    cfg_untagged_vidx,
  input  logic                 cfg_untagged_bit,

  input  logic [4:0]           rd_port,
  input  logic [VIDX_W-1:0]    rd_vidx,

  output port_cfg_t            cfg,
  output logic                 vlan_is_untagged_here,

  output logic [CNT_W-1:0]     bits_pvid,
  output logic [CNT_W-1:0]     bits_untagged_sets,
  output logic [CNT_W-1:0]     bits_total,
  output logic [5:0]           n_access_like,
  output logic [5:0]           n_trunk_like
);

  port_cfg_t          cfgs      [N_PORTS];
  // THE UNTAGGED SET. One bit per VLAN per port, and it is a SEPARATE
  // vector from Chapter 13.1's membership: a port may be a member of a
  // VLAN and carry it tagged, or be a member and carry it untagged. The
  // two questions are independent.
  logic [N_VLANS-1:0] untagged  [N_PORTS];

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int p = 0; p < N_PORTS; p++) begin
        cfgs[p]     <= '0;
        untagged[p] <= '0;
      end
    end else begin
      if (cfg_valid)           cfgs[cfg_port] <= cfg_data;
      if (cfg_untagged_set_we) untagged[cfg_port][cfg_untagged_vidx] <= cfg_untagged_bit;
    end
  end

  assign cfg                   = cfgs[rd_port];
  assign vlan_is_untagged_here = untagged[rd_port][rd_vidx];

  // Cost. Section 10 compares these against Chapter 13.1's 60 KiB.
  assign bits_pvid          = CNT_W'(N_PORTS) * CNT_W'(VID_W);
  assign bits_untagged_sets = CNT_W'(N_PORTS) * CNT_W'(N_VLANS);
  assign bits_total         = bits_pvid + bits_untagged_sets +
                              (CNT_W'(N_PORTS) * CNT_W'(3));

  // "Access-like" and "trunk-like" are DERIVED, not configured: a port
  // whose untagged set has exactly one bit behaves as an access port, and
  // one with several tagged VLANs behaves as a trunk. Reporting the
  // derived shape catches a port configured as neither.
  always_comb begin
    n_access_like = 6'd0;
    n_trunk_like  = 6'd0;
    for (int p = 0; p < N_PORTS; p++) begin
      if ($countones(untagged[p]) == 1) n_access_like = n_access_like + 6'd1;
      else if ($countones(untagged[p]) == 0) n_trunk_like = n_trunk_like + 6'd1;
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that "access port" and "trunk port" are not modes in the hardware. They are names for particular settings of the five fields — an access port is a port whose untagged set contains exactly one VLAN and whose PVID is that VLAN; a trunk is one whose untagged set is empty and which is a member of many. A design that implements them as an enumerated mode has hard-coded two points of a continuous space, and the third case — a hybrid port carrying one VLAN untagged and several tagged, which is how every IP phone with a PC behind it is connected — becomes impossible to express.

And it teaches that the untagged set is separate from Chapter 13.1's membership array, because the two questions are independent. Membership asks may this VLAN's traffic use this port. The untagged set asks how is it represented when it does. A port can be a member of VLAN 10 and carry it tagged, or a member and carry it untagged — and a design that merges the arrays has made every member VLAN untagged, which on a trunk destroys every VID it forwards.

Deliberately simplified: a dense untagged vector over 64 configured VLANs. Chapter 13.1 §10 established that the full VID range makes these arrays 12 KiB each and that production designs put a VID-to-index map in front — Chapter 13.4 owns that map, and this module consumes its output as rd_vidx.

Production implication: n_access_like and n_trunk_like are derived rather than configured, and the gap between them and the port count is the interesting number. A port that is neither — an empty untagged set and membership of exactly one VLAN, or several untagged VLANs — is almost always a misconfiguration, and it is one that forwards traffic normally. Several untagged VLANs on one port is the worst case: every one of them arrives at the far end untagged and indistinguishable, so the far end assigns them all its own PVID and several VLANs have silently merged into one.

4. The Six Ingress Cases

Two things a frame can be, three things a port will accept. Six combinations, and four of them are the ones deployments actually meet.

Frame arrivesAFT_ADMIT_ALLAFT_TAGGED_ONLYAFT_UNTAGGED_ONLY
untaggedassign PVIDrefuseVR_WRONG_TYPEassign PVID
tagged, VID 1–4094use the carried VIDuse the carried VIDrefuse
tagged, VID 0assign PVID, keep PCPassign PVID, keep PCPrefuse — it is tagged

The bottom row is the case Chapter 13.2 §4 set up and could not finish. A frame with VID 0 is tagged — a TPID at octet 12, four octets that moved everything after them, and a PCP worth honouring — but it carries no VLAN assignment. So the port assigns its PVID, exactly as it would for an untagged frame, and keeps the priority the sender asked for.

That is the entire purpose of VID 0: it lets a station express a priority without expressing a VLAN, and it is why Chapter 13.2's c_vid_zero is a configuration signal rather than an error.

And the right-hand column is the one that surprises people. AFT_UNTAGGED_ONLY refuses a VID-0 frame because the test is on the frame's form, not its content — it arrived with a tag, so a port that accepts only untagged frames refuses it, priority-only or not.

Which is a distinction worth being precise about, because two plausible readings of "untagged" diverge here:

"carries no tag""carries no VLAN assignment"
a plain frameyesyes
a VID-0 framenoyes
the acceptable-frame-types test usesthis onenot this one
the PVID assignment rule usesnot this onethis one

Two rules in the same module, using two different senses of the same word. Section 5 tests the form; Section 7 tests the content.

5. RTL 2 — Acceptable Frame Types

The first gate, and the only one in this chapter that a station can trip by doing something legal.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// acceptable_frame_filter -- does this port accept a frame of this FORM?
//
// The test is on the frame's form -- did it arrive with a tag -- and NOT
// on whether it carries a VLAN assignment. A VID-0 frame is tagged and is
// refused by an untagged-only port, even though it assigns no VLAN.
// Section 4's table has the two senses side by side.
// -----------------------------------------------------------------------
module acceptable_frame_filter
  import vlanport_pkg::*;
#(
  parameter int CNT_W = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             frame_valid,
  input  logic             arrived_tagged,     // Chapter 13.2's tag_count != 0
  input  logic [VID_W-1:0] carried_vid,
  input  accept_types_e    accept,

  output logic             admit,
  output vlan_reject_e     reject,

  output logic [CNT_W-1:0] c_admitted,
  output logic [CNT_W-1:0] c_refused_type,
  output logic [CNT_W-1:0] c_vid_zero_admitted,
  output logic [CNT_W-1:0] c_reserved_refused
);

  logic vid_reserved;
  // Chapter 13.2 Section 4: 4095 is reserved and names no VLAN. Unlike 0,
  // it has no defined alternative meaning, so it is refused outright.
  assign vid_reserved = arrived_tagged && (carried_vid == 12'd4095);

  always_comb begin
    admit  = 1'b0;
    reject = VR_OK;

    if (frame_valid) begin
      if (vid_reserved) begin
        reject = VR_RESERVED_VID;
      end else begin
        unique case (accept)
          AFT_ADMIT_ALL:     admit = 1'b1;
          // THE FORM TEST. A VID-0 frame is TAGGED and passes here, even
          // though Section 7 will assign it the PVID as though it were
          // untagged.
          AFT_TAGGED_ONLY:   begin
            admit  = arrived_tagged;
            if (!arrived_tagged) reject = VR_WRONG_TYPE;
          end
          AFT_UNTAGGED_ONLY: begin
            admit  = !arrived_tagged;
            if (arrived_tagged) reject = VR_WRONG_TYPE;
          end
          default: admit = 1'b1;
        endcase
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_admitted          <= '0;
      c_refused_type      <= '0;
      c_vid_zero_admitted <= '0;
      c_reserved_refused  <= '0;
    end else if (frame_valid) begin
      if (admit) begin
        c_admitted <= c_admitted + 1'b1;
        // A priority-only frame that was accepted. Chapter 13.2's
        // c_vid_zero counted them at the parser; this counts the ones
        // this port's policy let through.
        if (arrived_tagged && (carried_vid == 12'd0))
          if (!(&c_vid_zero_admitted))
            c_vid_zero_admitted <= c_vid_zero_admitted + 1'b1;
      end else begin
        if (reject == VR_WRONG_TYPE)
          if (!(&c_refused_type)) c_refused_type <= c_refused_type + 1'b1;
        if (reject == VR_RESERVED_VID)
          if (!(&c_reserved_refused))
            c_reserved_refused <= c_reserved_refused + 1'b1;
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the form test and the assignment test use two different senses of "untagged", in adjacent modules, and conflating them is a real bug. A VID-0 frame arrived with a tag — so AFT_UNTAGGED_ONLY refuses it — and carries no VLAN assignment — so Section 7 gives it the PVID. Both are correct and they disagree about the word.

And it teaches why 4095 is refused where 0 is not. Chapter 13.2 §4 established both as reserved, but 0 has a defined alternative meaning — priority-only — and 4095 has none. A frame carrying 4095 is malformed rather than special, and there is no assignment rule that could rescue it.

Deliberately simplified: a combinational decision on a single arrival. Production designs fold this into the same pipeline stage as Chapter 13.2's parse, because arrived_tagged is that parser's output and the two decisions have the same input.

Production implication: c_refused_type on an access port is the counter that catches the misconfiguration Chapter 13.2 §11's tagged_fraction_pct predicted. A port set to AFT_UNTAGGED_ONLY and receiving tagged frames refuses every one of them — which is loud, correct, and far better than the alternative: a port set to AFT_ADMIT_ALL receiving the same frames accepts them and uses their carried VID, placing a station's traffic in whatever VLAN it asked for. The permissive setting is the dangerous one, and that is not the intuition most people bring to an "accept everything" option.

6. The Assignment Rule

One rule, three inputs, and the only place in a switch where a frame acquires an attribute it did not arrive with.

If the frame carries a usable VID — tagged, 1 to 4094 — that is its VLAN. Otherwise the port's PVID is its VLAN.

"Otherwise" covers two cases that look different and behave identically: a plain untagged frame, and a tagged frame carrying VID 0.

ArrivalVLAN assignedPCP taken from
untaggedthe port's PVIDthe port's default priority
tagged, VID 0the port's PVIDthe frame's PCP
tagged, VID 1–4094the frame's VIDthe frame's PCP

The middle row is the only one where the two halves of the tag are used differently, and it is worth being explicit about why. Chapter 13.2 §15 established that PCP and VID are independent fields with unrelated consumers. A VID-0 frame exercises that independence directly: its priority is honoured and its VLAN is discarded, because the sender expressed one and not the other.

And the top row's right-hand cell is a configuration item this chapter has not mentioned yet. A port assigning a PVID must also assign a priority, because the frame has none — and the usual default is 0, which is why Chapter 13.2 §4's c_by_pcp histogram on a network of untagged hosts is entirely in bucket 0.

7. RTL 3 — Assigning the VLAN

Four lines of logic that decide which VLAN a frame is in for the rest of its life inside this switch.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ingress_vid_assigner -- the one place a frame acquires an attribute it
// did not arrive with.
//
// After this module the frame's VLAN is fixed for the remainder of its
// path through the switch. Chapter 13.1's P6 asserted the VID does not
// change while crossing a switch; this module is where the value that
// property protects is established.
// -----------------------------------------------------------------------
module ingress_vid_assigner
  import vlanport_pkg::*;
#(
  parameter int CNT_W = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             admit,
  input  logic             arrived_tagged,
  input  logic [VID_W-1:0] carried_vid,
  input  logic [PCP_W-1:0] carried_pcp,
  input  logic [VID_W-1:0] port_pvid,
  input  logic [PCP_W-1:0] port_default_pcp,

  output logic             assign_valid,
  output logic [VID_W-1:0] assigned_vid,
  output logic [PCP_W-1:0] assigned_pcp,
  output logic             vid_was_carried,   // provenance -- Section 16
  output logic             vid_was_invented,

  output logic [CNT_W-1:0] c_carried,
  output logic [CNT_W-1:0] c_invented,
  output logic [CNT_W-1:0] c_priority_only
);

  // "Usable" is Chapter 13.2 Section 4's test: tagged, and not 0 or 4095.
  // A VID-0 frame is tagged and NOT usable, so it falls to the PVID --
  // which is the same outcome as an untagged frame by a different route.
  logic vid_usable;
  assign vid_usable = arrived_tagged &&
                      (carried_vid != 12'd0) && (carried_vid != 12'd4095);

  assign assign_valid = admit;
  assign assigned_vid = vid_usable ? carried_vid : port_pvid;

  // THE ASYMMETRY. The VID falls back to the port; the PCP does not. A
  // tagged frame's priority is honoured even when its VLAN is discarded,
  // because Chapter 13.2 Section 15 established the two fields are
  // independent and have unrelated consumers.
  assign assigned_pcp = arrived_tagged ? carried_pcp : port_default_pcp;

  // PROVENANCE. Section 16 needs to know whether this VLAN came off the
  // wire or out of a register, because a mismatch is only possible in the
  // second case.
  assign vid_was_carried  = admit && vid_usable;
  assign vid_was_invented = admit && !vid_usable;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_carried       <= '0;
      c_invented      <= '0;
      c_priority_only <= '0;
    end else if (admit) begin
      if (vid_usable) begin
        if (!(&c_carried)) c_carried <= c_carried + 1'b1;
      end else begin
        if (!(&c_invented)) c_invented <= c_invented + 1'b1;
        if (arrived_tagged)
          if (!(&c_priority_only)) c_priority_only <= c_priority_only + 1'b1;
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the VID falls back to the port and the PCP does not, and the asymmetry is deliberate rather than an oversight. A tagged frame carrying VID 0 has a sender who expressed a priority and declined to express a VLAN — so the design honours the half that was expressed and supplies the half that was not. A design that discards the whole tag on VID 0 has thrown away the only thing the sender actually asked for.

And vid_was_carried against vid_was_invented is the provenance distinction Section 16 depends on. A VLAN that came off the wire was chosen by the transmitting switch and cannot be mismatched — both ends read the same 12 bits. A VLAN that was invented came out of a local register, and whether it matches the far end's intent is a question about two configurations that nothing in the frame can answer.

Deliberately simplified: a single default priority per port. Production designs support a priority regeneration table that remaps arriving PCP values on ingress, because a switch at an administrative boundary cannot trust a neighbour's priority marking any more than Chapter 12.2 §16 could trust its source addresses.

Production implication: c_invented against c_carried is the same measurement Chapter 13.2 §11's tagged_fraction_pct made from the other side, and the pair is more useful than either. On an access port every frame should be invented; on a trunk almost none should be. A trunk with a high c_invented is receiving untagged frames, which Section 14's native-VLAN rule is about to place somewhere by default — and that number is the earliest warning of the failure this chapter exists to explain.

8. Ingress Filtering, and Why It Is Optional

A frame has been admitted and assigned a VLAN. One question remains before it enters the forwarding path: is this port actually a member of that VLAN?

The surprising part is that checking is optional, and that switches ship with it disabled.

ingress filtering oningress filtering off
tagged frame, VID 10, port not in VLAN 10discardedaccepted into VLAN 10
what the frame can then reachnothingevery port that is in VLAN 10
Chapter 13.1 §12's isolationintactintact — the egress is a member
what an operator seesa counternothing

Read the third row. Chapter 13.1's isolation invariant is about the egress port — no frame leaves a port that is not a member of the frame's VLAN. A frame injected into VLAN 10 through a non-member port satisfies it perfectly, because every port it then reaches is a member.

So ingress filtering is the check that closes the other half of the boundary, and its absence is not an isolation failure by Chapter 13.1's definition. It is an admission failure, and the two are different enough that a switch can be provably isolated and still let anybody into any VLAN.

Why it is ever off: because the check costs a membership lookup on the ingress path, and because a switch in the middle of a trunk-only topology is a member of everything anyway. Neither reason survives contact with a port facing a station.

9. RTL 4 — The Membership Check on the Way In

One comparator, and the counter that matters is the one it keeps when the check is disabled.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// ingress_vlan_filter -- is this port a member of the VLAN the frame was
// just assigned?
//
// Chapter 13.1 Section 12's isolation invariant is about the EGRESS. This
// is the other half of the boundary, and a switch can satisfy that
// invariant perfectly while letting any station into any VLAN.
// -----------------------------------------------------------------------
module ingress_vlan_filter
  import vlanport_pkg::*;
#(
  parameter int N_PORTS = 24,
  parameter int N_VLANS = 64,
  parameter int VIDX_W  = 6,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 assign_valid,
  input  logic [VIDX_W-1:0]    assigned_vidx,
  input  logic [4:0]           ingress_port,
  input  logic                 vid_was_carried,
  input  logic [N_PORTS-1:0]   vlan_members [N_VLANS],
  input  logic                 filter_enable,

  output logic                 pass,
  output vlan_reject_e         reject,

  output logic [CNT_W-1:0]     c_filtered,
  output logic [CNT_W-1:0]     c_would_have_filtered,   // with the check OFF
  output logic [4:0]           worst_port,
  output logic                 admission_gap_open
);

  logic is_member;
  assign is_member = vlan_members[assigned_vidx][ingress_port];

  always_comb begin
    pass   = 1'b0;
    reject = VR_OK;
    if (assign_valid) begin
      if (filter_enable && !is_member) reject = VR_INGRESS_FILTER;
      else                             pass   = 1'b1;
    end
  end

  logic [CNT_W-1:0] by_port [N_PORTS];

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_filtered            <= '0;
      c_would_have_filtered <= '0;
      for (int p = 0; p < N_PORTS; p++) by_port[p] <= '0;
      worst_port            <= '0;
      admission_gap_open    <= 1'b0;
    end else if (assign_valid && !is_member) begin
      if (filter_enable) begin
        if (!(&c_filtered)) c_filtered <= c_filtered + 1'b1;
      end else begin
        // THE COUNTER THAT MATTERS. With the check disabled, this counts
        // frames that WOULD have been filtered -- frames admitted into a
        // VLAN through a port that is not a member of it. It costs one
        // comparator and it is the only evidence the gap is being used.
        if (!(&c_would_have_filtered))
          c_would_have_filtered <= c_would_have_filtered + 1'b1;
        by_port[ingress_port] <= by_port[ingress_port] + 1'b1;
        worst_port            <= ingress_port;
        // A frame ENTERED a VLAN it had no business entering, and
        // Chapter 13.1's isolation monitor will not see it, because every
        // port it reaches IS a member.
        admission_gap_open    <= vid_was_carried;
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that c_would_have_filtered costs one comparator and is the only evidence the gap is being used. A design that disables ingress filtering by omitting the check has no way to know whether it matters. A design that performs the comparison and acts on it only when enabled knows exactly how many frames entered a VLAN through a non-member port — and can turn the check on with an informed idea of what will break.

And admission_gap_open narrows it further, using Section 7's provenance. A frame whose VID was invented cannot violate membership: the PVID is a property of the port, and a sensibly configured port is a member of its own PVID. A frame whose VID was carried is the dangerous one — the sender chose the VLAN, and with filtering off the switch honoured that choice from a port that has no membership in it.

Deliberately simplified: a single membership lookup on the ingress path. The lookup is the same array Chapter 13.1 §9 built, read at a second point — which is one more read port on a structure that is already read once per frame at the egress, and a real reason designs sometimes skip it.

Production implication: the setting to worry about is not the check but the acceptable-frame-types setting behind it. A port set to AFT_ADMIT_ALL with ingress filtering off will place a station's traffic in any VLAN that station asks for, by tagging its own frames — and both settings are common defaults. Section 5's counter and this one together identify every port where that combination is actually being exercised, which is a much shorter list than the ports where it is configured.

10. Egress Is Not the Mirror of Ingress

Ingress does one thing: it assigns. Egress does three, and which one depends on a per-port, per-VLAN bit rather than on anything about the frame.

The VLAN is…Egress actionFrame length
in this port's untagged setstrip the tag if present−4 if it had one
in this port's tagged set — a member, not untaggedinsert a tag if absent+4 if it had none
not a member at alldropChapter 13.1 §12

And there is a fourth case that looks like a fourth action and is not: the frame already has the right form. A tagged frame leaving a tagged port, or an untagged frame leaving an untagged port, needs no transformation at all — and recognising that is worth doing, because Section 13 shows every transformation costs an FCS recomputation.

The asymmetry is worth stating plainly, because "strip on the way in, tag on the way out" is a common and wrong mental model:

ingressegress
how many outcomesassign, from two sourcesstrip, insert, pass or drop
what decidesthe frame's form and the port's PVIDthe port's untagged set
the frame's VLANis established hereis represented here
can it change the VLANyes — that is its jobno — Chapter 13.1's P6
changes the frame's lengthnoyes, by ±4

Read the last two rows together. Ingress changes the frame's meaning and not its bytes; egress changes its bytes and must not change its meaning. A design that alters the VID at egress has done Chapter 13.1 §12's v_vid_changed — relabelling, which that chapter established is worse than leaking.

At ingress a port decides a frame's VLAN from two possible sources: a usable identifier carried in the frame's tag, or the port's own configured port VLAN identifier when the frame arrives untagged or carrying identifier zero. This changes the frame's meaning and not a single one of its bytes. At egress the port decides how to represent that VLAN, using a per-port per-VLAN bit rather than anything about the frame: if the VLAN is in this port's untagged set the tag is stripped, if it is in the tagged set a tag is inserted, if the frame already has the right form no transformation is needed, and if the port is not a member of the VLAN at all the frame is dropped. Every transformation changes the frame's length by four octets and requires the frame check sequence to be recomputed, and none of them may change the VLAN identifier itself, because altering it at egress is relabelling rather than forwarding.Ingress: assigncarried VID, or the PVIDMeaning changesbytes do notAcross the fabricVLAN in a descriptorEgress: representthe port's untagged setdecidesStrip — −4 octetsVLAN is untagged hereInsert — +4 octetsVLAN is tagged hereBytes change, VLANmust notchanging it isrelabelling12
Figure 2 — ingress establishes the VLAN and never changes the bytes; egress changes the bytes and must never change the VLAN.

11. RTL 5 — The Egress Transformation

Three actions plus a no-op, chosen by one bit, and every one of the three changes the frame.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// egress_tag_transformer -- strip, insert, pass or drop.
//
// The action is chosen by a per-port per-VLAN bit and NOT by anything
// about the frame. Recognising the no-op case matters: Section 13 shows
// every real transformation costs an FCS recomputation, so a design that
// always strips-then-inserts pays that cost on every frame for nothing.
// -----------------------------------------------------------------------
module egress_tag_transformer
  import vlanport_pkg::*;
#(
  parameter int N_PORTS = 24,
  parameter int N_VLANS = 64,
  parameter int VIDX_W  = 6,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 eg_valid,
  input  logic [4:0]           egress_port,
  input  logic [VIDX_W-1:0]    vidx,
  input  logic [VID_W-1:0]     vid,
  input  logic [PCP_W-1:0]     pcp,
  input  logic                 frame_is_tagged,     // as it sits in the fabric
  input  logic [13:0]          frame_len,
  input  logic [N_PORTS-1:0]   vlan_members [N_VLANS],
  input  logic                 vlan_untagged_here,

  output logic                 act_valid,
  output egress_action_e       action,
  output logic [13:0]          new_len,
  output logic [15:0]          insert_tci,
  output logic                 fcs_recompute,
  output vlan_reject_e         reject,

  output logic [CNT_W-1:0]     c_strip,
  output logic [CNT_W-1:0]     c_insert,
  output logic [CNT_W-1:0]     c_pass,
  output logic [CNT_W-1:0]     c_drop_nonmember
);

  logic is_member;
  assign is_member = vlan_members[vidx][egress_port];

  always_comb begin
    action        = EG_DROP;
    new_len       = frame_len;
    fcs_recompute = 1'b0;
    reject        = VR_OK;
    act_valid     = 1'b0;

    if (eg_valid) begin
      act_valid = 1'b1;

      // Chapter 13.1 Section 12's isolation invariant, applied here. A
      // non-member port never emits the frame, whatever its untagged set
      // happens to say about a VLAN it does not carry.
      if (!is_member) begin
        action = EG_DROP;
        reject = VR_EGRESS_NOT_MEM;

      end else if (vlan_untagged_here) begin
        if (frame_is_tagged) begin
          action        = EG_STRIP;
          new_len       = frame_len - 14'd4;
          fcs_recompute = 1'b1;
        end else begin
          // THE NO-OP. Already in the right form. Section 13 explains
          // why recognising this is worth the comparator.
          action = EG_PASS;
        end

      end else begin
        if (!frame_is_tagged) begin
          action        = EG_TAG;
          new_len       = frame_len + 14'd4;
          fcs_recompute = 1'b1;
        end else begin
          action = EG_PASS;
        end
      end
    end
  end

  // The tag this port would insert. Chapter 13.2 Section 4's TCI split,
  // rebuilt from the frame's assigned VLAN and priority -- note that the
  // DEI bit is not preserved across a strip-and-reinsert, which is a
  // small honest loss worth knowing about.
  assign insert_tci = {pcp, 1'b0, vid};

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_strip <= '0; c_insert <= '0; c_pass <= '0; c_drop_nonmember <= '0;
    end else if (eg_valid) begin
      unique case (action)
        EG_STRIP: if (!(&c_strip))  c_strip  <= c_strip + 1'b1;
        EG_TAG:   if (!(&c_insert)) c_insert <= c_insert + 1'b1;
        EG_PASS:  if (!(&c_pass))   c_pass   <= c_pass + 1'b1;
        EG_DROP:  if (!(&c_drop_nonmember))
                    c_drop_nonmember <= c_drop_nonmember + 1'b1;
        default: ;
      endcase
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the no-op is worth recognising explicitly. A tagged frame leaving a tagged port and an untagged frame leaving an untagged port both need nothing done to them — and a design that unconditionally strips on ingress and inserts on egress pays an FCS recomputation and a realignment on every frame to arrive at the bytes it already had. Section 13 prices that.

And it teaches a small honest loss: the DEI bit does not survive a strip-and-reinsert. The frame's tag is destroyed at one hop and rebuilt at another from the VLAN and priority the switch carried internally — and unless the descriptor carries DEI too, the rebuilt tag has it clear. That is a real, standards-visible difference between a frame that passed through and one that was retagged, and it is the kind of thing that shows up only when somebody is relying on the bit.

Deliberately simplified: the membership array read at the egress, matching Chapter 13.1 §9. The insert_tci is built combinationally; a real design carries the whole TCI in the descriptor so that DEI and any provider-specific bits survive.

Production implication: c_strip, c_insert and c_pass in proportion describe what a port is actually doing, and the healthy shapes are distinctive. An access port should be almost entirely c_strip and c_pass. A trunk should be almost entirely c_insert and c_pass. A port with a substantial mixture of all three is a hybrid — which is legitimate for a phone-and-PC port and a misconfiguration everywhere else, and no other counter in the switch distinguishes those two cases.

12. Stripping a Tag Can Make a Runt

The transformation changes the frame's length by four octets, and at one end of the size range that crosses a boundary Chapter 5.6 established twenty years before VLANs existed.

A minimum-length tagged frame is 64 octets on the wire:

DA 6 + SA 6 + TAG 4 + EtherType 2 + payload/pad 42 + FCS 4 = 64

Strip the tag and it becomes:

DA 6 + SA 6 + EtherType 2 + payload/pad 42 + FCS 4 = 60 octets — a runt.

Chapter 7.3 requires every frame to be at least 64 octets, so the stripped frame is illegal and every receiver will discard it. The egress must re-pad, adding four octets and taking the payload floor back from 42 to 46 — which is exactly Chapter 13.2 §6's arithmetic run backwards.

The full table, and it is worth having in one place:

ArrivesStrip →Insert →Pass →
6460 — runt, re-pad to 646864
68647268
151815141522 — legal only because tagged max is 15221518
152215181526 — legal only as a double tag1522

Both ends of the range have a trap and they are different traps.

At the bottom, stripping produces an illegal frame that must be repaired. A design that strips and forwards without re-padding emits runts that every downstream device discards — and Chapter 13.2 §13's split means the addresses are intact, so the frame looks structurally fine right up to the length check.

At the top, inserting produces a frame that is legal only if the length checker knows about tags. Chapter 13.2 §6 established that a switch checking against a fixed 1518 discards legal 1522-octet frames — and here the switch is the one that made them 1522. A design that inserts a tag and then applies an untagged length check rejects its own output.

13. RTL 6 — Repairing the Length

Four octets of padding, one FCS recomputation, and a check that the transformation did not produce something illegal at either end.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// egress_length_repair -- makes the transformed frame legal again.
//
// A strip can produce a runt; an insert can produce an oversize. Both are
// the switch's own doing, and both must be detected against the limits
// that apply AFTER the transformation rather than before it.
// -----------------------------------------------------------------------
module egress_length_repair
  import vlanport_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             act_valid,
  input  egress_action_e   action,
  input  logic [13:0]      new_len,
  input  logic [2:0]       tags_after,      // tag count AFTER the action

  output logic [13:0]      final_len,
  output logic [13:0]      pad_octets,
  output logic             need_pad,
  output logic             fcs_recompute,
  output logic             length_ok,
  output vlan_reject_e     reject,

  output logic [CNT_W-1:0] c_repadded,
  output logic [CNT_W-1:0] c_oversize_self_inflicted,
  output logic [13:0]      max_after
);

  // THE LIMIT THAT APPLIES IS THE ONE FOR THE TAG COUNT AFTER THE
  // TRANSFORMATION. Chapter 13.2 Section 6: 1518 untagged, 1522 with one
  // tag, 1526 with two. A design that checks the pre-transformation limit
  // rejects its own output.
  assign max_after = 14'(MAX_UNTAG) + (14'(tags_after) * 14'd4);

  // A strip can take a 64-octet frame to 60. Chapter 7.3 requires 64, so
  // the four octets come back as padding -- and Chapter 5.6 established
  // that padding beyond the payload's own declared length is invisible to
  // the layer above.
  assign need_pad   = act_valid && (new_len < 14'(MIN_FRAME));
  assign pad_octets = need_pad ? (14'(MIN_FRAME) - new_len) : 14'd0;
  assign final_len  = need_pad ? 14'(MIN_FRAME) : new_len;

  // Every transformation changes the frame, so the check sequence over it
  // is no longer valid. Chapter 5.8's CRC-32 is already in the datapath.
  assign fcs_recompute = act_valid &&
                         ((action == EG_STRIP) || (action == EG_TAG) || need_pad);

  always_comb begin
    length_ok = 1'b1;
    reject    = VR_OK;
    if (act_valid && (final_len > max_after)) begin
      length_ok = 1'b0;
      reject    = VR_LENGTH;
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_repadded                <= '0;
      c_oversize_self_inflicted <= '0;
    end else if (act_valid) begin
      if (need_pad)
        if (!(&c_repadded)) c_repadded <= c_repadded + 1'b1;
      // An oversize the switch CREATED by inserting a tag. Distinct from
      // an oversize that arrived, because the remedy is different: this
      // one means the far end's limit and ours disagree about tagging.
      if (!length_ok && (action == EG_TAG))
        if (!(&c_oversize_self_inflicted))
          c_oversize_self_inflicted <= c_oversize_self_inflicted + 1'b1;
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the length limit that applies is the one for the tag count after the transformation, and getting that wrong is a design error the switch inflicts on itself. A frame that arrived at 1518 octets untagged, checked correctly against 1518, becomes 1522 when this port inserts a tag — and a checker still holding 1518 rejects it. The switch has created a frame and then refused to emit it.

And c_oversize_self_inflicted is deliberately separate from an ordinary oversize count, because the two mean different things. An oversize that arrived means a neighbour is sending frames longer than this port accepts. An oversize this port created means our tagging and the far end's limits disagree — and the remedy is a configuration conversation, not a filter.

Deliberately simplified: padding as a length adjustment rather than an octet-level insertion. A real design writes the padding octets and must decide what to write; zeros are conventional and the standard does not require any particular value, which is a small and genuine source of frames that differ byte-for-byte between vendors.

Production implication: c_repadded should be non-zero on any access port carrying small frames, and its absence is the finding. A port stripping tags from minimum-length frames and never re-padding is emitting 60-octet runts — which every downstream receiver discards under Chapter 7.3, and which appears at those receivers as an error counter with no local cause. Chapter 12.6 §9's relocated-evidence problem, produced by a transformation rather than by a cable.

14. The Native VLAN

A trunk carries many VLANs tagged. It may also carry exactly one untagged — the native VLAN — and that single exception is the largest silent failure surface in VLAN deployment.

Mechanically it is nothing new. The native VLAN is simply the trunk port's PVID, and the trunk's untagged set contains exactly that one VLAN. Section 7's assignment rule and Section 11's egress transformation handle it without a special case.

Operationally it is a hole in Section 2's diagram. Every other VLAN on the trunk travels tagged, so its identity is carried on the wire and the two switches read the same 12 bits. The native VLAN travels untagged, so its identity is destroyed at transmit and re-invented at receive — from a number the receiving switch's operator configured independently.

Which produces the failure. Two switches, trunk between them:

switch Aswitch B
trunk native VLAN199
A sends VLAN 1untagged — no VID on the wireassigns PVID 99
the frame is now inVLAN 1VLAN 99
B sends VLAN 99untaggedassigns PVID 1
the frame is now inVLAN 1VLAN 99

The leak is bidirectional, complete, and invisible to every check either switch performs.

Chapter 13.1 §12's isolation monitor is clean at both ends. Switch A emitted a VLAN 1 frame out a port that is a member of VLAN 1 — correct. Switch B emitted a VLAN 99 frame out a port that is a member of VLAN 99 — also correct. No frame left a non-member port. No frame's VID changed while crossing a switch, because inside each switch the VID never changed; it changed between them, where neither switch was looking.

And there is no counter for it, because there is nothing anomalous to count. Both switches received an untagged frame on a trunk — which is exactly what a native VLAN is — and assigned their PVID, which is exactly the rule.

15. RTL 7 — Detecting a Mismatch From Local Evidence

Neither switch can see the other's PVID. What each one can see is a pattern that a mismatch produces and a match does not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// native_vlan_detector -- infers a native-VLAN mismatch from evidence
// available at one end.
//
// The far end's PVID is not observable. What IS observable: which
// addresses appear in which VLAN, and whether an address seen tagged in
// VLAN X on this trunk is also seen untagged. A mismatch makes those two
// populations disagree in a way a match never does.
// -----------------------------------------------------------------------
module native_vlan_detector
  import vlanport_pkg::*;
#(
  parameter int N_TRACK = 16,
  parameter int ADDR_W  = 48,
  parameter int VIDX_W  = 6,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 learn_valid,
  input  logic [ADDR_W-1:0]    learn_mac,
  input  logic [VIDX_W-1:0]    learn_vidx,
  input  logic [4:0]           learn_port,
  input  logic                 arrived_tagged,
  input  logic                 port_is_trunk,
  input  logic [VIDX_W-1:0]    port_native_vidx,

  output logic [CNT_W-1:0]     c_untagged_on_trunk,
  output logic [CNT_W-1:0]     c_addr_in_two_vlans_one_trunk,
  output logic [ADDR_W-1:0]    suspect_mac,
  output logic [VIDX_W-1:0]    suspect_vidx_a,
  output logic [VIDX_W-1:0]    suspect_vidx_b,
  output logic                 native_mismatch_suspected,
  output logic [4:0]           suspect_port
);

  logic [ADDR_W-1:0] t_mac  [N_TRACK];
  logic [VIDX_W-1:0] t_vidx [N_TRACK];
  logic              t_tag  [N_TRACK];
  logic              t_val  [N_TRACK];
  logic [$clog2(N_TRACK)-1:0] wr_ptr;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < N_TRACK; i++) begin
        t_mac[i] <= '0; t_vidx[i] <= '0; t_tag[i] <= 1'b0; t_val[i] <= 1'b0;
      end
      wr_ptr                        <= '0;
      c_untagged_on_trunk           <= '0;
      c_addr_in_two_vlans_one_trunk <= '0;
      suspect_mac                   <= '0;
      suspect_vidx_a                <= '0;
      suspect_vidx_b                <= '0;
      suspect_port                  <= '0;
      native_mismatch_suspected     <= 1'b0;
    end else if (learn_valid && port_is_trunk) begin
      // Untagged arrivals on a trunk are the native VLAN's traffic. They
      // are NORMAL -- this counter is a denominator, not an alarm.
      if (!arrived_tagged)
        if (!(&c_untagged_on_trunk))
          c_untagged_on_trunk <= c_untagged_on_trunk + 1'b1;

      begin
        automatic int hit = -1;
        for (int i = 0; i < N_TRACK; i++)
          if (t_val[i] && (t_mac[i] == learn_mac)) hit = i;

        if (hit >= 0) begin
          // THE SIGNATURE. One address, seen on ONE trunk port, in TWO
          // different VLANs, where at least one sighting was untagged.
          // With matching natives this cannot happen: the untagged
          // traffic is always the same VLAN at both ends. With a
          // mismatch, the far end's native traffic arrives untagged and
          // lands in OUR native VLAN, while that same station's tagged
          // traffic lands in its real one.
          if ((t_vidx[hit] != learn_vidx) &&
              (!t_tag[hit] || !arrived_tagged)) begin
            if (!(&c_addr_in_two_vlans_one_trunk))
              c_addr_in_two_vlans_one_trunk <= c_addr_in_two_vlans_one_trunk + 1'b1;
            suspect_mac    <= learn_mac;
            suspect_vidx_a <= t_vidx[hit];
            suspect_vidx_b <= learn_vidx;
            suspect_port   <= learn_port;
            // Chapter 13.1 Section 14 established that one address in two
            // VLANs is NORMAL -- a router with several legs. What is not
            // normal is that pattern arriving through ONE trunk port with
            // an untagged sighting among them.
            native_mismatch_suspected <=
              (learn_vidx == port_native_vidx) ||
              (t_vidx[hit] == port_native_vidx);
          end
          t_vidx[hit] <= learn_vidx;
          t_tag[hit]  <= arrived_tagged;
        end else begin
          t_mac[wr_ptr]  <= learn_mac;
          t_vidx[wr_ptr] <= learn_vidx;
          t_tag[wr_ptr]  <= arrived_tagged;
          t_val[wr_ptr]  <= 1'b1;
          wr_ptr         <= wr_ptr + 1'b1;
        end
      end
    end
  end

endmodule

Classification: synthesizable, sized as a small tracking window rather than a full table.

What it teaches: that a mismatch is detectable from one end even though the far end's configuration is not observable, and the evidence is a pattern rather than a value. With matching natives, the untagged traffic on a trunk is always the same VLAN at both ends — one address's untagged frames and its tagged frames land in different VLANs only if it genuinely has legs in both. With a mismatch, the far end's native traffic arrives untagged and is placed in our native VLAN, while that same station's tagged traffic lands in its real one — so one address appears in two VLANs through one trunk port, with an untagged sighting among them.

And it teaches why Chapter 13.1 §14's tracker is not sufficient on its own. That module established that one address in two VLANs is normal — a router with several legs — and this one adds the two qualifiers that make it suspicious: through a single trunk port, and with at least one sighting untagged. A router's legs arrive on different ports or arrive tagged; neither qualifier fits.

Deliberately simplified: a 16-entry window and a single-bit tagged/untagged history. A production detector samples during Chapter 12.5 §13's ageing sweep and keeps a per-address tagged-sighting bitmap, so the pattern can be established across the whole table rather than a recent sample.

Production implication: c_untagged_on_trunk is a denominator, not an alarm — untagged frames on a trunk are the native VLAN working as designed, and a switch that flags them is flagging normal operation. native_mismatch_suspected is the alarm, and it is deliberately named as a suspicion: the detector cannot prove a mismatch, because proving it requires the far end's PVID. What it can do is point at one address, two VLANs and one port — which is enough for an operator to compare two configurations and settle it in a minute.

16. What a Mismatch Actually Does

"Traffic leaks between VLANs" understates it. Work out precisely what is joined to what, because the answer is not symmetric and it is not a leak in the usual sense.

Switch A's native is VLAN 1; switch B's is VLAN 99.

TrafficLeaves A asArrives at B asNet effect
A's VLAN 1untaggedVLAN 99A's VLAN 1 is joined to B's VLAN 99
B's VLAN 99untaggedVLAN 1the same join, from the other side
A's VLAN 10tagged, VID 10VLAN 10correct
B's VLAN 10tagged, VID 10VLAN 10correct
A's VLAN 99tagged, VID 99VLAN 99correct — and it now shares B's VLAN 99 with A's VLAN 1

The last row is the one that makes this worse than a simple swap. A's VLAN 99 traffic travels tagged — A is a member of VLAN 99 and it is not A's native — so it arrives correctly in B's VLAN 99. And A's VLAN 1 traffic arrives there too.

So B's VLAN 99 now contains two of A's VLANs. Two broadcast domains that A keeps separate have been merged at B, and every one of Chapter 12.4 §10's arithmetic arguments now applies to their union.

What each side observes:

ObservationSwitch ASwitch B
Chapter 13.1 §12 isolationcleanclean
v_vid_changedzero — the VID never changed inside Azero
ingress filtering violationsnone — the trunk is a member of its nativenone
a length or format errornone — the frames are perfectly formednone
Chapter 12.2 §9's move detectorquietquiet
the only signalSection 15's pattern

Every mechanism this track has built reports success, and it is not that they are failing to look — there is nothing anomalous to see. Each switch received a well-formed untagged frame on a trunk and applied its configured rule.

17. RTL 8 — Conformance for a Transformation

The monitor's difficulty here is that Chapter 12.1's P11 — a frame is never modified — is false by design in this chapter. The check must be the narrower invariant Section 12's callout stated.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// portmode_conformance_monitor -- checks a datapath whose job is to
// modify frames.
//
// Chapter 12.1's P11 asserted a frame is never modified in transit. This
// chapter strips, inserts, re-pads and recomputes the FCS. So the
// invariant is restated: the addresses, the EtherType, the payload up to
// its declared length, and the VLAN ASSIGNMENT are unmodified, and
// everything else is the switch's to change.
// -----------------------------------------------------------------------
module portmode_conformance_monitor
  import vlanport_pkg::*;
#(
  parameter int N_PORTS = 24,
  parameter int N_VLANS = 64,
  parameter int VIDX_W  = 6,
  parameter int ADDR_W  = 48,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic [N_PORTS-1:0]   vlan_members [N_VLANS],

  input  logic                 tx_valid,
  input  logic [4:0]           egress_port,
  input  logic [VIDX_W-1:0]    frame_vidx,
  input  logic [VIDX_W-1:0]    ingress_vidx,
  input  logic                 vlan_untagged_here,
  input  logic                 emitted_tagged,
  input  logic [VID_W-1:0]     emitted_vid,
  input  logic [VID_W-1:0]     assigned_vid,
  input  logic [13:0]          emitted_len,
  input  logic [2:0]           emitted_tags,
  input  logic [ADDR_W-1:0]    in_da,  input logic [ADDR_W-1:0] out_da,
  input  logic [ADDR_W-1:0]    in_sa,  input logic [ADDR_W-1:0] out_sa,
  input  logic                 fcs_recomputed,

  output logic [CNT_W-1:0]     v_form_wrong,       // tagged where untagged
  output logic [CNT_W-1:0]     v_vid_rewritten,    // relabelled at egress
  output logic [CNT_W-1:0]     v_addr_modified,
  output logic [CNT_W-1:0]     v_illegal_length,
  output logic [CNT_W-1:0]     v_stale_fcs,        // changed without recompute
  output logic [CNT_W-1:0]     v_nonmember_emit,
  output logic                 conformant
);

  logic [13:0] max_after;
  assign max_after = 14'd1518 + (14'(emitted_tags) * 14'd4);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_form_wrong     <= '0;
      v_vid_rewritten  <= '0;
      v_addr_modified  <= '0;
      v_illegal_length <= '0;
      v_stale_fcs      <= '0;
      v_nonmember_emit <= '0;
    end else if (tx_valid) begin
      // THE FORM CHECK. The emitted frame's tagged-ness must match this
      // port's untagged set for this VLAN. A tagged frame on an untagged
      // port reaches a station that cannot parse it; an untagged frame on
      // a trunk lands in the far end's native VLAN.
      if (emitted_tagged == vlan_untagged_here)
        if (!(&v_form_wrong)) v_form_wrong <= v_form_wrong + 1'b1;

      // Chapter 13.1's P6, at the point the tag is built. Egress may
      // change the frame's FORM and never its VLAN.
      if (emitted_tagged && (emitted_vid != assigned_vid))
        if (!(&v_vid_rewritten)) v_vid_rewritten <= v_vid_rewritten + 1'b1;
      if (frame_vidx != ingress_vidx)
        if (!(&v_vid_rewritten)) v_vid_rewritten <= v_vid_rewritten + 1'b1;

      // THE RESTATED P11. Addresses survive every transformation.
      if ((out_da != in_da) || (out_sa != in_sa))
        if (!(&v_addr_modified)) v_addr_modified <= v_addr_modified + 1'b1;

      // The limit for the tag count AFTER the transformation -- Section 13.
      if ((emitted_len < 14'd64) || (emitted_len > max_after))
        if (!(&v_illegal_length)) v_illegal_length <= v_illegal_length + 1'b1;

      // A frame whose length changed and whose FCS was not recomputed is
      // a frame carrying a check sequence over bytes it no longer has.
      if ((emitted_tagged != (emitted_tags != 3'd0)) && !fcs_recomputed)
        if (!(&v_stale_fcs)) v_stale_fcs <= v_stale_fcs + 1'b1;

      // Chapter 13.1 Section 12's invariant, unchanged.
      if (!vlan_members[frame_vidx][egress_port])
        if (!(&v_nonmember_emit)) v_nonmember_emit <= v_nonmember_emit + 1'b1;
    end
  end

  assign conformant = (v_form_wrong     == '0) && (v_vid_rewritten  == '0) &&
                      (v_addr_modified  == '0) && (v_illegal_length == '0) &&
                      (v_stale_fcs      == '0) && (v_nonmember_emit == '0);

endmodule

Classification: synthesizable, and intended to stay in silicon.

What it teaches: that v_form_wrong is the check with no analogue anywhere else in the track. Every other conformance monitor asks whether the frame's content is right. This one asks whether its form matches the port it is leaving through — and both errors are silent in opposite directions. A tagged frame emitted onto an untagged access port reaches a station that parses the TPID as an EtherType and misreads everything after it. An untagged frame emitted onto a trunk lands in the far end's native VLAN, which is Section 16's merge produced by one bit rather than by two configurations.

And v_stale_fcs catches the failure a transformation makes uniquely easy. The frame's bytes changed; the check sequence over them did not. Every receiver discards it, the ingress port that made the change reports nothing, and Chapter 12.6 §9's relocated evidence appears one hop away.

Deliberately simplified: address comparison against captured ingress values. A production monitor tags the descriptor with a hash of the immutable fields at ingress and compares the hash at egress, because carrying 96 bits of address alongside every frame to check them is more expensive than the check is worth.

Production implication: conformant here means the transformation was the one this port's configuration calls for, it preserved everything it was required to preserve, and it changed everything it was required to change. It does not mean the frame is in the VLAN anybody intended — Section 16 established that no signal in this switch can mean that, because the intent lives in a configuration at the far end. Section 18's rejected property is the attempt to claim it anyway.

On a trunk between two switches, every tagged VLAN carries its identifier in octets fourteen and fifteen of each frame, so both ends read the same twelve bits and cannot disagree about which VLAN a frame belongs to. The native VLAN travels untagged, so its identifier is not on the wire at all: the transmitting switch strips it and the receiving switch assigns its own configured port VLAN identifier on arrival. If the two switches are configured with different native VLANs, switch A's native traffic arrives at switch B and is placed in switch B's native VLAN, and switch B's native traffic arrives at switch A and is placed in switch A's native VLAN, so two broadcast domains are joined in both directions. Every isolation check at both ends remains clean, because each switch emitted frames only from member ports and no frame's identifier changed while inside either switch: it changed between them, where neither switch was looking.Tagged VLANsVID in octets 14–15Identity on the wireboth ends read the samebitsCannot disagreethe bits are the bitsNative VLANtravels untaggedIdentity nowherestripped, thenre-inventedTwo registers, nevercomparedA says 1, B says 99Domains merged, bothends cleanno counter anywhere12
Figure 3 — every tagged VLAN's identity is on the wire; the native VLAN's is in two registers that nothing compares.

18. Properties Worth Asserting, and One Worth Refusing

Every property here is about one switch's transformation. Not one is about what the frame's VLAN means at the far end, and this section's rejected class is why.

Acceptance and assignment

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. The acceptable-frame-types test is on the frame's FORM. A VID-0
// frame is TAGGED and is refused by an untagged-only port, even though
// Section 7 will assign it the PVID as though it were untagged.
property p_form_test_uses_form;
  @(posedge clk) disable iff (!rst_n)
  (frame_valid && (accept == AFT_UNTAGGED_ONLY) && arrived_tagged) |-> !admit;
endproperty
a_form_test: assert property (p_form_test_uses_form);

// P2. A tagged-only port refuses untagged frames.
property p_tagged_only_refuses_untagged;
  @(posedge clk) disable iff (!rst_n)
  (frame_valid && (accept == AFT_TAGGED_ONLY) && !arrived_tagged)
    |-> (!admit && (reject == VR_WRONG_TYPE));
endproperty
a_tagged_only: assert property (p_tagged_only_refuses_untagged);

// P3. VID 4095 is refused outright -- unlike 0, it has no defined
// alternative meaning and no assignment rule could rescue it.
property p_vid_4095_refused;
  @(posedge clk) disable iff (!rst_n)
  (frame_valid && arrived_tagged && (carried_vid == 12'd4095))
    |-> (!admit && (reject == VR_RESERVED_VID));
endproperty
a_4095_refused: assert property (p_vid_4095_refused);

// P4. THE ASSIGNMENT RULE. A usable carried VID is the frame's VLAN;
// anything else falls back to the port's PVID.
property p_assignment_rule;
  @(posedge clk) disable iff (!rst_n)
  assign_valid |-> (assigned_vid ==
                    ((arrived_tagged && (carried_vid != 12'd0) &&
                      (carried_vid != 12'd4095)) ? carried_vid : port_pvid));
endproperty
a_assignment: assert property (p_assignment_rule);

// P5. THE ASYMMETRY. The VID falls back to the port; the PCP does not.
// A VID-0 frame's priority is honoured even though its VLAN is
// discarded, because the sender expressed one and not the other.
property p_pcp_survives_vid_fallback;
  @(posedge clk) disable iff (!rst_n)
  (assign_valid && arrived_tagged) |-> (assigned_pcp == carried_pcp);
endproperty
a_pcp_honoured: assert property (p_pcp_survives_vid_fallback);

// P6. An untagged frame takes the port's default priority, because it
// expressed none.
property p_untagged_takes_default_pcp;
  @(posedge clk) disable iff (!rst_n)
  (assign_valid && !arrived_tagged) |-> (assigned_pcp == port_default_pcp);
endproperty
a_default_pcp: assert property (p_untagged_takes_default_pcp);

// P7. PROVENANCE. Every assignment is recorded as carried or invented,
// because only an invented VLAN can be mismatched with the far end.
property p_provenance_recorded;
  @(posedge clk) disable iff (!rst_n)
  assign_valid |-> (vid_was_carried ^ vid_was_invented);
endproperty
a_provenance: assert property (p_provenance_recorded);

Ingress filtering

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P8. With filtering enabled, a frame assigned a VLAN the ingress port is
// not a member of is discarded.
property p_ingress_filter_drops_nonmember;
  @(posedge clk) disable iff (!rst_n)
  (assign_valid && filter_enable && !is_member)
    |-> (!pass && (reject == VR_INGRESS_FILTER));
endproperty
a_ingress_filter: assert property (p_ingress_filter_drops_nonmember);

// P9. With filtering DISABLED the frame passes -- and the comparison is
// still performed, because c_would_have_filtered is the only evidence
// the gap is being used.
property p_gap_is_measured;
  @(posedge clk) disable iff (!rst_n)
  (assign_valid && !filter_enable && !is_member)
    |=> (c_would_have_filtered > $past(c_would_have_filtered));
endproperty
a_gap_measured: assert property (p_gap_is_measured);

// P10. An invented VID never trips the filter -- a sensibly configured
// port is a member of its own PVID, so only a CARRIED VID can violate
// membership.
property p_invented_vid_is_a_member;
  @(posedge clk) disable iff (!rst_n)
  (assign_valid && vid_was_invented) |-> is_member;
endproperty
a_pvid_is_member: assert property (p_invented_vid_is_a_member);

// P11. Chapter 13.1's isolation invariant is about the EGRESS, so a
// missing ingress filter does not violate it. The two are different
// halves of the boundary.
property p_ingress_gap_is_not_isolation;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && !filter_enable) |-> vlan_members[frame_vidx][egress_port];
endproperty
a_egress_still_holds: assert property (p_ingress_gap_is_not_isolation);

The egress transformation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P12. THE ACTION IS CHOSEN BY THE PORT, not by the frame.
property p_action_from_untagged_set;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && is_member)
    |-> (action == (vlan_untagged_here
                     ? (frame_is_tagged ? EG_STRIP : EG_PASS)
                     : (frame_is_tagged ? EG_PASS  : EG_TAG)));
endproperty
a_action_choice: assert property (p_action_from_untagged_set);

// P13. A non-member port drops, whatever its untagged set says about a
// VLAN it does not carry.
property p_nonmember_drops;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && !is_member) |-> (action == EG_DROP);
endproperty
a_nonmember_drop: assert property (p_nonmember_drops);

// P14. THE NO-OP IS RECOGNISED. A frame already in the right form is not
// stripped and reinserted -- Section 13 shows that costs an FCS
// recomputation for nothing.
property p_noop_recognised;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && (action == EG_PASS)) |-> !fcs_recompute;
endproperty
a_noop_free: assert property (p_noop_recognised);

// P15. Every real transformation changes the length by exactly 4.
property p_length_delta_is_four;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && (action inside {EG_STRIP, EG_TAG}))
    |-> ((new_len == frame_len - 14'd4) || (new_len == frame_len + 14'd4));
endproperty
a_delta_four: assert property (p_length_delta_is_four);

// P16. Egress changes the frame's FORM and never its VLAN. Chapter 13.1's
// P6, at the point the tag is built.
property p_egress_never_relabels;
  @(posedge clk) disable iff (!rst_n)
  (tx_valid && emitted_tagged) |-> (emitted_vid == assigned_vid);
endproperty
a_no_relabel: assert property (p_egress_never_relabels);

// P17. THE FORM CHECK. The emitted frame's tagged-ness matches this
// port's untagged set for this VLAN.
property p_emitted_form_matches_port;
  @(posedge clk) disable iff (!rst_n)
  tx_valid |-> (emitted_tagged != vlan_untagged_here);
endproperty
a_form_matches: assert property (p_emitted_form_matches_port);

Length repair

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P18. A strip that produces a runt is RE-PADDED. Chapter 7.3 requires
// 64 octets and a 64-octet tagged frame becomes 60 without one.
property p_runt_is_repadded;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && (new_len < 14'd64)) |-> (need_pad && (final_len == 14'd64));
endproperty
a_repad: assert property (p_runt_is_repadded);

// P19. The limit that applies is the one for the tag count AFTER the
// transformation. A design checking the pre-transformation limit rejects
// its own output.
property p_limit_is_post_transform;
  @(posedge clk) disable iff (!rst_n)
  act_valid |-> (max_after == (14'd1518 + (14'(tags_after) * 14'd4)));
endproperty
a_post_limit: assert property (p_limit_is_post_transform);

// P20. Every length change recomputes the FCS. A frame whose bytes
// changed and whose check sequence did not is discarded by every
// receiver, and the port that changed it reports nothing.
property p_fcs_follows_every_change;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && ((action inside {EG_STRIP, EG_TAG}) || need_pad))
    |-> fcs_recompute;
endproperty
a_fcs_recompute: assert property (p_fcs_follows_every_change);

// P21. A self-inflicted oversize is counted SEPARATELY from one that
// arrived -- the remedies differ.
property p_self_inflicted_counted_apart;
  @(posedge clk) disable iff (!rst_n)
  (act_valid && !length_ok && (action == EG_TAG))
    |=> (c_oversize_self_inflicted > $past(c_oversize_self_inflicted));
endproperty
a_self_oversize: assert property (p_self_inflicted_counted_apart);

// P22. THE RESTATED P11. Chapter 12.1 asserted a frame is never modified;
// this chapter modifies frames by design. What survives every
// transformation is the addresses.
property p_addresses_survive;
  @(posedge clk) disable iff (!rst_n)
  tx_valid |-> ((out_da == in_da) && (out_sa == in_sa));
endproperty
a_addresses_intact: assert property (p_addresses_survive);

Native VLAN and conformance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P23. Untagged arrivals on a trunk are NORMAL -- the native VLAN
// working as designed. The counter is a denominator, not an alarm.
property p_untagged_on_trunk_is_not_an_error;
  @(posedge clk) disable iff (!rst_n)
  (learn_valid && port_is_trunk && !arrived_tagged) |-> (reject == VR_OK);
endproperty
a_native_is_normal: assert property (p_untagged_on_trunk_is_not_an_error);

// P24. A mismatch is a SUSPICION, not a verdict -- proving it needs the
// far end's PVID, which is not observable.
property p_mismatch_is_advisory;
  @(posedge clk) disable iff (!rst_n)
  native_mismatch_suspected |-> (suspect_vidx_a != suspect_vidx_b);
endproperty
a_suspicion_named: assert property (p_mismatch_is_advisory);

// P25. The signature requires BOTH qualifiers: one trunk port, and an
// untagged sighting. A router with legs in several VLANs has neither.
property p_signature_needs_both_qualifiers;
  @(posedge clk) disable iff (!rst_n)
  $rose(native_mismatch_suspected) |-> port_is_trunk;
endproperty
a_both_qualifiers: assert property (p_signature_needs_both_qualifiers);

// P26. A stale FCS is caught. The frame's bytes changed and the check
// sequence over them did not.
property p_no_stale_fcs;
  @(posedge clk) disable iff (!rst_n)
  tx_valid |-> (v_stale_fcs == $past(v_stale_fcs));
endproperty
a_no_stale_fcs: assert property (p_no_stale_fcs);

// P27. Chapter 13.1 Section 12's invariant survives every transformation
// in this chapter.
property p_isolation_survives_transformation;
  @(posedge clk) disable iff (!rst_n)
  tx_valid |-> vlan_members[frame_vidx][egress_port];
endproperty
a_isolation_holds: assert property (p_isolation_survives_transformation);

// P28. Conformance means the transformation was the one this port's
// configuration calls for -- never that the frame is in the VLAN anybody
// intended.
property p_conformant_definition;
  @(posedge clk) disable iff (!rst_n)
  conformant |-> ((v_form_wrong == '0) && (v_vid_rewritten == '0) &&
                  (v_addr_modified == '0) && (v_illegal_length == '0) &&
                  (v_stale_fcs == '0) && (v_nonmember_emit == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);

// P29. "Access port" and "trunk port" are DERIVED from the untagged set,
// not configured as a mode -- a hybrid port must remain expressible.
property p_mode_is_derived;
  @(posedge clk) disable iff (!rst_n)
  ($countones(untagged[rd_port]) == 1) |-> access_like[rd_port];
endproperty
a_mode_derived: assert property (p_mode_is_derived);

// P30. Several untagged VLANs on one port is expressible and is almost
// always wrong -- they all arrive at the far end indistinguishable.
property p_multiple_untagged_flagged;
  @(posedge clk) disable iff (!rst_n)
  ($countones(untagged[rd_port]) > 1) |-> multi_untagged_warning[rd_port];
endproperty
a_multi_untagged: assert property (p_multiple_untagged_flagged);
Chapter 12.1 asserted that a frame is never modified in transit, and this chapter modifies frames by design. The invariant is therefore restated into three parts. What must survive every transformation: the destination and source addresses, the EtherType, and the payload up to its own declared length. What must change when the port's configuration calls for it: the presence or absence of a tag, the frame's length by four octets in either direction, any padding needed to restore a stripped minimum length frame to sixty-four octets, and the frame check sequence, which must be recomputed because the bytes it covers have changed. What must never change: the VLAN assignment itself, because altering it at egress is relabelling rather than forwarding, and the frame must never be emitted from a port that is not a member of its VLAN. A frame whose bytes changed and whose check sequence did not is discarded by every receiver while the port that changed it reports nothing.Must surviveaddresses, EtherType,payloadMust changetag, ±4 octets, paddingFCS recomputedthe bytes it covers movedStale FCSevery receiver discardsitMust never changethe VLAN assignmentRelabellingworse than leakingNever a non-memberportChapter 13.1 §12,unchanged12
Figure 4 — a datapath whose job is to modify frames needs a restated invariant: what must survive, what must change, and what must never change.

19. Verification Scenarios

Seventy-two scenarios. The transformation scenarios have no acceptable failure; the native-VLAN ones have expected outcomes that include two switches both behaving correctly while traffic crosses between VLANs.

Acceptance

#ScenarioExpected
1Untagged frame, AFT_ADMIT_ALLadmitted
2Untagged frame, AFT_TAGGED_ONLYrefused, VR_WRONG_TYPE
3Untagged frame, AFT_UNTAGGED_ONLYadmitted
4Tagged VID 10, AFT_ADMIT_ALLadmitted, VLAN 10
5Tagged VID 10, AFT_UNTAGGED_ONLYrefused
6Tagged VID 0, AFT_UNTAGGED_ONLYrefused — it arrived tagged
7Tagged VID 0, AFT_ADMIT_ALLadmitted, c_vid_zero_admitted
8Tagged VID 4095, any settingrefused, VR_RESERVED_VID
9Access port, AFT_ADMIT_ALL, station tags its own framesthe station's chosen VLAN is honoured
10Same port, AFT_UNTAGGED_ONLYrefused — the safe setting is the restrictive one

Assignment

#ScenarioExpected
11Untagged arrival, PVID 10VLAN 10, vid_was_invented
12Tagged VID 20 arrival, PVID 10VLAN 20, vid_was_carried
13Tagged VID 0, PVID 10, PCP 5VLAN 10, PCP 5 — VLAN discarded, priority kept
14Untagged arrival, port default PCP 3PCP 3
15Any assignmentvid_was_carried ^ vid_was_invented
16Trunk with high c_inventedreceiving untagged frames — Section 14's warning
17Access port with high c_carriedstations are tagging; check accept

Ingress filtering

#ScenarioExpected
18Tagged VID 10, port not a member, filter ondiscarded, VR_INGRESS_FILTER
19Same, filter offadmitted into VLAN 10
20Same, filter offc_would_have_filtered increments
21Same, filter off, then the frame is forwardedChapter 13.1 §12 isolation still clean
22Untagged arrival, filter on, port is a member of its PVIDpasses — an invented VID never trips it
23Port not a member of its own PVIDa configuration error; every untagged frame filtered
24AFT_ADMIT_ALL + filter off on a station portany station can enter any VLAN

Egress transformation

#ScenarioExpected
25Tagged frame, VLAN in the untagged setEG_STRIP, −4, FCS recomputed
26Untagged frame, VLAN in the tagged setEG_TAG, +4, FCS recomputed
27Tagged frame, VLAN in the tagged setEG_PASS — no transformation, no FCS work
28Untagged frame, VLAN in the untagged setEG_PASS
29Port not a member of the VLANEG_DROP, whatever the untagged set says
30Any emitted frametagged-ness matches the port's untagged set
31Emitted VIDequals the assigned VID — never rewritten
32Access port over a runmostly c_strip and c_pass
33Trunk over a runmostly c_insert and c_pass
34Substantial mixture of all threea hybrid port — legitimate or a misconfiguration
35Strip-then-reinsert of the same frameDEI is lost unless the descriptor carries it

Length

#ScenarioExpected
3664-octet tagged frame, stripped60 — a runt; re-padded to 64
37Same, not re-paddedevery downstream receiver discards it
3868-octet tagged frame, stripped64 — no padding needed
391518-octet untagged frame, tag inserted1522 — legal, tagged maximum
40Same, checked against a fixed 1518the switch rejects its own output
411522-octet tagged frame, second tag inserted1526 — legal only as a double tag
42Any transformationFCS recomputed
43Length changed, FCS not recomputedv_stale_fcs
44Self-inflicted oversizecounted apart from an arriving one
45Access port with small frames, c_repadded = 0runts are being emitted

Native VLAN

#ScenarioExpected
46Trunk, natives match at both endsuntagged traffic stays in one VLAN
47A native 1, B native 99, A sends VLAN 1arrives in B's VLAN 99
48Same, B sends VLAN 99arrives in A's VLAN 1 — bidirectional
49Same, A sends VLAN 99 taggedarrives correctly in B's VLAN 99
50Result at BA's VLAN 1 and A's VLAN 99 are merged
51Chapter 13.1 §12 isolation at A and Bboth clean
52v_vid_changed at A and Bboth zero
53Chapter 12.2 §9's move detectorquiet
54Section 15's detectorone address, two VLANs, one trunk port, untagged sighting
55Same, a router with legs in two VLANsnot flagged — neither qualifier fits
56Trunk with an empty untagged setuntagged arrivals refused — the failure becomes a counter

Configuration shape and conformance

#ScenarioExpected
57Untagged set with exactly one VLANaccess_like
58Empty untagged settrunk_like
59Several untagged VLANs on one portflagged — they merge at the far end
60Per-port config state, 24 ports, 4094 VLANs12.0 KiB — a fifth of Chapter 13.1's 60 KiB
61Addresses across every transformationunchanged
62Healthy run, mixed port modes, one million framesconformant high throughout
63Station tags its own frames, AFT_ADMIT_ALL, filter offthe station chooses its VLAN
64Same, AFT_UNTAGGED_ONLYc_refused_type — the attempt becomes a counter
65Trunk-to-trunk tagged frame, Section 11's transformerzero FCS recomputations, EG_PASS both ends
66Same, strip-on-ingress/insert-on-egress designtwo recomputations and two realignments for no net change
67Priority regeneration table, boundary portarriving PCP 7 remapped to 0 — 24 bits per port
68Trunk to a peer under the same administrationPCP passed through unchanged
69Access port, tagged frame emitted onto itv_form_wrong — the station cannot parse it
70Trunk, untagged frame emitted onto itlands in the far end's native VLAN — the same bit, opposite direction
71Membership array merged with the untagged setevery member VLAN untagged — a trunk destroys every VID
72Per-port state, 24 ports, 4094 VLANs12.0 KiB — a fifth of Chapter 13.1's 60 KiB

20. Debugging Port Modes

Every row produces a switch that forwards frames. Several produce two switches that are both provably correct while traffic crosses between VLANs.

SymptomLikely causeThe observable that decides it
Two VLANs behaving as one across a trunknative VLAN mismatchnative_mismatch_suspected — nothing else fires
Same, and both switches report clean isolationexpected — Section 16's tablecompare the two PVIDs by hand; no device can
A station reaches a VLAN it should notAFT_ADMIT_ALL + ingress filtering offc_would_have_filtered, worst_port
Same, and isolation is cleanexpected — the gap is at the ingressadmission_gap_open
A station receives frames it cannot parsea tagged frame emitted onto an access portv_form_wrong
Downstream FCS errors from one portstale FCS after a transformationv_stale_fcs, and c_repadded = 0 on small frames
Downstream runt errors from one portstripping without re-paddingc_repadded = 0 while c_strip is high
The switch rejects frames it just createdlength checked against the pre-transform limitc_oversize_self_inflicted
A trunk's traffic lands in one VLANthe far end sends untagged; our PVID absorbs ithigh c_invented on a trunk
Several VLANs merged at the far endseveral untagged VLANs on one port$countones(untagged[port]) > 1
A phone-and-PC port called misconfiguredit is a legitimate hybridc_strip, c_insert and c_pass all substantial
DEI lost on a pathstrip-and-reinsert rebuilt the tagc_strip and c_insert on consecutive hops
Priority honoured from an untrusted portno regeneration table at the boundaryChapter 13.2 §4's c_by_pcp collapsed into one bucket
Two FCS recomputations per frame on a trunk pathstrip-on-ingress/insert-on-egressc_strip and c_insert both high where c_pass should be
A frame the sender and receiver disagree about byte-for-bytere-padding after a stripc_repadded — the switch is not a transparent relay
A trunk carrying untagged traffic nobody configuredthe far end's native, arriving as oursc_invented on a trunk, before any user symptom
Two VLANs that behave as one only for broadcastmerged at the far end by a shared nativeSection 16's table — the merge is at B, not at A
An access port whose station cannot see the networkAFT_TAGGED_ONLY on a port facing an untagged hostc_refused_type at 100% of arrivals
Isolation verified, admission never checkedthe two halves of the boundary confusedc_would_have_filtered — isolation cannot see the ingress

21. Common Misconceptions

1 — "A frame carries its VLAN."

The wrong model: the VLAN is an attribute of the frame, like its addresses.

What it costs: the entire native-VLAN failure becomes inexplicable, and Section 18's rejected property looks reasonable. An untagged frame contains no VLAN information of any kind — no field, no flag, no encoding — so its VLAN is a number the receiving port invented milliseconds after the station finished transmitting.

The corrected model: a VLAN is carried only on a tagged link, in octets 14–15. Everywhere else it is a descriptor field inside a switch, and across an untagged link it is destroyed at transmit and re-invented at receive. Continuity is a property of two configurations agreeing.

2 — "Access port and trunk port are hardware modes."

The wrong model: a port is set to one of two modes and behaves accordingly.

What it costs: the hybrid port becomes impossible to express — one VLAN untagged and several tagged, which is how every IP phone with a PC behind it is connected. A design implementing two enumerated modes has hard-coded two points of a continuous space.

The corrected model: the behaviour comes from five configuration fields plus the untagged set, and "access" and "trunk" are names for particular settings. An access port's untagged set has exactly one VLAN; a trunk's is empty. Section 3 derives the names from the settings rather than the other way round, which keeps the third case expressible and makes a port that is neither visible.

3 — "Ingress filtering is a minor optimisation."

The wrong model: the egress membership check provides isolation, so the ingress check is redundant.

What it costs: with AFT_ADMIT_ALL and filtering off, any station can place its traffic in any VLAN by tagging its own frames — and Chapter 13.1 §12's isolation invariant stays clean throughout, because every port the frame then reaches is a member of the VLAN it claimed.

The corrected model: isolation is about the egress; admission is about the ingress, and they are different halves of the boundary. A switch can be provably isolated and still let anybody into any VLAN. c_would_have_filtered costs one comparator and is the only evidence the gap is being used.

4 — "Stripping a tag is the reverse of adding one."

The wrong model: the two transformations are symmetric, so what one does the other undoes.

What it costs: the runt. A minimum-length tagged frame is 64 octets; stripping four leaves 60, which Chapter 7.3 makes illegal and every receiver discards. The strip must be followed by a re-pad, and the padding is four octets that the original frame did not have — so the frame the receiver sees is not the frame the sender sent.

The corrected model: every transformation changes the frame's length, its FCS, and — at the bottom of the size range — its padding. Chapter 12.1's P11, that a frame is never modified, is false by design in this chapter, and Section 12's callout restates it into what must survive, what must change and what must never change.

5 — "A native VLAN mismatch shows up as an error."

The wrong model: something that joins two broadcast domains must trip something.

What it costs: weeks. Every check this track has built reports success — isolation clean at both ends, v_vid_changed zero at both ends, no filtering violations, no format errors, Chapter 12.2 §9's move detector quiet. And they are not failing to look: nothing anomalous happened. Each switch received a well-formed untagged frame on a trunk and applied its configured rule.

The corrected model: the disagreement is between two registers in two buildings, and no frame ever carried either of them. The only signal is Section 15's pattern — one address, two VLANs, one trunk port, with an untagged sighting among them — and it is a suspicion rather than a proof, because proving it needs the far end's PVID.

6 — "Carrying a native VLAN is free."

The wrong model: it is just one more VLAN on the trunk.

What it costs: it is the one VLAN on the trunk whose identity is not transmitted. Every tagged VLAN's VID is in the frame, so two ends disagreeing about what VLAN 10 means is a naming disagreement that cannot move traffic. The native VLAN's identity is in two registers that nothing compares, and a disagreement there moves traffic between broadcast domains.

The corrected model: the mitigation is to carry no native VLAN — an empty untagged set, every VLAN tagged, and AFT_TAGGED_ONLY so untagged arrivals are refused rather than assigned. The cost is that the trunk cannot carry untagged traffic, and the benefit is that the failure becomes a counter. Given that Section 16's table shows every other observable is clean, that is not a close trade.

22. Interview Reasoning

Q1 — "Where does an untagged frame's VLAN come from?"

Reason through it. From the receiving port's configured PVID, invented at ingress. An untagged Ethernet frame contains no VLAN information of any kindChapter 5.1's layout has destination, source, EtherType, payload and FCS, and none of them says anything about a VLAN. The strong answer traces the whole journey: the station has no VLAN concept, the access link carries no VID, the port assigns one, a trunk carries it in octets 14–15, and the far end's access port strips it again — so the VLAN exists for the middle of the journey and nowhere else. And it draws the consequence: across any untagged link, continuity is a property of two configurations agreeing rather than of the frame.

Q2 — "What does a port do with a frame tagged VID 0?"

Reason through it. Two rules apply and they use different senses of "untagged". The acceptable-frame-types test is on the frame's form — it arrived tagged, so an AFT_UNTAGGED_ONLY port refuses it. The assignment rule is on its content — VID 0 names no VLAN, so the port supplies its PVID. The strong answer names the asymmetry that makes VID 0 useful at all: the VID falls back to the port and the PCP does not — the frame's priority is honoured even though its VLAN is discarded, because Chapter 13.2 §15 established the two fields are independent and the sender expressed one and not the other. A design that discards the whole tag on VID 0 has thrown away the only thing the sender asked for.

Q3 — "Ingress filtering is disabled and isolation is verified working. Is the switch secure?"

Reason through it. No, and the two facts are compatible. Isolation is about the egress: no frame leaves a port that is not a member of its VLAN. A frame injected into VLAN 10 through a non-member ingress port satisfies that perfectly, because every port it then reaches is a member. The strong answer names the combination that makes it exploitable: AFT_ADMIT_ALL plus filtering off means a station can place its traffic in any VLAN by tagging its own frames, and both are common defaults. And it names the evidence: c_would_have_filtered costs one comparator, is the only record that the gap is being used, and — with Section 7's provenance — admission_gap_open narrows it to the frames whose VID was carried, since an invented VID cannot violate membership.

Q4 — "A 64-octet tagged frame leaves an access port. What does the switch have to do?"

Reason through it. Strip the tag, which takes the frame to 60 octets — a runt — then re-pad to 64 and recompute the FCS. Chapter 7.3 requires 64 octets, so the stripped frame is illegal and every receiver discards it. The strong answer notes what that costs in honesty: the frame the receiver sees is not the frame the sender sent — different length, different FCS, and four octets of padding that were not there. Chapter 12.1's P11, that a frame is never modified in transit, is false by design here, and the invariant has to be restated as the addresses, the EtherType and the payload up to its declared length survive; everything else is the switch's to change.

Q5 — "Two switches, trunk between them, natives 1 and 99. What happens, and what fires?"

Reason through it. A's VLAN 1 traffic crosses untagged and lands in B's VLAN 99; B's VLAN 99 traffic crosses untagged and lands in A's VLAN 1 — a bidirectional join. And it is worse than a swap: A's VLAN 99 traffic crosses tagged and arrives correctly in B's VLAN 99, so B's VLAN 99 now contains two of A's VLANs, merged. The strong answer enumerates what fires: nothing. Isolation clean at both ends, v_vid_changed zero at both, no filtering violations, no format errors, the move detector quiet — and not because the checks are broken, but because each switch received a well-formed untagged frame on a trunk and applied its configured rule. The only signal is the pattern: one address in two VLANs through one trunk port with an untagged sighting among them.

Q6 — "Why can't a switch assert that a frame stays in the VLAN it started in?"

Reason through it. Because on any path with an untagged link, the frame's "source VLAN" is not a property of the frame — it is a number the first switch's ingress port invented from a register. The property compares two values, neither of which the frame carried. The strong answer places it against the track's other rejected properties: Chapter 12.4's subject never travelled back, Chapter 12.6's had not been transmitted yet, and this one's is not on the wire at all, in either direction, ever. What is assertable is the local transformation, the provenance and the pattern — the assignment rule applied exactly, the egress form matching the port, one bit recording whether the VLAN was carried or invented, and a suspicion naming an address, two VLANs and a port when the evidence supports one.

23. Understanding Check

24. What's Next

Module 13 has one chapter left, and it is the one that builds what the previous three have been specifying.

Chapter 13.4 — VLAN Processing in the Switch Datapath assembles it: the VID-to-index map Chapter 13.1 §10 priced at 4 KiB against a dense array's 60, the shared table with its 60-bit key that chapter's Section 8 argued for, and the mapping from Chapter 13.2'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. It also recomputes Chapter 12.5's capacity arithmetic at 60 bits, which changes less than most people expect and one thing by exactly 25%.

Then Module 14 — Flow Control takes up the question every chapter since Chapter 12.1 has deferred. That chapter's §6 established that discarding is a switch's specified response to congestion and rejected backpressure as a cure, on the grounds that it converts a local congestion into a global one. Module 14 asks what a mechanism that applies backpressure anyway has to look like — where the queues actually build, what the PAUSE frame does, and why the pathology Chapter 12.1 predicted is exactly what happens.

And this chapter's PCP field is how that mechanism is eventually made per-class rather than per-link, which is where Module 14 ends.

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.