Skip to content
VLSI Mentor

Ethernet · Module 13

Why VLANs Exist

Segmentation is the only lever on the broadcast-domain limit, and it turns every singleton in a switch into a vector. Supporting all 4094 VLANs costs 60 KiB — 0.94× the entire forwarding table.

Chapter 12.4 §10 derived a limit and could not act on it. A broadcast domain tops out near 200 stations — not for any reason inside the switch, but because every station must process every broadcast and no hardware filter can discard one. Chapter 12.4 §12 then showed that storm control cannot hold a domain inside that budget: the cap that would is 43.5 frames per second per port, 0.00292% of line rate, finer than any switch expresses.

The limit is (N − 1) × B, and the only variable a designer controls is N.

Segmentation reduces N — and it does so without discarding a single frame. Splitting 200 stations into four groups of 50 cuts every station's received broadcast load by 4.06×, and every broadcast is still delivered in full to everyone with any business receiving it.

This chapter is about what that costs the switch.

And the cost is not a feature bolted on. It is that every singleton resource in Module 12's switch becomes a vector — and the multiplication is not uniform. Some things multiply by the VLAN count, one thing merely widens, and one thing must not be partitioned even though partitioning is the obvious implementation.

1. Scope — What This Chapter Owns

This chapter owns the requirement: why segmentation is the only remaining lever, what a switch must keep per-VLAN that it kept once, which resources widen and which multiply, and the isolation invariant that defines what a VLAN is.

It does not own the tag. The four octets that carry a VID, their fields and what inserting them does to a frame parser are Chapter 13.2's subject. This chapter treats the VID as a number that has arrived by some means and asks what the switch does with it.

It does not own port modes. Which ports carry tagged frames, which carry untagged, and where a tag is inserted or stripped is Chapter 13.3. This chapter assumes the VID is available at the ingress and stops there.

And it does not own the datapath implementation. VID lookup structure, per-VLAN table organisation and priority mapping into egress queues are Chapter 13.4. This chapter states what must be true and hands the building to that chapter — Section 8's shared-table argument is the requirement 13.4 implements, not the implementation.

Module 12 is assumed throughout, and heavily: Chapter 12.3's six gates, Chapter 12.4's flood mask and domain limit, Chapter 12.5's table structure, Chapter 12.2's learning rule. Every one of them acquires a VLAN dimension here.

2. What Segmentation Has to Achieve

State the requirement precisely before choosing a mechanism, because the precise version rules out the obvious implementations.

A broadcast domain is the set of stations that receive each other's broadcasts. Chapter 12.4 §10 established that this set's size is bounded by the receivers' CPU, not by anything in the switch. Segmentation must make one physical switch behave as several independent switches, each with its own smaller domain.

Which means, per VLAN:

RequirementWhy
a broadcast reaches only ports in its VLANotherwise N has not been reduced
an unknown-unicast flood reaches only ports in its VLANsame — Chapter 12.4's three causes all flood
a multicast flood reaches only ports in its VLANsame
a unicast forward crosses only within its VLANotherwise isolation is decorative
the table answers per VLANthe same address may exist in two
port state is per VLANa port may forward in one and block in another

And one requirement that is easy to miss and is the reason a VLAN is not merely a filter on flooding:

Learning must be per VLAN. Chapter 12.2's inference was a frame with source S arrived on port P, therefore S is reachable through P. With VLANs it becomes a frame with source S in VLAN V arrived on port P, therefore S in V is reachable through P — and the two are different claims, because S in a different VLAN may be somewhere else entirely.

A frame arrives carrying a VLAN identifier alongside its destination and source addresses. In a correct implementation the identifier is concatenated with the destination address to form a sixty bit lookup key, so the table answers per VLAN, and it also selects the flood mask so that a flood reaches only the ports belonging to that VLAN. In the tempting but incorrect implementation the switch forwards exactly as it would without VLANs, generating copies for every port, and then discards at the egress any copy destined for a port not in the frame's VLAN. That produces correct isolation while wasting fabric bandwidth on copies that are built and thrown away, and more seriously it cannot repair a lookup that returned the wrong port, because with a MAC only key the same address in two VLANs shares one entry updated by whichever station transmitted last, so the frame is forwarded to a port in the wrong VLAN and silently discarded while the correct destination receives nothing.Frame with VIDVID, destination, sourceKey = {VID, MAC}60 bits — the tableanswers per VLANMask = VLAN membersthe flood is scopedCorrect per VLANisolation is aconsequenceForward as beforeMAC-only keyFilter at the egressdiscard wrong-VLAN copiesFrame silently lostwrong port, thendiscarded12
Figure 1 — the VID enters at the key and at the mask, not at the exit; a boundary filter gives the right answer for the wrong reason.

3. RTL 1 — Modelling the Requirement

Before any mechanism, write down what "these stations can hear each other" means as something checkable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlanreq_pkg -- shared types for VLAN segmentation.
//
// This chapter owns the REQUIREMENT. Chapter 13.2 owns the tag that
// carries a VID, Chapter 13.3 owns how ports insert and strip it, and
// Chapter 13.4 owns the datapath that implements the lookup. The types
// here are the contract between those chapters.
// -----------------------------------------------------------------------
package vlanreq_pkg;

  localparam int VID_W    = 12;
  localparam int ADDR_W   = 48;
  localparam int KEY_W    = VID_W + ADDR_W;   // 60 -- Section 7

  // 0 and 4095 are reserved by 802.1Q, leaving 4094 usable.
  localparam int VID_MIN  = 1;
  localparam int VID_MAX  = 4094;

  // How a resource acquires the VLAN dimension. Getting a resource into
  // the wrong category is this chapter's catalogue of bugs.
  typedef enum logic [1:0] {
    RD_WIDENED     = 2'd0,  // the key: 48 -> 60 bits, one table
    RD_MULTIPLIED  = 2'd1,  // masks, port state: one copy per VLAN
    RD_PARTITIONED = 2'd2,  // WRONG for the table -- Section 8
    RD_UNCHANGED   = 2'd3   // the frame buffer, the arbiter
  } resource_dim_e;

  typedef enum logic [2:0] {
    VE_OK              = 3'd0,
    VE_VID_RESERVED    = 3'd1,  // 0 or 4095
    VE_PORT_NOT_MEMBER = 3'd2,  // ingress port not in this VLAN
    VE_NO_MEMBERS      = 3'd3,  // VLAN exists but has no other ports
    VE_VID_UNKNOWN     = 3'd4   // VID not configured on this switch
  } vlan_reject_e;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// domain_membership_model -- who can hear whom.
//
// The requirement, made checkable. Two stations are in the same broadcast
// domain if and only if their ports share a VLAN. Everything else in this
// chapter is machinery for making that true cheaply.
// -----------------------------------------------------------------------
module domain_membership_model
  import vlanreq_pkg::*;
