Skip to content
VLSI Mentor

Ethernet · Module 13

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.

Chapter 13.1 priced the state and specified the requirement. Chapter 13.2 decoded the tag. Chapter 13.3 assigned the VLAN and represented it on the wire. Every one of them handed something to this chapter and stopped.

Three things arrive here: a 12-bit VID that must index arrays sized for far fewer VLANs, a 60-bit key that Chapter 12.5's structure must absorb, and a 3-bit priority that must select an egress queue.

The first is where this chapter's danger lives. Chapter 13.1 §10 established that dense arrays over the full 4094-VLAN range cost 60 KiB — 0.94× the entire forwarding table — and that real designs put a VID-to-index map in front. That map is a many-to-one function from 4094 values onto however many VLANs are configured, and every check downstream of it operates on the index.

Which means two VIDs sharing an index are one VLAN as far as every mechanism in Modules 12 and 13 is concerned — including Chapter 13.1 §12's isolation monitor, which will report perfect isolation between VLANs that have already been merged.

1. Scope — What This Chapter Owns

This chapter owns the datapath: the VID-to-index map and its aliasing hazard, the 60-bit key and what it does to Chapter 12.5's structure, the recomputation of that chapter's capacity arithmetic, and the mapping from Chapter 13.2's PCP into egress queues.

It does not own the requirementChapter 13.1 established what must become per-VLAN, categorised every resource, and priced the state.

It does not own the tagChapter 13.2 owns TPID, PCP, DEI and VID, and the parser that resolves their offsets.

It does not own the transformationChapter 13.3 owns assignment at ingress and representation at egress. This chapter takes an assigned VID as its input and hands a queued frame to that chapter's egress transformer.

And it does not own scheduling policy beyond the mapping. Section 11's scheduler exists to show what PCP selects and what a bad selection costs; the arbitration itself is Chapter 12.1 §9's, and Module 14 is where the queue's depth becomes the subject.

2. Two Representations, Two Jobs

A VLAN arrives at this datapath as a 12-bit VID and immediately needs to be two things at once.

As an identity, it is part of the forwarding key. Chapter 13.1 §7 established that a station's identity is {VID, MAC} — the same address in two VLANs is two different stations — so the table must distinguish 4094 possible VLANs, and the key carries all twelve bits.

As an array subscript, it selects a membership bitmap, a flood mask and a set of port states. Chapter 13.1 §10 showed that indexing those arrays by the raw VID costs 60 KiB on a 24-port switch — 0.94× the whole forwarding table — for state that holds no addresses.

The map exists to reconcile those two demands and it does so by giving up something: distinctness.

DesignArrays sized forMapTotalTwo VIDs can collide
dense4094none60.0 KiBno
indexed, 1024 configured10245.0 KiB20.0 KiByes, if misprogrammed
indexed, 256 configured2564.0 KiB7.8 KiByes
indexed, 64 configured643.0 KiB4.9 KiByes

The map is 4094 entries wide regardless — every possible VID needs a translation — and it is ⌈log₂ N_VLANS⌉ bits deep. At 256 configured VLANs that is 4094 × 8 = 4.0 KiB, and the arrays it feeds shrink from 60 KiB to 3.8.

And the column on the right is the price. A dense design cannot alias, because the array subscript is the VID. An indexed design can, because the map is a function that two inputs may share an output of — and nothing downstream of the map can tell.

3. RTL 1 — The VID-to-Index Map

4094 entries of eight bits, one lookup per frame, and one hazard that no downstream check can see.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlanidx_pkg -- shared types for the VLAN datapath.
//
// The distinction this package exists to enforce: vid_t is the identity
// and goes in the KEY; vidx_t is a subscript and goes in the ARRAYS. They
// are different types on purpose, because the only thing standing between
// an indexed design and two merged VLANs is not confusing them.
// -----------------------------------------------------------------------
package vlanidx_pkg;

  localparam int VID_W    = 12;      // the identity -- 4094 usable
  localparam int VIDX_W   = 8;       // the subscript -- 256 configured
  localparam int ADDR_W   = 48;
  localparam int KEY_W    = VID_W + ADDR_W;   // 60 -- Chapter 13.1 Section 7
  localparam int PCP_W    = 3;
  localparam int N_QUEUES = 4;
  localparam int QSEL_W   = 2;

  typedef logic [VID_W-1:0]  vid_t;
  typedef logic [VIDX_W-1:0] vidx_t;

  // Why a VID could not be translated. The first is a configuration
  // state; the second is the failure this chapter is about.
  typedef enum logic [1:0] {
    MR_OK          = 2'd0,
    MR_UNCONFIGURED = 2'd1,  // this VID has no index -- the VLAN does not exist here
    MR_ALIASED      = 2'd2,  // two VIDs share this index -- Section 5
    MR_RESERVED     = 2'd3   // 0 or 4095
  } map_result_e;

  // Scheduling discipline for Section 11. Strict priority is the default
  // and it is the one that starves.
  typedef enum logic [1:0] {
    SCHED_STRICT   = 2'd0,
    SCHED_WEIGHTED = 2'd1,
    SCHED_ROUND    = 2'd2
  } sched_mode_e;

endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vid_index_map -- 12 bits to a compact subscript.
//
// A PROGRAMMED TRANSLATION TABLE, not a hash. It stores no key to compare
// against, so a collision is undetectable at lookup time and must be
// prevented at programming time -- which is what Section 5 exists for.
// -----------------------------------------------------------------------
module vid_index_map
  import vlanidx_pkg::*;
#(
  parameter int N_VIDS   = 4096,
  parameter int N_VLANS  = 256,
  parameter int CNT_W    = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  // Configuration: bind one VID to one index.
  input  logic             cfg_valid,
  input  vid_t             cfg_vid,
  input  vidx_t            cfg_vidx,
  input  logic             cfg_present,

  // Per-frame translation.
  input  logic             lk_valid,
  input  vid_t             lk_vid,

  output logic             idx_valid,
  output vidx_t            idx,
  output map_result_e      result,

  output logic [CNT_W-1:0] c_translated,
  output logic [CNT_W-1:0] c_unconfigured,
  output logic [11:0]      n_vids_bound,
  output logic [CNT_W-1:0] map_bits
);

  vidx_t              tbl_idx     [N_VIDS];
  logic               tbl_present [N_VIDS];

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int v = 0; v < N_VIDS; v++) begin
        tbl_idx[v]     <= '0;
        tbl_present[v] <= 1'b0;
      end
    end else if (cfg_valid) begin
      tbl_idx[cfg_vid]     <= cfg_vidx;
      tbl_present[cfg_vid] <= cfg_present;
    end
  end

  logic vid_reserved;
  assign vid_reserved = (lk_vid == 12'd0) || (lk_vid == 12'd4095);

  always_comb begin
    idx_valid = 1'b0;
    idx       = '0;
    result    = MR_OK;

    if (lk_valid) begin
      if (vid_reserved) begin
        result = MR_RESERVED;
      end else if (!tbl_present[lk_vid]) begin
        // The VID is legal and this switch has no VLAN for it. Distinct
        // from an aliasing failure: nothing is merged, the frame simply
        // has nowhere to go.
        result = MR_UNCONFIGURED;
      end else begin
        idx_valid = 1'b1;
        idx       = tbl_idx[lk_vid];
      end
    end
  end

  // NOTE WHAT IS ABSENT. There is no stored VID to compare the lookup
  // against, because the index IS the answer. Chapter 12.5's hashed table
  // stores the full key and compares it on every lookup; this cannot,
  // and that is the difference between a detectable collision and an
  // undetectable one.

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_translated   <= '0;
      c_unconfigured <= '0;
    end else if (lk_valid) begin
      if (idx_valid) begin
        c_translated <= c_translated + 1'b1;
      end else if (result == MR_UNCONFIGURED) begin
        if (!(&c_unconfigured)) c_unconfigured <= c_unconfigured + 1'b1;
      end
    end
  end

  always_comb begin
    n_vids_bound = 12'd0;
    for (int v = 0; v < N_VIDS; v++)
      if (tbl_present[v]) n_vids_bound = n_vids_bound + 12'd1;
  end

  assign map_bits = CNT_W'(N_VIDS) * CNT_W'(VIDX_W + 1);

endmodule

Classification: synthesizable, with tbl_idx inferring a 4096-entry memory of 9 bits.

What it teaches: the absence that matters. There is no stored VID to compare the lookup against, because the index is the answer. Chapter 12.5 §6's hashed table stores the full 48-bit key beside every entry and compares it on every lookup — so two addresses hashing to one set are still two distinct entries and a lookup for one never returns the other. This module has nothing analogous, and that single structural difference is what turns a detectable collision into an undetectable one.

And it teaches that MR_UNCONFIGURED is a different condition from aliasing. A VID with no index means this switch has no VLAN by that number — the frame has nowhere to go and is discarded, loudly, with a counter. Aliasing means two VIDs got the same index, which is not a refusal at all: both frames translate successfully and both proceed into a datapath that now believes they are one VLAN.

Deliberately simplified: a flat 4096-entry table, which is 4096 × 9 = 4.5 KiB and is the honest cost. Production designs sometimes compress it — a base-and-range scheme, or a small CAM over the configured VIDs — but every compression reintroduces a lookup that can fail, and the flat table's virtue is that its translation is a single deterministic read.

Production implication: n_vids_bound against N_VLANS is the commissioning check. A switch with 300 VIDs bound and 256 index slots has necessarily aliased at least 44 of them, and the pigeonhole argument is available to management software before a single frame is forwarded. Publishing n_vids_bound lets the configuration layer refuse the 257th binding rather than accept it and silently merge two VLANs — which is Chapter 13.1 §11's vlan_ceiling argument, arriving here as a concrete mechanism.

4. What an Aliased Index Actually Does

Two VIDs mapping to one index is not a subtle degradation. It is two VLANs becoming one, completely, in every mechanism this track has built — and every one of them reports success.

Suppose VID 10 and VID 20 both map to index 7.

MechanismWhat it doesResult
Chapter 13.1 §9's membershipreads members[7]one bitmap for both VLANs
Chapter 13.3 §11's untagged setreads untagged[port][7]one setting for both
Chapter 12.4's flood maskscoped to index 7a VLAN 10 broadcast floods to VLAN 20's ports
Chapter 13.1 §12's isolation monitorchecks members[7][port]clean — the port is a member of index 7
Chapter 12.3's port statereads pstate[7][port]one state for both
the table keyuses the full VIDstill distinct — 10 and 20 differ

The last row is the one that makes this diagnosable at all, and only if the key was built correctly.

Because the key carries the full 12-bit VID, the forwarding table still holds {10, A} and {20, A} as separate entries. A unicast lookup for A in VLAN 10 returns VLAN 10's port. So unicast forwarding is correct while flooding is merged — and that asymmetry is the signature.

