Skip to content
VLSI Mentor

Ethernet · Module 5

Unicast, Multicast and Broadcast

A group address is not looked up, it is tested — and the test is one-sided. Hardware can say definitely-not-subscribed cheaply and can never say definitely-subscribed, so the filter's job is to be cheap and to be wrong only in the direction that costs work rather than the direction that loses frames.

Chapter 5.3 showed that a receiver knows whether an address is individual or group one bit into the frame body, and that the two answers lead to different pipelines. Chapter 2.7 §4 built the filter that acts on that split, with five accept reasons and an exact-match table for group addresses — and said, explicitly, that production designs do not use an exact table because storing every subscribed group address does not scale.

This chapter delivers what that deferral promised, and the reason it matters is more interesting than the mechanism.

An individual address is matched by comparison. This station has one address, the frame carries one, and equality settles it.

A group address cannot be matched that way at any realistic scale. A station may subscribe to hundreds of groups; the address space of groups is enormous; and the comparison has to complete inside a frame time on every arriving frame. An exact-match structure large enough is not affordable in a receive path.

So the hardware does something else, and the something else has a property that shapes everything downstream: it can tell you an address is definitely not subscribed, and it can never tell you an address is definitely subscribed.

What does each address type actually cost a receiver, and what follows from a filter that is deliberately allowed to be wrong?

1. Scope — What This Chapter Owns, and How It Differs From 2.7

This chapter has the closest neighbour in the track, and the boundary is worth stating precisely before anything else.

Chapter 2.7 owns the forwarding view. Its §4 filter answers should this station accept this frame, and its §7 classifier answers where should a forwarding device send it — flood a group address, forward a known individual one, drop a bad one. Both are about dispositions, and both used exact matching because the chapter's subject was the decision rather than the lookup.

This chapter owns the matching itself, and the consequences of its being approximate. Specifically: why exact group matching does not scale, what a hash filter can and cannot tell you, why its error must be one-sided, how software completes what hardware starts, and what each address type costs the stations that are not its intended recipients.

It also does not own: the address's internal structure (Chapter 5.3), the collision-domain against broadcast-domain distinction (Chapter 2.7 §12), or group management protocols, which sit above this layer entirely.

The question this chapter answers that its neighbours do not: what does it cost to be a station that is not the intended recipient, and why is the mechanism that decides deliberately imprecise?

2. The Three Types, By Who Pays

The usual presentation is by how many recipients. Presenting it by who does work is more useful, because that is what a receive path is designed against.

TypeIntended recipientsStations that do workScales with
unicastoneonenothing
multicastthe subscriberssubscribers plus false positivessubscription overlap
broadcastallalldomain size

Read the third column against the fourth. Unicast is the only type whose cost is bounded by the sender's intent. Multicast's cost includes stations that were never meant to receive it — a consequence of Section 3's filter, and unavoidable. Broadcast's cost is borne by every station in the domain, and the sender has no control over how many that is.

3. Why Exact Group Matching Does Not Scale

Chapter 2.7 §4 used a small exact-match table and named its own limitation. Here is the limitation properly.

An exact-match filter must store every subscribed address. With 48-bit addresses and a station subscribed to k groups, that is k entries of 48 bits, each of which must be compared against the arriving address within a frame time.

The comparison is the binding constraint, not the storage. At minimum frame size and line rate, frames arrive continuously, and a 48-bit comparison against k stored values must complete before the next address begins. For small k that is a handful of comparators. For a station subscribed to hundreds of groups — routine on a system running several protocols that use group addressing — it is either hundreds of parallel comparators or a multi-cycle search that does not fit.

A content-addressable structure solves it and costs area and power out of proportion to the benefit, on a function that is discarding most of what it examines.

So the receive path does something cheaper and accepts being imprecise about it.

Group address matching in five stages. The address arrives and its group flag is known after one bit. A hash function maps the address to a bit position in a vector. If that bit is clear the frame is definitely unwanted and is dropped in hardware. If the bit is set the frame may be wanted, so it is delivered to software, which performs the exact comparison and discards any false positive.The two-stage match1Group flag knownone bit in — Chapter 5.32Hash the address48 bits to a vector index3Bit clear: dropdefinitely not subscribed4Bit set: deliverpossibly subscribed — not certain5Software comparesthe exact answer, on the residue
Figure 1 — a cheap one-sided test in hardware, an exact answer in software, on far less traffic.

Read stages 3 and 4 against each other. Stage 3 is a definite answer and it disposes of the overwhelming majority of group traffic at negligible cost. Stage 4 is not a definite answer, and everything that reaches it must be resolved somewhere else.

That asymmetry is not a weakness of hashing. It is the property that makes the design work, and Section 4 states why.

4. One-Sided Error Is the Design Principle

A filter that is allowed to be wrong must be wrong in a chosen direction, and the choice is forced.

A false positive delivers a frame nobody subscribed to. Software compares exactly, finds no match, discards it. Cost: wasted work. Nothing is lost.

A false negative would drop a frame the station did subscribe to. Cost: a lost frame, unrecoverable — no later stage sees it, nothing reports it as an error, and the sender has no idea. From above, the subscription simply does not work.

So the filter's contract is precise and asymmetric:

It may deliver frames that were not wanted. It must never drop a frame that was.

A hash filter satisfies this by construction, and that is the reason it is the right structure rather than merely a cheap one. If a subscribed address maps to bit b, then bit b is set — so any frame carrying that address finds the bit set and is delivered. No subscribed address can ever be rejected. Collisions cause the reverse error only, which is the survivable one.

5. RTL 1 — The Hash Filter, With Its Error Made Explicit

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Group-address filtering by hash.
//
// Chapter 2.7 §4 used an exact-match table and said production designs
// hash. This is that, and the naming is deliberate:
//
//   THE OUTPUT IS `maybe_subscribed`, NOT `subscribed`.
//
// A consumer that reads a signal called `subscribed` will treat it as
// exact. One that reads `maybe_subscribed` cannot. The name is doing real
// work -- it is the interface where the imprecision becomes visible, and
// Section 4 showed what goes wrong when it is not.
//
// THE ONE-SIDED GUARANTEE, and it holds by construction:
//   a subscribed address sets its bit when programmed, so a frame carrying
//   that address always finds the bit set. NO SUBSCRIBED ADDRESS CAN EVER
//   BE REJECTED. Collisions produce false positives only.
package group_filter_pkg;
  // Vector width. Larger means fewer collisions and more flops -- Section 6
  // does the arithmetic. 64 and 512 are both common.
  parameter int unsigned HASH_BITS = 64;
  parameter int unsigned HASH_W    = $clog2(HASH_BITS);
endpackage
 
module group_hash_filter
  import macaddr_pkg::*;
  import group_filter_pkg::*;