#(
  parameter int N_PORTS = 24,
  parameter int N_VLANS = 64,          // configured, not the full 4094
  parameter int VIDX_W  = 6,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  // membership[v][p] -- port p belongs to VLAN index v.
  input  logic [N_PORTS-1:0]   membership [N_VLANS],

  input  logic                 q_valid,
  input  logic [VIDX_W-1:0]    q_vidx,
  input  logic [4:0]           q_port_a,
  input  logic [4:0]           q_port_b,

  output logic                 same_domain,
  output logic [5:0]           domain_size,        // ports in this VLAN
  output logic [5:0]           largest_domain,
  output logic [VIDX_W-1:0]    largest_vidx,
  output logic [CNT_W-1:0]     c_queries,
  output logic                 domain_oversized    // above the CPU budget
);

  // THE DEFINITION. Two ports are in one broadcast domain exactly when
  // they share a VLAN. Not "when they are on the same switch", not "when
  // a cable connects them" -- membership is the whole of it.
  assign same_domain = q_valid &&
                       membership[q_vidx][q_port_a] &&
                       membership[q_vidx][q_port_b];

  always_comb begin
    domain_size = 6'd0;
    for (int p = 0; p < N_PORTS; p++)
      if (membership[q_vidx][p]) domain_size = domain_size + 6'd1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_queries        <= '0;
      largest_domain   <= '0;
      largest_vidx     <= '0;
      domain_oversized <= 1'b0;
    end else begin
      if (q_valid) begin
        c_queries <= c_queries + 1'b1;
        if (domain_size > largest_domain) begin
          largest_domain <= domain_size;
          largest_vidx   <= q_vidx;
        end
      end

      // Chapter 12.4 Section 10's limit, applied to a VLAN rather than a
      // switch. The port count is a lower bound on the station count --
      // a port with a shared segment behind it holds several stations --
      // so this is optimistic and still worth reporting.
      domain_oversized <= (largest_domain > 6'd32);
    end
  end

endmodule

Classification: model — membership is configuration, and this module exists to make the requirement checkable rather than to be synthesised into a datapath.

What it teaches: that membership is the entire definition of a broadcast domain, and it replaces every physical notion. Before VLANs, "the same domain" meant reachable without crossing a router — a topological fact about cables. After VLANs it means sharing a VLAN membership entry, which is a fact about configuration, and two ports on adjacent sockets of one switch may be in different domains while two ports in different buildings are in the same one.

And domain_oversized carries Chapter 12.4 §10's limit forward into the new unit. The limit was never about switches; it was about how many stations hear each other. Segmentation does not change the limit — it changes what the limit applies to, and a VLAN spanning forty switches has exactly the same problem a flat network of the same size had.

Deliberately simplified: a dense membership[v][p] bitmap over 64 configured VLANs. Section 10 shows why the full 4094-VLAN version of this array is 12 KiB and why production designs keep it dense anyway rather than compressing — the lookup is on the critical path.

Production implication: largest_domain and largest_vidx are the numbers that make Chapter 12.4 §10 operational. An operator who has created VLANs has not automatically solved the broadcast problem — they have solved it for the VLANs that are small, and one VLAN containing most of the ports has the original problem with extra configuration. The counter names it directly.

4. The Reduction, Measured

Segmentation's benefit is linear in the number of domains, and it discards nothing. Put it next to Chapter 12.4 §12's rate limiter, which is the alternative.

200 stations, each emitting B = 5 broadcasts per second:

VLANsStations eachEach station receivesHost CPU at 5 µsReduction
1200995 /s0.50% of a core
2100495 /s0.25%2.01×
450245 /s0.12%4.06×
825120 /s0.06%8.29×
161255 /s0.03%18.09×

And the comparison that matters is not the magnitude but the mechanism.

storm control — Chapter 12.4 §11segmentation
what it changesB, the injection rateN, the audience
frames discardedyes — legitimate ARP suppressednone
granularity available0.1% of line rate, 34× too coarseone VLAN
effect on a station's own VLANbroadcast is capped for everyoneunchanged — full delivery
what it costs the switcha counter per port60 KiB of state — Section 10
scales withnothingthe number of domains

Storm control trades correctness for survival: once the cap engages, an ARP request is suppressed and a connection does not establish.

Segmentation trades silicon for the same result. Every broadcast is delivered in full to every station in its VLAN. The stations that were never going to care are simply not in the domain any more — and Section 10 prices that in bytes of on-chip state.

What segmentation does not fix

Reducing N attacks one term of one problem. It is worth writing down what it leaves exactly where it was.

ProblemDoes segmentation help?Why
broadcast load per stationyes — linearly in Vfewer stations hear each other
unknown-unicast floodingpartlythe flood is scoped, but the miss still happens
multicast wastepartlyscoped to the VLAN; within it, Chapter 12.4 §13's 87% remains
a duplicate MAC addressnotwo stations in one VLAN still conflict
table capacitynothe table is shared; the same addresses are learned
congestion on an egress portnoa wire carries every VLAN's traffic
Chapter 12.5's hash clusteringno — slightly betterthe VID adds entropy, nothing more
a failing cablenoChapter 12.6's error propagation is unchanged

The second row is the one that surprises people. Segmentation makes an unknown-unicast flood cheaper — 3 copies instead of 23 — but it does not make the miss less likely. Chapter 12.4 §2's five reasons a present station produces a miss are all unaffected: a station that has never transmitted is still unlearned, an entry that aged out is still gone.

And the sixth row is worth stating because it is a common expectation. A VLAN is a broadcast-domain boundary, not a bandwidth partition. Two VLANs sharing a physical port share its bandwidth entirely, and Chapter 12.1 §9's arbiter does not know they are different — which is exactly why Section 6 put the egress arbiter in the unchanged category, and why per-VLAN bandwidth is a priority mechanism belonging to a different chapter.

5. RTL 2 — Scoping the Flood

This is the single change that makes a VLAN a VLAN. Chapter 12.4 §5's flood mask acquires a VLAN dimension, and every one of that chapter's three causes is scoped by it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// broadcast_scope_limiter -- builds Chapter 12.4's flood mask restricted
// to one VLAN's members.
//
// Chapter 12.3 Section 9 established the mask as
//     forwarding_mask & ~(1 << ingress_port)
// and this module makes it a THREE-way intersection by adding VLAN
// membership. All three conditions, not any two.
// -----------------------------------------------------------------------
module broadcast_scope_limiter
  import vlanreq_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                 flood_req,
  input  logic [VIDX_W-1:0]    vidx,
  input  logic [4:0]           ingress_port,
  input  logic [N_PORTS-1:0]   forwarding_mask,     // Chapter 12.3 gate 5
  input  logic [N_PORTS-1:0]   vlan_members [N_VLANS],

  output logic                 mask_valid,
  output logic [N_PORTS-1:0]   flood_mask,
  output logic [5:0]           fanout,
  output vlan_reject_e         reject,

  output logic [CNT_W-1:0]     c_floods,
  output logic [CNT_W-1:0]     c_copies_saved,      // vs an unscoped flood
  output logic [CNT_W-1:0]     c_no_members,
  output logic [15:0]          scope_ratio_pct      // scoped / unscoped
);

  logic [N_PORTS-1:0] all_but_ingress, unscoped, scoped;

  assign all_but_ingress = ~(N_PORTS'(1) << ingress_port);
  assign unscoped        = forwarding_mask & all_but_ingress;

  // THE THREE-WAY INTERSECTION. Members of this VLAN, AND permitted to
  // forward, AND not the ingress port. Dropping any one of the three is
  // a distinct bug:
  //   without members       -> no isolation at all
  //   without forwarding    -> frames to blocked ports, Chapter 12.3 P11
  //   without ~ingress      -> an echo, Chapter 12.1 P9
  assign scoped = vlan_members[vidx] & forwarding_mask & all_but_ingress;

  always_comb begin
    reject = VE_OK;
    if (flood_req) begin
      if (!vlan_members[vidx][ingress_port])      reject = VE_PORT_NOT_MEMBER;
      else if (scoped == '0)                      reject = VE_NO_MEMBERS;
    end
  end

  assign mask_valid = flood_req && (reject == VE_OK);
  assign flood_mask = (reject == VE_OK) ? scoped : '0;

  always_comb begin
    fanout = 6'd0;
    for (int p = 0; p < N_PORTS; p++) if (flood_mask[p]) fanout = fanout + 6'd1;
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_floods       <= '0;
      c_copies_saved <= '0;
      c_no_members   <= '0;
      scope_ratio_pct<= 16'd100;
    end else if (flood_req) begin
      c_floods <= c_floods + 1'b1;
      // THE FEATURE'S VALUE, IN COPIES. An unscoped flood on a 24-port
      // switch is 23 copies; a 4-member VLAN's flood is 3. The saving is
      // Chapter 12.4's amplification, avoided rather than discarded.
      c_copies_saved <= c_copies_saved +
                        CNT_W'($countones(unscoped) - $countones(scoped));
      if (reject == VE_NO_MEMBERS)
        if (!(&c_no_members)) c_no_members <= c_no_members + 1'b1;
      scope_ratio_pct <= ($countones(unscoped) == 0) ? 16'd0
                       : 16'(($countones(scoped) * 100) / $countones(unscoped));
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the mask is a three-way intersection and each conjunct prevents a different failure. Without vlan_members there is no isolation at all. Without forwarding_mask the flood reaches blocked ports — Chapter 12.3's P11. Without ~ingress the frame is echoed to its own segment — Chapter 12.1's P9, the most fundamental invariant in switching. Three conditions, three unrelated bugs, one &.

And c_copies_saved measures the feature rather than asserting it. Chapter 12.4 §3 showed a flood on a 24-port switch costs 23 copies. A flood in a 4-member VLAN costs 3 — and the 20 copies not made are fabric bandwidth never spent, not bandwidth spent and then discarded. Section 2's callout is why the distinction matters.

Deliberately simplified: a dense vlan_members array indexed by a compact VLAN index rather than a raw 12-bit VID. Chapter 13.4 owns that indirection — a switch supporting 4094 VIDs but only 64 configured VLANs keeps a VID-to-index map rather than 4094 masks.

Production implication: VE_NO_MEMBERS is a condition that reads as an error and usually is not. A VLAN with exactly one member port produces a flood mask of zero — the only station in it has nowhere to send a broadcast, so the frame goes nowhere and is correct. It is also, frequently, a misconfiguration: a VLAN created on the wrong switch, or one whose trunk membership was not extended. The counter cannot distinguish them and should not try — what it does is make an otherwise invisible condition visible to somebody who can.

6. Three Kinds of Change: Widened, Multiplied, Partitioned

Every resource in Module 12's switch acquires the VLAN dimension in exactly one of three ways, and choosing the wrong one is a distinct and diagnosable bug.

ResourceCorrect dimensionCostWhat the wrong choice does
the lookup keywidened — 48 → 60 bits+25%keeping 48 bits: Section 15's collision
the forwarding tableshared, one structurenonepartitioning it: Section 8's 2 entries per VLAN
the flood maskmultiplied — one per VLAN12 KiBone mask: no isolation
port statemultiplied36 KiBone state: a port cannot block per VLAN
membershipnew12 KiB
the frame bufferunchangednonemultiplying it: 4094× the memory, for nothing
the egress arbiterunchangednoneper-VLAN arbiters: 4094 schedulers per port
ageing intervalsmultipliedtinyone interval: acceptable, and less flexible

The two unchanged rows are worth as much attention as the others, because the instinct that "VLANs partition the switch" suggests multiplying everything.

A frame buffer holds octets. Octets have no VLAN — the buffer stores a frame and a descriptor names its VLAN, exactly as Chapter 12.4 §5's replicator stored one copy and emitted many handles. Multiplying the buffer per VLAN would multiply the switch's most expensive memory by up to 4094 to hold the same data.

An egress arbiter serves one physical port. Chapter 12.1 §9's round robin picks among ingress ports contending for one wire, and the wire does not know about VLANs. Per-VLAN arbitration is a priority question — Chapter 13.2's PCP field and Chapter 13.4's queue mapping — and it is a different mechanism with a different justification.

Which gives the rule this section exists for: a resource acquires the VLAN dimension exactly when its correct value differs per VLAN. The key does — the same address means different stations. The masks do. The buffer does not, because the frame's octets are the frame's octets.

A VLAN is not a switch

Chapter 12.4 §10 already established that N is topological rather than per-device. Segmentation does not change that — it changes which topology N is measured over.

A VLAN spans every switch that carries it. Four switches with 12 member ports each put 48 stations in one VLAN, and every one of them hears the other 47 — exactly as if they were on one device.

4 switches, one flat domain4 switches, 4 VLANs of 12 ports each
stations per domain4 × 23 = 9212
each receives at B = 5/s455 /s55 /s
the trunk carriesall 460 broadcasts/sall 460 still — every VLAN crosses it
host CPU0.23% of a core0.03%

The trunk row is the one worth noticing. Segmenting reduces what each station receives and does not reduce what the inter-switch link carries — every VLAN's broadcast still crosses it, because the link is a member of all of them. The trunk's load is a function of the total station count, not of how the stations are divided.

Which gives the design rule: segment for the hosts, and size the trunk for the sum. Chapter 12.4 §10's uplink arithmetic — 10 000 × 672 = 6.7 Mb/s of pure broadcast at 2000 stations — is unaffected by how many VLANs those 2000 stations are divided into.

A fifth category: per-VLAN by policy, not by necessity

Three resources sit outside Section 6's three categories, because their correct value does not have to differ per VLAN — an operator may simply want it to.

ResourceMust it differ per VLAN?Why it often does anyway
ageing intervalChapter 12.2 §7noa VLAN of servers idles differently from a VLAN of laptops
learning rate limitChapter 12.2 §11noa VLAN facing untrusted ports wants a tighter cap
storm-control thresholdChapter 12.4 §11noa VLAN carrying video multicast needs a higher one

The distinction matters because the cost model is different. A resource in the multiplied category must have a per-VLAN copy or the design is wrong — Section 10 prices those at 60 KiB. These three are policy knobs, and a design that offers only a global value is less flexible, not incorrect.

Which is the right place to economise. Per-VLAN ageing intervals cost 4094 × 9 = 36 864 bits, 4.5 KiB in the dense form — cheap next to the 36 KiB port-state array — but a switch that offers one global interval still isolates correctly, still forwards correctly, and still passes every property in Section 17.

And it is worth stating the rule explicitly, because "the operator might want it per VLAN" is an argument that never terminates: a resource belongs in the multiplied category when a single value makes the design incorrect, and in this fifth category when a single value merely makes it less adjustable. Port state is the first. Ageing is the second.

Three categories of resource change when a switch acquires VLANs. The lookup key widens from forty-eight bits to sixty, because the same MAC address in two different VLANs refers to two different stations that may be on different ports, so the identity of a station now requires both the VLAN identifier and the address. Masks and port state multiply, with one copy per VLAN, because a port may belong to one VLAN and not another and may forward in one while blocking in another, so their correct values differ per VLAN. The frame buffer and the egress arbiter do not change at all, because a frame's octets are the same octets regardless of which VLAN the frame belongs to, and an egress arbiter serves one physical wire which has no VLAN. The rule is that a resource acquires the VLAN dimension exactly when its correct value differs per VLAN, and multiplying a resource whose value does not differ wastes the switch's most expensive memory.Does the valuediffer per VLAN?the only questionWiden: the key48 → 60 bits, +25%Multiply: masks,state60 KiB at 4094 VLANsUnchanged: buffer,arbiteroctets have no VLANPartition the tablethe tempting fourthoption2 entries per VLANat 4094 VLANsShare it insteadone table, wider key12
Figure 2 — the same address is a different station in a different VLAN, so the key widens; the octets are the same octets, so the buffer does not multiply.

7. RTL 3 — Widening the Key

The station's identity is no longer its address. It is its address in a VLAN, and that is a 60-bit quantity.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_key_widener -- forms the {VID, MAC} lookup key and states what the
// widening costs Chapter 12.5's structure.
//
// SCOPE: this module produces the key and quantifies the cost. Chapter
// 13.4 owns how the wider key is organised in the datapath, and Chapter
// 12.5's hash and set-associative structure absorb it unchanged apart
// from the width.
// -----------------------------------------------------------------------
module vlan_key_widener
  import vlanreq_pkg::*;
#(
  parameter int N_ENTRIES = 8192,
  parameter int CNT_W     = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 in_valid,
  input  logic [VID_W-1:0]     vid,
  input  logic [ADDR_W-1:0]    mac,

  output logic                 key_valid,
  output logic [KEY_W-1:0]     key,
  output vlan_reject_e         reject,

  // Cost accounting against Chapter 12.5's structure.
  output logic [31:0]          cam_cells_48,
  output logic [31:0]          cam_cells_60,
  output logic [15:0]          cam_growth_pct,
  output logic [7:0]           entry_bits_48,
  output logic [7:0]           entry_bits_60,
  output logic [CNT_W-1:0]     c_keys,
  output logic [CNT_W-1:0]     c_reserved_vid
);

  // 802.1Q reserves VID 0 -- priority-tagged, no VLAN -- and 4095. A
  // frame carrying either must not produce a table key, because neither
  // names a VLAN. Chapter 13.2 owns what they DO mean.
  logic vid_reserved;
  assign vid_reserved = (vid == VID_W'(0)) || (vid == VID_W'(4095));

  always_comb begin
    reject = VE_OK;
    if (in_valid && vid_reserved) reject = VE_VID_RESERVED;
  end

  assign key_valid = in_valid && (reject == VE_OK);

  // THE KEY. VID in the high bits so that entries for one VLAN are
  // adjacent in any structure that preserves key order -- which matters
  // for the per-VLAN flush a topology change requires.
  assign key = {vid, mac};

  // What the widening costs. Chapter 12.5 Section 4's CAM comparison and
  // Section 6's entry, recomputed at 60 bits.
  assign cam_cells_48   = 32'(N_ENTRIES) * 32'd48;
  assign cam_cells_60   = 32'(N_ENTRIES) * 32'(KEY_W);
  assign cam_growth_pct = 16'(((32'(KEY_W) - 32'd48) * 32'd100) / 32'd48);
  // Chapter 12.5 Section 6: key + 5-bit port + 9-bit age + valid + static.
  assign entry_bits_48  = 8'd48 + 8'd16;
  assign entry_bits_60  = 8'(KEY_W) + 8'd16;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_keys <= '0; c_reserved_vid <= '0;
    end else if (in_valid) begin
      if (key_valid) c_keys <= c_keys + 1'b1;
      else if (reject == VE_VID_RESERVED)
        if (!(&c_reserved_vid)) c_reserved_vid <= c_reserved_vid + 1'b1;
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that the widening is arithmetically modest and conceptually total. 48 → 60 bits is +25% on Chapter 12.5's CAM cells — 393 216 → 491 520 — and takes the entry from 64 bits to 76, which rounds to 80. Nothing about Chapter 12.5's hash, associativity or sweep changes; the structure absorbs a wider key without any structural difference.