And if the key were built from the index instead, even that would go. {7, A} is one entry, updated by whichever VLAN's station transmitted last, and Chapter 13.1 §15's silent frame loss appears on top of the merge.

key from the VIDkey from the index
unicast in VLAN 10correctwhichever VLAN transmitted last
broadcast in VLAN 10reaches VLAN 20 tooreaches VLAN 20 too
the symptombroadcast leaks, unicast does noteverything is intermittently wrong
what it looks likea routing or bridging odditya hardware fault

The left column's symptom is peculiar enough to be a clue. Broadcasts cross between two VLANs and unicast traffic does not is not a shape any other failure in this track produces — Chapter 13.3 §16's native-VLAN mismatch merges everything, and a membership misconfiguration merges nothing.

5. RTL 2 — Catching an Alias at Programming Time

A collision cannot be detected at lookup, so it must be prevented at configuration — and the check is a reverse map that costs one entry per index.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vid_alias_checker -- refuses a binding that would give two VIDs one
// index.
//
// The map stores no key to compare at lookup time, so the only place a
// collision can be caught is at the write. This module keeps the reverse
// direction -- which VID owns each index -- and refuses a second claimant.
// -----------------------------------------------------------------------
module vid_alias_checker
  import vlanidx_pkg::*;
#(
  parameter int N_VLANS = 256,
  parameter int CNT_W   = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             cfg_valid,
  input  vid_t             cfg_vid,
  input  vidx_t            cfg_vidx,
  input  logic             cfg_present,

  output logic             cfg_accept,
  output map_result_e      cfg_result,
  output vid_t             conflicting_vid,

  output logic [CNT_W-1:0] c_bindings,
  output logic [CNT_W-1:0] c_alias_refused,
  output logic [11:0]      indices_used,
  output logic             map_is_injective
);

  // THE REVERSE MAP. One VID per index, which is the property the forward
  // map cannot express. 256 entries of 13 bits is 416 octets -- against
  // the 4.5 KiB forward map, a rounding error.
  vid_t owner     [N_VLANS];
  logic owner_val [N_VLANS];

  always_comb begin
    cfg_accept      = 1'b0;
    cfg_result      = MR_OK;
    conflicting_vid = '0;

    if (cfg_valid) begin
      if ((cfg_vid == 12'd0) || (cfg_vid == 12'd4095)) begin
        cfg_result = MR_RESERVED;
      end else if (!cfg_present) begin
        // Unbinding is always permitted.
        cfg_accept = 1'b1;
      end else if (owner_val[cfg_vidx] && (owner[cfg_vidx] != cfg_vid)) begin
        // THE REFUSAL. This index already belongs to a different VID.
        // Accepting would merge two VLANs in every array in the design,
        // permanently, with no runtime signal.
        cfg_result      = MR_ALIASED;
        conflicting_vid = owner[cfg_vidx];
      end else begin
        cfg_accept = 1'b1;
      end
    end
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int i = 0; i < N_VLANS; i++) begin
        owner[i] <= '0; owner_val[i] <= 1'b0;
      end
      c_bindings      <= '0;
      c_alias_refused <= '0;
    end else if (cfg_valid) begin
      if (cfg_accept) begin
        if (cfg_present) begin
          owner[cfg_vidx]     <= cfg_vid;
          owner_val[cfg_vidx] <= 1'b1;
          if (!(&c_bindings)) c_bindings <= c_bindings + 1'b1;
        end else begin
          owner_val[cfg_vidx] <= 1'b0;
        end
      end else if (cfg_result == MR_ALIASED) begin
        if (!(&c_alias_refused)) c_alias_refused <= c_alias_refused + 1'b1;
      end
    end
  end

  always_comb begin
    indices_used = 12'd0;
    for (int i = 0; i < N_VLANS; i++)
      if (owner_val[i]) indices_used = indices_used + 12'd1;
  end

  // THE INVARIANT, as a signal. If the map is injective, no two VIDs
  // share an index and every mechanism downstream is looking at exactly
  // one VLAN.
  assign map_is_injective = 1'b1;   // maintained by construction above

endmodule

Classification: synthesizable.

What it teaches: that the reverse map is what the forward map cannot express, and it costs almost nothing. 256 entries of 13 bits is 416 octets, against the forward map's 4.5 KiB — a rounding error that converts an undetectable runtime merge into a refused configuration write.

And it teaches where the refusal has to happen. The lookup cannot detect a collision because there is nothing to compare. The frame cannot detect it because both VIDs translate successfully. No downstream mechanism can detect it because they all operate on the index. The write is the only moment at which both VIDs are visible together, and a design that does not check there has no other opportunity.

Deliberately simplified: one binding at a time with a combinational conflict check. Production configuration paths are often a bulk write of an entire VLAN table, which needs the same check applied across the whole batch — and a bulk write that checks each entry against the previous state while the batch is internally inconsistent will accept a set of bindings no single write would have.

Production implication: c_alias_refused should be zero in a working deployment and non-zero during commissioning is a good sign — it means management software attempted a binding the hardware refused, which is the mechanism working. A design without this check reports nothing in either case, and the difference between a correctly configured switch and one with two merged VLANs is invisible from every interface. indices_used against N_VLANS is the same pigeonhole argument Section 3's n_vids_bound makes, from the index side.

A frame arrives carrying a twelve bit VLAN identifier, which the datapath needs to use in two different ways at once. As an identity it forms part of the sixty bit forwarding key alongside the forty-eight bit address, and it must carry all twelve bits because the same address in two VLANs is two different stations. As an array subscript it selects a membership bitmap, a flood mask and a set of port states, and indexing those arrays by the raw identifier costs sixty kibibytes on a twenty-four port switch, so a translation table maps four thousand and ninety-four identifiers onto however many VLANs are configured. That translation is many to one by construction, and unlike a hashed forwarding table it stores no key to compare against, so a collision cannot be detected at lookup time and every mechanism downstream operates on the collapsed index. Building the key from the index instead of the identifier makes two VLANs share every table entry, and indexing the arrays by the identifier instead of the index restores the sixty kibibyte cost.12-bit VID arrivesidentity and subscript atonceKey: {VID, MAC}all 12 bits — 60-bit keyForwarding tableVLANs stay distinctIndex in the keytwo VLANs share everyentryMap to an index4094 → 256, many-to-oneArrays: masks, state7.8 KiB instead of 60VID in the arrays60 KiB — 0.94× the table12
Figure 1 — the VID goes in the key and the index goes in the arrays; swap them and either two VLANs merge or the design costs 60 KiB.

6. RTL 3 — Building the Key

Sixty bits, and the only thing that matters is which twelve of them come from where.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_key_builder -- forms the {VID, MAC} key for Chapter 12.5's table.
//
// THE ONE RULE: the key carries the VID, never the index. The index is
// six to ten bits and many-to-one; the VID is twelve bits and injective
// over the VLANs that exist. A key built from the index makes two VLANs
// share every entry -- Section 4's right-hand column.
// -----------------------------------------------------------------------
module vlan_key_builder
  import vlanidx_pkg::*;
#(
  parameter int CNT_W = 32
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             in_valid,
  input  vid_t             vid,          // the IDENTITY
  input  vidx_t            vidx,         // the SUBSCRIPT -- deliberately unused here
  input  logic [ADDR_W-1:0] mac,

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

  output logic [7:0]       key_width,
  output logic [7:0]       vid_bits_in_key,
  output logic [CNT_W-1:0] c_keys,
  output logic             uses_index_in_key   // must be permanently 0
);

  // Chapter 13.1 Section 7: the VID goes in the HIGH bits so that one
  // VLAN's entries share a key prefix, which makes a per-VLAN flush a
  // range operation rather than a full-table walk.
  assign key       = {vid, mac};
  assign key_valid = in_valid;

  assign key_width       = 8'(KEY_W);
  assign vid_bits_in_key = 8'(VID_W);

  // A STANDING ASSERTION AS A SIGNAL. The index is an input to this
  // module and is deliberately not used. Exposing that as a wire lets a
  // reviewer check the claim against the code, and lets Section 14's
  // monitor check it against the built design.
  assign uses_index_in_key = 1'b0;

  // Tie off the unused subscript explicitly rather than leaving it
  // dangling, so that a future edit connecting it is visible in a diff.
  wire _unused_vidx = |vidx;

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

endmodule

Classification: synthesizable, and almost entirely wires.

What it teaches: that the index is an input to this module and is deliberately unused, and that the tie-off is the point rather than an artefact. A module that simply does not have a vidx port looks like it never needed one — and the next engineer, integrating it beside a datapath where every other block takes an index, may reasonably connect one. Taking the input and refusing to use it makes the refusal reviewable, and uses_index_in_key makes it checkable by Section 17.

And it teaches why the VID sits in the high bits, which Chapter 13.1 §7 argued and this chapter depends on. One VLAN's entries share a 12-bit key prefix, so a per-VLAN flush — required when a topology change affects one VLAN and not the other 4093 — is a masked range operation rather than a walk comparing a field on every entry.

Deliberately simplified: a flat concatenation. A production design may carry the key as two fields through the pipeline and concatenate only at the table interface, which is equivalent and makes the two halves' provenance visible for longer.

Production implication: key_width and vid_bits_in_key are elaboration-time constants exposed as outputs, and their value is in a diff. A design that changes the key width — to add a bridge domain, a tenant identifier, or the second tag Chapter 13.2 §14 described — changes these numbers, and every capacity figure in Section 8 changes with them. Exposing them means the recomputation is prompted by the code rather than remembered.

7. RTL 4 — The Table at Sixty Bits

Chapter 12.5's structure absorbs the wider key without any structural change. The adapter exists to make that claim precise and to expose what did change.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_table_adapter -- Chapter 12.5's hashed set-associative table with a
// 60-bit key instead of 48.
//
// WHAT DOES NOT CHANGE: the hash width, the set count, the associativity,
// the sweep, the eviction policy, and every capacity fraction in Chapter
// 12.5 Sections 7, 9 and 10.
// WHAT CHANGES: the stored key, the entry width, the set read width and
// the table's total memory -- all by exactly 25%.
// -----------------------------------------------------------------------
module vlan_table_adapter
  import vlanidx_pkg::*;
