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:
| Requirement | Why |
|---|---|
| a broadcast reaches only ports in its VLAN | otherwise N has not been reduced |
| an unknown-unicast flood reaches only ports in its VLAN | same — Chapter 12.4's three causes all flood |
| a multicast flood reaches only ports in its VLAN | same |
| a unicast forward crosses only within its VLAN | otherwise isolation is decorative |
| the table answers per VLAN | the same address may exist in two |
| port state is per VLAN | a 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.
3. RTL 1 — Modelling the Requirement
Before any mechanism, write down what "these stations can hear each other" means as something checkable.
// -----------------------------------------------------------------------
// 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// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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:
| VLANs | Stations each | Each station receives | Host CPU at 5 µs | Reduction |
|---|---|---|---|---|
| 1 | 200 | 995 /s | 0.50% of a core | — |
| 2 | 100 | 495 /s | 0.25% | 2.01× |
| 4 | 50 | 245 /s | 0.12% | 4.06× |
| 8 | 25 | 120 /s | 0.06% | 8.29× |
| 16 | 12 | 55 /s | 0.03% | 18.09× |
And the comparison that matters is not the magnitude but the mechanism.
| storm control — Chapter 12.4 §11 | segmentation | |
|---|---|---|
| what it changes | B, the injection rate | N, the audience |
| frames discarded | yes — legitimate ARP suppressed | none |
| granularity available | 0.1% of line rate, 34× too coarse | one VLAN |
| effect on a station's own VLAN | broadcast is capped for everyone | unchanged — full delivery |
| what it costs the switch | a counter per port | 60 KiB of state — Section 10 |
| scales with | nothing | the 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.
| Problem | Does segmentation help? | Why |
|---|---|---|
| broadcast load per station | yes — linearly in V | fewer stations hear each other |
| unknown-unicast flooding | partly | the flood is scoped, but the miss still happens |
| multicast waste | partly | scoped to the VLAN; within it, Chapter 12.4 §13's 87% remains |
| a duplicate MAC address | no | two stations in one VLAN still conflict |
| table capacity | no | the table is shared; the same addresses are learned |
| congestion on an egress port | no | a wire carries every VLAN's traffic |
| Chapter 12.5's hash clustering | no — slightly better | the VID adds entropy, nothing more |
| a failing cable | no | Chapter 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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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.
| Resource | Correct dimension | Cost | What the wrong choice does |
|---|---|---|---|
| the lookup key | widened — 48 → 60 bits | +25% | keeping 48 bits: Section 15's collision |
| the forwarding table | shared, one structure | none | partitioning it: Section 8's 2 entries per VLAN |
| the flood mask | multiplied — one per VLAN | 12 KiB | one mask: no isolation |
| port state | multiplied | 36 KiB | one state: a port cannot block per VLAN |
| membership | new | 12 KiB | — |
| the frame buffer | unchanged | none | multiplying it: 4094× the memory, for nothing |
| the egress arbiter | unchanged | none | per-VLAN arbiters: 4094 schedulers per port |
| ageing intervals | multiplied | tiny | one 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 domain | 4 switches, 4 VLANs of 12 ports each | |
|---|---|---|
| stations per domain | 4 × 23 = 92 | 12 |
each receives at B = 5/s | 455 /s | 55 /s |
| the trunk carries | all 460 broadcasts/s | all 460 still — every VLAN crosses it |
| host CPU | 0.23% of a core | 0.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.
| Resource | Must it differ per VLAN? | Why it often does anyway |
|---|---|---|
| ageing interval — Chapter 12.2 §7 | no | a VLAN of servers idles differently from a VLAN of laptops |
| learning rate limit — Chapter 12.2 §11 | no | a VLAN facing untrusted ports wants a tighter cap |
| storm-control threshold — Chapter 12.4 §11 | no | a 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.
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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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 configured | Entries per VLAN | Verdict |
|---|---|---|
| 2 | 4096 | workable |
| 4 | 2048 | workable |
| 16 | 512 | tight |
| 64 | 128 | smaller than one rack of servers |
| 256 | 32 | unusable |
| 4094 — the full range | 2 | absurd |
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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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.
| Structure | Bits | Size |
|---|---|---|
membership bitmap — 24 × 4094 | 98 256 | 12.0 KiB |
port state — 3 × 24 × 4094 | 294 768 | 36.0 KiB |
flood mask — 24 × 4094 | 98 256 | 12.0 KiB |
| total per-VLAN state | 491 280 | 60.0 KiB |
And the comparison that makes it land:
| Size | |
|---|---|
| Chapter 12.5's entire 8192-entry forwarding table | 8192 × 64 = 64 KiB |
| per-VLAN state at the full VID range | 60 KiB |
| ratio | 0.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.
| Design | VID map | Arrays sized for | Per-VLAN state |
|---|---|---|---|
| dense, all VIDs | none | 4094 | 60.0 KiB |
| indexed, 256 configured | 4094 × 8 bits = 4.0 KiB | 256 | 4.0 + 3.8 = 7.8 KiB |
| indexed, 64 configured | 4.0 KiB | 64 | 4.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.
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.
// -----------------------------------------------------------------------
// 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);
endmoduleClassification: 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.
// -----------------------------------------------------------------------
// 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);
endmoduleClassification: 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:
| Claim | Why 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.
// -----------------------------------------------------------------------
// 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
endmoduleClassification: 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.
| Step | With a {VID, MAC} key | With a MAC-only key |
|---|---|---|
| router transmits in VLAN 10, port 1 | entry {10, R} → port 1 | entry R → port 1 |
| router transmits in VLAN 20, port 1 | entry {20, R} → port 1 | entry R → port 1 (same) |
a host in VLAN 10 sends to R | hit {10, R} → port 1 | hit R → port 1 |
| now the router moves its VLAN 20 leg to port 5 | {20, R} → port 5; {10, R} unchanged | entry R → port 5 |
a host in VLAN 10 sends to R | hit {10, R} → port 1, correct | hit R → port 5 — wrong VLAN's port |
| what happens to that frame | delivered | Section 12's isolation check discards it |
| what the host sees | normal | the router is unreachable, intermittently |
| what any counter says | — | nothing |
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.
// -----------------------------------------------------------------------
// 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);
endmoduleClassification: 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.
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
// 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
// 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
// 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
// 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
// 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
// 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
| # | Scenario | Expected |
|---|---|---|
| 1 | VID 10, MAC A | key = {10, A}, 60 bits |
| 2 | Same MAC, VID 20 | different key — the widening working |
| 3 | Same key twice | one entry, refreshed |
| 4 | VID 0 | no key, VE_VID_RESERVED |
| 5 | VID 4095 | no key, reserved |
| 6 | VID 1 and VID 4094 | both accepted — the usable range |
| 7 | Key with zero high bits, frame VID 10 | v_key_missing_vid — a MAC-only key |
| 8 | Key widening cost | CAM 393 216 → 491 520 cells, +25% |
| 9 | Entry width | 64 → 76 bits, rounding to 80 |
| 10 | Chapter 12.5's lookup with a 60-bit key | still 4 cycles — structure unchanged |
Isolation
| # | Scenario | Expected |
|---|---|---|
| 11 | Broadcast in VLAN 10, ports 1–4 members | flood mask = ports 2,3,4 only |
| 12 | Same, port 3 blocking in VLAN 10 | mask = ports 2,4 — the three-way intersection |
| 13 | Same, port 3 blocking in VLAN 20 only | mask still includes port 3 — state is per VLAN |
| 14 | Unknown unicast in VLAN 10 | flooded within VLAN 10 only |
| 15 | Multicast in VLAN 10 | flooded within VLAN 10 only |
| 16 | Any emitted frame | out a member port, always |
| 17 | Frame emitted out a non-member port | v_leak, last_leak_port recorded |
| 18 | Frame's VID differs at egress from ingress | v_vid_changed — relabelled, worse than leaked |
| 19 | Frame accepted through a non-member ingress port | v_ingress_not_member |
| 20 | Flood mask containing the ingress port | Chapter 12.1's P9 violated |
| 21 | VLAN with one member port | empty mask, VE_NO_MEMBERS — correct |
| 22 | 1000 frames across 8 VLANs | isolated high throughout |
Learning per VLAN
| # | Scenario | Expected |
|---|---|---|
| 23 | Source A in VLAN 10 on port 1 | entry {10, A} → port 1 |
| 24 | Source A in VLAN 20 on port 5 | entry {20, A} → port 5, both present |
| 25 | Lookup A in VLAN 10 | port 1 — unaffected by the VLAN 20 entry |
| 26 | Lookup A in VLAN 20 | port 5 |
| 27 | Same, with a MAC-only key | one entry; the VLAN 10 lookup returns port 5 |
| 28 | Same, then isolation check | frame discarded — Section 15's silent loss |
| 29 | Learn from a non-member ingress port | v_learn_nonmember |
| 30 | Router with legs in 4 VLANs | 4 entries, multi_vlan_normal, no flap |
| 31 | Same MAC, same VLAN, two ports | c_same_vlan_dup — Chapter 12.2 §10's failure |
| 32 | c_multi_vlan_mac on a routed network | small and non-zero — the baseline |
| 33 | c_multi_vlan_mac = 0 on a routed network | the VLAN is not reaching the comparison |
The dimension categorisation
| # | Scenario | Expected |
|---|---|---|
| 34 | dim_key | RD_WIDENED |
| 35 | dim_table | RD_WIDENED — shared, not partitioned |
| 36 | dim_flood_mask, dim_port_state | RD_MULTIPLIED |
| 37 | dim_frame_buffer, dim_egress_arbiter | RD_UNCHANGED |
| 38 | Port forwarding in VLAN 10, blocking in VLAN 20 | both true simultaneously |
| 39 | Port-down flush | forwarding state changes, membership stable |
| 40 | Membership write | does not alter port state |
Capacity
| # | Scenario | Expected |
|---|---|---|
| 41 | 8192 entries, table partitioned across 64 VLANs | 128 entries each |
| 42 | Same, across 4094 VLANs | 2 entries each — absurd |
| 43 | 8192 entries shared, one VLAN with 3000 stations | all 3000 fit |
| 44 | Same, partitioned | 128 fit, 2872 flood, table 96% free |
| 45 | Dense per-VLAN state, 24 ports, 4094 VLANs | 60.0 KiB — 0.94× the whole table |
| 46 | Indexed, 256 configured VLANs | 7.8 KiB — a 7.7× saving |
| 47 | Creating VLAN N_VLANS + 1 | refused, not silently ignored |
| 48 | Same, refusal not implemented | ports stay in the default VLAN, no error |
| 49 | vlan_ceiling | published to management |
Segmentation's effect
| # | Scenario | Expected |
|---|---|---|
| 50 | 200 stations, 1 VLAN, B = 5/s | each receives 995/s — 0.50% of a core |
| 51 | Same, 4 VLANs | 245/s — 0.12%, a 4.06× reduction |
| 52 | Same, 16 VLANs | 55/s — 18.09× |
| 53 | Any of the above | zero frames discarded |
| 54 | One VLAN holding most of the ports | domain_oversized, largest_vidx names it |
| 55 | Per-VLAN ageing intervals, dense | 4094 × 9 = 4.5 KiB — cheap, and optional |
| 56 | Global ageing interval only | still correct — a policy limitation, not a fault |
| 57 | Learning rate limit budget | per port, not per port per VLAN |
| 58 | Flood-to-forward ratio in a 3-member VLAN | high, and expected — read it per VLAN |
| 59 | Chapter 12.5's set distribution with a 60-bit key | unchanged or better — the VID adds entropy |
| 60 | Chapter 12.1's drops_by_ingress | unchanged — congestion is a property of a wire |
| 61 | VLAN spanning 4 switches, 12 member ports each | 48 stations in one domain |
| 62 | Same, the trunk between them | carries every VLAN's broadcast — unreduced |
| 63 | 2000 stations divided into any number of VLANs | uplink still carries 6.7 Mb/s of broadcast |
| 64 | Segmentation against an unknown-unicast miss | the flood is scoped; the miss is unchanged |
| 65 | Segmentation against a duplicate MAC in one VLAN | no help — the two stations still conflict |
| 66 | Two VLANs sharing one physical egress port | share 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.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| Broadcast load unchanged after creating VLANs | one VLAN holds most of the ports | largest_domain, largest_vidx, domain_oversized |
| A host's default gateway is intermittently unreachable | MAC-only key, router in several VLANs | v_key_missing_vid, and c_multi_vlan_mac = 0 |
| A router reported as constantly flapping | same cause, seen from Chapter 12.2 §9 | multi_vlan_normal should be high, same_vlan_conflict low |
| Flooding in one VLAN, table 96% free | the table is partitioned, not shared | dim_table = RD_PARTITIONED — Section 8 |
| Flooding in a VLAN nobody changed | another VLAN was created, shrinking this one | same cause; the coupling Section 8 warns about |
| Frames leaving a port not in their VLAN | the mask lost its membership conjunct | v_leak, last_leak_port, last_leak_vidx |
| Frames arriving in the wrong VLAN, looking legitimate | relabelling, not leaking | v_vid_changed — no isolation monitor downstream will fire |
| A port blocks in every VLAN when blocked in one | port state is not per VLAN | dim_port_state should be RD_MULTIPLIED |
| Isolation configured but not applied | the VLAN ceiling was reached | configured_vlans at vlan_ceiling, no error |
| A VLAN's flood mask is empty | a single-member VLAN | VE_NO_MEMBERS — correct, or a trunk not extended |
| Frames discarded with no counter | forwarded to the wrong VLAN's port, then filtered | Section 15 — check v_key_missing_vid first |
| Chip area larger than budgeted | dense per-VLAN state at the full VID range | 60 KiB — Section 10; a VID map is 7.7× smaller |
| Flood ratio alarming on a small VLAN | the ratio is being read globally | evaluate it per VLAN — a 3-member VLAN floods to 2 ports |
| Ageing behaves oddly for one group of hosts | a global interval suits neither group | per-VLAN intervals cost 4.5 KiB and are optional |
| A VLAN's stations learned much more slowly than another's | the rate limit is being applied per VLAN | the 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 port — complete, 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 not — Chapter 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 2 — so 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 KiB — 60 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 means — learn_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
Related tutorials
- Related topic
Frame Format Overview
Every field exists to let a receiver make one decision at one moment, and the field order is the order those decisions must be made. The check value comes last because it covers everything before it — which makes every decision taken before it provisional.
- Related topic
Access Ports, Trunk Ports and Tag Handling
A frame's VLAN is assigned by the receiving port, not carried by the frame. Across an untagged link it is destroyed and re-invented — which is why a native-VLAN mismatch leaks both ways with every check clean.
- Related topic
VLAN Processing in the Switch Datapath
Widening the key to {VID, MAC} changes the capacity fractions by nothing and the memory by exactly 25%. The index map that makes 4094 VLANs affordable is also where two VLANs can silently become one.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
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.