And it teaches why the VID goes in the high bits. Any structure preserving key order keeps one VLAN's entries adjacent, which makes a per-VLAN flush a range operation rather than a full-table scan — and a per-VLAN flush is exactly what a topology change within one VLAN requires, without disturbing the other 4093.

Deliberately simplified: the reserved-VID check rejects and counts. Chapter 13.2 owns what VID 0 actually means — a priority-tagged frame carrying PCP with no VLAN assignment — and Chapter 13.3 owns what a port does with it, which is to apply the port's own default VLAN.

Production implication: c_reserved_vid rising is a configuration signal, not a fault. A stream of VID-0 frames means something upstream is emitting priority tags without VLAN assignment, which is legal and which this port is not configured to handle. The frames are not being isolated wrongly — they are not being placed in a VLAN at all, and where they end up is Chapter 13.3's default-VLAN rule rather than anything this chapter decides.

8. Why the Table Is Shared and Not Partitioned

The obvious implementation of "the switch behaves as several switches" is to give each VLAN its own table. Compute what that costs and the option disappears.

Chapter 12.5 built an 8192-entry table. Divide it:

VLANs configuredEntries per VLANVerdict
24096workable
42048workable
16512tight
64128smaller than one rack of servers
25632unusable
4094 — the full range2absurd

And the failure is worse than the numbers suggest, because it is unrelated to the actual load. A switch with 64 VLANs configured, of which one carries 3000 stations and the other 63 carry a handful each, has 128 entries for the busy one and 63 partitions sitting almost entirely empty. The table is 96% free and the VLAN that needs it is full.

A shared table with a widened key has none of this. All 8192 entries serve whichever VLANs need them, in whatever proportion the traffic demands, and the only cost is the 25% key widening Section 7 priced.

The requirement, stated for Chapter 13.4 to implement:

One table, keyed on {VID, MAC}, with entries allocated by demand rather than by configuration.

Which is why Section 6 put the table in the widened category and not the multiplied one, and why the distinction is worth a section. Both implementations isolate correctly. One of them makes the switch's headline capacity a function of how many VLANs an operator happened to create, which is a coupling nobody wants and which appears only under a load pattern nobody tested.

9. RTL 4 — The State That Multiplies