#(
  parameter int N_SETS    = 2048,
  parameter int WAYS      = 4,
  parameter int HASH_W    = 11,
  parameter int PORT_W    = 5,
  parameter int AGE_W     = 9,
  parameter int CNT_W     = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 lk_valid,
  input  logic [KEY_W-1:0]     lk_key,
  output logic                 lk_hit,
  output logic [PORT_W-1:0]    lk_port,
  output logic [2:0]           lk_latency_cy,

  input  logic                 ins_valid,
  input  logic [KEY_W-1:0]     ins_key,
  input  logic [PORT_W-1:0]    ins_port,

  // The hash. Chapter 12.5 Section 5's fold, over 60 bits instead of 48 --
  // and the extra 12 bits are the VID, which is far better distributed
  // than the OUI-heavy address space it joins.
  output logic [HASH_W-1:0]    hash_index,

  // What changed, computed rather than asserted.
  output logic [7:0]           entry_bits,
  output logic [15:0]          set_read_bits,
  output logic [CNT_W-1:0]     table_bits,
  output logic [7:0]           growth_pct
);

  // Entry: the full key plus port, age, valid and static. Chapter 12.5
  // Section 6 established that a hash is not reversible, so the ENTIRE
  // key is stored -- there is no tag saving at 48 bits and none at 60.
  localparam int ENT_RAW  = KEY_W + PORT_W + AGE_W + 2;
  localparam int ENT_ROUND = (ENT_RAW <= 64) ? 64 : ((ENT_RAW <= 80) ? 80 : 96);

  typedef struct packed {
    logic                valid;
    logic [KEY_W-1:0]    key;
    logic [PORT_W-1:0]   port;
    logic [AGE_W-1:0]    age;
    logic                stat;
  } ventry_t;

  ventry_t mem [N_SETS][WAYS];

  // THE HASH IS UNCHANGED IN WIDTH. 11 bits, 2048 sets. Widening the key
  // does not widen the index, which is why Section 8's capacity fractions
  // are identical to Chapter 12.5's.
  always_comb begin
    automatic logic [23:0] lo  = lk_key[23:0];
    automatic logic [23:0] mid = lk_key[47:24];
    automatic logic [11:0] hi  = lk_key[59:48];   // the VID
    automatic logic [23:0] mix;
    mix = lo ^ {mid[11:0], mid[23:12]} ^ {12'd0, hi};
    hash_index = mix[HASH_W-1:0] ^ mix[23:24-HASH_W];
  end

  logic [WAYS-1:0] way_match;

  always_comb begin
    way_match = '0;
    for (int w = 0; w < WAYS; w++)
      // THE FULL 60-BIT COMPARE. Chapter 12.5 Section 6's rule, wider by
      // twelve bits and otherwise identical.
      if (mem[hash_index][w].valid && (mem[hash_index][w].key == lk_key))
        way_match[w] = 1'b1;
  end

  always_comb begin
    lk_hit  = 1'b0;
    lk_port = '0;
    if (lk_valid) begin
      lk_hit = |way_match;
      for (int w = WAYS-1; w >= 0; w--)
        if (way_match[w]) lk_port = mem[hash_index][w].port;
    end
  end

  // Chapter 12.5 Section 16: hash, one set read, compare, select.
  // Unchanged -- a wider comparator is still one comparator.
  assign lk_latency_cy = 3'd4;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int s = 0; s < N_SETS; s++)
        for (int w = 0; w < WAYS; w++) mem[s][w] <= '0;
    end else if (ins_valid) begin
      for (int w = 0; w < WAYS; w++)
        if (!mem[hash_index][w].valid) begin
          mem[hash_index][w] <= '{valid: 1'b1, key: ins_key,
                                  port: ins_port, age: '0, stat: 1'b0};
          break;
        end
    end
  end

  assign entry_bits    = 8'(ENT_ROUND);
  assign set_read_bits = 16'(ENT_ROUND) * 16'(WAYS);
  assign table_bits    = CNT_W'(N_SETS) * CNT_W'(WAYS) * CNT_W'(ENT_ROUND);
  assign growth_pct    = 8'(((ENT_ROUND - 64) * 100) / 64);

endmodule

Classification: synthesizable, with mem inferring a two-dimensional SRAM whose row is one 320-bit set.

What it teaches: that the hash index width does not change. Eleven bits, 2048 sets, whatever the key's width — and since Chapter 12.5 §7's overflow arithmetic depends only on the number of sets, the associativity and the number of addresses offered, every capacity fraction in that chapter is identical at 60 bits. Section 8 does the recomputation and the answer is that nothing moved.

And it teaches where the extra twelve bits actually help. Chapter 12.5 §5's callout established that MAC addresses are not uniformly distributed — a rack of identical servers shares a 24-bit OUI and differs in a handful of low serial bits. The VID is a small, densely-used, operator-assigned space that correlates with nothing in the address, so folding it into the hash adds entropy exactly where the address space has none.

Deliberately simplified: insertion without eviction or the refusal policy — Chapter 12.5 §6 and §11 own those and neither changes here. The break in a for loop is illustrative; a real design uses a priority encoder over the free-way mask.

Production implication: growth_pct is the number to carry into a floorplan review, and it is 25% — the entry goes from 64 bits to 76, which rounds to 80, so an 8192-entry table goes from 64 KiB to 80 KiB and a four-way set read from 256 bits to 320. The set read width is the one that touches timing, because Chapter 12.5 §16's four-cycle lookup depends on a whole set arriving in one memory access — and a 320-bit row is a different memory compile from a 256-bit one, on a path with ten cycles of margin and no more.

8. Chapter 12.5 Recomputed at Sixty Bits

The prompt for this chapter was to redo Chapter 12.5's capacity arithmetic with the widened key and report what changed. The answer is worth stating before the tables: the fractions did not move, and the memory grew by exactly 25%.

Chapter 12.5 §7 — sets full at a given occupancy, 4-way × 2048:

Occupancy48-bit key60-bit key
25%0.37% — 7.5 sets0.37% — 7.5 sets
50%5.27% — 107.8 sets5.27% — 107.8 sets
75%18.47% — 378.3 sets18.47% — 378.3 sets

Chapter 12.5 §9 — effective capacity, offered 8192 distinct addresses:

Structure48-bit key60-bit key
direct-mapped5178 — 63.2%5178 — 63.2%
2-way5975 — 72.9%5975 — 72.9%
4-way6592 — 80.5%6592 — 80.5%
8-way7049 — 86.0%7049 — 86.0%

Nothing moved, and the reason is structural rather than a coincidence. Chapter 12.5 §7's arithmetic is a balls-in-bins calculation over S sets, W ways and n offered addresses. The key's width appears nowhere in it. Widening the key widens what is stored and compared; it does not widen the index, which is still eleven bits selecting one of 2048 sets.

What did change is memory, and every figure by the same 25%:

Quantity48-bit key60-bit keyChange
key stored per entry48 bits60 bits+25%
entry, raw48 + 16 = 6460 + 16 = 76
entry, rounded64 bits80 bits+25%
8192-entry table64 KiB80 KiB+25%
4-way set read256 bits320 bits+25%
CAM cells, 8192 entries393 216491 520+25%
lookup latency — §164 cycles4 cyclesnone

And two things that did not change at all are worth naming, because both look as though they should have.

The lookup latency is still four cycles. Chapter 12.5 §16's pipeline is hash, one set read, compare, select — and a 60-bit comparator is still one comparator. It is wider and therefore slower in absolute terms, which is a timing-closure question rather than a cycle-count one.

The ageing sweep is still 0.0033% duty. It visits 8192 entries once a second regardless of how wide they are; the entries are read as whole rows and the row got 25% wider, so the sweep's bandwidth grew 25% — from a number four orders of magnitude below the traffic to a slightly larger number four orders of magnitude below the traffic.

9. RTL 5 — Mapping Priority to a Queue

Eight priorities, four queues, and a map that is sixteen bits per port and decides whether the priority mechanism does anything at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// pcp_queue_mapper -- Chapter 13.2's three-bit PCP to an egress queue.
//
// Chapter 13.1 Section 6 categorised the egress arbiter as UNCHANGED by
// VLANs and said priority was a different mechanism. This is that
// mechanism, and its whole content is a per-port table of eight entries.
// -----------------------------------------------------------------------
module pcp_queue_mapper
  import vlanidx_pkg::*;
#(
  parameter int N_PORTS = 24,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic                 cfg_valid,
  input  logic [4:0]           cfg_port,
  input  logic [PCP_W-1:0]     cfg_pcp,
  input  logic [QSEL_W-1:0]    cfg_queue,

  input  logic                 in_valid,
  input  logic [4:0]           egress_port,
  input  logic [PCP_W-1:0]     pcp,
  input  logic                 drop_eligible,   // Chapter 13.2's DEI

  output logic [QSEL_W-1:0]    queue,
  output logic                 discard_first,   // to Chapter 12.1 Section 9

  output logic [CNT_W-1:0]     c_by_queue [N_QUEUES],
  output logic [CNT_W-1:0]     c_by_pcp   [8],
  output logic                 map_is_identity,   // 8 -> 8, no compression
  output logic                 map_is_flat,       // everything to one queue
  output logic [15:0]          map_bits_per_port
);

  logic [QSEL_W-1:0] map [N_PORTS][8];

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int p = 0; p < N_PORTS; p++)
        for (int i = 0; i < 8; i++)
          // A SENSIBLE DEFAULT, not zero. Mapping every priority to
          // queue 0 makes the feature inert; mapping proportionally at
          // least honours the ordering the sender expressed.
          map[p][i] <= QSEL_W'(i * N_QUEUES / 8);
    end else if (cfg_valid) begin
      map[cfg_port][cfg_pcp] <= cfg_queue;
    end
  end

  assign queue = map[egress_port][pcp];

  // Chapter 13.2 Section 4: DEI is a hint to Chapter 12.1 Section 9's
  // discard policy and is advisory in both directions. It selects no
  // queue -- it marks a preferred victim within one.
  assign discard_first = in_valid && drop_eligible;

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int q = 0; q < N_QUEUES; q++) c_by_queue[q] <= '0;
      for (int i = 0; i < 8; i++)        c_by_pcp[i]   <= '0;
    end else if (in_valid) begin
      c_by_queue[queue] <= c_by_queue[queue] + 1'b1;
      c_by_pcp[pcp]     <= c_by_pcp[pcp] + 1'b1;
    end
  end

  // Two degenerate configurations that make the mechanism inert, both
  // reported rather than assumed away.
  always_comb begin
    map_is_flat     = 1'b1;
    map_is_identity = (N_QUEUES == 8);
    for (int i = 1; i < 8; i++)
      if (map[0][i] != map[0][0]) map_is_flat = 1'b0;
  end

  assign map_bits_per_port = 16'd8 * 16'(QSEL_W);

endmodule

Classification: synthesizable.

What it teaches: that the default matters more than the configurability. A map initialised to all zeros sends every priority to queue 0, so the feature is present, consuming queues, and inert — and nothing reports that unless map_is_flat is computed. A proportional default — pcp × N_QUEUES ÷ 8 — at least honours the ordering the sender expressed, which is the part of PCP that survives compression from eight levels to four.

And it teaches that DEI selects no queue. Chapter 13.2 §4 established it as a hint to Chapter 12.1 §9's discard policy — a preferred victim within a queue, not a different queue — and a design that routes DEI into the queue selection has turned a discard hint into a priority demotion, which is a different and much stronger action than the sender asked for.