#(
  parameter int unsigned CNT_W = 24
) (
  input  logic        clk,
  input  logic        rst_n,
 
  // From Chapter 5.3's decomposer.
  input  logic        addr_valid,
  input  logic [47:0] address,
  input  logic        is_group,
  input  logic        is_broadcast,
 
  // The subscription vector, programmed by software (Section 6).
  input  logic [HASH_BITS-1:0] hash_vector,
  // Accept every group address regardless of the vector. A real mode, and
  // distinct from promiscuous -- Chapter 2.7 §4 kept those apart and so
  // does this.
  input  logic        accept_all_multicast,
 
  output logic        result_valid,
 
  // ── The three outcomes, deliberately not two ────────────────────────────
  // Broadcast is accepted unconditionally and BYPASSES the filter entirely.
  // Section 10 explains why it is not "multicast to everyone".
  output logic        accept_broadcast,
  // The honest name. Software must still compare exactly.
  output logic        maybe_subscribed,
  // A definite answer, and the one that disposes of most traffic.
  output logic        definitely_not_subscribed,
 
  output logic [HASH_W-1:0] hash_index,
 
  output logic [CNT_W-1:0] c_group_frames,
  output logic [CNT_W-1:0] c_hardware_rejected,
  output logic [CNT_W-1:0] c_delivered_maybe
);
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;   // saturating
  endfunction
 
  // The hash. A CRC over the address, of which some bits index the vector,
  // is the conventional choice -- it spreads structured addresses well,
  // and group addresses are HEAVILY structured (Section 8), so a naive
  // hash such as the low bits would collide catastrophically.
  //
  // This is a compact CRC-style reduction, not any specific standard's.
  function automatic logic [HASH_W-1:0] addr_hash(input logic [47:0] a);
    automatic logic [31:0] crc = 32'hFFFF_FFFF;
    for (int unsigned i = 0; i < 48; i++) begin
      automatic logic fb = crc[31] ^ a[i];
      crc = {crc[30:0], 1'b0};
      if (fb) crc = crc ^ 32'h04C1_1DB7;
    end
    addr_hash = crc[31 -: HASH_W];
  endfunction
 
  logic [HASH_W-1:0] idx_c;
  assign idx_c = addr_hash(address);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      result_valid              <= 1'b0;
      accept_broadcast          <= 1'b0;
      maybe_subscribed          <= 1'b0;
      definitely_not_subscribed <= 1'b0;
      hash_index                <= '0;
      c_group_frames            <= '0;
      c_hardware_rejected       <= '0;
      c_delivered_maybe         <= '0;
    end else begin
      result_valid              <= 1'b0;
      accept_broadcast          <= 1'b0;
      maybe_subscribed          <= 1'b0;
      definitely_not_subscribed <= 1'b0;
 
      if (addr_valid && is_group) begin
        result_valid   <= 1'b1;
        hash_index     <= idx_c;
        c_group_frames <= bump(c_group_frames, 1'b1);
 
        if (is_broadcast) begin
          // Unconditional, and it does not consult the vector at all.
          accept_broadcast <= 1'b1;
        end else if (accept_all_multicast) begin
          maybe_subscribed  <= 1'b1;
          c_delivered_maybe <= bump(c_delivered_maybe, 1'b1);
        end else if (hash_vector[idx_c]) begin
          // SOME subscribed address maps here. Possibly this one.
          maybe_subscribed  <= 1'b1;
          c_delivered_maybe <= bump(c_delivered_maybe, 1'b1);
        end else begin
          // NO subscribed address maps here. Definite, and cheap.
          definitely_not_subscribed <= 1'b1;
          c_hardware_rejected       <= bump(c_hardware_rejected, 1'b1);
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the output's name is part of the design. A signal called subscribed will be treated as exact by whoever consumes it, and every error in Section 4's list follows. maybe_subscribed cannot be misread, and it costs nothing.

It also teaches why the hash must be a CRC-style reduction rather than something simpler. Section 8 shows group addresses are heavily structured — many share long prefixes — so a hash taking the low bits, or any bits directly, would map large families of real addresses to the same index and collide catastrophically on exactly the traffic a deployment actually carries.

Deliberately simplified: the CRC polynomial and bit selection here are illustrative, not any specific standard's. What is faithful is the shape — a full-width reduction whose output bits are then indexed — and the one-sided guarantee, which holds for any hash.

Production implication: broadcast bypasses the filter entirely rather than being a vector entry. That is not an optimisation. A station is not permitted to decline broadcast, so making it depend on a programmable vector creates a configuration that can silently disable it — and Section 10 shows what that breaks.

Later ownership: how a station decides which groups to subscribe to is a protocol matter above this layer.

6. Sizing the Vector, and Why the Rate Must Be Measured

The false-positive rate follows from the vector width and the number of subscriptions, and the arithmetic is worth doing because it is the parameter's only justification.

With m bits in the vector and k subscribed addresses, assuming the hash spreads uniformly, the probability that a given bit is set is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
P(bit set) = 1 − (1 − 1/m)^k  ≈  1 − exp(−k/m)

An unsubscribed group address finds a set bit — a false positive — with that same probability. Worked for a 64-bit vector:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
m = 64,  k =  4  →  about 6%
m = 64,  k = 16  →  about 22%
m = 64,  k = 32  →  about 39%
m = 64,  k = 64  →  about 63%
 
m = 512, k = 16  →  about 3%
m = 512, k = 64  →  about 12%

Read the first block downward. A 64-bit vector holding 32 subscriptions delivers roughly two unsubscribed group frames in five to software. That is not a broken filter — it is a filter operating at a load its width does not suit, and the fix is a wider vector rather than a different mechanism.

7. RTL 2 — Subscription State, and Why Unsubscribing Is the Hard Half

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Subscription state and vector maintenance.
//
// SUBSCRIBING IS EASY: set the bit the address hashes to.
//
// UNSUBSCRIBING IS NOT, and this is the module's reason for existing.
// Several subscribed addresses can hash to the same bit. Clearing that bit
// when one of them unsubscribes SILENTLY UNSUBSCRIBES THE OTHERS -- and
// the failure is invisible, because the filter then correctly rejects
// frames the station still wanted. Section 4's unrecoverable direction.
//
// So the exact list must be kept and the vector REBUILT from it. The vector
// is a derived cache, never the source of truth.
module subscription_manager
  import macaddr_pkg::*;
  import group_filter_pkg::*;
#(
  parameter int unsigned MAX_GROUPS = 32,
  parameter int unsigned IDX_W      = $clog2(MAX_GROUPS),
  parameter int unsigned CNT_W      = 16
) (
  input  logic        clk,
  input  logic        rst_n,
 
  input  logic        subscribe_req,
  input  logic        unsubscribe_req,
  input  logic [47:0] group_address,
  output logic        req_done,
  output logic        req_rejected,     // the list is full
 
  // The derived vector, consumed by Section 5's filter.
  output logic [HASH_BITS-1:0] hash_vector,
  output logic                 vector_rebuilding,
 
  // The exact list, which software uses to complete the match (Section 8).
  output logic [47:0]      group_list  [MAX_GROUPS],
  output logic [MAX_GROUPS-1:0] group_valid,
 
  output logic [CNT_W-1:0] c_subscribed,
  output logic [CNT_W-1:0] c_unsubscribed,
  output logic [CNT_W-1:0] c_rebuilds,
 
  // Subscriptions currently held, and how full the vector is. Section 6
  // showed the false-positive rate follows from both, so a design that
  // cannot read them cannot know its own rate.
  output logic [IDX_W:0]        active_groups,
  output logic [$clog2(HASH_BITS+1)-1:0] vector_bits_set
);
 
  logic [47:0]           list_q [MAX_GROUPS];
  logic [MAX_GROUPS-1:0] valid_q;
  logic [HASH_BITS-1:0]  vector_q;
  logic [IDX_W:0]        rebuild_idx_q;
  logic                  rebuilding_q;
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  function automatic logic [HASH_W-1:0] addr_hash(input logic [47:0] a);
    automatic logic [31:0] crc = 32'hFFFF_FFFF;
    for (int unsigned i = 0; i < 48; i++) begin
      automatic logic fb = crc[31] ^ a[i];
      crc = {crc[30:0], 1'b0};
      if (fb) crc = crc ^ 32'h04C1_1DB7;
    end
    addr_hash = crc[31 -: HASH_W];
  endfunction
 
  logic                found_c;
  logic [IDX_W-1:0]    found_idx_c;
  logic                free_c;
  logic [IDX_W-1:0]    free_idx_c;
 
  always_comb begin
    found_c = 1'b0; found_idx_c = '0;
    free_c  = 1'b0; free_idx_c  = '0;
    for (int unsigned i = 0; i < MAX_GROUPS; i++) begin
      if (valid_q[i] && (list_q[i] == group_address)) begin
        found_c     = 1'b1;
        found_idx_c = (IDX_W)'(i);
      end
      if (!valid_q[i] && !free_c) begin
        free_c     = 1'b1;
        free_idx_c = (IDX_W)'(i);
      end
    end
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      for (int unsigned i = 0; i < MAX_GROUPS; i++) list_q[i] <= '0;
      valid_q        <= '0;
      vector_q       <= '0;
      rebuilding_q   <= 1'b0;
      rebuild_idx_q  <= '0;
      req_done       <= 1'b0;
      req_rejected   <= 1'b0;
      c_subscribed   <= '0;
      c_unsubscribed <= '0;
      c_rebuilds     <= '0;
    end else begin
      req_done     <= 1'b0;
      req_rejected <= 1'b0;
 
      if (rebuilding_q) begin
        // Rebuild the vector from the exact list, one entry per cycle.
        // DURING THE REBUILD THE VECTOR IS INCOMPLETE, which would drop
        // wanted frames -- so Section 5's filter must be told. That is what
        // vector_rebuilding is for, and Section 11 asserts it.
        if (rebuild_idx_q == (IDX_W+1)'(MAX_GROUPS)) begin
          rebuilding_q <= 1'b0;
          req_done     <= 1'b1;
        end else begin
          if (valid_q[rebuild_idx_q[IDX_W-1:0]])
            vector_q[addr_hash(list_q[rebuild_idx_q[IDX_W-1:0]])] <= 1'b1;
          rebuild_idx_q <= rebuild_idx_q + 1'b1;
        end
      end else if (subscribe_req) begin
        if (found_c) begin
          req_done <= 1'b1;              // already subscribed
        end else if (free_c) begin
          list_q[free_idx_c]  <= group_address;
          valid_q[free_idx_c] <= 1'b1;
          // Adding is safe to do incrementally: setting a bit can only
          // create false positives, never false negatives.
          vector_q[addr_hash(group_address)] <= 1'b1;
          c_subscribed <= bump(c_subscribed, 1'b1);
          req_done     <= 1'b1;
        end else begin
          req_rejected <= 1'b1;
        end
      end else if (unsubscribe_req && found_c) begin
        valid_q[found_idx_c] <= 1'b0;
        c_unsubscribed       <= bump(c_unsubscribed, 1'b1);
        // THE POINT OF THE MODULE. The bit cannot simply be cleared --
        // another subscribed address may hash to it. Clear the whole
        // vector and rebuild from the list that remains.
        vector_q      <= '0;
        rebuilding_q  <= 1'b1;
        rebuild_idx_q <= '0;
        c_rebuilds    <= bump(c_rebuilds, 1'b1);
      end
    end
  end
 
  assign hash_vector       = vector_q;
  assign vector_rebuilding = rebuilding_q;
  assign group_list        = list_q;
  assign group_valid       = valid_q;
 
  always_comb begin
    active_groups   = '0;
    vector_bits_set = '0;
    for (int unsigned i = 0; i < MAX_GROUPS; i++)
      if (valid_q[i]) active_groups = active_groups + 1'b1;
    for (int unsigned b = 0; b < HASH_BITS; b++)
      if (vector_q[b]) vector_bits_set = vector_bits_set + 1'b1;
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the vector is a derived cache and the list is the source of truth, and that this asymmetry is forced by the hash. Subscribing can update the vector incrementally, because setting a bit only ever creates false positives — the survivable direction. Unsubscribing cannot, because clearing a bit can create a false negative for another address that maps to it, and Section 4 established that direction is unrecoverable.

Deliberately simplified: a linear search over a small list, and a rebuild that walks every slot. A production design uses a set structure and may maintain per-bit reference counts, which avoids the full rebuild at the cost of storage.

Production implication: vector_rebuilding must be visible to the filter. During a rebuild the vector is incomplete, so a frame for a still-subscribed group can find its bit clear and be rejected — exactly the unrecoverable error the whole design is arranged to prevent. The correct behaviour during a rebuild is to accept all group addresses, trading a burst of false positives for zero losses, and Section 11 asserts it.

And vector_bits_set against active_groups is the self-diagnosis. Section 6's arithmetic needs both, so a design that cannot read them cannot know its own false-positive rate — it can only guess from a design-time formula the deployment may not match.

8. RTL 3 — Completing the Match in Software

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. The completion stage, and the measurement that matters.
//
// Hardware said "maybe". This resolves it against the exact list, and --
// more importantly -- COUNTS THE OUTCOME.
//
// Section 6 showed the design-time false-positive formula assumes uniform
// hashing, which real group addresses violate. So the true rate cannot be
// calculated; it must be measured HERE, where both the hardware verdict
// and the exact answer are available together.
module group_match_completion
  import macaddr_pkg::*;
  import group_filter_pkg::*;
#(
  parameter int unsigned MAX_GROUPS = 32,
  parameter int unsigned CNT_W      = 24
) (
  input  logic        clk,
  input  logic        rst_n,
  input  logic        clear,
 
  input  logic        maybe_subscribed,
  input  logic [47:0] address,
  input  logic [47:0] group_list [MAX_GROUPS],
  input  logic [MAX_GROUPS-1:0] group_valid,
 
  output logic        deliver,          // exactly matched: genuinely wanted
  output logic        false_positive,   // hardware said maybe, list says no
 
  output logic [CNT_W-1:0] c_true_positive,
  output logic [CNT_W-1:0] c_false_positive,
 
  // The measured rate, scaled by 256. THE number that says whether the
  // vector is wide enough for THIS deployment -- which Section 6's formula
  // cannot know, because it assumes a uniformity real addresses lack.
  output logic [15:0] false_positive_rate_x256,
 
  // Worst rate seen over any window since reset. Survives `clear`: a rate
  // that was fine at install and is not now means the subscription set or
  // the traffic mix has changed.
  output logic [15:0] worst_rate_x256
);
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  logic exact_hit_c;
  always_comb begin
    exact_hit_c = 1'b0;
    for (int unsigned i = 0; i < MAX_GROUPS; i++)
      if (group_valid[i] && (group_list[i] == address)) exact_hit_c = 1'b1;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      deliver          <= 1'b0;
      false_positive   <= 1'b0;
      c_true_positive  <= '0;
      c_false_positive <= '0;
      worst_rate_x256  <= '0;
    end else begin
      deliver        <= 1'b0;
      false_positive <= 1'b0;
 
      if (clear) begin
        c_true_positive  <= '0;
        c_false_positive <= '0;
        // worst_rate_x256 deliberately survives.
      end else if (maybe_subscribed) begin
        if (exact_hit_c) begin
          deliver         <= 1'b1;
          c_true_positive <= bump(c_true_positive, 1'b1);
        end else begin
          // Hardware was wrong, in the survivable direction. Not an error.
          false_positive   <= 1'b1;
          c_false_positive <= bump(c_false_positive, 1'b1);
        end
      end
 
      if (false_positive_rate_x256 > worst_rate_x256)
        worst_rate_x256 <= false_positive_rate_x256;
    end
  end
 
  always_comb begin
    automatic logic [CNT_W:0] total = c_true_positive + c_false_positive;
    false_positive_rate_x256 = (total == 0) ? 16'd0
                             : 16'((c_false_positive * 256) / total);
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the false-positive rate is measurable only here, because this is the only place where the hardware verdict and the exact answer exist together. Section 6's formula gives a floor computed under a uniformity assumption that real group addresses violate — so the measured rate is the only one that describes the deployment.

Deliberately simplified: a linear exact search. A real driver uses a hash set or a sorted structure, and the comparison may not be in hardware at all — what matters is the interface and the accounting.

Production implication: false_positive must not be counted as an error. It is the filter working as designed, and a design that alarms on it will have the alarm disabled — after which nobody notices when the rate climbs to the point where it is costing real throughput. Count it as a rate, alarm on the rate's change, never on its existence.

And worst_rate_x256 surviving a clear is what makes the trend visible. A rate that was three percent at installation and is now thirty means either the subscription set has grown past what the vector supports, or the traffic mix now contains a family that collides — and Section 13's method separates those.

9. RTL 4 — Per-Type Accounting, and the Broadcast Rate

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE INSTRUMENTATION.
//
// The three types cost differently and scale differently (Section 2), so
// the RATIO between them is the diagnostic, not any one count:
//
//   unicast dominant          -> normal. Cost bounded by senders' intent.
//   multicast dominant        -> check the false-positive rate: is this
//                                real subscription traffic or filter noise?
//   BROADCAST dominant        -> every station in the domain is paying, and
//                                the cost scales with domain size rather
//                                than with anything a sender controls.
//
// Broadcast gets its own rate measurement because it is the only type a
// station cannot decline, so it is the only one whose cost is entirely
// outside this station's control.
module address_type_accounting
  import macaddr_pkg::*;
#(
  parameter int unsigned CNT_W  = 32,
  parameter int unsigned WINDOW = 1_000_000,
  parameter int unsigned WIN_W  = $clog2(WINDOW + 1),
  // Broadcast frames per window above which the domain is considered to be
  // in a storm. Illustrative -- a real threshold comes from the deployment.
  parameter int unsigned STORM_THRESHOLD = 1000
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic frame_valid,
  input  logic is_group,
  input  logic is_broadcast,
  input  logic unicast_matched,      // this station's own address
  input  logic delivered,            // survived the exact match
  input  logic false_positive,
 
  output logic [CNT_W-1:0] c_unicast_mine,
  output logic [CNT_W-1:0] c_unicast_other,
  output logic [CNT_W-1:0] c_multicast_delivered,
  output logic [CNT_W-1:0] c_multicast_false,
  output logic [CNT_W-1:0] c_broadcast,
 
  // Broadcast frames in the window that just closed. A RATE, because
  // Chapter 2.7 §12 showed broadcast cost scales with domain size -- so an
  // absolute count says nothing without the interval.
  output logic [CNT_W-1:0] window_broadcast,
  output logic             window_valid,
  output logic             storm_suspected,
 
  // Highest broadcast rate ever seen. Survives `clear`: it describes the
  // domain this station sits in, not a measurement interval.
  output logic [CNT_W-1:0] worst_broadcast_rate
);
 
  logic [WIN_W-1:0]  win_q;
  logic [CNT_W-1:0]  bcast_win_q;
 
  function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
                                            input logic             en);
    bump = (en && !(&v)) ? (v + 1'b1) : v;
  endfunction
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      c_unicast_mine        <= '0;
      c_unicast_other       <= '0;
      c_multicast_delivered <= '0;
      c_multicast_false     <= '0;
      c_broadcast           <= '0;
      win_q                 <= '0;
      bcast_win_q           <= '0;
      window_broadcast      <= '0;
      window_valid          <= 1'b0;
      storm_suspected       <= 1'b0;
      worst_broadcast_rate  <= '0;
    end else begin
      window_valid <= 1'b0;
 
      if (clear) begin
        c_unicast_mine        <= '0;
        c_unicast_other       <= '0;
        c_multicast_delivered <= '0;
        c_multicast_false     <= '0;
        c_broadcast           <= '0;
        // worst_broadcast_rate deliberately survives.
      end else if (frame_valid) begin
        if (is_broadcast) begin
          c_broadcast <= bump(c_broadcast, 1'b1);
          bcast_win_q <= bump(bcast_win_q, 1'b1);
        end else if (is_group) begin
          c_multicast_delivered <= bump(c_multicast_delivered, delivered);
          c_multicast_false     <= bump(c_multicast_false,     false_positive);
        end else begin
          c_unicast_mine  <= bump(c_unicast_mine,  unicast_matched);
          c_unicast_other <= bump(c_unicast_other, !unicast_matched);
        end
      end
 
      if (win_q == WIN_W'(WINDOW - 1)) begin
        window_broadcast <= bcast_win_q;
        window_valid     <= 1'b1;
        storm_suspected  <= (bcast_win_q >= CNT_W'(STORM_THRESHOLD));
        if (bcast_win_q > worst_broadcast_rate) worst_broadcast_rate <= bcast_win_q;
        bcast_win_q <= '0;
        win_q       <= '0;
      end else begin
        win_q <= win_q + 1'b1;
      end
    end
  end
 
endmodule

Classification: synthesizable instrumentation.

What it teaches: that broadcast needs a rate where the other types need counts. Chapter 2.7 §12 showed broadcast cost scales with domain size and that switching does nothing about it — so an absolute broadcast count tells a station nothing, while a rate tells it whether the domain it sits in has become expensive to be part of.

Deliberately simplified: a fixed storm threshold. A real one is derived from the deployment's normal rate, which is what worst_broadcast_rate trended over time supplies.

Production implication: separating c_multicast_delivered from c_multicast_false is what makes multicast load interpretable. Multicast traffic rising is not one condition — it may be genuine subscription traffic, which is the system working, or filter false positives, which is the vector being too narrow for the subscription count. Section 8's rate distinguishes them, and a single multicast counter cannot.

And c_unicast_other is worth keeping even though those frames are discarded at the first octet. It is the denominator for everything else: a station seeing overwhelmingly other-unicast traffic is on a busy shared segment, and one seeing almost none is on a switched port where the fabric is already filtering for it.

10. Broadcast Is Not Multicast to Everyone

The three types are often presented as a progression — one, some, all — and the progression is misleading in a way that matters for design.

Broadcast is not the limiting case of multicast. It is structurally different in three ways:

MulticastBroadcast
may a station decline?yes — subscription is voluntaryno
does the filter apply?yes — it is the mechanismno — bypassed entirely
who decides who receives it?each receiver, by subscribingthe sender, alone

Read the last row. Multicast is a receiver-controlled mechanism: a sender addresses a group and the receivers decide whether they are in it. Broadcast is sender-controlled: the sender imposes the cost on every station in the domain, and no receiver has any say.

That is why Section 5's filter bypasses broadcast rather than treating it as a vector entry. Making it a vector entry would create a configuration in which a station can silently stop accepting broadcast — and since broadcast carries the discovery and resolution traffic that lets a station find anything at all, such a station appears to be on the network and cannot communicate with anything it has not already learned about.

11. The Subscription Lifecycle, Drawn

Section 7's subscription_manager has a shape worth seeing, because the asymmetry between joining and leaving is where the bugs are.

A four-state subscription machine. From STABLE, a subscribe request writes one vector bit and returns immediately to STABLE. An unsubscribe request removes the entry from the list and enters REBUILD, which clears the whole vector and re-walks every remaining entry before returning to STABLE. While in REBUILD the filter is deliberately held permissive rather than being allowed to reject, because a cleared vector would drop wanted frames.STABLESET BITREBUILDRE-WALKsubscribesubscribeone write, doneone write, doneunsubscribeunsubscribeclear vectorclear vectorevery remaining entryevery remaining entry
Figure 2 — joining is one write; leaving is a rebuild, because a set bit records no evidence of who set it.

The two paths out of STABLE differ by an order of magnitude in cost, and the reason is a property of the vector itself: a set bit records that at least one address mapped there, and nothing about which one. Setting is therefore idempotent and cheap. Clearing is not decidable locally — a bit that the departing address maps to may also be the bit some remaining address maps to, and the vector cannot tell you.

So the only correct removal is to rebuild from the list, which is why the list is the source of truth and the vector is a cache.

And read the REBUILD state's own note. While the vector is being rebuilt it is momentarily incomplete, and an incomplete vector rejects frames it should accept — the one error the design forbids. So the filter is held permissive across the rebuild: it accepts more than it should for a few microseconds, which costs software a burst of exact comparisons and loses nothing. Choosing the survivable error again, under a transient this time.

12. RTL 5 — Proving the Error Stays One-Sided

Everything above rests on one claim: the filter never rejects a subscribed address. That claim is a property of the vector being consistent with the list, and it can be checked continuously in hardware.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE DIAGNOSTIC.
//
// The chapter's entire design rests on ONE property:
//
//     every subscribed address hashes to a bit that is SET
//
// A false positive is a cost. A FALSE NEGATIVE is a lost frame, and no
// later stage can recover it -- so this is the only fatal failure mode in
// the receive filter, and it is invisible in traffic: the frame simply
// never arrives, and the station has no record that it existed.
//
// This module walks the subscription list in the background and re-checks
// each entry against the live vector. It is a CONSISTENCY check, not a
// traffic check, which is why it can find the fault before any frame is
// lost rather than after.
module filter_false_negative_monitor
  import macaddr_pkg::*;
#(
  parameter int unsigned MAX_GROUPS = 256,
  parameter int unsigned IDX_W      = $clog2(MAX_GROUPS),
  parameter int unsigned VEC_W      = 512,
  parameter int unsigned HASH_W     = $clog2(VEC_W),
  // Cycles between successive entry checks. The walk is deliberately slow:
  // it must not compete with the receive path for the vector's read port.
  parameter int unsigned PACE       = 64,
  parameter int unsigned PACE_W     = $clog2(PACE + 1)
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  // Subscription list (source of truth).
  input  logic [MAX_GROUPS-1:0]  entry_occupied,
  input  mac_addr_t              entry_addr [MAX_GROUPS],
 
  // Live vector (the derived cache).
  input  logic [VEC_W-1:0]       vector,
 
  // Held low while the vector is being rebuilt: an incomplete vector is
  // EXPECTED to be inconsistent, so checking it would report a fault that
  // is not one.
  input  logic                   vector_stable,
 
  output logic                   inconsistent,       // sticky
  output logic [IDX_W-1:0]       first_bad_entry,    // first cause
  output mac_addr_t              first_bad_addr,
  output logic [HASH_W-1:0]      first_bad_index,
  output logic [31:0]            sweeps_completed
);
 
  logic [IDX_W-1:0] cursor_q;
  logic [PACE_W-1:0] pace_q;
 
  // Same hash the filter uses. Sharing the function is deliberate: a
  // monitor with its own copy of the hash would silently stop checking the
  // thing that matters the moment the two drifted apart.
  function automatic logic [HASH_W-1:0] hash_addr(input mac_addr_t a);
    logic [HASH_W-1:0] h;
    h = '0;
    for (int unsigned i = 0; i < 48; i++)
      h[i % HASH_W] = h[i % HASH_W] ^ a[i];
    hash_addr = h;
  endfunction
 
  wire logic [HASH_W-1:0] cur_index = hash_addr(entry_addr[cursor_q]);
  wire logic              cur_ok    = vector[cur_index];
  wire logic              cur_check = vector_stable && entry_occupied[cursor_q];
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      cursor_q         <= '0;
      pace_q           <= '0;
      inconsistent     <= 1'b0;
      first_bad_entry  <= '0;
      first_bad_addr   <= '0;
      first_bad_index  <= '0;
      sweeps_completed <= '0;
    end else if (clear) begin
      // sweeps_completed is cleared; `inconsistent` is NOT. A filter that
      // was ever inconsistent has already lost frames, and clearing a
      // counter must not erase that.
      sweeps_completed <= '0;
    end else begin
      if (pace_q != PACE_W'(PACE - 1)) begin
        pace_q <= pace_q + 1'b1;
      end else begin
        pace_q <= '0;
 
        if (cur_check && !cur_ok && !inconsistent) begin
          inconsistent    <= 1'b1;
          first_bad_entry <= cursor_q;
          first_bad_addr  <= entry_addr[cursor_q];
          first_bad_index <= cur_index;
        end
 
        if (cursor_q == IDX_W'(MAX_GROUPS - 1)) begin
          cursor_q         <= '0;
          sweeps_completed <= sweeps_completed + 1'b1;
        end else begin
          cursor_q <= cursor_q + 1'b1;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable diagnostic.

What it teaches: that a one-sided guarantee has to be monitored on the side that is guaranteed. False positives are self-announcing — Section 8 counts them and the rate is visible. A false negative announces nothing: the frame does not arrive, the station never knew it was sent, and the only symptom is a protocol above complaining that a peer stopped responding. The failure with no signature is the one that needs a dedicated checker.

Deliberately simplified: a linear background walk. Real designs also re-check on demand immediately after a rebuild, which is where an inconsistency is most likely to be introduced.

Production implication: vector_stable is the subtle input. Without it, every rebuild reports a fault, the monitor cries wolf, and someone disables it — after which the real fault is invisible. A diagnostic that fires during known-transient states trains its readers to ignore it, which is worse than not having it, and the same argument Chapter 4.5 made about not trusting a management read while the bus is turning around.

13. Assertions — Checking a Guarantee That Is Deliberately One-Sided

The properties below are all written from the same starting point: the filter is allowed to be wrong, and the assertions must say in which direction. An assertion that forbids all error would forbid the design.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------
// P1 -- THE ONLY FATAL PROPERTY.
// A subscribed address must never be rejected. Everything else in this
// chapter is a cost; this is a lost frame.
// ---------------------------------------------------------------------
property p_no_false_negative;
  @(posedge clk) disable iff (!rst_n)
    (frame_valid && is_group && !is_broadcast && addr_is_subscribed && vector_stable)
      |-> maybe_subscribed;
endproperty
a_no_false_negative: assert property (p_no_false_negative)
  else $error("FATAL: subscribed group address rejected by the filter");
 
// ---------------------------------------------------------------------
// P2 -- Broadcast bypasses the filter entirely (Section 10). A station
// must not be able to configure itself into not receiving broadcast.
// ---------------------------------------------------------------------
property p_broadcast_always_accepted;
  @(posedge clk) disable iff (!rst_n)
    (frame_valid && is_broadcast) |-> accept;
endproperty
a_broadcast_always_accepted: assert property (p_broadcast_always_accepted);
 
// ---------------------------------------------------------------------
// P3 -- A clear bit is a DEFINITE answer, so it must dispose of the frame
// in hardware. If a cleared bit still reached software, the cheap stage
// would not be saving anything.
// ---------------------------------------------------------------------
property p_clear_bit_drops_in_hardware;
  @(posedge clk) disable iff (!rst_n)
    (frame_valid && is_group && !is_broadcast && !maybe_subscribed)
      |-> !deliver_to_software;
endproperty
a_clear_bit_drops_in_hardware: assert property (p_clear_bit_drops_in_hardware);
 
// ---------------------------------------------------------------------
// P4 -- Subscribing takes effect immediately: one write, and the bit is
// set on the next cycle (Section 11's short path out of STABLE).
// ---------------------------------------------------------------------
property p_subscribe_sets_bit;
  @(posedge clk) disable iff (!rst_n)
    (subscribe_valid && !list_full)
      |=> vector[$past(hash_of_request)];
endproperty
a_subscribe_sets_bit: assert property (p_subscribe_sets_bit);
 
// ---------------------------------------------------------------------
// P5 -- Unsubscribing does NOT take effect immediately, and the assertion
// says so rather than pretending otherwise: the vector is unstable until
// the rebuild completes.
// ---------------------------------------------------------------------
property p_unsubscribe_enters_rebuild;
  @(posedge clk) disable iff (!rst_n)
    unsubscribe_valid |=> !vector_stable;
endproperty
a_unsubscribe_enters_rebuild: assert property (p_unsubscribe_enters_rebuild);
 
// ---------------------------------------------------------------------
// P6 -- The rebuild must be PERMISSIVE, not restrictive (Section 11). An
// unstable vector may over-accept; it must never under-accept.
// ---------------------------------------------------------------------
property p_rebuild_is_permissive;
  @(posedge clk) disable iff (!rst_n)
    (!vector_stable && frame_valid && is_group) |-> deliver_to_software;
endproperty
a_rebuild_is_permissive: assert property (p_rebuild_is_permissive);
 
// ---------------------------------------------------------------------
// P7 -- The rebuild terminates. An unbounded rebuild leaves the filter
// permanently permissive, which is not a lost frame but is a permanent
// software cost that looks like a traffic problem.
// ---------------------------------------------------------------------
property p_rebuild_terminates;
  @(posedge clk) disable iff (!rst_n)
    $fell(vector_stable) |-> ##[1:REBUILD_BOUND] vector_stable;
endproperty
a_rebuild_terminates: assert property (p_rebuild_terminates);
 
// ---------------------------------------------------------------------
// P8 -- A false positive is by definition a frame the filter admitted and
// the exact comparison rejected. It cannot be reported on a frame that
// was never admitted.
// ---------------------------------------------------------------------
property p_false_positive_implies_admitted;
  @(posedge clk) disable iff (!rst_n)
    false_positive |-> (maybe_subscribed && !exact_match);
endproperty
a_false_positive_implies_admitted: assert property (p_false_positive_implies_admitted);
 
// ---------------------------------------------------------------------
// P9 -- Delivered and false-positive are mutually exclusive on one frame.
// Section 9's two counters are only interpretable if they partition.
// ---------------------------------------------------------------------
property p_delivered_xor_false;
  @(posedge clk) disable iff (!rst_n)
    (frame_valid && is_group && !is_broadcast && maybe_subscribed)
      |-> (delivered ^ false_positive);
endproperty
a_delivered_xor_false: assert property (p_delivered_xor_false);
 
// ---------------------------------------------------------------------
// P10 -- The I/G bit alone decides which pipeline runs (Chapter 5.3).
// A group frame must never be resolved by the unicast comparator.
// ---------------------------------------------------------------------
property p_group_never_uses_unicast_path;
  @(posedge clk) disable iff (!rst_n)
    (frame_valid && is_group) |-> !unicast_matched;
endproperty
a_group_never_uses_unicast_path: assert property (p_group_never_uses_unicast_path);
 
// ---------------------------------------------------------------------
// P11 -- Broadcast is a group address, so anything that classifies it as
// individual has mis-decoded the first bit.
// ---------------------------------------------------------------------
property p_broadcast_is_group;
  @(posedge clk) disable iff (!rst_n)
    is_broadcast |-> is_group;
endproperty
a_broadcast_is_group: assert property (p_broadcast_is_group);
 
// ---------------------------------------------------------------------
// P12 -- The false-negative monitor is sticky. A filter that was ever
// inconsistent has already lost frames, and `clear` must not erase it.
// ---------------------------------------------------------------------
property p_inconsistent_is_sticky;
  @(posedge clk) disable iff (!rst_n)
    inconsistent |=> inconsistent;
endproperty
a_inconsistent_is_sticky: assert property (p_inconsistent_is_sticky);
 
// ---------------------------------------------------------------------
// P13 -- First cause is captured once. The address that broke the
// guarantee is the one worth having, not the tenth one found.
// ---------------------------------------------------------------------
property p_first_bad_addr_stable;
  @(posedge clk) disable iff (!rst_n)
    inconsistent |=> $stable(first_bad_addr);
endproperty
a_first_bad_addr_stable: assert property (p_first_bad_addr_stable);
 
// ---------------------------------------------------------------------
// P14 -- The monitor must not report a fault while the vector is being
// rebuilt (Section 12). A diagnostic that fires during a known transient
// gets disabled, and then the real fault is invisible.
// ---------------------------------------------------------------------
property p_monitor_quiet_during_rebuild;
  @(posedge clk) disable iff (!rst_n)
    (!vector_stable && !$past(inconsistent)) |-> !inconsistent;
endproperty
a_monitor_quiet_during_rebuild: assert property (p_monitor_quiet_during_rebuild);
 
// ---------------------------------------------------------------------
// P15 -- Broadcast rate is measured per window (Section 9), so the
// reported value must never exceed the window length.
// ---------------------------------------------------------------------
property p_broadcast_rate_bounded;
  @(posedge clk) disable iff (!rst_n)
    window_valid |-> (window_broadcast <= WINDOW);
endproperty
a_broadcast_rate_bounded: assert property (p_broadcast_rate_bounded);
 
// ---------------------------------------------------------------------
// P16 -- The worst rate survives `clear`, because it describes the
// DOMAIN this station sits in, not a measurement interval.
// ---------------------------------------------------------------------
property p_worst_rate_monotonic;
  @(posedge clk) disable iff (!rst_n)
    ##1 (worst_broadcast_rate >= $past(worst_broadcast_rate));
endproperty
a_worst_rate_monotonic: assert property (p_worst_rate_monotonic);
 
// ---------------------------------------------------------------------
// P17 -- COVERAGE, not assertion. A verification run that never produced
// a false positive never exercised the path this chapter is about.
// ---------------------------------------------------------------------
c_false_positive_seen: cover property (
  @(posedge clk) disable iff (!rst_n) false_positive
);
 
// ---------------------------------------------------------------------
// P18 -- COVERAGE. A frame arriving DURING a rebuild is the interesting
// corner and random stimulus reaches it rarely.
// ---------------------------------------------------------------------
c_frame_during_rebuild: cover property (
  @(posedge clk) disable iff (!rst_n) (frame_valid && is_group && !vector_stable)
);

14. Verification — Twenty Scenarios and One Test Random Stimulus Will Not Produce

A filter that is allowed to be wrong needs a verification plan organised around which wrongness, not around whether any occurred.

#ScenarioStimulusWhat must be observed
1Own unicast addressexact match on all six octetsaccepted; c_unicast_mine increments
2Other unicast addressdiffers in the first octet onlyrejected at the first octet; c_unicast_other increments
3Other unicast, differs in last octetfive octets matchrejected — the comparison must not conclude early on a partial match
4Subscribed group addressexact list entrymaybe_subscribed high; delivered high; false_positive low
5Unsubscribed, hash bit clearaddress chosen to map to a clear bitdropped in hardware; software never sees it (P3)
6Unsubscribed, hash bit setaddress engineered to collide with a subscriptionmaybe_subscribed high, delivered low, false_positive high
7Broadcastall octets FFaccepted regardless of filter state (P2); c_broadcast increments
8Broadcast with the vector entirely clearno subscriptions at allstill accepted — broadcast bypasses the filter
9Subscribe then immediate frameframe in the cycle after the writeaccepted (P4) — no window in which a just-joined group is missed
10Unsubscribe then immediate frameframe during the rebuilddelivered to software (P6) — permissive, never restrictive
11Unsubscribe of a colliding pairtwo subscriptions on one bit, remove onethe bit stays set; the survivor is still accepted
12Unsubscribe of the last user of a bitremove the only address mapping thereafter rebuild, the bit is clear
13Rebuild boundmaximum-length subscription listvector_stable returns within REBUILD_BOUND (P7)
14Subscription list fullone more join than capacityreported as full; no silent drop of the request
15False-positive rate at design loadsubscription count at the sizing targetmeasured rate within the budget of Section 6
16False-positive rate above design loadsubscriptions well past the targetrate rises smoothly — degradation, not a cliff
17Deliberate vector corruptionforce one bit clear under a subscribed addressinconsistent asserts; first cause names that address (P12, P13)
18Corruption during rebuildsame corruption while vector_stable is lowmonitor stays quiet (P14) — no false alarm
19Broadcast burstbroadcast rate above the storm thresholdstorm_suspected asserts on the window boundary; worst_broadcast_rate updates
20Counter clear under stormassert clear mid-burstper-type counters zero; worst_broadcast_rate and inconsistent survive (P16)

15. Debugging — Reading the Three Symptoms Apart

Three symptoms of high receive load lead to three different diagnoses. Rising multicast load splits into two causes: genuine subscription traffic, indicated by the delivered counter rising, and filter noise, indicated by the false-positive counter rising. A missing frame with no counter movement indicates a false negative in the filter, found by the consistency monitor rather than by traffic. Rising load across all stations at once indicates broadcast, found by the windowed rate.Multicast load upsoftware busier thanexpecteddelivered risingreal subscription trafficfalse risingvector too narrowA frame never arrivedno counter moved at allConsistency monitorfound without trafficEvery station busiernot just this oneWindowed ratecost of the domain, not thelinkWhich counter moved?the question that separatesthem12
Figure 3 — three receive-path complaints that look alike from software, and the counter that separates them.

Symptom — the driver reports far more multicast frames than the application subscribed to.

Two causes, and they call for opposite responses. Read c_multicast_delivered against c_multicast_false from Section 9. If delivered dominates, the station really is subscribed to noisy groups and the fix is above this layer — leave a group. If false dominates, the vector is undersized for the current subscription count, and Section 6's arithmetic says how much wider it needs to be. A single "multicast frames" counter cannot tell you which, which is the entire reason Section 9 splits it.

Symptom — one specific peer's frames never arrive, and no error counter moves.

This is the signature of a false negative, and its defining feature is the absence of evidence. Nothing was dropped for a reason, so nothing was counted; the frame was never admitted, so no exact comparison ran; the protocol above simply times out. Check inconsistent from Section 12 first — if it is set, first_bad_addr names the subscription whose bit went missing and the investigation is over in one read. If it is clear, the frame is being lost somewhere other than the filter, and the filter has been eliminated as a suspect without needing to reproduce the failure.

Symptom — receive load rose on every station at once, including idle ones.

Only broadcast does that, because it is the only type no station can decline. window_broadcast and storm_suspected from Section 9 confirm it, and worst_broadcast_rate says whether this is new. Note what the diagnosis is not: it is not a fault in this station, this link, or this NIC. It is a property of the broadcast domain, and the response is at the domain level — which is why Chapter 2.7 §12 was careful that switching does not reduce broadcast domains.

Symptom — the false-positive rate is high immediately after boot and settles later.

Expected, and worth recognising so it is not chased. A station typically joins several groups during initialisation, and each join is a bit set. The rate is measured against the vector's set-bit density, which climbs as the subscriptions arrive. Compare against the rate at steady state, not against zero — a rate of zero during subscription setup would mean the counter is not running.

Symptom — inconsistent sets exactly once, shortly after a subscription change, and never again.

Almost certainly vector_stable is being deasserted a cycle too late, so the monitor sampled a vector mid-rebuild. This is P14 failing, not the filter failing — and the distinction matters because the two have opposite fixes. Confirm by correlating the timestamp against the rebuild window: an inconsistency inside a rebuild is a qualification bug; one in steady state is a genuine lost-frame fault.

16. Common Misconceptions

"The receiver looks up group addresses."

The wrong model: the same operation as a unicast match, against a bigger table.

What it costs: you cannot explain why software has to compare the address again after hardware already accepted the frame, and the second comparison looks like redundant work to be optimised away. Removing it turns every collision into a delivered frame the station did not subscribe to.

The corrected model: hardware tests, it does not look up. The test is one-sided — definite when it says no, provisional when it says yes — so the software comparison is not a duplicate of the hardware's work but the only stage that produces a definite yes.

"A false positive is a bug."

The wrong model: the filter admitting an unsubscribed frame is a defect to be fixed.

What it costs: Section 13's rejected property, and then one of its two bad fixes — a vector enlarged until the test stimulus stops producing collisions, or an exact comparator moved into hardware that reintroduces the cost the design existed to avoid.

The corrected model: the false positive is what the design is buying with. Admitting it is what makes a fixed-size vector able to stand in for an unbounded subscription list. It is a budget with a threshold, measured by Section 8 and sized by Section 6 — not a correctness property.

"Broadcast is just multicast to everyone."

The wrong model: one point on a continuum from one recipient to all of them.

What it costs: you design the filter with broadcast as a vector entry, which creates a configuration in which a station stops accepting broadcast — and a station that receives no broadcast cannot participate in discovery or resolution, so it appears healthy and can reach nothing it has not already learned.

The corrected model: multicast is receiver-controlled and broadcast is sender-controlled. A station may decline a group; it may not decline broadcast. Section 10's table is the distinction, and it is why broadcast bypasses the filter rather than sitting inside it.

"Unsubscribing is the reverse of subscribing."

The wrong model: joining sets a bit, so leaving clears it.

What it costs: clearing the bit removes every address that maps there, including subscriptions the station still holds. The result is a false negative — the one error that loses frames — and it is silent, because the lost frames were never counted.

The corrected model: a set bit records that at least one address mapped there and nothing about which. Removal is therefore not decidable from the vector, and the only correct implementation rebuilds from the list — which is why the list is the source of truth and Section 11's two paths out of STABLE differ by an order of magnitude.

"Traffic this station discards is free."

The wrong model: frames rejected by the filter cost nothing worth accounting for.

What it costs: you lose the denominator. A station with high multicast delivery and one with high multicast false-positives look identical without the split, and a station on a broadcast-heavy domain looks like a station with a slow driver.

The corrected model: discarded traffic is cheap, not free, and its composition is diagnostic. Section 9 counts other-unicast precisely because it is the baseline against which the other types are read — and broadcast gets a rate rather than a count, because its cost scales with the domain rather than with anything this station or any sender controls.

17. Interview Reasoning

"How does a NIC filter multicast addresses?"

The weak answer is "it matches them against a list". The answer that ends the topic starts with why a list does not work — a station may hold hundreds of subscriptions and the comparison has to finish inside a frame time on every frame — then names the mechanism: a hash into a bit vector, whose answer is definite only when it says no. The payoff is the direction of the error: false positives cost software work, false negatives lose frames, and the design is arranged so only the first can happen.

"Why does software re-check an address hardware already accepted?"

The trap is to treat it as redundancy. It is not: hardware never produced a yes, only a not-no. Adding that the arrangement is deliberate — cheap and approximate first, exact on a small residue — and that the false-positive rate is a measurable quantity with a budget derived from vector width and subscription count, shows the mechanism is understood as an engineering trade rather than a curiosity.

"You are told to add an assertion that the multicast filter never accepts an unsubscribed address. What do you say?"

That it fails on correct hardware and would be made to pass by damaging the design. The property forbids the collision that pays for the filter's affordability. Then the constructive half: assert the direction that must never happen — a subscribed address rejected — and measure the direction that may, against a budget. Naming the two bad fixes people reach for, a widened vector that hides the property behind unrepresentative stimulus and an exact comparator in hardware that undoes the whole design, is what makes the answer complete.

"A peer's frames stopped arriving and no counter moved. Where do you look?"

The instinct is to trace traffic, and traffic is exactly what has no evidence to offer — nothing was dropped for a reason, so nothing was counted. The strong answer names the failure class first: this is the signature of a false negative, the one failure mode with no traffic signature, which is why it needs a consistency checker rather than a traffic counter. Then the mechanism: a background walk of the subscription list against the live vector, qualified so it stays quiet during a rebuild, capturing first cause.

18. Understanding Check

Because the vector stores a consequence of the addresses, not the addresses.

Every subscribed address sets the bit its hash selects. So if a bit is clear, no subscribed address maps there, and an arriving address that hashes there is certainly not one of them. That direction is a proof.

The other direction is not, because the hash is not injective. Folding 48 bits down to a vector index maps many addresses onto each bit. A set bit says some subscribed address maps here — it cannot say which, and it cannot distinguish "this one" from "a different one that collides".

The asymmetry is structural, not a quality-of-hash problem. A better hash spreads addresses more evenly and lowers the collision rate; no hash into a vector smaller than the address space can eliminate collisions, because there are more addresses than bits.

Which is why the exact comparison exists somewhere — and why it can afford to be in software: it runs only on the residue the cheap stage admitted, not on every frame.

19. What's Next

The claim this chapter defended: a group address is not looked up, it is tested — and the test is one-sided by design, not by accident.

Hardware can say definitely not subscribed cheaply, because a clear bit proves no subscribed address maps there. It can never say definitely subscribed, because folding 48 bits into a vector index maps many addresses onto each bit. So the false positive is not a defect being tolerated; it is what the design buys its affordability with, and the only property that must hold absolutely is the other direction — a subscribed address must never be rejected, because that error loses a frame and leaves no evidence it existed.

Everything else followed from that asymmetry: a rebuild held permissive rather than restrictive, a consistency monitor rather than a traffic counter to catch the silent failure, a false-positive rate with a budget instead of an assertion, and broadcast structurally outside the filter because it is the one type a station has no decision to make about.

Chapter 5.5 — EtherType and Length returns to the frame walk with the field the last three chapters have deferred. Chapter 5.1 §6 placed it two octets after the addresses and noted the difficulty: the same two octets carry either a payload length or a protocol identifier, and the parser must decide which without being told.

That is a genuinely different kind of problem from this chapter's. Here the mechanism was allowed to be wrong in a bounded direction. There it may not be wrong at all — a frame parsed under the wrong interpretation is misdelivered rather than dropped, and 5.5 shows why the ambiguity is nonetheless resolvable deterministically, what the boundary value is, and what happens to the field's position once tags are inserted ahead of it.

The full path is on the Ethernet curriculum index.

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.