Masks and port state are the resources whose correct value genuinely differs per VLAN, and they are what makes 4094 VLANs expensive.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// per_vlan_state_vector -- membership, flood mask and port state, each
// with a VLAN dimension.
//
// These are the resources Section 6 categorised as MULTIPLIED. Each has a
// genuinely different correct value per VLAN, which is why none of them
// can be shared the way the table is.
// -----------------------------------------------------------------------
module per_vlan_state_vector
  import vlanreq_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,

  // Configuration writes.
  input  logic                 cfg_valid,
  input  logic [VIDX_W-1:0]    cfg_vidx,
  input  logic [4:0]           cfg_port,
  input  logic                 cfg_member,
  input  logic [2:0]           cfg_port_state,   // Chapter 12.3's five states

  // Per-frame reads.
  input  logic                 rd_valid,
  input  logic [VIDX_W-1:0]    rd_vidx,
  input  logic [4:0]           rd_port,

  output logic                 is_member,
  output logic [2:0]           port_state,
  output logic [N_PORTS-1:0]   members,
  output logic [N_PORTS-1:0]   forwarding_in_vlan,

  output logic [CNT_W-1:0]     bits_membership,
  output logic [CNT_W-1:0]     bits_port_state,
  output logic [CNT_W-1:0]     bits_total,
  output logic [5:0]           configured_vlans
);

  logic [N_PORTS-1:0] member_bm [N_VLANS];
  // THREE bits per port per VLAN. A port may be FORWARDING in one VLAN
  // and BLOCKING in another -- that is the whole reason this is not a
  // single per-port register, and it is what makes the array 36 KiB at
  // the full VID range.
  logic [2:0]         pstate    [N_VLANS][N_PORTS];
  logic [N_VLANS-1:0] vlan_used;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int v = 0; v < N_VLANS; v++) begin
        member_bm[v] <= '0;
        for (int p = 0; p < N_PORTS; p++) pstate[v][p] <= 3'd0;
      end
      vlan_used <= '0;
    end else if (cfg_valid) begin
      member_bm[cfg_vidx][cfg_port] <= cfg_member;
      pstate[cfg_vidx][cfg_port]    <= cfg_port_state;
      if (cfg_member) vlan_used[cfg_vidx] <= 1'b1;
    end
  end

  assign is_member  = rd_valid && member_bm[rd_vidx][rd_port];
  assign port_state = pstate[rd_vidx][rd_port];
  assign members    = member_bm[rd_vidx];

  // The per-VLAN forwarding mask: a member AND in the forwarding state,
  // for THIS VLAN. Chapter 12.3's gate 5, now two-dimensional.
  always_comb begin
    for (int p = 0; p < N_PORTS; p++)
      forwarding_in_vlan[p] = member_bm[rd_vidx][p] &&
                              (pstate[rd_vidx][p] == 3'd4);   // FORWARDING
  end

  // Cost, computed rather than asserted -- Section 10 uses these numbers.
  assign bits_membership = CNT_W'(N_PORTS) * CNT_W'(N_VLANS);
  assign bits_port_state = CNT_W'(N_PORTS) * CNT_W'(N_VLANS) * CNT_W'(3);
  assign bits_total      = bits_membership + bits_port_state +
                           (CNT_W'(N_PORTS) * CNT_W'(N_VLANS));

  always_comb begin
    configured_vlans = 6'd0;
    for (int v = 0; v < N_VLANS; v++) if (vlan_used[v]) configured_vlans = configured_vlans + 6'd1;
  end

endmodule

Classification: synthesizable, with the arrays inferring register files or small SRAMs depending on N_VLANS.

What it teaches: that port state is three bits per port per VLAN, and the reason is not symmetry — it is that a port genuinely can be forwarding in one VLAN and blocking in another. Chapter 12.3 §7's five states apply independently in each VLAN, because a loop-prevention decision in one VLAN says nothing about another, and a port administratively disabled for one department's traffic may carry another's.

And it teaches why the membership bitmap and the forwarding mask are separate arrays despite looking redundant. Membership is configuration — an operator said this port carries this VLAN. Forwarding state is protocol — it changes on link events and topology transitions. Chapter 12.2 §15's port-down flush changes the second and must not touch the first.

Deliberately simplified: dense arrays over 64 configured VLANs indexed by a compact index. Chapter 13.4 owns the VID-to-index map that makes 4094 possible VIDs fit into 64 configured entries, and Section 10 explains why that indirection is not optional.

Production implication: configured_vlans against N_VLANS is the number to watch during commissioning, and it fails in a direction nobody expects. A switch supporting 64 VLANs and a network needing 65 does not degrade — the sixty-fifth VLAN simply cannot be created, and every port an operator intended to place in it stays in whatever VLAN it was in, which is usually the default. The isolation the operator believes they configured does not exist, the frames flow, and nothing reports an error — only configured_vlans sitting at its maximum says why.

10. What 4094 VLANs Actually Costs

Take Section 9's arrays to the full VID range on a 24-port switch and the number is larger than most people expect.

StructureBitsSize
membership bitmap — 24 × 409498 25612.0 KiB
port state — 3 × 24 × 4094294 76836.0 KiB
flood mask — 24 × 409498 25612.0 KiB
total per-VLAN state491 28060.0 KiB

And the comparison that makes it land:

Size
Chapter 12.5's entire 8192-entry forwarding table8192 × 64 = 64 KiB
per-VLAN state at the full VID range60 KiB
ratio0.94×

Supporting all 4094 VLANs nearly doubles the switch's control memory — for state that holds no addresses, forwards no frames and exists purely to say which ports belong where.

Which is why real switches do not build it. A switch supporting 4094 VIDs typically supports far fewer simultaneously configured VLANs — 64, 256, 1024 — and keeps a VID-to-index map in front of Section 9's arrays. The 12-bit VID indexes a small map; the map's output indexes the dense arrays.

DesignVID mapArrays sized forPer-VLAN state
dense, all VIDsnone409460.0 KiB
indexed, 256 configured4094 × 8 bits = 4.0 KiB2564.0 + 3.8 = 7.8 KiB
indexed, 64 configured4.0 KiB644.0 + 0.9 = 4.9 KiB

The indexed design at 256 configured VLANs costs 7.8 KiB against 60 KiB — a 7.7× saving — and its limitation is Section 9's configured_vlans ceiling, which is the failure mode that produces isolation an operator believes exists and does not.

Chapter 13.4 owns that map. What this chapter establishes is why it is not optional: the dense design is the same size as the entire forwarding table, for state that does nothing but partition.

On a twenty-four port switch supporting the full range of four thousand and ninety-four usable VLAN identifiers, a dense membership bitmap costs twelve kibibytes, a dense port state array at three bits per port per VLAN costs thirty-six kibibytes, and a dense flood mask costs a further twelve kibibytes, totalling sixty kibibytes. Chapter 12.5's entire eight thousand one hundred and ninety-two entry forwarding table is sixty-four kibibytes, so the per-VLAN state is 0.94 times the whole table while holding no addresses and forwarding no frames. Real switches therefore place a small map in front of these arrays, translating the twelve bit VLAN identifier into a compact index over the number of VLANs actually configured, which at two hundred and fifty-six configured VLANs costs four kibibytes for the map plus three point eight kibibytes for the arrays, a seven point seven times saving. The limitation is a ceiling on simultaneously configured VLANs, and exceeding it silently leaves ports in whatever VLAN they were already in.Dense, 4094 VLANsarrays indexed by raw VIDMembership 12 KiB24 × 4094 bitsPort state 36 KiB3 bits per port per VLAN60 KiB total0.94× the whole tableVID → index map4094 × 8 bits = 4 KiBArrays over 2563.8 KiB7.8 KiB — 7.7×savingceiling: configured VLANs12
Figure 3 — dense per-VLAN state at the full VID range costs as much as the whole forwarding table, which is why a VID-to-index map is structural rather than an optimisation.

11. RTL 5 — Accounting for the Dimension

Every resource either took the VLAN dimension or did not, and a switch should be able to say which.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_resource_accountant -- reports which resources multiplied, which
// widened, and what the whole thing costs.
//
// This is a reporting module, and its value is that the categorisation in
// Section 6 becomes a readable fact about the built design rather than a
// claim in a specification.
// -----------------------------------------------------------------------
module vlan_resource_accountant
  import vlanreq_pkg::*;
#(
  parameter int N_PORTS       = 24,
  parameter int N_VLANS_CFG   = 64,
  parameter int N_VIDS        = 4094,
  parameter int N_ENTRIES     = 8192,
  parameter int USE_VID_MAP   = 1,
  parameter int CNT_W         = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  output resource_dim_e    dim_key,
  output resource_dim_e    dim_table,
  output resource_dim_e    dim_flood_mask,
  output resource_dim_e    dim_port_state,
  output resource_dim_e    dim_frame_buffer,
  output resource_dim_e    dim_egress_arbiter,

  output logic [CNT_W-1:0] bits_vid_map,
  output logic [CNT_W-1:0] bits_per_vlan_state,
  output logic [CNT_W-1:0] bits_table,
  output logic [CNT_W-1:0] bits_key_growth,
  output logic [15:0]      state_vs_table_pct,
  output logic [11:0]      vlan_ceiling
);

  // THE CATEGORISATION, AS HARDWARE. Section 6's table, encoded where a
  // reviewer can read it off the design rather than from a document that
  // may not match.
  assign dim_key            = RD_WIDENED;
  assign dim_table          = RD_WIDENED;      // shared -- Section 8
  assign dim_flood_mask     = RD_MULTIPLIED;
  assign dim_port_state     = RD_MULTIPLIED;
  assign dim_frame_buffer   = RD_UNCHANGED;    // octets have no VLAN
  assign dim_egress_arbiter = RD_UNCHANGED;    // a wire has no VLAN

  localparam int VLAN_SLOTS = (USE_VID_MAP != 0) ? N_VLANS_CFG : N_VIDS;

  assign bits_vid_map = (USE_VID_MAP != 0)
                      ? CNT_W'(N_VIDS) * CNT_W'($clog2(N_VLANS_CFG))
                      : '0;

  // membership + flood mask (1 bit each) + port state (3 bits), per port
  // per VLAN slot.
  assign bits_per_vlan_state = CNT_W'(N_PORTS) * CNT_W'(VLAN_SLOTS) * CNT_W'(5);

  assign bits_table       = CNT_W'(N_ENTRIES) * CNT_W'(KEY_W + 16);
  assign bits_key_growth  = CNT_W'(N_ENTRIES) * CNT_W'(KEY_W - ADDR_W);

  assign state_vs_table_pct = 16'(((bits_vid_map + bits_per_vlan_state) *
                                   CNT_W'(100)) / bits_table);

  // The ceiling that produces Section 9's silent failure.
  assign vlan_ceiling = 12'(VLAN_SLOTS);

endmodule

Classification: synthesizable, and almost entirely constant — its outputs are elaboration-time facts about the design.

What it teaches: that the categorisation is worth encoding. Section 6's table is a design decision, and design decisions written only in documents drift from the RTL. dim_frame_buffer = RD_UNCHANGED is a statement a reviewer can check against the buffer's actual ports, and if somebody has quietly added a VLAN index to the buffer's address, the constant and the code disagree visibly.

And state_vs_table_pct is the number that justifies the VID map to somebody deciding whether to build it. With the map at 64 configured VLANs it is around 10%; without it, at the full VID range, it is 94% — Section 10's ratio. The parameter USE_VID_MAP makes the two configurations comparable in one place.

Deliberately simplified: a flat accounting that ignores the ageing intervals and per-VLAN counters a full implementation adds. Those are small, and including them would obscure that the port-state array is 60% of the total on its own.

Production implication: vlan_ceiling should be exposed to management software, because Section 9 established that exceeding it fails silently. A switch that cannot create the sixty-fifth VLAN leaves its ports in the default VLAN, forwards their frames normally, and reports no error — the isolation an operator configured does not exist and nothing says so. Publishing the ceiling lets the management layer refuse the configuration rather than accept it and not apply it.

12. RTL 6 — The Isolation Invariant