Deliberately simplified: a per-port map with no ingress regeneration. Chapter 13.3 §7's callout established that a boundary port cannot trust an arriving PCP; the regeneration table belongs at ingress and this map at egress, and a design with only one of them either trusts a stranger's marking or cannot express a per-egress policy.

Production implication: c_by_pcp and c_by_queue together diagnose the two ways this feature fails silently. Everything in PCP 0 means nobody is marking — the map is fine and there is nothing for it to do. Everything in one queue with a spread of PCPs means the map is flat — the senders are marking and the switch is discarding the distinction. The two look identical from a throughput measurement and are fixed in completely different places.

10. Eight Priorities Into Four Queues

Compression is the normal case, and which eight-into-four map is chosen decides what the priority mechanism actually delivers.

MapPCP 0–7 → queueWhat it delivers
flatall → 0nothing — the feature is inert
proportional0,0 1,1 2,2 3,3ordering preserved, granularity halved
top-heavy0,0,0,0 1,2,3,3four low priorities merged, three high ones separated
bottom-heavy0,1,2,3 3,3,3,3low priorities separated, all high traffic merged
identityrequires 8 queuesfull granularity, 8 × 3 = 24 bits per port

The top-heavy row is what most real configurations look like, and the reason is a property of the traffic rather than of the mechanism.

Priority marking in practice is bimodal. Chapter 13.2 §4's c_by_pcp histogram on a real network is dominated by PCP 0 — untagged hosts and anything that does not mark — with a small population at one or two high values used for voice, video or control. The middle of the range is nearly empty.

So a map that spends its four queues evenly across eight priorities is spending three of them on a range nothing uses, while merging the values that carry the traffic that actually needed separating.

And the cost of the map itself is trivial in every configuration:

QueuesBits per PCP entryMap per port24 ports
218 bits24 octets
4216 bits48 octets
8324 bits72 octets

Forty-eight octets for a 24-port switch. Against Chapter 13.1 §10's 60 KiB of per-VLAN state and this chapter's 4.5 KiB VID map, the priority map is free — and the thing that is not free is the queues themselves, which is why the compression exists.

11. RTL 6 — Scheduling the Queues

The map chose a queue. Something must now choose between queues, and the obvious discipline starves.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// egress_queue_scheduler -- selects among per-priority queues on one port.
//
// Chapter 12.1 Section 9's arbiter chose among INGRESS ports contending
// for one egress. This chooses among PRIORITY CLASSES contending for one
// egress. The two are in series, not in competition, and this one runs
// first.
// -----------------------------------------------------------------------
module egress_queue_scheduler
  import vlanidx_pkg::*;
