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.
| Type | Intended recipients | Stations that do work | Scales with |
|---|---|---|---|
| unicast | one | one | nothing |
| multicast | the subscribers | subscribers plus false positives | subscription overlap |
| broadcast | all | all | domain 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.
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
// 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
endmoduleClassification: 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:
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:
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
// 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
endmoduleClassification: 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
// 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
endmoduleClassification: 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
// 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
endmoduleClassification: 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:
| Multicast | Broadcast | |
|---|---|---|
| may a station decline? | yes — subscription is voluntary | no |
| does the filter apply? | yes — it is the mechanism | no — bypassed entirely |
| who decides who receives it? | each receiver, by subscribing | the 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.
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.
// 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
endmoduleClassification: 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.
// ---------------------------------------------------------------------
// 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.
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Own unicast address | exact match on all six octets | accepted; c_unicast_mine increments |
| 2 | Other unicast address | differs in the first octet only | rejected at the first octet; c_unicast_other increments |
| 3 | Other unicast, differs in last octet | five octets match | rejected — the comparison must not conclude early on a partial match |
| 4 | Subscribed group address | exact list entry | maybe_subscribed high; delivered high; false_positive low |
| 5 | Unsubscribed, hash bit clear | address chosen to map to a clear bit | dropped in hardware; software never sees it (P3) |
| 6 | Unsubscribed, hash bit set | address engineered to collide with a subscription | maybe_subscribed high, delivered low, false_positive high |
| 7 | Broadcast | all octets FF | accepted regardless of filter state (P2); c_broadcast increments |
| 8 | Broadcast with the vector entirely clear | no subscriptions at all | still accepted — broadcast bypasses the filter |
| 9 | Subscribe then immediate frame | frame in the cycle after the write | accepted (P4) — no window in which a just-joined group is missed |
| 10 | Unsubscribe then immediate frame | frame during the rebuild | delivered to software (P6) — permissive, never restrictive |
| 11 | Unsubscribe of a colliding pair | two subscriptions on one bit, remove one | the bit stays set; the survivor is still accepted |
| 12 | Unsubscribe of the last user of a bit | remove the only address mapping there | after rebuild, the bit is clear |
| 13 | Rebuild bound | maximum-length subscription list | vector_stable returns within REBUILD_BOUND (P7) |
| 14 | Subscription list full | one more join than capacity | reported as full; no silent drop of the request |
| 15 | False-positive rate at design load | subscription count at the sizing target | measured rate within the budget of Section 6 |
| 16 | False-positive rate above design load | subscriptions well past the target | rate rises smoothly — degradation, not a cliff |
| 17 | Deliberate vector corruption | force one bit clear under a subscribed address | inconsistent asserts; first cause names that address (P12, P13) |
| 18 | Corruption during rebuild | same corruption while vector_stable is low | monitor stays quiet (P14) — no false alarm |
| 19 | Broadcast burst | broadcast rate above the storm threshold | storm_suspected asserts on the window boundary; worst_broadcast_rate updates |
| 20 | Counter clear under storm | assert clear mid-burst | per-type counters zero; worst_broadcast_rate and inconsistent survive (P16) |
15. Debugging — Reading the Three Symptoms Apart
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
Related tutorials
- Related topic
The MAC Layer
Framing, addressing, error detection, sizing, interframe gap and transmit access. Each exists because the medium is unreliable, shared, or both — and knowing which reason applies predicts exactly what full duplex deleted and what it left untouched.
- Related topic
The 48-Bit MAC Address — OUI, I/G and U/L
The individual/group flag is the first bit of the frame body on the wire and the universal/local flag the second, so a receiver can select a matching pipeline 47 bit times before the address completes — and the address's global uniqueness is an administrative claim nothing enforces.
- Related topic
Address Filtering in Hardware
The receive filter is not a predicate but a priority-ordered set of accept reasons that overlap by design — and a station that cannot say which reason took a frame cannot explain the traffic it is receiving.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