A VLAN's definition reduces to one checkable statement, and it is worth building the checker into the silicon rather than trusting the mask logic.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_isolation_monitor -- no frame leaves a port that is not a member of
// the frame's VLAN.
//
// This is the invariant that DEFINES a VLAN. Every other mechanism in the
// chapter exists to make it true; this module checks it independently of
// all of them, on the emitted frame rather than on the mask that was
// supposed to produce it.
// -----------------------------------------------------------------------
module vlan_isolation_monitor
  import vlanreq_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 [N_PORTS-1:0]   vlan_members [N_VLANS],

  input  logic                 emit_valid,
  input  logic [VIDX_W-1:0]    emit_vidx,
  input  logic [4:0]           emit_port,
  input  logic [4:0]           ingress_port,
  input  logic [VIDX_W-1:0]    ingress_vidx,

  output logic [CNT_W-1:0]     v_leak,             // out a non-member port
  output logic [CNT_W-1:0]     v_vid_changed,      // VLAN changed in transit
  output logic [CNT_W-1:0]     v_ingress_not_member,
  output logic [CNT_W-1:0]     c_checked,
  output logic [4:0]           last_leak_port,
  output logic [VIDX_W-1:0]    last_leak_vidx,
  output logic                 isolated
);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_leak               <= '0;
      v_vid_changed        <= '0;
      v_ingress_not_member <= '0;
      c_checked            <= '0;
      last_leak_port       <= '0;
      last_leak_vidx       <= '0;
    end else if (emit_valid) begin
      c_checked <= c_checked + 1'b1;

      // THE INVARIANT. Checked against the emitted frame and the
      // membership configuration -- deliberately NOT against the flood
      // mask, so that a mask built wrongly is caught rather than
      // confirmed.
      if (!vlan_members[emit_vidx][emit_port]) begin
        if (!(&v_leak)) v_leak <= v_leak + 1'b1;
        last_leak_port <= emit_port;
        last_leak_vidx <= emit_vidx;
      end

      // A frame's VLAN is a property of the frame, not of the path. A
      // switch that forwards a frame into a different VLAN has not
      // leaked it -- it has RELABELLED it, which is worse, because the
      // frame now looks legitimate everywhere it goes.
      if (emit_vidx != ingress_vidx)
        if (!(&v_vid_changed)) v_vid_changed <= v_vid_changed + 1'b1;

      // The ingress port must itself be a member. A frame accepted into
      // a VLAN through a port that is not in it has entered the domain
      // from outside, which is the same failure seen from the other end.
      if (!vlan_members[ingress_vidx][ingress_port])
        if (!(&v_ingress_not_member))
          v_ingress_not_member <= v_ingress_not_member + 1'b1;
    end
  end

  assign isolated = (v_leak == '0) && (v_vid_changed == '0) &&
                    (v_ingress_not_member == '0);

endmodule

Classification: synthesizable, and intended to stay in silicon.

What it teaches: that the monitor checks the emitted frame against the configuration, not against the mask. Checking against the mask would confirm that the replicator did what the mask said — which is Chapter 12.4 §16's ledger and a different question. The isolation invariant is about whether the mask was right, and answering it requires an independent path to the membership data.

And v_vid_changed catches something worse than a leak. A frame emitted into a different VLAN has not escaped its domain — it has been relabelled into another one, where it looks entirely legitimate. Every downstream switch will forward it correctly within the wrong VLAN, no isolation monitor anywhere will fire, and the frame is delivered to stations that should never have seen it, by a network behaving perfectly.

Deliberately simplified: membership read combinationally from the same array the datapath uses. A truly independent check would use a shadow copy written by a separate path, which is what a design carrying a security claim would build — and the reason is that a single corrupted membership word makes the datapath and the monitor agree on the wrong answer.

Production implication: isolated is the one bit in this chapter that means what its name says, and it is worth stating why. Chapter 12.2 §16's conformant could not mean "the table is correct" and Chapter 12.6 §15's could not mean "the frames were good" — both quantified over facts outside the design. Isolation does not. Membership is configuration the switch holds, the emitted port is a signal the switch drives, and the invariant is entirely internal. It is the rare case where the narrow claim and the broad one coincide.

13. The Isolation Invariant, and What It Is Not

isolated is a strong claim about this switch and a weak one about the network, and the gap is where most VLAN incidents live.

What the invariant establishes: no frame left this switch through a port that is not a member of the frame's VLAN, and no frame's VLAN changed while crossing it.

What it does not establish, and cannot:

ClaimWhy this switch cannot make it
"VLAN 10 is isolated across the network"other switches have their own membership configuration
"the VLANs match end to end"a trunk misconfiguration relabels frames elsewhere
"VLAN 10 means engineering"VID 10 is a number; its meaning is an agreement between operators
"traffic cannot cross VLANs"a router with a leg in both is doing exactly that, legitimately
"this is a security boundary"it is a broadcast-domain boundary that is often used as one

The third row is the one that surprises people, and it follows from what a VID actually is.

A VLAN identifier is 12 bits in a frame. Nothing anywhere binds VID 10 to engineering — that binding exists in configuration on every switch that participates, and two switches configured differently will happily forward frames between what their operators believed were different VLANs. Chapter 12.2 §16 made the analogous point about MAC addresses: the number in the field is not authenticated, and the meaning is an agreement.

And the fifth row is worth stating plainly because the industry treats it loosely. A VLAN boundary is enforced by every switch that honours it, and it is exactly as strong as the configuration on the weakest of them. It is a genuine boundary against broadcast, which is what this chapter's arithmetic is about. Whether it is a boundary against a determined adversary depends on things outside this chapter and outside this layer.

14. RTL 7 — The Same Address in Two VLANs

Before VLANs, a MAC address identified a station. After VLANs it identifies a station within a VLAN, and the same 48 bits can legitimately name two different things on two different ports.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// duplicate_mac_tracker -- observes addresses that appear in more than one
// VLAN, and separates the legitimate cases from the pathological ones.
//
// Chapter 12.2 Section 9's move detector fires when one address appears on
// two PORTS. With VLANs that is no longer sufficient information: the same
// address on two ports in DIFFERENT VLANs is normal, and on two ports in
// the SAME VLAN is Chapter 12.2's duplicate-address failure.
// -----------------------------------------------------------------------
module duplicate_mac_tracker
  import vlanreq_pkg::*;
#(
  parameter int N_TRACK = 16,
  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,

  output logic [CNT_W-1:0]     c_multi_vlan_mac,   // same MAC, 2+ VLANs
  output logic [CNT_W-1:0]     c_same_vlan_dup,    // Chapter 12.2's failure
  output logic [ADDR_W-1:0]    last_multi_vlan_mac,
  output logic [VIDX_W-1:0]    multi_vlan_a,
  output logic [VIDX_W-1:0]    multi_vlan_b,
  output logic                 multi_vlan_normal,  // a router or a host trunk
  output logic                 same_vlan_conflict  // genuinely a problem
);

  logic [ADDR_W-1:0] t_mac  [N_TRACK];
  logic [VIDX_W-1:0] t_vidx [N_TRACK];
  logic [4:0]        t_port [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_port[i] <= '0; t_val[i] <= 1'b0;
      end
      wr_ptr              <= '0;
      c_multi_vlan_mac    <= '0;
      c_same_vlan_dup     <= '0;
      last_multi_vlan_mac <= '0;
      multi_vlan_a        <= '0;
      multi_vlan_b        <= '0;
      multi_vlan_normal   <= 1'b0;
      same_vlan_conflict  <= 1'b0;
    end else if (learn_valid) 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
        if (t_vidx[hit] != learn_vidx) begin
          // DIFFERENT VLAN. This is normal and expected: a router with a
          // leg in several VLANs presents the same address in each, and a
          // virtualisation host's uplink carries one MAC across many.
          // Chapter 12.2's move detector would call this a move; with the
          // widened key it is two independent, correct entries.
          if (!(&c_multi_vlan_mac)) c_multi_vlan_mac <= c_multi_vlan_mac + 1'b1;
          last_multi_vlan_mac <= learn_mac;
          multi_vlan_a        <= t_vidx[hit];
          multi_vlan_b        <= learn_vidx;
          multi_vlan_normal   <= 1'b1;
        end else if (t_port[hit] != learn_port) begin
          // SAME VLAN, different port. Chapter 12.2 Section 10's genuine
          // duplicate-address failure, unchanged by VLANs.
          if (!(&c_same_vlan_dup)) c_same_vlan_dup <= c_same_vlan_dup + 1'b1;
          same_vlan_conflict <= 1'b1;
        end
        t_vidx[hit] <= learn_vidx;
        t_port[hit] <= learn_port;
      end else begin
        t_mac[wr_ptr]  <= learn_mac;
        t_vidx[wr_ptr] <= learn_vidx;
        t_port[wr_ptr] <= learn_port;
        t_val[wr_ptr]  <= 1'b1;
        wr_ptr         <= wr_ptr + 1'b1;
      end
    end
  end

endmodule

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

What it teaches: that Chapter 12.2 §9's move detector is no longer sufficient on its own. That module fired when one address appeared on two ports and offered three causes: a real move, a duplicate address, or a loop. With VLANs there is a fourth, and it is not a fault at all — the same address on two ports in different VLANs is a router with a leg in each, or a virtualisation host's uplink, and it is completely normal.

And the discrimination is one comparison. Same address, different VLAN → two independent, correct entries. Same address, same VLAN, different port → Chapter 12.2 §10's duplicate-address failure, unchanged. Without the VLAN in the comparison, a design reports a flapping address for a router that is behaving perfectly, every second, forever.

Deliberately simplified: a 16-entry tracking window with a wrapping write pointer. A production tracker samples during Chapter 12.5 §13's ageing sweep, which reads every key anyway and can therefore find every multi-VLAN address rather than a recent sample.

Production implication: c_multi_vlan_mac should be small and non-zero on any network with inter-VLAN routing, and its value is as a baseline. A sudden rise means something began presenting one address across many VLANs — a misconfigured trunk, a bridging loop between VLANs, or a host that has started tagging. A value of zero on a network that has a router means the VLAN is not reaching the comparison at all, which is the signature of a MAC-only key and Section 15's failure.

15. What a MAC-Only Key Does

Section 7 widened the key and Section 14 showed why. Now work through what happens if it is not widened, because the failure is subtle, silent and extremely common in first implementations.

A router has a leg in VLAN 10 and VLAN 20. It presents the same MAC address in both — that is what a router with sub-interfaces does, and it is entirely standard.

StepWith a {VID, MAC} keyWith a MAC-only key
router transmits in VLAN 10, port 1entry {10, R} → port 1entry R → port 1
router transmits in VLAN 20, port 1entry {20, R} → port 1entry R → port 1 (same)
a host in VLAN 10 sends to Rhit {10, R}port 1hit R → port 1
now the router moves its VLAN 20 leg to port 5{20, R} → port 5; {10, R} unchangedentry Rport 5
a host in VLAN 10 sends to Rhit {10, R}port 1, correcthit Rport 5 — wrong VLAN's port
what happens to that framedeliveredSection 12's isolation check discards it
what the host seesnormalthe router is unreachable, intermittently
what any counter saysnothing

The MAC-only key produced a single entry for two stations that happen to share an address, and the entry is updated by whichever one transmitted most recently. Chapter 12.2 §9's move detector sees an address alternating between two ports and reports a flap — on a router that has not moved.

And Section 2's warning lands here. The isolation post-filter does prevent the leak: the frame forwarded to port 5 in VLAN 10 is discarded because port 5 is not a VLAN 10 member. So there is no security failure and no leak — the frame is simply lost, and the correct destination on port 1 receives nothing.

Which is the worst combination available: correct isolation, incorrect forwarding, no error counter, and a symptom — intermittent unreachability of the default gateway — that points at everything except the lookup key.

16. RTL 8 — Conformance for a Partitioned Switch

Every mechanism in Module 12 acquired a VLAN dimension, and the monitor's job is to check that each acquired the right one.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_conformance_monitor -- checks that the VLAN dimension reached every
// mechanism that needed it and none that did not.
//
// The failures this catches are all of one shape: a mechanism that kept
// working after VLANs were added, and is now answering a question that is
// no longer the question being asked.
// -----------------------------------------------------------------------
module vlan_conformance_monitor
  import vlanreq_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 [N_PORTS-1:0]   vlan_members [N_VLANS],

  // Learning, keyed.
  input  logic                 learn_valid,
  input  logic [KEY_W-1:0]     learn_key,
  input  logic [VID_W-1:0]     learn_vid,
  input  logic [4:0]           learn_port,
  input  logic [VIDX_W-1:0]    learn_vidx,

  // Lookup, keyed.
  input  logic                 lookup_valid,
  input  logic [KEY_W-1:0]     lookup_key,
  input  logic [VID_W-1:0]     lookup_vid,

  // Flooding.
  input  logic                 flood_valid,
  input  logic [N_PORTS-1:0]   flood_mask,
  input  logic [VIDX_W-1:0]    flood_vidx,

  output logic [CNT_W-1:0]     v_key_missing_vid,   // key is 48 bits again
  output logic [CNT_W-1:0]     v_learn_wrong_vlan,
  output logic [CNT_W-1:0]     v_flood_outside_vlan,
  output logic [CNT_W-1:0]     v_learn_nonmember,
  output logic [CNT_W-1:0]     c_checked,
  output logic                 conformant
);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_key_missing_vid    <= '0;
      v_learn_wrong_vlan   <= '0;
      v_flood_outside_vlan <= '0;
      v_learn_nonmember    <= '0;
      c_checked            <= '0;
    end else begin
      // THE SECTION 15 CHECK. The key's high bits must carry the VID. A
      // key whose top 12 bits are zero on a frame whose VID is not zero
      // is a MAC-only key that somebody widened structurally and never
      // wired.
      if (learn_valid) begin
        c_checked <= c_checked + 1'b1;
        if ((learn_key[KEY_W-1 -: VID_W] != learn_vid))
          if (!(&v_key_missing_vid))
            v_key_missing_vid <= v_key_missing_vid + 1'b1;

        // Chapter 12.2's rule, with the VLAN dimension: the learned entry
        // belongs to the VLAN the frame arrived in.
        if (learn_key[KEY_W-1 -: VID_W] != learn_vid)
          if (!(&v_learn_wrong_vlan))
            v_learn_wrong_vlan <= v_learn_wrong_vlan + 1'b1;

        // A frame cannot be learned into a VLAN through a port that is
        // not a member of it -- the entry would name a port that can
        // never legitimately carry that VLAN's traffic.
        if (!vlan_members[learn_vidx][learn_port])
          if (!(&v_learn_nonmember))
            v_learn_nonmember <= v_learn_nonmember + 1'b1;
      end

      if (lookup_valid)
        if (lookup_key[KEY_W-1 -: VID_W] != lookup_vid)
          if (!(&v_key_missing_vid))
            v_key_missing_vid <= v_key_missing_vid + 1'b1;

      // Section 5's three-way intersection, checked against membership
      // rather than against the mask logic that produced it.
      if (flood_valid)
        if ((flood_mask & ~vlan_members[flood_vidx]) != '0)
          if (!(&v_flood_outside_vlan))
            v_flood_outside_vlan <= v_flood_outside_vlan + 1'b1;
    end
  end

  assign conformant = (v_key_missing_vid    == '0) &&
                      (v_learn_wrong_vlan   == '0) &&
                      (v_flood_outside_vlan == '0) &&
                      (v_learn_nonmember    == '0);

endmodule

Classification: synthesizable, and intended to stay in silicon.

What it teaches: that every failure this monitor catches has the same shape: a mechanism that kept working after VLANs were added. Chapter 12.2's learner still learns. Chapter 12.5's table still stores and retrieves. Chapter 12.4's replicator still floods. None of them fails; each of them is now answering a question that is no longer the question being asked.

And v_key_missing_vid is the check for the most likely integration error. A design that widens the key structurally — declaring KEY_W = 60 and sizing every array accordingly — and then forgets to actually drive the VID into the top 12 bits produces a table full of keys whose high bits are zero. It works perfectly for a single-VLAN network, passes every functional test on a lab bench with one VLAN, and produces Section 15's failure the moment a second VLAN exists.

Deliberately simplified: the checks read membership from the same array the datapath uses, as Section 12's monitor did, with the same caveat about a shadow copy.

Production implication: the four counters map to four distinct integration failures, and the mapping is worth publishing. v_key_missing_vid — the VID is not reaching the key. v_learn_wrong_vlan — it reaches the key but the wrong VID is used. v_flood_outside_vlan — Section 5's intersection dropped a conjunct. v_learn_nonmember — the ingress classification and the membership table disagree, which is Chapter 13.3's territory arriving as a symptom here.

Each mechanism built in Module 12 continues to operate correctly after VLANs are introduced, and each one is now answering a question that has changed. The learner still learns a source address onto an ingress port, but the entry now needs to record which VLAN the frame arrived in, and a learner that does not is storing one entry for two different stations. The table still stores and retrieves by key, but the key must now be the VLAN identifier concatenated with the address, and a table whose key is still forty-eight bits returns the wrong port for a router present in several VLANs. The replicator still floods to every forwarding port except the ingress, but the mask must additionally be intersected with the VLAN's membership, and a replicator that is not scoped provides no isolation at all. In every case nothing fails, no error counter increments, and the mechanism reports success while producing an answer to the wrong question.Learner still learnsbut into which VLAN?Table stillretrievesbut keyed on what?Replicator stillfloodsbut scoped to what?Nothing failsno counter movesWiden the keyv_key_missing_vidScope the maskv_flood_outside_vlanCheck membershipv_learn_nonmember12
Figure 4 — every VLAN failure has one shape: a Module 12 mechanism still working correctly, on a question that is no longer the one being asked.

17. Properties Worth Asserting, and One Worth Refusing

The isolation properties are absolute. The resource-dimension properties are structural. And the rejected property is one that was true when it was written.

The key

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. The key carries the VID in its high bits. A key whose top 12 bits
// are zero on a frame with a non-zero VID is a MAC-only key that was
// widened structurally and never wired.
property p_key_carries_vid;
  @(posedge clk) disable iff (!rst_n)
  key_valid |-> (key[KEY_W-1 -: VID_W] == vid);
endproperty
a_key_has_vid: assert property (p_key_carries_vid);

// P2. The key carries the address in its low bits.
property p_key_carries_mac;
  @(posedge clk) disable iff (!rst_n)
  key_valid |-> (key[ADDR_W-1:0] == mac);
endproperty
a_key_has_mac: assert property (p_key_carries_mac);

// P3. Reserved VIDs produce no key. 0 and 4095 name no VLAN.
property p_reserved_vid_no_key;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && ((vid == VID_W'(0)) || (vid == VID_W'(4095)))) |-> !key_valid;
endproperty
a_reserved_no_key: assert property (p_reserved_vid_no_key);

// P4. Two frames with the same address in different VLANs produce
// DIFFERENT keys -- which is the whole point of the widening.
property p_same_mac_different_vlan_differs;
  @(posedge clk) disable iff (!rst_n)
  (key_valid && (mac == $past(mac)) && (vid != $past(vid)))
    |-> (key != $past(key));
endproperty
a_vlan_separates: assert property (p_same_mac_different_vlan_differs);

Isolation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P5. THE INVARIANT. No frame leaves a port that is not a member of the
// frame's VLAN.
property p_no_leak;
  @(posedge clk) disable iff (!rst_n)
  emit_valid |-> vlan_members[emit_vidx][emit_port];
endproperty
a_no_leak: assert property (p_no_leak);

// P6. A frame's VLAN does not change while crossing the switch.
// Relabelling is worse than leaking: the frame then looks legitimate
// everywhere it goes.
property p_vid_preserved;
  @(posedge clk) disable iff (!rst_n)
  emit_valid |-> (emit_vidx == ingress_vidx);
endproperty
a_vid_preserved: assert property (p_vid_preserved);

// P7. A frame is accepted into a VLAN only through a member port.
property p_ingress_is_member;
  @(posedge clk) disable iff (!rst_n)
  emit_valid |-> vlan_members[ingress_vidx][ingress_port];
endproperty
a_ingress_member: assert property (p_ingress_is_member);

// P8. The flood mask is a subset of the VLAN's membership.
property p_flood_within_vlan;
  @(posedge clk) disable iff (!rst_n)
  mask_valid |-> ((flood_mask & ~vlan_members[vidx]) == '0);
endproperty
a_flood_scoped: assert property (p_flood_within_vlan);

// P9. THE THREE-WAY INTERSECTION. Members AND forwarding AND not the
// ingress port -- dropping any conjunct is a distinct bug.
property p_flood_three_way;
  @(posedge clk) disable iff (!rst_n)
  mask_valid |-> (flood_mask == (vlan_members[vidx] & forwarding_mask &
                                 ~(N_PORTS'(1) << ingress_port)));
endproperty
a_three_way: assert property (p_flood_three_way);

// P10. Chapter 12.1's P9 survives the VLAN dimension: never out the
// ingress port.
property p_never_ingress_echo;
  @(posedge clk) disable iff (!rst_n)
  mask_valid |-> !flood_mask[ingress_port];
endproperty
a_no_echo: assert property (p_never_ingress_echo);

// P11. Chapter 12.3's P11 survives it too: never to a blocked port, and
// blocked is now per VLAN.
property p_never_blocked_in_vlan;
  @(posedge clk) disable iff (!rst_n)
  mask_valid |-> ((flood_mask & ~forwarding_in_vlan) == '0);
endproperty
a_no_blocked: assert property (p_never_blocked_in_vlan);

Learning and the table

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P12. Learning is per VLAN. Chapter 12.2's inference, with the dimension.
property p_learn_into_arriving_vlan;
  @(posedge clk) disable iff (!rst_n)
  learn_valid |-> (learn_key[KEY_W-1 -: VID_W] == learn_vid);
endproperty
a_learn_vlan: assert property (p_learn_into_arriving_vlan);

// P13. A frame is not learned into a VLAN through a non-member port.
property p_no_learn_from_nonmember;
  @(posedge clk) disable iff (!rst_n)
  learn_valid |-> vlan_members[learn_vidx][learn_port];
endproperty
a_learn_member: assert property (p_no_learn_from_nonmember);

// P14. The table is SHARED, not partitioned -- entries are allocated by
// demand. Asserted as: no VLAN has a fixed entry budget.
property p_table_not_partitioned;
  @(posedge clk) disable iff (!rst_n)
  (ins_result == TI_SET_FULL) |-> table_globally_pressured;
endproperty
a_shared_table: assert property (p_table_not_partitioned);

// P15. Chapter 12.5's structure absorbs the wider key unchanged: the
// lookup latency is still constant.
property p_lookup_latency_unchanged;
  @(posedge clk) disable iff (!rst_n)
  lk_done |-> (lk_latency_cy == 3'd4);
endproperty
a_latency_unchanged: assert property (p_lookup_latency_unchanged);

The dimension categorisation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P16. The frame buffer did NOT acquire the dimension. Octets have no
// VLAN, and multiplying the buffer would multiply the switch's most
// expensive memory for nothing.
property p_buffer_not_multiplied;
  @(posedge clk) disable iff (!rst_n)
  (dim_frame_buffer == RD_UNCHANGED);
endproperty
a_buffer_shared: assert property (p_buffer_not_multiplied);

// P17. The egress arbiter did not either. A wire has no VLAN; priority
// is a different mechanism -- Chapter 13.2's PCP.
property p_arbiter_not_multiplied;
  @(posedge clk) disable iff (!rst_n)
  (dim_egress_arbiter == RD_UNCHANGED);
endproperty
a_arbiter_shared: assert property (p_arbiter_not_multiplied);

// P18. Masks and port state DID acquire it.
property p_masks_multiplied;
  @(posedge clk) disable iff (!rst_n)
  ((dim_flood_mask == RD_MULTIPLIED) && (dim_port_state == RD_MULTIPLIED));
endproperty
a_masks_per_vlan: assert property (p_masks_multiplied);

// P19. Port state is per port PER VLAN -- a port may forward in one and
// block in another.
property p_port_state_is_per_vlan;
  @(posedge clk) disable iff (!rst_n)
  rd_valid |-> (port_state == pstate[rd_vidx][rd_port]);
endproperty
a_state_per_vlan: assert property (p_port_state_is_per_vlan);

// P20. Membership and forwarding state are SEPARATE. Membership is
// configuration; forwarding state is protocol, and a port-down flush
// changes the second without touching the first.
property p_membership_survives_flush;
  @(posedge clk) disable iff (!rst_n)
  port_down_flush |=> $stable(member_bm);
endproperty
a_membership_stable: assert property (p_membership_survives_flush);

Multi-VLAN addresses

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P21. The same address in two VLANs is NORMAL and produces two entries.
property p_multi_vlan_mac_is_two_entries;
  @(posedge clk) disable iff (!rst_n)
  (learn_valid && multi_vlan_normal) |-> (learn_key != $past(learn_key));
endproperty
a_two_entries: assert property (p_multi_vlan_mac_is_two_entries);

// P22. The same address on two ports in the SAME VLAN is Chapter 12.2's
// duplicate-address failure and is reported as such.
property p_same_vlan_dup_reported;
  @(posedge clk) disable iff (!rst_n)
  same_vlan_conflict |-> (c_same_vlan_dup != '0);
endproperty
a_dup_reported: assert property (p_same_vlan_dup_reported);

// P23. A multi-VLAN address does NOT trigger Chapter 12.2's move
// detector. A router with legs in four VLANs is not flapping.
property p_router_not_a_flap;
  @(posedge clk) disable iff (!rst_n)
  multi_vlan_normal |-> !same_vlan_conflict;
endproperty
a_router_not_flap: assert property (p_router_not_a_flap);

Capacity and conformance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P24. The VLAN ceiling is published, because exceeding it fails
// SILENTLY -- ports stay in the VLAN they were already in.
property p_ceiling_published;
  @(posedge clk) disable iff (!rst_n)
  (vlan_ceiling != 12'd0);
endproperty
a_ceiling_visible: assert property (p_ceiling_published);

// P25. A VLAN that cannot be created is REFUSED, not silently ignored.
property p_vlan_create_refused_not_ignored;
  @(posedge clk) disable iff (!rst_n)
  (cfg_valid && (configured_vlans >= 6'(N_VLANS)) && cfg_member)
    |-> cfg_refused;
endproperty
a_create_refused: assert property (p_vlan_create_refused_not_ignored);

// P26. A single-member VLAN produces an empty flood mask -- correct, and
// counted so it is visible.
property p_single_member_empty_flood;
  @(posedge clk) disable iff (!rst_n)
  (flood_req && ($countones(vlan_members[vidx]) == 1))
    |-> (reject == VE_NO_MEMBERS);
endproperty
a_single_member: assert property (p_single_member_empty_flood);

// P27. Conformance means the dimension reached everything that needed it.
property p_conformant_definition;
  @(posedge clk) disable iff (!rst_n)
  conformant |-> ((v_key_missing_vid == '0) && (v_learn_wrong_vlan == '0) &&
                  (v_flood_outside_vlan == '0) && (v_learn_nonmember == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);

// P28. Isolation is checked against the CONFIGURATION, not against the
// mask that was supposed to produce it.
property p_isolation_checked_independently;
  @(posedge clk) disable iff (!rst_n)
  emit_valid |-> (isolated == ((v_leak == '0) && (v_vid_changed == '0) &&
                               (v_ingress_not_member == '0)));
endproperty
a_independent_check: assert property (p_isolation_checked_independently);

// P29. Ageing intervals may be global without making the design
// incorrect -- they are a policy knob, not a correctness requirement.
property p_global_ageing_is_legal;
  @(posedge clk) disable iff (!rst_n)
  age_tick |-> (age_limit_in_use != '0);
endproperty
a_ageing_policy: assert property (p_global_ageing_is_legal);

// P30. The learning rate limit's budget is PER PORT, not per port per
// VLAN -- the resource it protects is the shared table.
property p_rate_limit_per_port;
  @(posedge clk) disable iff (!rst_n)
  (learn_req && !learn_permit) |-> (new_this_second[learn_port] >= CNT_W'(NEW_PER_SEC));
endproperty
a_limit_per_port: assert property (p_rate_limit_per_port);

// P31. The flood-to-forward ratio is evaluated PER VLAN -- a small
// VLAN's high ratio is expected and says nothing about health.
property p_ratio_is_per_vlan;
  @(posedge clk) disable iff (!rst_n)
  window_valid |-> (ratio_vidx == evaluated_vidx);
endproperty
a_ratio_per_vlan: assert property (p_ratio_is_per_vlan);

18. Verification Scenarios

Sixty-six scenarios. The isolation ones have no acceptable failure; the capacity ones have expected outcomes that include refusing to create a VLAN.

The key

#ScenarioExpected
1VID 10, MAC Akey = {10, A}, 60 bits
2Same MAC, VID 20different key — the widening working
3Same key twiceone entry, refreshed
4VID 0no key, VE_VID_RESERVED
5VID 4095no key, reserved
6VID 1 and VID 4094both accepted — the usable range
7Key with zero high bits, frame VID 10v_key_missing_vid — a MAC-only key
8Key widening costCAM 393 216 → 491 520 cells, +25%
9Entry width64 → 76 bits, rounding to 80
10Chapter 12.5's lookup with a 60-bit keystill 4 cycles — structure unchanged

Isolation

#ScenarioExpected
11Broadcast in VLAN 10, ports 1–4 membersflood mask = ports 2,3,4 only
12Same, port 3 blocking in VLAN 10mask = ports 2,4 — the three-way intersection
13Same, port 3 blocking in VLAN 20 onlymask still includes port 3 — state is per VLAN
14Unknown unicast in VLAN 10flooded within VLAN 10 only
15Multicast in VLAN 10flooded within VLAN 10 only
16Any emitted frameout a member port, always
17Frame emitted out a non-member portv_leak, last_leak_port recorded
18Frame's VID differs at egress from ingressv_vid_changed — relabelled, worse than leaked
19Frame accepted through a non-member ingress portv_ingress_not_member
20Flood mask containing the ingress portChapter 12.1's P9 violated
21VLAN with one member portempty mask, VE_NO_MEMBERS — correct
221000 frames across 8 VLANsisolated high throughout

Learning per VLAN

#ScenarioExpected
23Source A in VLAN 10 on port 1entry {10, A} → port 1
24Source A in VLAN 20 on port 5entry {20, A} → port 5, both present
25Lookup A in VLAN 10port 1 — unaffected by the VLAN 20 entry
26Lookup A in VLAN 20port 5
27Same, with a MAC-only keyone entry; the VLAN 10 lookup returns port 5
28Same, then isolation checkframe discarded — Section 15's silent loss
29Learn from a non-member ingress portv_learn_nonmember
30Router with legs in 4 VLANs4 entries, multi_vlan_normal, no flap
31Same MAC, same VLAN, two portsc_same_vlan_dupChapter 12.2 §10's failure
32c_multi_vlan_mac on a routed networksmall and non-zero — the baseline
33c_multi_vlan_mac = 0 on a routed networkthe VLAN is not reaching the comparison

The dimension categorisation

#ScenarioExpected
34dim_keyRD_WIDENED
35dim_tableRD_WIDENED — shared, not partitioned
36dim_flood_mask, dim_port_stateRD_MULTIPLIED
37dim_frame_buffer, dim_egress_arbiterRD_UNCHANGED
38Port forwarding in VLAN 10, blocking in VLAN 20both true simultaneously
39Port-down flushforwarding state changes, membership stable
40Membership writedoes not alter port state

Capacity

#ScenarioExpected
418192 entries, table partitioned across 64 VLANs128 entries each
42Same, across 4094 VLANs2 entries each — absurd
438192 entries shared, one VLAN with 3000 stationsall 3000 fit
44Same, partitioned128 fit, 2872 flood, table 96% free
45Dense per-VLAN state, 24 ports, 4094 VLANs60.0 KiB — 0.94× the whole table
46Indexed, 256 configured VLANs7.8 KiB — a 7.7× saving
47Creating VLAN N_VLANS + 1refused, not silently ignored
48Same, refusal not implementedports stay in the default VLAN, no error
49vlan_ceilingpublished to management

Segmentation's effect

#ScenarioExpected
50200 stations, 1 VLAN, B = 5/seach receives 995/s — 0.50% of a core
51Same, 4 VLANs245/s — 0.12%, a 4.06× reduction
52Same, 16 VLANs55/s — 18.09×
53Any of the abovezero frames discarded
54One VLAN holding most of the portsdomain_oversized, largest_vidx names it
55Per-VLAN ageing intervals, dense4094 × 9 = 4.5 KiB — cheap, and optional
56Global ageing interval onlystill correct — a policy limitation, not a fault
57Learning rate limit budgetper port, not per port per VLAN
58Flood-to-forward ratio in a 3-member VLANhigh, and expected — read it per VLAN
59Chapter 12.5's set distribution with a 60-bit keyunchanged or better — the VID adds entropy
60Chapter 12.1's drops_by_ingressunchanged — congestion is a property of a wire
61VLAN spanning 4 switches, 12 member ports each48 stations in one domain
62Same, the trunk between themcarries every VLAN's broadcast — unreduced
632000 stations divided into any number of VLANsuplink still carries 6.7 Mb/s of broadcast
64Segmentation against an unknown-unicast missthe flood is scoped; the miss is unchanged
65Segmentation against a duplicate MAC in one VLANno help — the two stations still conflict
66Two VLANs sharing one physical egress portshare its bandwidth entirely — a wire has no VLAN

19. Debugging a Segmented Switch

Every row produces a switch forwarding correctly with healthy links. Several of them produce isolation that works alongside forwarding that does not, which is the combination this chapter exists to make visible.

SymptomLikely causeThe observable that decides it
Broadcast load unchanged after creating VLANsone VLAN holds most of the portslargest_domain, largest_vidx, domain_oversized
A host's default gateway is intermittently unreachableMAC-only key, router in several VLANsv_key_missing_vid, and c_multi_vlan_mac = 0
A router reported as constantly flappingsame cause, seen from Chapter 12.2 §9multi_vlan_normal should be high, same_vlan_conflict low
Flooding in one VLAN, table 96% freethe table is partitioned, not shareddim_table = RD_PARTITIONED — Section 8
Flooding in a VLAN nobody changedanother VLAN was created, shrinking this onesame cause; the coupling Section 8 warns about
Frames leaving a port not in their VLANthe mask lost its membership conjunctv_leak, last_leak_port, last_leak_vidx
Frames arriving in the wrong VLAN, looking legitimaterelabelling, not leakingv_vid_changed — no isolation monitor downstream will fire
A port blocks in every VLAN when blocked in oneport state is not per VLANdim_port_state should be RD_MULTIPLIED
Isolation configured but not appliedthe VLAN ceiling was reachedconfigured_vlans at vlan_ceiling, no error
A VLAN's flood mask is emptya single-member VLANVE_NO_MEMBERS — correct, or a trunk not extended
Frames discarded with no counterforwarded to the wrong VLAN's port, then filteredSection 15 — check v_key_missing_vid first
Chip area larger than budgeteddense per-VLAN state at the full VID range60 KiB — Section 10; a VID map is 7.7× smaller
Flood ratio alarming on a small VLANthe ratio is being read globallyevaluate it per VLAN — a 3-member VLAN floods to 2 ports
Ageing behaves oddly for one group of hostsa global interval suits neither groupper-VLAN intervals cost 4.5 KiB and are optional
A VLAN's stations learned much more slowly than another'sthe rate limit is being applied per VLANthe budget protects the shared table — it belongs per port

20. Common Misconceptions

1 — "A VLAN is a filter that stops frames crossing between groups."

The wrong model: forward as before, then discard anything crossing a boundary.

What it costs: Chapter 12.4 §5's replicator still builds N−1 copies and throws most away — the fabric bandwidth is spent, the descriptors built, the queues touched. And it does not fix the table: with a MAC-only key a lookup returns the wrong VLAN's port, the filter discards the result, and the frame is lost rather than leaked.

The corrected model: the VID enters at the key and at the mask, not at the exit. Isolation is a consequence of forwarding correctly per VLAN, not a check applied to forwarding done wrongly — and Section 5's three-way intersection is where it happens.

2 — "Each VLAN should get its own forwarding table."

The wrong model: the switch behaves as several switches, so give each one its own resources.

What it costs: at 64 VLANs, 128 entries each; at the full range, 2. And the failure is decoupled from load: a VLAN with 3000 stations gets 128 entries while 63 partitions sit empty. Worse, creating a VLAN for an unrelated administrative reason shrinks every other VLAN's capacity, so flooding appears in a VLAN nobody touched.

The corrected model: one table, keyed on {VID, MAC}, allocated by demand. The cost is Section 7's 25% key widening and nothing else — and Chapter 12.5's hash, associativity and sweep absorb the wider key unchanged.

3 — "A MAC address identifies a station."

The wrong model: table[mac] has one answer; two ports reporting one address is a fault.

What it costs: Section 17's rejected property. A router with legs in four VLANs presents one address in all four, and a design holding this model reports a permanent flap on a device behaving perfectly — so the check is disabled, taking Chapter 12.2 §10's genuine duplicate-address detection with it.

The corrected model: the station's identity is {VID, MAC}. The same 48 bits in two VLANs name two different things. Same address, different VLAN → two correct entries. Same address, same VLAN, different port → still a duplicate-address failure. One comparison separates them.

4 — "Supporting 4094 VLANs is just a matter of making the arrays bigger."

The wrong model: the VID is 12 bits, so index everything by it.

What it costs: 60 KiB of per-VLAN state on a 24-port switch — 0.94× the entire 8192-entry forwarding table — for state that holds no addresses and forwards no frames. The port-state array alone is 36 KiB.

The corrected model: real switches support 4094 VIDs and far fewer simultaneously configured VLANs, with a VID-to-index map in front of dense arrays. At 256 configured that is 7.8 KiB — a 7.7× saving. The limitation is a ceiling, and Section 9 showed exceeding it fails silently, which is why the ceiling must be published.

5 — "VLAN isolation is a security boundary."

The wrong model: frames cannot cross VLANs, therefore VLANs separate trust domains.

What it costs: a security argument resting on configuration consistency across every switch in the path. A VID is 12 bits in a frame; nothing binds VID 10 to engineering — the binding lives in configuration on every participating switch, and two switches configured differently will forward frames between what their operators believed were different VLANs.

The corrected model: the invariant this chapter proves is this switch did not emit a frame out of a non-member portcomplete, checkable and local. The network is isolated is a conjunction over every switch's configuration, and that is a claim about parties this design cannot observe. It is a genuine broadcast-domain boundary, which is what the arithmetic in Section 4 is about.

6 — "VLANs multiply everything in the switch."

The wrong model: partitioning is total, so every resource gets a VLAN index.

What it costs: the frame buffer — the switch's most expensive memory — multiplied by up to 4094 to hold the same octets, and 4094 egress schedulers per port.

The corrected model: a resource acquires the VLAN dimension exactly when its correct value differs per VLAN. The key does; masks and port state do. A frame's octets do notChapter 12.4 §5 already established that replication is a descriptor operation — and a wire has no VLAN, so the egress arbiter does not either. Per-VLAN scheduling is a priority question, which is Chapter 13.2's PCP field and Chapter 13.4's queue mapping.

21. Interview Reasoning

Q1 — "Why do VLANs exist? Answer with arithmetic."

Reason through it. Because Chapter 12.4 §10's broadcast-domain limit is (N − 1) × B against a per-station CPU budget, and the only variable a designer controls is N. No network card can filter a broadcast — the address means everyone, and filtering it would break ARP and DHCP — so every broadcast reaches every host's CPU, and at B = 5/s with a 1000/s budget the domain tops out near 200 stations. Storm control cannot hold it there: Chapter 12.4 §12 derived that the required cap is 43.5 frames per second per port, 0.00292% of line rate, roughly 34× finer than any switch expresses, and it discards legitimate ARP when it engages. The strong answer closes with the comparison: segmentation reduces N linearly — 200 stations in 4 VLANs is a 4.06× reduction — and discards nothing.

Q2 — "What must a switch keep per VLAN that it kept once, and what must it not?"

Reason through it. The rule is that a resource acquires the VLAN dimension exactly when its correct value differs per VLAN. The key widens — 48 to 60 bits, because the same address in two VLANs is two stations. Masks and port state multiply — a port may belong to one VLAN and not another, and may forward in one while blocking in another. The frame buffer and the egress arbiter do not change at all: a frame's octets are the same octets, and a wire has no VLAN. The strong answer names the fourth category and rejects it: the forwarding table looks like it should be partitioned and must not be — at 64 VLANs that gives 128 entries each, at 4094 it gives 2so it is shared with a widened key and allocated by demand.

Q3 — "A host reports its default gateway as intermittently unreachable. Isolation is verified working. What do you check?"

Reason through it. The lookup key, not the isolation. A router with legs in several VLANs presents one MAC address in all of them; with a MAC-only key that is a single table entry, updated by whichever leg transmitted most recently. A lookup in VLAN 10 then returns the port of the VLAN 20 leg — and the isolation check correctly discards the frame, because that port is not a VLAN 10 member. So there is no leak, no security failure and no error counter: the frame is simply lost. The observable is v_key_missing_vid, or equivalently c_multi_vlan_mac reading zero on a network that demonstrably has a router. The strong answer adds the second symptom that confirms it: Chapter 12.2 §9's move detector reporting a permanent flap on a router that has not moved.

Q4 — "What does supporting all 4094 VLANs cost a 24-port switch, and what do real designs do?"

Reason through it. Dense arrays indexed by the raw VID: membership 24 × 4094 = 12 KiB, port state at 3 bits per port per VLAN 36 KiB, flood mask 12 KiB60 KiB total, which is 0.94× Chapter 12.5's entire 8192-entry forwarding table for state that holds no addresses and forwards no frames. Real designs put a VID-to-index map in front: 4094 × 8 bits = 4 KiB of map plus dense arrays over the number of VLANs actually configured — 7.8 KiB at 256 configured, a 7.7× saving. The strong answer names the cost of that choice: a ceiling on simultaneously configured VLANs, which fails silently — the ports an operator intended to place in the new VLAN stay where they were, frames flow normally, and nothing reports an error. The ceiling must therefore be published so management software can refuse the configuration rather than accept and not apply it.

Q5 — "An assertion that a MAC address maps to one port has started failing. It used to pass. What happened?"

Reason through it. The key widened and the property did not. Before VLANs a MAC address identified a station and the property was a faithful statement of intent that caught real bugs. After VLANs the identity is {VID, MAC}, and a router with legs in four VLANs legitimately presents one address on four entries — so the property now fires on correct behaviour. The strong answer names the danger: the usual response is to disable or waive it, and Chapter 12.2 §10's genuine duplicate-address detection goes with it. The fix is to rewrite it against the whole key: same address in different VLANs is two correct entries; same address on two ports in the same VLAN is still a duplicate-address failure — one comparison separates them, and both checks survive.

Q6 — "Is a VLAN a security boundary?"

Reason through it. It is a broadcast-domain boundary, which is what its arithmetic delivers and what this chapter proves. As a security boundary it is exactly as strong as the configuration on every switch in the path. A VID is 12 bits in a frame — nothing binds VID 10 to any meaning, and the binding lives in configuration that two switches can disagree about. The strong answer distinguishes the two claims precisely: this switch did not emit a frame out of a non-member port is complete, checkable and local — membership is configuration the switch holds and the emitted port is a wire it drives. The network is isolated is a conjunction over every participating switch's configuration, which is a claim about parties this design cannot observe — the same shape Chapter 12.2 §16 identified for the forwarding table's correctness.

22. Understanding Check

23. What's Next

This chapter established the requirement and never said how a VID reaches the switch.

Every module here took the VID as an input that had arrived by some meanslearn_vid, flood_vidx, emit_vidx. Chapter 13.2 — The 802.1Q Tag is that means: four octets carrying a 16-bit protocol identifier and a 16-bit control field holding the 3-bit priority, the drop-eligible bit and the 12-bit VID this chapter has been consuming.

And those four octets are inserted in the middle of a frame — after the addresses, before the EtherType — which moves every field offset after them by four. Chapter 5.5's length/type resolution, Chapter 12.6's commit point at octet 14, and every parser offset in the switch are all computed against a layout that a tagged frame does not have.

Then Chapter 13.3 — Access Ports, Trunk Ports and Tag Handling owns where a tag is inserted and stripped, and what an untagged frame on a trunk means.

And Chapter 13.4 — VLAN Processing in the Switch Datapath builds what Section 10 priced: the VID-to-index map, the shared table with its widened key, and the mapping from Chapter 13.2's priority field into egress queues — the one piece of per-VLAN behaviour Section 6 deliberately categorised as belonging to a different mechanism.

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.