#(
  parameter int CNT_W  = 32,
  parameter int WIN    = 1_000_000
)(
  input  logic                 clk,
  input  logic                 rst_n,

  input  logic [N_QUEUES-1:0]  q_nonempty,
  input  logic [7:0]           q_weight [N_QUEUES],
  input  sched_mode_e          mode,
  input  logic                 grant_taken,

  output logic                 sel_valid,
  output logic [QSEL_W-1:0]    sel_queue,

  output logic [CNT_W-1:0]     c_served [N_QUEUES],
  output logic [CNT_W-1:0]     starved_cycles [N_QUEUES],
  output logic                 starvation_detected,
  output logic [QSEL_W-1:0]    starved_queue,
  output logic [15:0]          lowest_share_pct
);

  logic [7:0]        credit [N_QUEUES];
  logic [QSEL_W-1:0] rr_q;
  logic [CNT_W-1:0]  win_total;

  always_comb begin
    sel_valid = |q_nonempty;
    sel_queue = '0;

    unique case (mode)
      // STRICT PRIORITY. The highest non-empty queue always wins. It is
      // the default, it is what "priority" means to most people, and it
      // starves every lower queue for as long as a higher one has
      // anything to send.
      SCHED_STRICT: begin
        for (int q = 0; q < N_QUEUES; q++)
          if (q_nonempty[q]) sel_queue = QSEL_W'(q);
      end

      // WEIGHTED. Each queue gets credit proportional to its weight and
      // is skipped when its credit is exhausted, so a low-priority queue
      // always gets SOME share.
      SCHED_WEIGHTED: begin
        for (int q = N_QUEUES-1; q >= 0; q--)
          if (q_nonempty[q] && (credit[q] != 8'd0)) sel_queue = QSEL_W'(q);
      end

      default: begin
        for (int q = 0; q < N_QUEUES; q++) begin
          automatic int idx = (int'(rr_q) + q) % N_QUEUES;
          if (q_nonempty[idx]) sel_queue = QSEL_W'(idx);
        end
      end
    endcase
  end

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int q = 0; q < N_QUEUES; q++) begin
        c_served[q]       <= '0;
        starved_cycles[q] <= '0;
        credit[q]         <= 8'd0;
      end
      rr_q                <= '0;
      win_total           <= '0;
      starvation_detected <= 1'b0;
      starved_queue       <= '0;
      lowest_share_pct    <= 16'd100;
    end else begin
      if (sel_valid && grant_taken) begin
        c_served[sel_queue] <= c_served[sel_queue] + 1'b1;
        win_total           <= win_total + 1'b1;
        rr_q                <= sel_queue + 1'b1;
        if (credit[sel_queue] != 8'd0)
          credit[sel_queue] <= credit[sel_queue] - 8'd1;
      end

      // THE MEASUREMENT THAT MAKES STARVATION VISIBLE. A queue with
      // frames waiting and no grant is being starved right now, and
      // counting those cycles is the only way the condition is ever
      // reported -- nothing else in the switch distinguishes "this
      // traffic is slow" from "this traffic is never scheduled".
      for (int q = 0; q < N_QUEUES; q++)
        if (q_nonempty[q] && !(sel_valid && (sel_queue == QSEL_W'(q))))
          starved_cycles[q] <= starved_cycles[q] + 1'b1;

      if (win_total >= CNT_W'(WIN)) begin
        automatic logic [CNT_W-1:0] lo = '1;
        automatic logic [QSEL_W-1:0] lq = '0;
        for (int q = 0; q < N_QUEUES; q++) begin
          if (c_served[q] < lo) begin lo = c_served[q]; lq = QSEL_W'(q); end
          credit[q] <= q_weight[q];
        end
        // A queue served for less than a thousandth of the window while
        // it had frames waiting is starved, not merely low priority.
        starvation_detected <= (lo < (win_total >> 10)) &&
                               (starved_cycles[lq] != '0);
        starved_queue       <= lq;
        lowest_share_pct    <= 16'((lo * CNT_W'(100)) / win_total);
        win_total           <= '0;
        for (int q = 0; q < N_QUEUES; q++) c_served[q] <= '0;
      end
    end
  end

endmodule

Classification: synthesizable.

What it teaches: that strict priority is the default, is what "priority" means to most people, and starves. A queue is served only when every higher queue is empty — so a high-priority class that is continuously busy prevents a low-priority class from ever transmitting, indefinitely, with no timeout and no escape. That is not a defect in the scheduler; it is the exact behaviour strict priority specifies, and it is why weighted disciplines exist.

And starved_cycles is the measurement that makes the condition reportable. Every other observable says this traffic is slow — throughput is low, latency is high — and none of them distinguishes slow from never scheduled. Counting cycles in which a queue had frames and did not get the grant is the only signal that separates congestion from starvation, and the two have completely different remedies.

Deliberately simplified: a combinational selection across four queues with a per-window credit refresh. Production schedulers refresh credit continuously rather than per window, support a strict class above the weighted ones for genuinely time-critical traffic, and are per port — so a 24-port switch has 24 of these.

Production implication: this scheduler and Chapter 12.1 §9's arbiter are in series, not in competition, and confusing them produces a real design error. This one chooses which class transmits next on one egress port; that one chose which ingress port wins access to an egress. A design that merges them into a single arbiter over ports × classes has made the priority of one port's traffic contend with the port identity of another's — and the resulting fairness properties are not what either mechanism was specified to provide.

Frames destined for one egress port are placed into per-priority queues by the priority code point map, which translates three bits of priority into a queue selector. The queue scheduler then chooses which class transmits next on that port, using strict priority by default, which serves a queue only when every higher queue is empty and therefore starves lower classes indefinitely while a higher class remains busy. Separately, Chapter 12.1's fabric arbiter chooses which ingress port wins access to an egress port, using round robin so that no ingress port monopolises the wire. The two arbiters are in series and answer different questions: one chooses a class, the other chooses a source. A design that merges them into a single arbiter over ports times classes makes one port's traffic priority contend with another port's identity, and the resulting fairness properties are not what either mechanism was specified to provide.PCP — 3 bitsChapter 13.2's fieldMap to a queue8 → 4, 16 bits per portClass schedulerwhich class transmitsChapter 12.1'sarbiterwhich ingress port winsStrict prioritystarvesno timeout, no escapestarved_cyclesthe only signal thatseparates themMerging the twoarbiterspriority contends withidentity12
Figure 2 — two arbiters in series: priority chooses the class on one egress, then Chapter 12.1's round robin chooses the ingress port. Merging them breaks both.

12. Where the Six Mechanisms Sit

Assemble the chapter and the order matters, because two of the six can only be checked before the others run.

#StageConsumesProducesCan it fail silently
1VID → index — §312-bit VIDa subscriptyes — aliasing, undetectable at lookup
2key build — §6VID, MAC60-bit keyyes — if built from the index
3table — §7the keyhit and portno — Chapter 12.5's counters
4masks and stateChapter 13.1 §9the indexmembership, flood maskinherits stage 1's aliasing
5PCP → queue — §93-bit PCPa queue selectoryes — a flat map is inert
6queue schedule — §11queue occupancywhich class transmitsyes — strict priority starves

Stages 1 and 2 are the pair this chapter exists to keep apart, and the table's right-hand column shows why the separation is load-bearing: stage 1 deliberately collapses distinctness and stage 2 must not.

And stage 4 is where stage 1's failure becomes visible as behaviour. The masks and port state are indexed, so an aliased index gives two VLANs one membership bitmap — and every check that reads that bitmap agrees, correctly, about a VLAN that is now two.

Two of the six can be checked before any frame is forwarded. Stage 1's aliasing is a pigeonhole argument over the binding count — §13's alias_possible. Stage 5's flat map is a property of a table, readable at configuration time. The other four need traffic.

The VLAN datapath is six stages in series. First the twelve bit identifier is translated to a compact index, which can alias two VLANs together and is undetectable at lookup time. Second the sixty bit key is built from the identifier and the address, which fails silently if it is built from the index instead. Third the forwarding table is searched with that key, whose failures are reported by Chapter 12.5's counters. Fourth the membership bitmap, flood mask and port state are read using the index, which inherits any aliasing introduced by the first stage. Fifth the three bit priority selects an egress queue through a per port map, which is inert if the map is flat. Sixth the queue scheduler chooses which class transmits, which starves lower classes by construction under strict priority. The first stage's aliasing can be proved absent by a counting argument before a single frame is forwarded, and the fifth stage's flat map is a readable property of a configuration table, so two of the six failure modes need no traffic to detect while the other four do.1 — VID → indexaliasing, undetectable atlookup2 — build the keymust use the VID3 — table searchChapter 12.5's counters4 — masks and stateindexed — inherits stage15 — PCP → queueflat map is inert6 — class schedulestrict priority starvesCheckable with notrafficpigeonhole, and a flatmap12
Figure 3 — six stages in series, and the two that can fail silently are the two that can be checked before any traffic exists.

13. RTL 7 — Datapath Telemetry

Six mechanisms in series, each with a distinct way of being silently wrong, and one place to read whether any of them is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_datapath_telemetry -- the per-window health of the whole VLAN
// datapath.
//
// Every quantity here is derived from signals the datapath already has.
// The value is that the six mechanisms' failure modes are unrelated and
// a single throughput measurement conflates all of them.
// -----------------------------------------------------------------------
module vlan_datapath_telemetry
  import vlanidx_pkg::*;
#(
  parameter int N_VLANS = 256,
  parameter int CNT_W   = 32,
  parameter int WIN     = 1_000_000
)(
  input  logic             clk,
  input  logic             rst_n,

  input  logic             frame_valid,
  input  map_result_e      map_result,
  input  logic             lk_hit,
  input  logic [PCP_W-1:0] pcp,
  input  logic [QSEL_W-1:0] queue,
  input  logic [11:0]      n_vids_bound,
  input  logic [11:0]      indices_used,
  input  logic             starvation_detected,
  input  logic             map_is_flat,

  output logic             window_valid,
  output logic [15:0]      unconfigured_pct,
  output logic [15:0]      hit_rate_pct,
  output logic [15:0]      pcp0_share_pct,
  output logic             alias_possible,       // pigeonhole, before any frame
  output logic             priority_inert,       // marked but not honoured
  output logic             priority_unused,      // not marked at all
  output logic [CNT_W-1:0] c_unconfigured,
  output logic [CNT_W-1:0] c_frames
);

  logic [CNT_W-1:0] win_frames, win_unconf, win_hit, win_pcp0;

  // THE PIGEONHOLE CHECK, available before a single frame is forwarded.
  // More VIDs bound than indices available means at least two VIDs share
  // an index, by counting alone.
  assign alias_possible = (n_vids_bound > 12'(N_VLANS)) ||
                          (n_vids_bound > indices_used);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      win_frames <= '0; win_unconf <= '0; win_hit <= '0; win_pcp0 <= '0;
      c_unconfigured   <= '0;
      c_frames         <= '0;
      window_valid     <= 1'b0;
      unconfigured_pct <= '0;
      hit_rate_pct     <= '0;
      pcp0_share_pct   <= '0;
      priority_inert   <= 1'b0;
      priority_unused  <= 1'b0;
    end else begin
      window_valid <= 1'b0;

      if (frame_valid) begin
        win_frames <= win_frames + 1'b1;
        c_frames   <= c_frames + 1'b1;
        if (map_result == MR_UNCONFIGURED) begin
          win_unconf     <= win_unconf + 1'b1;
          c_unconfigured <= c_unconfigured + 1'b1;
        end
        if (lk_hit)             win_hit  <= win_hit + 1'b1;
        if (pcp == 3'd0)        win_pcp0 <= win_pcp0 + 1'b1;
      end

      if (win_frames >= CNT_W'(WIN)) begin
        unconfigured_pct <= 16'((win_unconf * CNT_W'(100)) / win_frames);
        hit_rate_pct     <= 16'((win_hit    * CNT_W'(100)) / win_frames);
        pcp0_share_pct   <= 16'((win_pcp0   * CNT_W'(100)) / win_frames);

        // THE TWO WAYS PRIORITY FAILS SILENTLY, separated. Senders are
        // marking and the map discards the distinction -- fix the map.
        // Nobody is marking -- the map is fine and there is nothing to do.
        priority_inert  <= map_is_flat && (win_pcp0 < (win_frames - (win_frames >> 3)));
        priority_unused <= (win_pcp0 >= (win_frames - (win_frames >> 6)));

        win_frames <= '0; win_unconf <= '0; win_hit <= '0; win_pcp0 <= '0;
        window_valid <= 1'b1;
      end
    end
  end

  wire _unused_q = |queue;
  wire _unused_s = starvation_detected;

endmodule

Classification: synthesizable.

What it teaches: that alias_possible is available before a single frame is forwarded, and that it is a counting argument rather than an observation. More VIDs bound than index slots means at least two share one, by the pigeonhole principle — no traffic required, no detection needed, and the conclusion is certain. A design that publishes this lets management software refuse a configuration instead of accepting it and merging two VLANs at the first broadcast.

And it teaches that priority_inert and priority_unused are opposite conditions with identical symptoms. Both produce a switch whose priority queues carry indistinguishable traffic. One means the senders are marking and the map is flattening it — fix the map. The other means nobody is marking — the map is fine and there is nothing to fix. A single "priority is not working" observation cannot tell them apart; these two bits can.

Deliberately simplified: one global window across all ports. Production telemetry keeps the priority histogram per port, because Chapter 13.3 §7's regeneration argument means a boundary port and a trusted trunk should show different distributions and an aggregate is a mean over two populations with different policies.

Production implication: unconfigured_pct is the counter that distinguishes a VLAN problem from a configuration gap. A frame whose VID has no index is not a failure of this switch — the VLAN genuinely does not exist here, and the frame is discarded correctly. But a rising count on a trunk means a neighbour is sending VLANs this switch was never configured for, which is either a legitimate expansion nobody propagated or Chapter 13.3 §14's mismatch arriving from a different direction.

14. RTL 8 — Conformance for a Datapath With an Indirection

The monitor's difficulty is that the failure it most needs to catch happens at a configuration write, not on a frame — so half of it runs before any traffic exists.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// vlan_dp_conformance_monitor -- checks the VLAN datapath's invariants.
//
// What it CAN check on frames: that the key carries the VID and not the
// index, that the arrays were indexed by the index and not the VID, that
// the priority map produced a queue in range, and that no aliased index
// was ever used.
// What it must check at CONFIGURATION time: injectivity of the map --
// because Section 3 established that a collision is undetectable on any
// frame that traverses it.
// -----------------------------------------------------------------------
module vlan_dp_conformance_monitor
  import vlanidx_pkg::*;
#(
  parameter int N_VLANS = 256,
  parameter int CNT_W   = 32
)(
  input  logic                 clk,
  input  logic                 rst_n,

  // Configuration-time.
  input  logic                 cfg_valid,
  input  logic                 cfg_accept,
  input  map_result_e          cfg_result,
  input  logic [11:0]          n_vids_bound,
  input  logic [11:0]          indices_used,

  // Frame-time.
  input  logic                 frame_valid,
  input  vid_t                 vid,
  input  vidx_t                vidx,
  input  logic [KEY_W-1:0]     key,
  input  logic                 uses_index_in_key,
  input  logic [QSEL_W-1:0]    queue,
  input  logic                 idx_valid,

  output logic [CNT_W-1:0]     v_key_lacks_vid,     // the Section 18 failure
  output logic [CNT_W-1:0]     v_index_in_key,
  output logic [CNT_W-1:0]     v_alias_accepted,    // config-time, and fatal
  output logic [CNT_W-1:0]     v_queue_out_of_range,
  output logic [CNT_W-1:0]     v_used_unmapped_index,
  output logic                 injective,
  output logic                 conformant
);

  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      v_key_lacks_vid       <= '0;
      v_index_in_key        <= '0;
      v_alias_accepted      <= '0;
      v_queue_out_of_range  <= '0;
      v_used_unmapped_index <= '0;
    end else begin
      // CONFIGURATION TIME. A binding that would alias must have been
      // refused. If one was ACCEPTED, two VLANs are merged from this
      // moment on and no frame will ever reveal it.
      if (cfg_valid && cfg_accept && (cfg_result == MR_ALIASED))
        if (!(&v_alias_accepted)) v_alias_accepted <= v_alias_accepted + 1'b1;

      if (frame_valid) begin
        // THE KEY CHECK. The high twelve bits of the key must be the VID.
        // A key built from the index has zeros or a subscript up there,
        // and two VLANs then share every table entry.
        if (key[KEY_W-1 -: VID_W] != vid)
          if (!(&v_key_lacks_vid)) v_key_lacks_vid <= v_key_lacks_vid + 1'b1;

        // The structural claim Section 6 exposed as a wire, checked
        // against the built design rather than the documentation.
        if (uses_index_in_key)
          if (!(&v_index_in_key)) v_index_in_key <= v_index_in_key + 1'b1;

        if (queue >= QSEL_W'(N_QUEUES))
          if (!(&v_queue_out_of_range))
            v_queue_out_of_range <= v_queue_out_of_range + 1'b1;

        // An index used on a frame whose VID had no binding. Section 3
        // refuses these; using one anyway indexes an array belonging to
        // whatever VLAN happens to occupy that slot.
        if (!idx_valid && (vidx != '0))
          if (!(&v_used_unmapped_index))
            v_used_unmapped_index <= v_used_unmapped_index + 1'b1;
      end
    end
  end

  // The pigeonhole invariant, as a standing signal. Section 13's
  // alias_possible is the same argument from the telemetry side.
  assign injective = (n_vids_bound <= indices_used) &&
                     (n_vids_bound <= 12'(N_VLANS));

  assign conformant = (v_key_lacks_vid       == '0) &&
                      (v_index_in_key        == '0) &&
                      (v_alias_accepted      == '0) &&
                      (v_queue_out_of_range  == '0) &&
                      (v_used_unmapped_index == '0) &&
                      injective;

endmodule

Classification: synthesizable, and intended to stay in silicon.

What it teaches: that half of this monitor runs at configuration time and half on frames, and the split is forced by the mechanism rather than chosen. A map collision is undetectable on any frame that traverses it — Section 3 established there is no stored VID to compare against — so the only moment at which the invariant can be checked is the write that would break it. v_alias_accepted is a check on the checker: it fires if Section 5's refusal was bypassed.

And injective is included in conformant deliberately, which is unusual — every other conformance bit in this track is a conjunction of violation counters. Here one clause is a standing property of the configuration rather than a history of events, because a switch whose map is not injective is misconfigured whether or not a frame has yet exposed it.

Deliberately simplified: the key check compares the top twelve bits against the VID, which catches a key built from an index or from zeros. A production monitor also samples the key against the table's stored key during Chapter 12.5 §13's ageing sweep, because a key that was built correctly and stored wrongly is a different fault with the same symptom.

Production implication: conformant here means the VID reached the key, the index reached the arrays, the map is injective, and the priority map produced a queue that exists. It does not mean the VLANs are configured as anybody intended — Chapter 13.3 §18 established that no switch can claim that — and it does not mean the priority map is useful, only that it is in range. Section 13's priority_inert is the bit for that, and it is deliberately outside the conformance conjunction because a flat map is a policy choice rather than a defect.

15. The Aliasing Hazard Beyond VLANs

The shape of Section 4's failure is not specific to VLANs, and recognising it elsewhere is most of the value of having worked through it here.

The pattern has three parts. A wide identifier is mapped many-to-one onto a compact subscript to make arrays affordable. Every downstream mechanism operates on the subscript. And the mapping stores nothing that would let a collision be detected after the fact.

SystemWide identifierCompact subscriptWhat a collision merges
this chapter12-bit VID6–10 bit indextwo broadcast domains
a multi-tenant fabrictenant IDa hardware context numbertwo tenants' isolation
a bridge-domain switchbridge domain IDa table partition indextwo L2 domains
an interrupt controllerdevice IDa vector numbertwo devices' handlers
a translation cacheaddress space IDa tagged-TLB contexttwo address spaces

Every row has the same property: the collision is invisible to everything downstream, because downstream is where the distinctness was already lost.

And every row has the same remedy: a reverse map checked at programming time. Section 5's checker is 256 entries of 13 bits — 416 octets — and it converts an undetectable runtime merge into a refused configuration write. The cost is negligible in every one of these systems and the failure is severe in all of them.

The general test, and it is worth carrying: does this design map a wide identifier onto a narrow one, and does anything downstream depend on the two being distinct? If yes, the mapping needs an injectivity check at the write, because no check after it can see the difference.

The aliasing hazard has three parts and appears far beyond VLANs. A wide identifier is mapped many to one onto a compact subscript so that arrays indexed by it become affordable. Every mechanism downstream operates on the subscript rather than the identifier. And the mapping stores nothing that would let a collision be detected afterwards, unlike a hashed table which stores the full key beside every entry and compares it on every lookup. The same three parts appear in a multi-tenant fabric mapping tenant identifiers onto hardware context numbers, in a bridge domain switch mapping domain identifiers onto table partitions, in an interrupt controller mapping device identifiers onto vector numbers, and in a translation cache mapping address space identifiers onto tagged context numbers. In every case a collision merges two things that were meant to be separate and is invisible to everything downstream, and in every case the remedy is the same: a reverse map checked at programming time, because no check after the mapping can see the difference.Wide identifierVID, tenant, ASID, deviceMany-to-one mapto make arrays affordableNarrow subscripteverything downstreamreads thisA collision mergestwoinvisible downstreamA hash stores thekeycollisions are reportedA map stores nothingthe subscript is theanswerReverse map at thewrite416 octets, the onlymoment12
Figure 4 — the same shape in five systems: a wide identifier folded onto a narrow subscript, with nothing stored to distinguish the originals.

16. What Each Layer of Module 13 Can Claim

Four chapters, four conformance bits, and the module's honest summary is the conjunction of four narrow statements rather than any one broad one.

ChapterThe claimWhat it deliberately does not say
13.1no frame left a non-member port; no VLAN changed inside this switchthe network is isolated
13.2the parser resolved the layout and every consumer used what it resolvedthe tagging was intended
13.3the VLAN was assigned by the configured rule and represented in the configured formthe VLAN is the one anybody meant
13.4the VID reached the key, the index reached the arrays, and the map is injectivethe VLANs are configured correctly

Joined, Module 13's strongest true statement is:

This frame's VLAN was assigned by my configured rule from a tag my parser resolved, carried into a key that distinguishes all 4094 VLANs, used to index arrays through a map I have proved injective, represented on the wire in the form my configuration specifies, and emitted only from ports that are members of that VLAN.

Six clauses, every one local and falsifiable, and not one of them says the VLANs are right.

And that is not a shortcoming of the design — it is a property of what a VLAN is. Chapter 13.3 §18 established it: on an untagged link a frame's VLAN is not transmitted, so continuity is a property of two configurations agreeing. A switch can prove everything about its own handling and nothing about the agreement.

17. The Cost of the Whole Datapath

Add up everything Module 13 asks a 24-port switch to hold, and the total is smaller than the forwarding table it augments.

StructureChapterSize
VID-to-index map, 4096 × 9 bits13.4 §34.5 KiB
reverse map, 256 × 13 bits13.4 §50.4 KiB
membership, flood mask, port state — 256 indices13.1 §103.8 KiB
per-port untagged sets, PVIDs, admit rules13.3 §30.8 KiB
PCP→queue maps, 24 ports × 16 bits13.4 §90.05 KiB
total VLAN state9.6 KiB
the forwarding table's growth from the wider key13.4 §816 KiB
total added by VLANs25.6 KiB
for comparison — the 8192-entry table itself12.580 KiB

Two things in that table are worth reading against each other.

The dedicated VLAN state is 9.6 KiB. Every array, every map, every per-port setting — less than a seventh of the forwarding table.

And the largest single cost is not VLAN state at all. It is the 16 KiB the forwarding table grew when its key went from 48 bits to 60 — Section 8's uniform 25% — which is 62% of everything VLANs added.

Which is a result worth stating plainly, because the intuition runs the other way. Chapter 13.1 §10's dense design was 60 KiB and looked like the dominant cost; the indexed design collapses it to 3.8, and what remains is a linear widening of a structure that was already there.

The comparison against Chapter 13.1 §10's dense alternative:

DesignPer-VLAN arraysMapTable growthTotal
dense, 4094 VLANs60.0 KiBnone16 KiB76.0 KiB
indexed, 256 VLANs3.8 KiB4.9 KiB16 KiB24.7 KiB
saving3.1×

And the 4.9 KiB of map is what buys the 56 KiB of array, at the price of Section 4's aliasing hazard and Section 5's 416-octet checker.

18. Properties Worth Asserting, and One Worth Refusing

Every property here distinguishes the identity from the subscript, or checks the map at the one moment a collision is visible. The rejected property checks distinctness on the side where it has already been lost.

The map

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. A reserved VID never translates. 0 and 4095 name no VLAN.
property p_reserved_vid_no_index;
  @(posedge clk) disable iff (!rst_n)
  (lk_valid && ((lk_vid == 12'd0) || (lk_vid == 12'd4095))) |-> !idx_valid;
endproperty
a_reserved_no_idx: assert property (p_reserved_vid_no_index);

// P2. An unbound VID produces MR_UNCONFIGURED and NO index -- distinct
// from aliasing, which produces a perfectly valid index.
property p_unbound_vid_no_index;
  @(posedge clk) disable iff (!rst_n)
  (lk_valid && !tbl_present[lk_vid]) |-> (!idx_valid && (result == MR_UNCONFIGURED));
endproperty
a_unbound: assert property (p_unbound_vid_no_index);

// P3. Translation is deterministic -- the same VID always yields the same
// index while the binding is unchanged.
property p_map_is_a_function;
  @(posedge clk) disable iff (!rst_n)
  (idx_valid && (lk_vid == $past(lk_vid)) && $stable(tbl_idx[lk_vid]))
    |-> (idx == $past(idx));
endproperty
a_map_deterministic: assert property (p_map_is_a_function);

// P4. THE INJECTIVITY INVARIANT. No two bound VIDs share an index. This
// cannot be checked on a frame, so it is a standing property of the
// configuration.
property p_map_is_injective;
  @(posedge clk) disable iff (!rst_n)
  (n_vids_bound <= indices_used);
endproperty
a_injective: assert property (p_map_is_injective);

// P5. A binding that would alias is REFUSED at the write -- the only
// moment at which both VIDs are visible together.
property p_alias_refused_at_write;
  @(posedge clk) disable iff (!rst_n)
  (cfg_valid && cfg_present && owner_val[cfg_vidx] &&
   (owner[cfg_vidx] != cfg_vid)) |-> (!cfg_accept && (cfg_result == MR_ALIASED));
endproperty
a_alias_refused: assert property (p_alias_refused_at_write);

// P6. A refusal names the conflicting VID, so an operator has both
// halves of the collision rather than a rejection code.
property p_refusal_names_conflict;
  @(posedge clk) disable iff (!rst_n)
  (cfg_valid && (cfg_result == MR_ALIASED)) |-> (conflicting_vid != cfg_vid);
endproperty
a_conflict_named: assert property (p_refusal_names_conflict);

// P7. THE PIGEONHOLE CHECK, available before a single frame. More VIDs
// bound than index slots means at least two share one, by counting alone.
property p_pigeonhole_reported;
  @(posedge clk) disable iff (!rst_n)
  (n_vids_bound > 12'(N_VLANS)) |-> alias_possible;
endproperty
a_pigeonhole: assert property (p_pigeonhole_reported);

Identity against subscript

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P8. THE CENTRAL PROPERTY. The key's high twelve bits are the VID --
// the identity, never the subscript.
property p_key_carries_the_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_the_vid);

// P9. The key never contains the index. Section 6 exposes this as a wire
// so the claim is checkable against the built design.
property p_key_never_has_index;
  @(posedge clk) disable iff (!rst_n)
  key_valid |-> !uses_index_in_key;
endproperty
a_no_index_in_key: assert property (p_key_never_has_index);

// P10. Two VLANs with the same address produce DIFFERENT keys, even when
// they share an index. This is what makes an aliased design's unicast
// forwarding still correct -- Section 4's left-hand column.
property p_distinct_vids_distinct_keys;
  @(posedge clk) disable iff (!rst_n)
  (key_valid && (mac == $past(mac)) && (vid != $past(vid)))
    |-> (key != $past(key));
endproperty
a_vids_separate_keys: assert property (p_distinct_vids_distinct_keys);

// P11. The arrays are indexed by the INDEX, not the VID -- that is what
// the map is for and what turns 60 KiB into 3.8.
property p_arrays_use_the_index;
  @(posedge clk) disable iff (!rst_n)
  (frame_valid && idx_valid) |-> (array_subscript == vidx);
endproperty
a_arrays_indexed: assert property (p_arrays_use_the_index);

// P12. An index is never used when the translation failed -- doing so
// reads an array belonging to whatever VLAN occupies that slot.
property p_no_index_without_translation;
  @(posedge clk) disable iff (!rst_n)
  (frame_valid && !idx_valid) |-> !array_read_enable;
endproperty
a_no_unmapped_index: assert property (p_no_index_without_translation);

The table at sixty bits

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P13. The hash index width is UNCHANGED at 11 bits. Widening the key
// does not widen the index, which is why Section 8's capacity fractions
// are identical to Chapter 12.5's.
property p_hash_width_unchanged;
  @(posedge clk) disable iff (!rst_n)
  lk_valid |-> (hash_index < HASH_W'(N_SETS));
endproperty
a_hash_width: assert property (p_hash_width_unchanged);

// P14. The comparison is over the FULL 60-bit key. A hash is not
// reversible, so nothing may be omitted -- Chapter 12.5 Section 6's rule,
// twelve bits wider.
property p_full_key_compared;
  @(posedge clk) disable iff (!rst_n)
  (lk_valid && lk_hit) |-> (mem[hash_index][hit_way].key == lk_key);
endproperty
a_full_compare: assert property (p_full_key_compared);

// P15. The lookup is still four cycles. A wider comparator is still one
// comparator.
property p_latency_unchanged;
  @(posedge clk) disable iff (!rst_n)
  lk_valid |-> (lk_latency_cy == 3'd4);
endproperty
a_four_cycles: assert property (p_latency_unchanged);

// P16. The entry width grew by exactly 25% -- 64 bits to 80.
property p_entry_growth_is_25pct;
  @(posedge clk) disable iff (!rst_n)
  (entry_bits == 8'd80) && (growth_pct == 8'd25);
endproperty
a_growth: assert property (p_entry_growth_is_25pct);

// P17. The VID contributes to the hash. It is a densely-used,
// operator-assigned space that correlates with nothing in the OUI-heavy
// address space it joins.
property p_vid_feeds_the_hash;
  @(posedge clk) disable iff (!rst_n)
  (lk_valid && (lk_key[59:48] != $past(lk_key[59:48])) &&
   (lk_key[47:0] == $past(lk_key[47:0])))
    |-> (hash_index != $past(hash_index));
endproperty
a_vid_in_hash: assert property (p_vid_feeds_the_hash);

Priority

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P18. The queue is in range, always.
property p_queue_in_range;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> (queue < QSEL_W'(N_QUEUES));
endproperty
a_queue_range: assert property (p_queue_in_range);

// P19. The map is a per-port table read by PCP -- it never depends on the
// VLAN. Chapter 13.1 Section 6 categorised priority as a separate
// mechanism and this is what that means in hardware.
property p_queue_depends_only_on_pcp_and_port;
  @(posedge clk) disable iff (!rst_n)
  in_valid |-> (queue == map[egress_port][pcp]);
endproperty
a_queue_from_pcp: assert property (p_queue_depends_only_on_pcp_and_port);

// P20. DEI selects NO queue. Chapter 13.2 Section 4 established it as a
// discard hint -- a preferred victim within a queue, not a demotion.
property p_dei_does_not_select_a_queue;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && (pcp == $past(pcp)) && (drop_eligible != $past(drop_eligible)))
    |-> (queue == $past(queue));
endproperty
a_dei_not_a_queue: assert property (p_dei_does_not_select_a_queue);

// P21. A flat map is REPORTED, not assumed away. It makes the feature
// inert and nothing else says so.
property p_flat_map_reported;
  @(posedge clk) disable iff (!rst_n)
  (map[0][0] == map[0][7]) |-> map_is_flat;
endproperty
a_flat_reported: assert property (p_flat_map_reported);

Scheduling

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P22. Strict priority serves the highest non-empty queue. This is the
// specified behaviour and it is what starves.
property p_strict_serves_highest;
  @(posedge clk) disable iff (!rst_n)
  (sel_valid && (mode == SCHED_STRICT) && q_nonempty[N_QUEUES-1])
    |-> (sel_queue == QSEL_W'(N_QUEUES-1));
endproperty
a_strict: assert property (p_strict_serves_highest);

// P23. A grant goes only to a non-empty queue.
property p_grant_to_nonempty;
  @(posedge clk) disable iff (!rst_n)
  sel_valid |-> q_nonempty[sel_queue];
endproperty
a_grant_nonempty: assert property (p_grant_to_nonempty);

// P24. STARVATION IS MEASURED, not inferred. A queue with frames and no
// grant is being starved right now, and nothing else distinguishes that
// from congestion.
property p_starvation_counted;
  @(posedge clk) disable iff (!rst_n)
  (q_nonempty[0] && sel_valid && (sel_queue != QSEL_W'(0)))
    |=> (starved_cycles[0] > $past(starved_cycles[0]));
endproperty
a_starvation_counted: assert property (p_starvation_counted);

// P25. Under a weighted discipline every queue eventually gets a grant.
property p_weighted_is_starvation_free;
  @(posedge clk) disable iff (!rst_n)
  ((mode == SCHED_WEIGHTED) && q_nonempty[0] && (q_weight[0] != 8'd0))
    |-> ##[1:$] (sel_valid && (sel_queue == QSEL_W'(0)));
endproperty
a_weighted_fair: assert property (p_weighted_is_starvation_free);

// P26. This scheduler and Chapter 12.1 Section 9's arbiter are in
// SERIES. This one picks a class; that one picks an ingress port.
property p_two_arbiters_in_series;
  @(posedge clk) disable iff (!rst_n)
  (sel_valid && grant_taken) |-> fabric_grant_valid;
endproperty
a_series_not_merged: assert property (p_two_arbiters_in_series);

Conformance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P27. A key that lacks the VID is caught on the first frame.
property p_key_check_fires_immediately;
  @(posedge clk) disable iff (!rst_n)
  (frame_valid && (key[KEY_W-1 -: VID_W] != vid))
    |=> (v_key_lacks_vid > $past(v_key_lacks_vid));
endproperty
a_key_check: assert property (p_key_check_fires_immediately);

// P28. An accepted aliasing binding is FATAL and counted. From that
// moment two VLANs are merged and no frame will reveal it.
property p_accepted_alias_is_recorded;
  @(posedge clk) disable iff (!rst_n)
  (cfg_valid && cfg_accept && (cfg_result == MR_ALIASED))
    |=> (v_alias_accepted > $past(v_alias_accepted));
endproperty
a_alias_recorded: assert property (p_accepted_alias_is_recorded);

// P29. Conformance INCLUDES the standing injectivity property, not only
// a history of violations -- a non-injective map is wrong whether or not
// a frame has yet exposed it.
property p_conformant_includes_injective;
  @(posedge clk) disable iff (!rst_n)
  conformant |-> injective;
endproperty
a_conformant_injective: assert property (p_conformant_includes_injective);

// P30. Conformance means the VID reached the key and the index reached
// the arrays -- never that the VLANs are configured correctly.
property p_conformant_definition;
  @(posedge clk) disable iff (!rst_n)
  conformant |-> ((v_key_lacks_vid == '0) && (v_index_in_key == '0) &&
                  (v_alias_accepted == '0) && (v_queue_out_of_range == '0) &&
                  (v_used_unmapped_index == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);

19. Verification Scenarios

Sixty-four scenarios. The map scenarios include one whose expected outcome is a refused configuration write, and one whose expected outcome is two VLANs behaving as one with every check clean.

The map

#ScenarioExpected
1VID 10 bound to index 3translates to 3
2Same VID twicethe same index, always
3VID 0no index, MR_RESERVED
4VID 4095no index, reserved
5VID 3000, unboundMR_UNCONFIGURED, no index, counted
6VID 20 bound to index 3, already owned by VID 10refused, MR_ALIASED, conflicting_vid = 10
7Same, refusal bypassedv_alias_accepted — fatal, and no frame will reveal it
8Unbinding VID 10, then binding VID 20 to index 3accepted
9257 VIDs bound, 256 index slotsalias_possible — pigeonhole, before any frame
10200 VIDs bound, 256 slotsinjective, alias_possible low
11Map size, 4096 × 9 bits4.5 KiB
12Reverse map, 256 × 13 bits0.4 KiB — 416 octets

An aliased index

#ScenarioExpected
13VID 10 and VID 20 share index 7, broadcast in VLAN 10floods to VLAN 20's ports
14Same, Chapter 13.1 §12's isolation monitorclean — the port is a member of index 7
15Same, unicast lookup for A in VLAN 10correct — the key carries the VID
16Same, but the key built from the indexwhichever VLAN transmitted last
17The symptom, key from the VIDbroadcast leaks, unicast does not
18The symptom, key from the indexeverything intermittently wrong
19Any downstream check on frame_vidxpasses — distinctness was lost upstream

Identity against subscript

#ScenarioExpected
20Key for VID 10, MAC Ahigh 12 bits = 10
21Key for VID 20, same MACdifferent key
22Key for two VIDs sharing an indexstill different — the key uses the VID
23uses_index_in_keypermanently 0
24A key whose high bits are the indexv_key_lacks_vid on the first frame
25Membership array readindexed by vidx, never by vid
26Arrays indexed by the raw VID60 KiBChapter 13.1 §10's dense cost returns
27Index used when translation failedv_used_unmapped_index

The table at sixty bits

#ScenarioExpected
28Hash index width11 bits — unchanged
29Sets full at 50% occupancy5.27% — 107.8 sets, identical to 48 bits
30Effective capacity, 4-way, 8192 offered6592 — 80.5%, identical
31Effective capacity, direct-mapped63.2%, identical
32Entry width60 + 16 = 7680 bits
338192-entry table80 KiB against 64 — +25%
34Four-way set read320 bits against 256 — +25%
35CAM cells491 520 against 393 216 — +25%
36Lookup latency4 cycles — unchanged
37Ageing sweep duty0.0033% — bandwidth +25%, duty still negligible
38Two keys differing only in VIDdifferent hash index — the VID feeds the hash
39The full 60-bit compareperformed — a hash is not reversible

Priority

#ScenarioExpected
40PCP 5, map pcp × 4 ÷ 8queue 2
41All eight PCPs, flat mapall queue 0, map_is_flat
42DEI set, same PCPthe same queue — DEI selects none
43DEI setdiscard_first to Chapter 12.1 §9
44Map for 4 queues16 bits per port, 48 octets for 24 ports
45Map for 8 queues24 bits per port
46Everything at PCP 0priority_unused — the map is fine
47Spread of PCPs, one queuepriority_inert — fix the map
48Both conditions from a throughput testindistinguishable — hence the two bits

Scheduling

#ScenarioExpected
49Strict priority, queue 3 continuously busyqueues 0–2 never served
50Samestarved_cycles rising on 0–2
51Samestarvation_detected, starved_queue = 0
52Weighted, all weights non-zeroevery queue eventually served
53Grantalways to a non-empty queue
54This scheduler and Chapter 12.1 §9's arbiterin series — class then ingress port
55The two merged into one arbiterpriority contends with port identity
56Low queue at under a thousandth of the windowstarvation, not congestion

Cost and conformance

#ScenarioExpected
57Total dedicated VLAN state9.6 KiB
58Forwarding-table growth from the wider key16 KiB — 62% of everything VLANs added
59Indexed against dense, total24.7 KiB against 76.0 — 3.1×
60conformant with a non-injective maplow — injectivity is inside the conjunction
61conformant with a flat priority maphigh — a flat map is policy, not a defect
62Unconfigured VIDs rising on a trunka neighbour is sending VLANs this switch lacks
63Every check downstream of an aliased mappasses
64Healthy run, injective map, one million framesconformant high throughout

20. Debugging the VLAN Datapath

Six mechanisms in series with unrelated failure modes, and a single throughput measurement conflates all of them.

SymptomLikely causeThe observable that decides it
Broadcasts cross two VLANs, unicast does notan aliased indexinjective, alias_possible, v_alias_accepted
Everything between two VLANs intermittently wrongaliased index and a key built from itv_key_lacks_vid on the first frame
Isolation verified, VLANs still mergedthe check reads the index — Section 18injective — the invariant's subject is the subscript
A VLAN's traffic discarded at one switchthat VID has no binding herec_unconfigured, MR_UNCONFIGURED
Unconfigured count rising on a trunka neighbour sends VLANs this switch lacksexpansion nobody propagated, or Chapter 13.3 §14
Table 25% larger than budgetedthe key widened to 60 bitsSection 8 — entry 64 → 80, exactly 25%
Capacity fractions unchanged after wideningexpected — the index did not widenSection 8's first two tables
Set memory failing timing after wideningthe set read went 256 → 320 bitsa different memory compile on a 4-cycle path
Priority configured and doing nothingsenders are not markingpriority_unused, c_by_pcp all in 0
Priority configured and doing nothingthe map is flatpriority_inert, map_is_flat
One traffic class never transmitsstrict priority, higher class always busystarved_cycles, starvation_detected
Low-priority traffic slow but presentcongestion, not starvationstarved_cycles zero — the distinction
Fairness properties nobody can explainthe two arbiters were mergedclass selection and port arbitration are in series

21. Common Misconceptions

1 — "The VID and the VLAN index are the same thing."

The wrong model: a VLAN is identified by a number, and which number does not matter.

What it costs: whichever way they are confused. Building the key from the index makes two VLANs share every table entry — Chapter 13.1 §15's silent frame loss on top of a merge. Indexing the arrays by the VID restores Chapter 13.1 §10's 60 KiB, which is 0.94× the whole forwarding table.

The corrected model: the VID is an identity — 12 bits, injective over the VLANs that exist, and it goes in the key. The index is a subscript — 6 to 10 bits, many-to-one by construction, and it goes in the arrays. Section 3's package makes them different types on purpose.

2 — "A map collision is like a hash collision."

The wrong model: both fold a wide value into a narrow one, so both degrade the same way.

What it costs: the assumption that the collision will be detected. Chapter 12.5 §6's table stores the full 48-bit key beside every entry and compares it on every lookup — two addresses hashing to one set are still two entries, and a lookup for one never returns the other. The map stores nothing to compare against.

The corrected model: a hash collision costs a way in a set and is reported as TI_SET_FULL. A map collision costs a VLAN and is reported by nothing. The map is a programmed translation table, not a hash, and its correctness is a property of the programming — which is why Section 5's checker exists and why it runs at the write.

3 — "Widening the key will hurt the table's capacity."

The wrong model: a bigger key means fewer entries fit, or more collisions.

What it costs: a design decision made against a fear that is arithmetically wrong. Section 8's recomputation: every capacity fraction is identical at 60 bits — 5.27% of sets full at 50% occupancy, 80.5% effective capacity on a 4-way table, 63.2% direct-mapped. Chapter 12.5 §7's arithmetic depends on the number of sets, the associativity and the offered count, and the key's width appears nowhere in it.

The corrected model: widening the key is priced in area and nothing else — the entry grows 64 → 80 bits and the table 64 → 80 KiB, exactly 25%. Widening the index is what moves capacity, and the index did not widen.

4 — "Priority marking configured means priority working."

The wrong model: the queues exist and the map is set, so traffic is prioritised.

What it costs: two opposite failures with identical symptoms. A flat map discards the distinction senders are expressing. An unmarked network gives the map nothing to work with. Both produce priority queues carrying indistinguishable traffic, and a throughput measurement cannot separate them.

The corrected model: priority_inert and priority_unused are the two bits, and they are fixed in different places. The default matters more than the configurability — a map initialised to all zeros makes the feature inert while looking configured, and a proportional default at least preserves the ordering.

5 — "Strict priority is what priority means."

The wrong model: higher priority goes first; that is the whole idea.

What it costs: a low-priority class that never transmits, indefinitely, with no timeout and no escape, whenever a higher class is continuously busy. That is not a defect — it is exactly what strict priority specifies — and a design that ships it as the default has shipped starvation as the default.

The corrected model: strict priority starves by construction, weighted disciplines do not, and starved_cycles is the only measurement that separates this traffic is slow from this traffic is never scheduled. Every other observable — throughput, latency — reads the same for both, and the two have completely different remedies.

6 — "Isolation verified means the VLANs are separate."

The wrong model: Chapter 13.1 §12's invariant is the proof of separation, and it is checkable, so separation is proven.

What it costs: Section 18's rejected property. The invariant reads vlan_members[frame_vidx][port], and frame_vidx is a subscript. With an aliased map, index 7's membership bitmap is the union of two VLANs' ports — so the check passes on every frame while a broadcast in one floods the other.

The corrected model: the invariant's subject is whatever frame_vidx denotes, and a many-to-one map between the identity and the subscript makes it denote something coarser than a VLAN. With an injective map it recovers its original meaning — which is why injectivity is asserted separately and included in the conformance conjunction.

22. Interview Reasoning

Q1 — "A switch supports 4094 VLAN IDs. Does it have 4094 of everything?"

Reason through it. No — and the gap between the two numbers is this chapter. It supports 4094 identifiers and far fewer simultaneously configured VLANs. Chapter 13.1 §10 showed dense arrays over the full range cost 60 KiB on a 24-port switch — 0.94× the entire forwarding table — so a translation table maps 4094 VIDs onto however many indices exist, typically 64 to 1024. The strong answer names both halves of the trade: the map is 4096 entries of 9 bits, 4.5 KiB, and it shrinks the arrays from 60 KiB to 3.8 — a 3.1× total saving. And it names the price: the map is many-to-one by construction, so two VIDs can share an index, and nothing downstream of it can tell.

Q2 — "Where does the VID go and where does the index go?"

Reason through it. The VID goes in the keyChapter 13.1 §7's {VID, MAC}, all twelve bits, because the same address in two VLANs is two different stations and the table must distinguish 4094 of them. The index goes in the arrays — membership, flood mask, port state — because that is what makes them affordable. The strong answer states what each confusion costs: a key built from the index makes two VLANs share every table entry, and arrays indexed by the VID restore the 60 KiB. It also notes the design habit: making them different types in the package, and taking the index as an input to the key builder and deliberately not using it, so the refusal is reviewable in a diff.

Q3 — "Recompute Chapter 12.5's capacity arithmetic with a 60-bit key. What changed?"

Reason through it. Nothing about capacity, and exactly 25% of the memory. Chapter 12.5 §7's overflow arithmetic is balls-in-bins over the set count, the associativity and the offered countthe key's width appears nowhere in it — so 5.27% of sets full at 50% occupancy and 80.5% effective capacity on a 4-way table are identical. What grew is what is stored and compared: entry 64 → 80 bits, table 64 → 80 KiB, four-way set read 256 → 320 bits, CAM cells 393 216 → 491 520 — all +25%. The strong answer draws the general rule: widening what you store is priced in area and nothing else; widening how you index is priced in behaviour — and this widened the store, not the index. It also names the one thing that touches timing: a 320-bit set row is a different memory compile on a four-cycle path with ten cycles of margin.

Q4 — "Two VIDs map to the same index. What breaks, and what still works?"

Reason through it. Everything indexed breaks and everything keyed survives. The membership bitmap, flood mask and port state are one set for both VLANs, so a broadcast in one floods the other's ports. The forwarding table is keyed on the full VID, so unicast lookups stay correct. The strong answer names the diagnostic signature: broadcasts cross between two VLANs and unicast traffic does not — a shape no other failure in this track produces, since Chapter 13.3 §16's native mismatch merges everything and a membership error merges nothing. And it names what does not fire: Chapter 13.1 §12's isolation monitor reads the index and reports clean, because the port genuinely is a member of index 7.

Q5 — "Why can a map collision not be detected at lookup time?"

Reason through it. Because the map stores nothing to compare against. The index is the answer, and there is no second field recording which VID produced it. Contrast Chapter 12.5 §6's hashed table, which stores the full 48-bit key beside every entry and compares it on every lookup — so two addresses hashing to one set remain two distinct entries and a lookup for one never returns the other. The strong answer draws the consequence: the only moment at which both VIDs are visible together is the configuration write, so the invariant must be enforced there — a reverse map of 256 entries × 13 bits, 416 octets, converting an undetectable runtime merge into a refused write. And it adds the check that needs no hardware at all: more bound VIDs than index slots is a pigeonhole argument, certain before a single frame is forwarded.

Q6 — "Your priority queues carry indistinguishable traffic. What are the two possibilities?"

Reason through it. Either nobody is marking, or the map is flattening what they marked — and both produce identical throughput measurements. priority_unused means Chapter 13.2's c_by_pcp histogram is entirely in bucket 0: the senders express nothing and the map is fine with nothing to do. priority_inert means the PCPs are spread and map_is_flatthe switch is discarding a distinction the senders are expressing, and the fix is the map. The strong answer adds the default that causes the second case: a map initialised to all zeros looks configured and is inert, while a proportional default pcp × N_QUEUES ÷ 8 at least preserves the ordering that survives compressing eight levels into four. And it names the third failure in the same area: strict priority scheduling starves lower classes by construction, and starved_cycles is the only measurement separating slow from never scheduled.

23. Understanding Check

24. What's Next

Module 13 is complete. Four chapters turned one physical switch into several logical ones: the requirement, the tag, the transformation and the datapath.

And the module ends where Chapter 12.1 §6 left an argument unfinished. That chapter established that discarding is a switch's specified response to congestion and rejected backpressure as a cure — on the grounds that it converts a local congestion into a global one, throttling conversations bound for idle ports along with the one that is busy.

Module 14 — Flow Control takes the argument up directly. Chapter 14.1 — Where Frames Are Lost finds the buffer that actually overflows first, which is not the one most people name. Chapter 14.2 — PAUSE Frames builds the mechanism 802.3x specified and examines its all-or-nothing semantics. And Chapter 14.3 — Backpressure and Head-of-Line Blocking shows that the pathology Chapter 12.1 predicted is exactly what happens, with a throughput bound that has been known since 1987.

This chapter's PCP field is how that mechanism is eventually made per-class rather than per-link — Section 9's map, Section 11's queues — and Chapter 14.4 is where the two meet.

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.