Ethernet · Module 12
The Forwarding Decision
Forwarding is not a table lookup. It is six gates, five of which can veto, and a design that enumerates only three outcomes cannot represent the case that will bite it.
Chapter 12.1 named three outcomes — forward, flood, filter. Chapter 12.2 built the table those outcomes are read from. This chapter is what actually happens between a destination address arriving and a frame leaving.
And the first thing to establish is that it is not a table lookup.
A table lookup is one input to the decision. There are five, they are independent, and any one of them can send the frame nowhere:
| # | Gate | Can it veto? | What its veto means |
|---|---|---|---|
| 1 | is this frame eligible to be forwarded at all | yes | malformed — Chapter 7.3 rejected it |
| 2 | is the ingress port in a forwarding state | yes | administratively or protocol-blocked |
| 3 | what does the table say | it narrows, it does not veto | hit, miss, or no answer in time |
| 4 | is the answer the ingress port | yes | the filter rule — the frame is already there |
| 5 | is the egress port in a forwarding state | yes | that port must not carry this frame |
| 6 | is there room on the egress | yes | Chapter 12.1 §6's discard |
Six gates, five of which veto, and a frame reaches a wire only when every one of them permits it.
1. Scope — What This Chapter Owns
This chapter owns the decision: the lookup and its deadline, the three-valued answer a lookup actually returns, the ingress-port filter rule in full, the port-state gates on both ends, the composition of all of it into one action, and the ordering guarantee the composition must not break.
It does not own the table's contents — Chapter 12.2 owns learning, ageing and moves, and this chapter consumes lookup_hit and lookup_port as they arrive.
It does not own the table's hardware — Chapter 12.5 owns CAM structures, hashing and collisions, which is where the deadline in Section 4 is actually met or missed.
It does not own flooding — Chapter 12.4 owns what a flood costs and why it is the only safe response to a miss. This chapter establishes when a flood is the answer; 12.4 establishes what it does to the network.
And it does not own congestion discard — Chapter 12.1 §6 and §9 established the queue, the arbiter and the drop policy. Gate 6 appears here only because a complete account of "why did this frame go nowhere" must include it.
2. A Miss Is Not an Absence
Start with the distinction that the rest of the chapter depends on, because it is stated wrongly almost everywhere.
A lookup miss does not mean the destination station is not present. It means the switch has no entry for that address. Those are entirely different claims, and Chapter 12.2 built every mechanism that separates them.
A station can be perfectly present and produce a miss for at least five reasons:
| Why the lookup missed | Is the station present? | How long it lasts |
|---|---|---|
| it has never transmitted | yes | forever, until it does |
| its entry aged out during a quiet period | yes | until it transmits again |
| the table was full when it first transmitted | yes | until room appears |
| its entry was flushed when a port bounced | yes | until it transmits again |
| it genuinely is not there | no | — |
Only the last row is an absence, and the switch cannot tell which row it is in.
Which is why the response to a miss is to flood rather than to discard. Flooding is the switch declining to guess: the frame goes everywhere except back where it came from, and if the station exists anywhere in the domain, the frame reaches it. A discard would be the switch acting on a conclusion — the station is not present — that its evidence does not support.
3. The Seven Outcomes, and Why the Encoding Must Hold Them All
Chapter 12.1 §8 listed three. A complete decision has at least seven, and the four that are usually missing are the ones that produce silent misbehaviour.
| # | Outcome | Egress | Which gate produced it |
|---|---|---|---|
| 1 | FORWARD | one port | table hit, not the ingress port, all gates permit |
| 2 | FLOOD | all but ingress | table miss, or a group destination |
| 3 | FILTER | none | gate 4 — the answer is the ingress port |
| 4 | BLOCKED_INGRESS | none | gate 2 — the ingress port is not forwarding |
| 5 | BLOCKED_EGRESS | none, or a reduced flood | gate 5 — that egress must not carry it |
| 6 | UNRESOLVED | flood, by policy | gate 3 — the lookup did not answer in time |
| 7 | DISCARD | none | gate 6 — no room, Chapter 12.1 §9 |
Outcomes 4, 5, 6 and 7 all produce "the frame went nowhere", and they mean four unrelated things. Collapsing them costs the operator the only information that distinguishes a configuration problem from a congestion problem from a timing failure.
Outcome 6 is the one that does not exist in most descriptions of switching, and it is a real state. Chapter 12.1 §12 derived the budget: a 24-port gigabit switch sustaining minimum-length frames on every port has 28 ns per lookup, 14 cycles at 500 MHz. A lookup that has not returned by then has not failed — it simply has no answer yet, and the frame is still arriving. The design must decide what to do, and "wait" is not available: the next frame is 672 ns behind and the queue is finite.
The policy this chapter adopts is that an unresolved lookup floods. It is the same reasoning as a miss — the switch does not know, so it declines to guess — and it is safe for the same reason. What it must never do is silently become a FLOOD in the encoding, because then the operator cannot tell a network with many unknown stations from a lookup engine that is missing its deadline, and those need completely different responses.
4. RTL 1 — The Lookup, and Its Deadline
A lookup returns three things, not two, and the third is the one designs omit.
// -----------------------------------------------------------------------
// fwd_decision_pkg -- shared types for the forwarding decision.
//
// The action encoding is SEVEN-valued on purpose. Section 3 explains why
// a three-valued encoding makes a real failure unobservable.
// -----------------------------------------------------------------------
package fwd_decision_pkg;
typedef enum logic [2:0] {
A_FORWARD = 3'd0, // one egress port
A_FLOOD = 3'd1, // all ports except ingress
A_FILTER = 3'd2, // hit, and the answer is the ingress port
A_BLOCKED_INGRESS = 3'd3, // ingress port not in a forwarding state
A_BLOCKED_EGRESS = 3'd4, // the named egress must not carry this
A_UNRESOLVED = 3'd5, // lookup missed its deadline -- floods, but
// is COUNTED separately
A_INELIGIBLE = 3'd6 // the frame was never forwardable
} fwd_action_e;
// A lookup's answer is THREE-valued. HIT and MISS are answers;
// TIMEOUT is the absence of one, and it is not a miss.
typedef enum logic [1:0] {
LK_HIT = 2'd0,
LK_MISS = 2'd1,
LK_TIMEOUT = 2'd2,
LK_IDLE = 2'd3
} lookup_answer_e;
// 802.1D port states. Two of five permit learning; ONE permits
// forwarding.
typedef enum logic [2:0] {
PS_DISABLED = 3'd0,
PS_BLOCKING = 3'd1,
PS_LISTENING = 3'd2,
PS_LEARNING = 3'd3, // learns, does NOT forward
PS_FORWARDING = 3'd4
} port_state_e;
localparam int ADDR_W = 48;
endpackage// -----------------------------------------------------------------------
// destination_lookup -- issues the lookup and enforces its deadline.
//
// Chapter 12.1 Section 12 derived the budget: 24 gigabit ports of minimum
// frames need one answer every 28 ns, which is 14 cycles at 500 MHz. This
// module does not make the lookup fast -- Chapter 12.5 does that. It makes
// the deadline VISIBLE, which is the difference between a switch that is
// slow and a switch that is slow and nobody knows.
// -----------------------------------------------------------------------
module destination_lookup
import fwd_decision_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int DEADLINE_CY = 14, // 28 ns at 500 MHz
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic req_valid,
input logic [ADDR_W-1:0] req_addr,
// To the table -- Chapter 12.2's store.
output logic tbl_req,
output logic [ADDR_W-1:0] tbl_addr,
input logic tbl_ack,
input logic tbl_hit,
input logic [PORT_BITS-1:0] tbl_port,
output logic ans_valid,
output lookup_answer_e ans,
output logic [PORT_BITS-1:0] ans_port,
output logic [3:0] ans_latency_cy,
output logic [CNT_W-1:0] c_hit,
output logic [CNT_W-1:0] c_miss,
output logic [CNT_W-1:0] c_timeout,
output logic [3:0] worst_latency_cy,
output logic over_budget
);
logic [3:0] cy_q;
logic busy_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
busy_q <= 1'b0;
cy_q <= '0;
tbl_req <= 1'b0;
tbl_addr <= '0;
ans_valid <= 1'b0;
ans <= LK_IDLE;
ans_port <= '0;
ans_latency_cy <= '0;
c_hit <= '0;
c_miss <= '0;
c_timeout <= '0;
worst_latency_cy <= '0;
over_budget <= 1'b0;
end else begin
ans_valid <= 1'b0;
tbl_req <= 1'b0;
if (!busy_q && req_valid) begin
busy_q <= 1'b1;
cy_q <= '0;
tbl_req <= 1'b1;
tbl_addr <= req_addr;
end else if (busy_q) begin
cy_q <= cy_q + 1'b1;
if (tbl_ack) begin
busy_q <= 1'b0;
ans_valid <= 1'b1;
ans <= tbl_hit ? LK_HIT : LK_MISS;
ans_port <= tbl_port;
ans_latency_cy <= cy_q;
if (tbl_hit) begin
if (!(&c_hit)) c_hit <= c_hit + 1'b1;
end else begin
if (!(&c_miss)) c_miss <= c_miss + 1'b1;
end
if (cy_q > worst_latency_cy) worst_latency_cy <= cy_q;
over_budget <= (cy_q >= 4'(DEADLINE_CY));
end else if (cy_q >= 4'(DEADLINE_CY)) begin
// THE DEADLINE. Not a failure of the table -- the answer may
// still arrive. It is a failure to answer IN TIME, and the
// frame cannot wait: the next one is 672 ns behind.
busy_q <= 1'b0;
ans_valid <= 1'b1;
ans <= LK_TIMEOUT;
ans_port <= '0;
ans_latency_cy <= cy_q;
if (!(&c_timeout)) c_timeout <= c_timeout + 1'b1;
over_budget <= 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that a lookup has a deadline, and missing it is a third answer rather than a variety of miss. The distinction costs one encoding value and buys the ability to tell two completely different problems apart. A network with many unknown stations raises c_miss. A lookup engine that cannot keep up raises c_timeout. Both produce flooding; only the second is fixed by changing the switch.
Deliberately simplified: one outstanding lookup. A real engine pipelines several, which means the answers can return out of order relative to the requests — the problem Section 12's ordering guard exists to solve, and one that does not arise until the lookup is pipelined.
Production implication: worst_latency_cy is the number that belongs on a bring-up report, and it is not the average. A lookup engine whose average is 6 cycles and whose worst case is 19 is over budget, because the worst case is what the arriving frame meets when the table is in its unlucky state — a hash bucket that is full, a concurrent learn, a refresh in flight. over_budget sticks on the first occurrence for a reason: an event that happens on one frame in ten thousand is invisible in an average and is exactly the event that matters.
5. RTL 2 — The Ingress-Port Filter
The filter is four lines of logic and it is the rule most reviewers get wrong. Chapter 12.1 §16 built a directed test around it; this is the mechanism.
// -----------------------------------------------------------------------
// ingress_port_filter -- decides between FORWARD and FILTER on a hit.
//
// A lookup that returns the INGRESS port is a SUCCESS, not a failure. The
// switch knows exactly where the destination is, and knows the frame has
// already been delivered there. The correct action is to emit nothing.
//
// The bug this module exists to prevent: treating "the answer is the
// ingress port" as "no useful answer" and falling back on a flood.
// -----------------------------------------------------------------------
module ingress_port_filter
import fwd_decision_pkg::*;
#(
parameter int PORT_BITS = 5,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic ans_valid,
input lookup_answer_e ans,
input logic [PORT_BITS-1:0] ans_port,
input logic [PORT_BITS-1:0] ingress_port,
output logic dec_valid,
output fwd_action_e dec_action,
output logic [PORT_BITS-1:0] dec_port,
output logic [CNT_W-1:0] c_forward,
output logic [CNT_W-1:0] c_filter,
output logic [CNT_W-1:0] c_flood_miss,
output logic [CNT_W-1:0] c_flood_unresolved
);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
dec_valid <= 1'b0;
dec_action <= A_FLOOD;
dec_port <= '0;
c_forward <= '0;
c_filter <= '0;
c_flood_miss <= '0;
c_flood_unresolved <= '0;
end else begin
dec_valid <= 1'b0;
if (ans_valid) begin
dec_valid <= 1'b1;
dec_port <= ans_port;
unique case (ans)
LK_HIT: begin
if (ans_port == ingress_port) begin
// THE FILTER. The destination shares a segment with the
// sender -- a hub, an unmanaged switch, two virtual
// machines on one host bridge. The frame ALREADY reached
// it. Emitting anything would duplicate it.
dec_action <= A_FILTER;
if (!(&c_filter)) c_filter <= c_filter + 1'b1;
end else begin
dec_action <= A_FORWARD;
if (!(&c_forward)) c_forward <= c_forward + 1'b1;
end
end
LK_MISS: begin
// The switch does not KNOW. Section 2: five reasons, four of
// which have the station present. Flood.
dec_action <= A_FLOOD;
if (!(&c_flood_miss)) c_flood_miss <= c_flood_miss + 1'b1;
end
LK_TIMEOUT: begin
// Same egress behaviour as a miss, DIFFERENT action code.
// Section 3: collapsing these makes an over-budget lookup
// engine indistinguishable from a network of unknown
// stations.
dec_action <= A_UNRESOLVED;
if (!(&c_flood_unresolved))
c_flood_unresolved <= c_flood_unresolved + 1'b1;
end
default: begin
dec_valid <= 1'b0;
dec_action <= A_FLOOD;
end
endcase
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that a hit has two outcomes and they are opposites. A hit naming another port is the switch's most useful answer — one frame, one port, no waste. A hit naming the ingress port is the switch's most useful non-answer — the lookup succeeded completely, and the correct response is to do nothing at all.
And it teaches why A_UNRESOLVED is separate from A_FLOOD even though both flood. The egress behaviour is identical; the diagnostic meaning is not. Two counters, one extra encoding value, and an entire class of performance failure becomes visible.
Deliberately simplified: a single-cycle decision on an answer that has already been resolved. A production design folds the filter into the same pipeline stage as the lookup return, because a whole cycle is 7% of the 14-cycle budget.
Production implication: c_filter should be non-zero on any port with a shared segment behind it and zero everywhere else. A port with a hub, a desktop switch, or a virtualisation host behind it produces filtered frames continuously; a port with a single station behind it produces none. A c_filter of zero on a port that is known to have a shared segment behind it means the filter is not firing — and the frames that should have been filtered are being flooded to 23 other ports instead, producing duplicates on the shared segment and wasted bandwidth everywhere else, with no error counter moving.
6. Why the Filter Rule Is Not Optional
Work through what a switch without the filter does, because the failure is not "slightly wasteful".
Two stations, A and B, share a segment behind port 3 of a 24-port switch. B sends to A.
| Step | With the filter | Without it (treating the hit as a miss) |
|---|---|---|
| the frame reaches A directly | yes — shared segment | yes |
| the switch learns B on port 3 | yes | yes |
| the lookup for A returns | port 3 — the ingress port | port 3 — the ingress port |
| the switch's action | FILTER — emit nothing | FLOOD — 23 copies |
| copies on the shared segment | the one A already has | two — A receives a duplicate |
| copies elsewhere | none | 23 ports carry a frame for a station on none of them |
| bandwidth cost per frame | zero | 23 × frame |
| what the operator sees | nothing | duplicate frames, blamed on a loop |
The bandwidth arithmetic is the smaller half. At 1.4881 Mpps of minimum-length frames, flooding instead of filtering on one busy shared-segment port costs 23 × 672 bits × 1.4881 M = 23 Gb/s of the switch's 48 Gb/s aggregate — half the switch, spent delivering frames to ports that do not lead anywhere useful.
The duplicate is the larger half, because of who gets blamed. A receives B's frame twice: once over the shared medium and once echoed back by the switch. Duplicate frames on a segment are the classic signature of a topology loop, and an operator seeing them will go looking for a redundant cable. The switch, reporting valid links, zero errors and a rising flood count that looks like an ordinary busy network, is the last place anyone looks.
7. RTL 3 — Port State, and the Twenty-Five Combinations
Gates 2 and 5 are the same rule applied at two ends, and together they permit forwarding in one of twenty-five combinations.
// -----------------------------------------------------------------------
// port_state_gate -- applies 802.1D port state to both ends of a decision.
//
// Five states. TWO permit learning; ONE permits forwarding. The gate is
// asymmetric on purpose: a port in LEARNING contributes evidence to
// Chapter 12.2's table while carrying no traffic, which is what makes a
// state transition safe rather than disruptive.
// -----------------------------------------------------------------------
module port_state_gate
import fwd_decision_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input port_state_e state [N_PORTS],
input logic dec_valid,
input fwd_action_e dec_action,
input logic [PORT_BITS-1:0] dec_port,
input logic [PORT_BITS-1:0] ingress_port,
output logic gated_valid,
output fwd_action_e gated_action,
output logic [PORT_BITS-1:0] gated_port,
output logic [N_PORTS-1:0] forwarding_mask, // ports that may transmit
output logic may_learn, // for Chapter 12.2
output logic [CNT_W-1:0] c_blocked_ingress,
output logic [CNT_W-1:0] c_blocked_egress
);
// ONE state forwards. Two learn. The asymmetry is the whole point of
// having five states rather than two.
function automatic logic can_forward(input port_state_e s);
can_forward = (s == PS_FORWARDING);
endfunction
function automatic logic can_learn(input port_state_e s);
can_learn = (s == PS_LEARNING) || (s == PS_FORWARDING);
endfunction
always_comb begin
for (int p = 0; p < N_PORTS; p++)
forwarding_mask[p] = can_forward(state[p]);
end
assign may_learn = can_learn(state[ingress_port]);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
gated_valid <= 1'b0;
gated_action <= A_FLOOD;
gated_port <= '0;
c_blocked_ingress <= '0;
c_blocked_egress <= '0;
end else begin
gated_valid <= 1'b0;
gated_port <= dec_port;
gated_action <= dec_action;
if (dec_valid) begin
gated_valid <= 1'b1;
// GATE 2 -- the ingress end. A frame arriving on a port that is
// not forwarding is not forwarded ANYWHERE, whatever the table
// says. Note that it may still be LEARNED from: a port in
// LEARNING exists precisely to populate the table before it
// starts carrying traffic.
if (!can_forward(state[ingress_port])) begin
gated_action <= A_BLOCKED_INGRESS;
if (!(&c_blocked_ingress))
c_blocked_ingress <= c_blocked_ingress + 1'b1;
// GATE 5 -- the egress end, for a unicast forward. The table's
// answer is correct and the port still must not carry it.
end else if ((dec_action == A_FORWARD) &&
!can_forward(state[dec_port])) begin
gated_action <= A_BLOCKED_EGRESS;
if (!(&c_blocked_egress))
c_blocked_egress <= c_blocked_egress + 1'b1;
end
// A FLOOD is NOT blocked here -- it is REDUCED. Section 9's mask
// builder intersects the flood with forwarding_mask, so a flood
// reaches every port that may carry it and no others.
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the table's answer is a candidate, not a decision. A lookup can be correct — the station really is on port 7 — and the frame still must not go there, because port 7 is blocking. The table describes the network's topology; the port state describes what this switch is currently permitted to do about it, and the two are independent.
And it teaches why five states exist rather than two. A port in PS_LEARNING learns and does not forward, which is what makes bringing a port into service non-disruptive: Chapter 12.2's table is populated with the stations behind that port before any traffic is sent there. A design with only blocking and forwarding must choose between a port that carries traffic to an empty table and a port that learns nothing until it is live.
Deliberately simplified: one global state per port. Real switches keep per-VLAN state, so a port may be forwarding for one VLAN and blocking for another simultaneously — which multiplies the state space by the VLAN count and is where Chapter 13's tagging becomes structural rather than cosmetic.
Production implication: c_blocked_egress rising steadily is the signature of a stale table entry pointing at a blocked port, and it is self-correcting only if the entry ages out. The frames are lost in the meantime, and the count is the only evidence. A switch that collapses this into a generic drop counter gives an operator a number that says "frames were lost" and nothing that says "because the port your table names is administratively down", which is a five-second fix rather than a packet capture.
8. The Twenty-Five Combinations, and the Four That Matter
Two ports, five states each, is twenty-five combinations. Exactly one forwards.
| Ingress state | Egress state | Frame forwarded? | Learned from? | Action |
|---|---|---|---|---|
FORWARDING | FORWARDING | yes — the only one | yes | A_FORWARD |
FORWARDING | anything else | no | yes | A_BLOCKED_EGRESS |
LEARNING | anything | no | yes | A_BLOCKED_INGRESS |
LISTENING | anything | no | no | A_BLOCKED_INGRESS |
BLOCKING | anything | no | no | A_BLOCKED_INGRESS |
DISABLED | anything | no | no | A_BLOCKED_INGRESS |
1 ÷ 25 = 4% of the state space forwards, and the interesting rows are the two middle ones.
A port in FORWARDING with an egress in any other state still learns. The frame arrived legitimately; its source address is evidence regardless of where the frame is going. Learning is not conditional on the forwarding outcome — Chapter 12.2 §4 established this, and gate 5 is one more case of it.
A port in LEARNING learns and forwards nothing. That is the state's entire purpose, and it is why bringing a port into service is not disruptive: for the duration of the learning state, the switch discovers what is behind that port without sending anything there. By the time the port reaches FORWARDING, the table already knows.
9. RTL 4 — Turning an Action Into an Egress Mask
The action says what to do. The mask says where. The translation is where a flood meets the port states, and where the ingress bit must be cleared exactly once.
// -----------------------------------------------------------------------
// egress_mask_builder -- action plus port states to a set of egress ports.
//
// Every action maps to a mask. The two that are easy to get wrong:
// FLOOD -- all forwarding ports EXCEPT the ingress. Both conditions,
// not either.
// FILTER -- the empty mask, which must be reached deliberately rather
// than by a flood that happens to exclude everything.
// -----------------------------------------------------------------------
module egress_mask_builder
import fwd_decision_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5
)(
input logic clk,
input logic rst_n,
input logic gated_valid,
input fwd_action_e gated_action,
input logic [PORT_BITS-1:0] gated_port,
input logic [PORT_BITS-1:0] ingress_port,
input logic [N_PORTS-1:0] forwarding_mask,
output logic mask_valid,
output logic [N_PORTS-1:0] egress_mask,
output fwd_action_e mask_action,
output logic flood_reduced // some ports excluded by state
);
logic [N_PORTS-1:0] all_but_ingress;
logic [N_PORTS-1:0] full_flood;
assign all_but_ingress = ~(N_PORTS'(1) << ingress_port);
// BOTH conditions. A flood goes to every port that MAY forward AND is
// not the ingress port -- not to every port that may forward, and not
// to every port except the ingress.
assign full_flood = forwarding_mask & all_but_ingress;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mask_valid <= 1'b0;
egress_mask <= '0;
mask_action <= A_FILTER;
flood_reduced <= 1'b0;
end else begin
mask_valid <= 1'b0;
flood_reduced <= 1'b0;
if (gated_valid) begin
mask_valid <= 1'b1;
mask_action <= gated_action;
unique case (gated_action)
A_FORWARD: begin
egress_mask <= (N_PORTS'(1) << gated_port);
end
// A miss and an unresolved lookup produce the SAME mask and
// are still distinct actions -- Section 3.
A_FLOOD, A_UNRESOLVED: begin
egress_mask <= full_flood;
flood_reduced <= (full_flood != all_but_ingress);
end
// Every remaining action emits nothing, and each is reached by
// its own branch so that no case falls through to a default
// that floods.
A_FILTER,
A_BLOCKED_INGRESS,
A_BLOCKED_EGRESS,
A_INELIGIBLE: begin
egress_mask <= '0;
end
default: begin
// Unreachable by construction. It emits NOTHING rather than
// flooding, because a default that floods turns an encoding
// error into a domain-wide broadcast.
egress_mask <= '0;
mask_valid <= 1'b0;
end
endcase
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that a flood is the intersection of two conditions and not either one alone. forwarding_mask & all_but_ingress — every port that may forward and is not the ingress. Using only all_but_ingress sends frames to blocked ports, which is how a flood becomes a loop. Using only forwarding_mask echoes the frame back to its own segment, which is Chapter 12.1's P9 violated.
And it teaches the safe default. The unreachable default branch emits nothing. A default that floods converts any encoding error, any unhandled state, any future action added without updating this case statement, into a frame sent to every port in the domain. The failure mode of a silent default is one lost frame; the failure mode of a flooding default is a broadcast storm.
Deliberately simplified: an unregistered flood mask over a flat set of ports. Production designs qualify the flood by VLAN membership as well as port state — Chapter 13 — which makes full_flood a three-way intersection.
Production implication: flood_reduced says the flood did not reach every port, which is usually correct and occasionally the whole problem. During a topology transition many ports are not forwarding, and a flood that reaches four ports out of twenty-four will not find a station behind any of the other twenty. The frame is legitimately lost, no counter calls it a loss, and the symptom is a host that is unreachable for exactly as long as the transition lasts. Without this bit, that interval is invisible.
10. RTL 5 — Composing the Gates Into One Answer With a Reason
Six gates produce one action. What the operator needs is the action and which gate produced it, and the second is thrown away by almost every implementation.
// -----------------------------------------------------------------------
// decision_composer -- runs the gates in order and records WHICH one
// decided.
//
// The ordering matters and is not arbitrary: cheap and absolute checks
// first, expensive and conditional ones last. A frame rejected by gate 1
// never consumes a lookup slot, which matters when the lookup engine is
// the bottleneck -- Chapter 12.1 Section 12.
// -----------------------------------------------------------------------
module decision_composer
import fwd_decision_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_valid,
input logic frame_eligible, // gate 1
input logic ingress_forwarding, // gate 2
input logic dst_is_group,
input logic ans_valid, // gate 3
input lookup_answer_e ans,
input logic [PORT_BITS-1:0] ans_port,
input logic [PORT_BITS-1:0] ingress_port,
input logic egress_forwarding, // gate 5
input logic egress_has_room, // gate 6
output logic out_valid,
output fwd_action_e out_action,
output logic [PORT_BITS-1:0] out_port,
output logic [2:0] deciding_gate, // 1..6, 0 = permitted
output logic lookup_needed,
output logic [CNT_W-1:0] by_gate [7]
);
// Gate 3 is skipped entirely for a group destination -- Chapter 12.1
// Section 8 established that broadcast and multicast have a known
// answer, so consulting the table wastes a lookup slot the engine is
// short of.
assign lookup_needed = frame_valid && frame_eligible &&
ingress_forwarding && !dst_is_group;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
out_valid <= 1'b0;
out_action <= A_INELIGIBLE;
out_port <= '0;
deciding_gate <= 3'd0;
for (int g = 0; g < 7; g++) by_gate[g] <= '0;
end else begin
out_valid <= 1'b0;
out_port <= ans_port;
if (frame_valid) begin
out_valid <= 1'b1;
// GATE 1 -- eligibility. Cheapest, most absolute, first.
if (!frame_eligible) begin
out_action <= A_INELIGIBLE;
deciding_gate <= 3'd1;
by_gate[1] <= by_gate[1] + 1'b1;
// GATE 2 -- ingress port state.
end else if (!ingress_forwarding) begin
out_action <= A_BLOCKED_INGRESS;
deciding_gate <= 3'd2;
by_gate[2] <= by_gate[2] + 1'b1;
// A group destination bypasses gate 3 and gate 4 entirely.
end else if (dst_is_group) begin
out_action <= A_FLOOD;
deciding_gate <= 3'd0;
by_gate[0] <= by_gate[0] + 1'b1;
// GATE 3 -- the lookup's three-valued answer.
end else if (ans_valid && (ans == LK_TIMEOUT)) begin
out_action <= A_UNRESOLVED;
deciding_gate <= 3'd3;
by_gate[3] <= by_gate[3] + 1'b1;
end else if (ans_valid && (ans == LK_MISS)) begin
out_action <= A_FLOOD;
deciding_gate <= 3'd0;
by_gate[0] <= by_gate[0] + 1'b1;
// GATE 4 -- the ingress filter.
end else if (ans_valid && (ans_port == ingress_port)) begin
out_action <= A_FILTER;
deciding_gate <= 3'd4;
by_gate[4] <= by_gate[4] + 1'b1;
// GATE 5 -- egress port state.
end else if (!egress_forwarding) begin
out_action <= A_BLOCKED_EGRESS;
deciding_gate <= 3'd5;
by_gate[5] <= by_gate[5] + 1'b1;
// GATE 6 -- admission. Chapter 12.1 Section 9 owns the policy;
// this is only where it sits in the order.
end else if (!egress_has_room) begin
out_action <= A_FORWARD; // the decision was to forward
deciding_gate <= 3'd6; // and the queue refused it
by_gate[6] <= by_gate[6] + 1'b1;
end else begin
out_action <= A_FORWARD;
deciding_gate <= 3'd0;
by_gate[0] <= by_gate[0] + 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that deciding_gate is worth more than out_action, because several actions collapse to the same visible behaviour and only the gate distinguishes them. A frame that went nowhere may have been ineligible, ingress-blocked, filtered, egress-blocked or discarded — five causes, one observable, and four of them are fixed by completely different things.
And it teaches why gate 6 does not change the action. A frame discarded by a full queue was decided to be forwarded; the decision was correct and the resource was unavailable. Recording it as A_FORWARD with deciding_gate = 6 preserves both facts, where recording it as a discard action would lose the information that the lookup succeeded and the table was right.
Deliberately simplified: a flat combinational chain. A real pipeline evaluates gates 1 and 2 in the first stage, issues the lookup in the second, and applies gates 4, 5 and 6 as the answer returns — which is what makes lookup_needed load-bearing rather than cosmetic.
Production implication: lookup_needed is the throughput lever. Chapter 12.1 §12 showed the shared lookup engine has a 28 ns budget and is the first thing to saturate. Every group destination that skips the lookup is a slot returned to the engine, and on a network with heavy broadcast — ARP, discovery protocols, a chatty control plane — that is a double-digit percentage of all frames. A design that looks up broadcast addresses and discovers they miss every time has spent its scarcest resource learning nothing.
11. The Cost of Each Gate, in Cycles
Six gates in a 14-cycle budget. Where the cycles go decides whether the switch meets line rate.
| Gate | Typical cost | Why |
|---|---|---|
| 1 — eligibility | 0 | already resolved by the receiver, store-and-forward |
| 2 — ingress state | 0 | one bit of a register read in parallel with everything |
| 3 — the lookup | 4 to 14 | a memory access plus collision handling — Chapter 12.5 |
| 4 — ingress filter | 0 | one comparator, folded into the lookup return stage |
| 5 — egress state | 0 | one bit, indexed by the lookup's answer |
| 6 — admission | 1 | a queue occupancy compare |
Gate 3 is the entire budget and every other gate is free. Which is why the ordering in Section 10 matters: gates 1 and 2 come before the lookup and can retire a frame without consuming the scarce resource, and gates 4, 5 and 6 come after and cost nothing.
The arithmetic that makes this concrete. A 24-port gigabit switch sustaining minimum-length frames needs 35.71 M lookups per second, one every 28 ns. A shared engine at 500 MHz has 14 cycles. If broadcast and multicast are 15% of frames and each skips the lookup, the engine sees 35.71 M × 0.85 = 30.35 Mpps, which is 33 ns per lookup — 16.5 cycles, and the design that was 18% over budget is now inside it.
One engine or twenty-four
The 28 ns figure is the budget for a shared engine. Giving each port its own changes the arithmetic completely, and the trade is timing against ports on a memory.
| one shared engine | one engine per port | |
|---|---|---|
| lookups per second | 24 × 1.4881 M = 35.71 M | 1.4881 M each |
| budget per lookup | 1 ÷ 35.71 M = 28 ns | 1 ÷ 1.4881 M = 672 ns |
| at 500 MHz | 14 cycles | 336 cycles |
| search logic | one copy | 24 copies |
| the table | one memory, one port | one memory, 24 read ports — or 24 copies |
| a learn while a lookup runs | one write against one read | one write against 24 reads |
A 336-cycle budget makes the lookup itself trivially easy — a linear walk of 8192 entries at one per cycle takes 16 384 cycles and is still far too slow, but any hashed structure fits with enormous margin.
And the cost lands on the memory instead. Twenty-four ports reading one table concurrently means either a 24-read-port memory, which is not a thing that gets built at this size, or 24 copies of the table kept coherent — and every learn must then be broadcast to all 24, which reintroduces the shared-write bottleneck the split was meant to remove.
Which is why real switches land in between: a small number of lookup engines, each serving a group of ports, with the table banked so that concurrent lookups usually touch different banks. The 28 ns figure is the worst case for the fully shared arrangement, and it remains the right number to design against because it is the one that holds when every port is busy at once — which is exactly the condition under which nothing else is going right either.
12. RTL 6 — Keeping Order Through a Variable-Latency Decision
A pipelined lookup answers in four cycles sometimes and fourteen other times. Two frames between the same pair can therefore finish out of order, and Ethernet does not permit that.
// -----------------------------------------------------------------------
// ordering_guard -- prevents a variable-latency decision path from
// reordering frames between the same source and destination.
//
// The hazard: frame 1 misses (14 cycles), frame 2 from the same source to
// the same destination hits (4 cycles). Frame 2's decision completes 10
// cycles -- 20 ns at 500 MHz -- before frame 1's. Without this guard, the
// second frame reaches the wire first.
//
// Ethernet has no sequence number and no reassembly. A receiver cannot
// detect or correct a reorder; it simply hands the frames up in the wrong
// order, and every protocol above assumes it did not.
// -----------------------------------------------------------------------
module ordering_guard
import fwd_decision_pkg::*;
#(
parameter int DEPTH = 8,
parameter int TAG_W = 3,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
// Issue side -- one tag per frame entering the decision pipeline.
input logic issue_valid,
output logic [TAG_W-1:0] issue_tag,
output logic issue_ready,
// Completion side -- decisions may complete OUT OF ORDER.
input logic done_valid,
input logic [TAG_W-1:0] done_tag,
// Release side -- strictly IN ORDER.
output logic release_valid,
output logic [TAG_W-1:0] release_tag,
output logic [CNT_W-1:0] c_held, // completed early, waited
output logic [3:0] max_hold_depth,
output logic [CNT_W-1:0] c_released
);
logic [DEPTH-1:0] done_q;
logic [TAG_W-1:0] head_q, tail_q;
logic [TAG_W:0] count_q;
assign issue_ready = (count_q < (TAG_W+1)'(DEPTH));
assign issue_tag = tail_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
done_q <= '0;
head_q <= '0;
tail_q <= '0;
count_q <= '0;
release_valid <= 1'b0;
release_tag <= '0;
c_held <= '0;
c_released <= '0;
max_hold_depth <= '0;
end else begin
release_valid <= 1'b0;
if (issue_valid && issue_ready) begin
done_q[tail_q] <= 1'b0;
tail_q <= tail_q + 1'b1;
count_q <= count_q + 1'b1;
end
if (done_valid) begin
done_q[done_tag] <= 1'b1;
// A completion that is NOT at the head has finished early and
// must wait. Counting these measures how much reordering the
// decision path is actually producing.
if (done_tag != head_q)
if (!(&c_held)) c_held <= c_held + 1'b1;
end
// Release strictly in issue order, one per cycle.
if ((count_q != '0) && done_q[head_q]) begin
release_valid <= 1'b1;
release_tag <= head_q;
done_q[head_q] <= 1'b0;
head_q <= head_q + 1'b1;
count_q <= count_q - 1'b1;
if (!(&c_released)) c_released <= c_released + 1'b1;
end
if (count_q[3:0] > max_hold_depth) max_hold_depth <= count_q[3:0];
end
end
endmoduleClassification: synthesizable.
What it teaches: that variable latency in a decision path is a correctness problem and not only a performance one. The lookup is fast on a hit and slow on a miss, and the difference is exactly the case where reordering matters: a station's first frame misses while its second hits, so the second overtakes the first by up to 14 − 4 = 10 cycles, 20 ns at 500 MHz.
And it teaches why the guard is unconditional rather than per-conversation. A precise guard would only order frames sharing a source and destination pair, which needs a comparison against every in-flight frame. A single in-order release queue costs a few flip-flops and orders everything, and since the decision path is only 14 cycles deep the head-of-line cost is bounded by the same 14 cycles.
Deliberately simplified: one global order across all ports. A real switch orders per ingress port, because frames arriving on different ports have no ordering relationship to preserve and forcing one couples unrelated traffic.
Production implication: c_held is the direct measurement of how much reordering the decision path would have produced, and it is not zero on any pipelined design. A c_held of zero on a switch with a pipelined lookup means the guard is not wired to the completion signal — the frames are being released in completion order, and the reorder is happening. max_hold_depth approaching DEPTH means the pipeline is deeper than the queue that reorders it, which stalls issue and shows up as throughput loss with no error anywhere.
13. RTL 7 — Telemetry: Attributing Every Frame to a Gate
Every frame that entered the decision leaves it through exactly one gate. A histogram over that gate is the most compact complete description of what a switch is doing.
// -----------------------------------------------------------------------
// forwarding_telemetry -- a histogram over deciding_gate, plus the derived
// ratios that make it readable.
//
// Seven counters. Their SHAPE, not their absolute values, is what tells an
// operator whether the switch is healthy.
// -----------------------------------------------------------------------
module forwarding_telemetry
import fwd_decision_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int CNT_W = 32,
parameter int WINDOW = 1_000_000
)(
input logic clk,
input logic rst_n,
input logic out_valid,
input fwd_action_e out_action,
input logic [2:0] deciding_gate,
input logic [PORT_BITS-1:0] ingress_port,
output logic [CNT_W-1:0] c_action [7],
output logic [CNT_W-1:0] c_gate [7],
output logic [CNT_W-1:0] filter_by_port [N_PORTS],
output logic window_valid,
output logic flood_dominant, // acting like a hub
output logic timeout_present, // lookup over budget
output logic blocked_significant, // stale table or config
output logic [2:0] top_gate
);
logic [CNT_W-1:0] win_total;
logic [CNT_W-1:0] win_gate [7];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < 7; i++) begin
c_action[i] <= '0; c_gate[i] <= '0; win_gate[i] <= '0;
end
for (int p = 0; p < N_PORTS; p++) filter_by_port[p] <= '0;
win_total <= '0;
window_valid <= 1'b0;
flood_dominant <= 1'b0;
timeout_present <= 1'b0;
blocked_significant <= 1'b0;
top_gate <= 3'd0;
end else begin
window_valid <= 1'b0;
if (out_valid) begin
c_action[out_action[2:0]] <= c_action[out_action[2:0]] + 1'b1;
c_gate[deciding_gate] <= c_gate[deciding_gate] + 1'b1;
win_gate[deciding_gate] <= win_gate[deciding_gate] + 1'b1;
win_total <= win_total + 1'b1;
// Filtering is a PER-PORT property: it fires only on ports with a
// shared segment behind them, so the distribution across ports is
// the useful form, not the total.
if (out_action == A_FILTER)
filter_by_port[ingress_port] <= filter_by_port[ingress_port] + 1'b1;
end
if (win_total >= CNT_W'(WINDOW)) begin
automatic logic [CNT_W-1:0] hi = '0;
automatic logic [2:0] hg = 3'd0;
for (int g = 0; g < 7; g++)
if (win_gate[g] > hi) begin hi = win_gate[g]; hg = 3'(g); end
top_gate <= hg;
// Floods above a quarter of all decisions is hub-like behaviour.
flood_dominant <=
((c_action[A_FLOOD] + c_action[A_UNRESOLVED]) > (win_total >> 2));
// ANY timeout is a finding. This is not a rate threshold.
timeout_present <= (win_gate[3] != '0);
// Blocked frames above a thousandth means a table entry names a
// port that is not forwarding, persistently.
blocked_significant <=
((win_gate[2] + win_gate[5]) > (win_total >> 10));
for (int g = 0; g < 7; g++) win_gate[g] <= '0;
win_total <= '0;
window_valid <= 1'b1;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that a seven-way histogram over the deciding gate answers questions that no per-port statistic can. Ports report bytes, frames and errors. None of them says why a frame went nowhere, and that is the only question worth asking about a switch that is passing traffic and still not working.
And it teaches the shape to expect. In a healthy switch c_gate[0] — permitted, no veto — dominates overwhelmingly, gate 4 (filter) is non-zero only on ports with shared segments, gate 3 (timeout) is exactly zero, and gates 2 and 5 are near zero outside topology transitions. Any other shape is a finding.
Deliberately simplified: one global window. Production telemetry keeps per-port histograms, which multiplies the counter count by the port count and is where the area actually goes.
Production implication: timeout_present has no threshold on purpose. A rate threshold on this signal would be wrong, because a lookup engine that misses its deadline once per million frames is over budget in exactly the same sense as one that misses it every frame — the design does not meet its timing, and the only difference is how unlucky the traffic has to be. A single occurrence is the finding, which is why the signal is a presence bit rather than a ratio.
14. RTL 8 — Conformance for a Six-Gate Decision
The monitor's job is to prove that every frame's outcome follows from the gates, and that no outcome was reached by a path nobody wrote down.
// -----------------------------------------------------------------------
// forwarding_conformance_monitor -- checks the decision against its inputs.
//
// The central check is EXHAUSTIVENESS: every frame that entered the
// decision left it through exactly one gate, and the gate accounts sum to
// the frame count. A missing frame means a path exists that nobody
// enumerated -- which is Section 16's rejected property, made observable.
// -----------------------------------------------------------------------
module forwarding_conformance_monitor
import fwd_decision_pkg::*;
#(
parameter int N_PORTS = 24,
parameter int PORT_BITS = 5,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic frame_in, // entered the decision
input logic out_valid,
input fwd_action_e out_action,
input logic [2:0] deciding_gate,
input logic [N_PORTS-1:0] egress_mask,
input logic [PORT_BITS-1:0] ingress_port,
input logic [N_PORTS-1:0] forwarding_mask,
output logic [CNT_W-1:0] v_unaccounted, // in, never out
output logic [CNT_W-1:0] v_ingress_echo, // egress includes ingress
output logic [CNT_W-1:0] v_blocked_egress, // egress not forwarding
output logic [CNT_W-1:0] v_mask_mismatch, // mask disagrees w/ action
output logic [CNT_W-1:0] v_gate_unknown, // gate outside 0..6
output logic conformant
);
logic [CNT_W-1:0] n_in, n_out;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_in <= '0; n_out <= '0;
v_unaccounted <= '0;
v_ingress_echo <= '0;
v_blocked_egress <= '0;
v_mask_mismatch <= '0;
v_gate_unknown <= '0;
end else begin
if (frame_in) n_in <= n_in + 1'b1;
if (out_valid) n_out <= n_out + 1'b1;
// EXHAUSTIVENESS. Every frame that entered produced a decision.
// A frame that entered and never left took a path the enumeration
// does not contain.
if (n_in > (n_out + CNT_W'(16)))
if (!(&v_unaccounted)) v_unaccounted <= v_unaccounted + 1'b1;
if (out_valid) begin
// THE ABSOLUTE INVARIANT -- Chapter 12.1's P9, restated at the
// point where the mask is built rather than where it is used.
if (egress_mask[ingress_port])
if (!(&v_ingress_echo)) v_ingress_echo <= v_ingress_echo + 1'b1;
// No frame reaches a port that may not forward.
if ((egress_mask & ~forwarding_mask) != '0)
if (!(&v_blocked_egress)) v_blocked_egress <= v_blocked_egress + 1'b1;
// The mask must match what the action claims.
unique case (out_action)
A_FORWARD:
if (deciding_gate == 3'd0 && $countones(egress_mask) != 1)
if (!(&v_mask_mismatch)) v_mask_mismatch <= v_mask_mismatch + 1'b1;
A_FLOOD, A_UNRESOLVED:
if (egress_mask == '0)
if (!(&v_mask_mismatch)) v_mask_mismatch <= v_mask_mismatch + 1'b1;
A_FILTER, A_BLOCKED_INGRESS, A_BLOCKED_EGRESS, A_INELIGIBLE:
if (egress_mask != '0)
if (!(&v_mask_mismatch)) v_mask_mismatch <= v_mask_mismatch + 1'b1;
default:
if (!(&v_mask_mismatch)) v_mask_mismatch <= v_mask_mismatch + 1'b1;
endcase
// A gate value outside the enumerated range means the composer
// took a branch that was not written down.
if (deciding_gate > 3'd6)
if (!(&v_gate_unknown)) v_gate_unknown <= v_gate_unknown + 1'b1;
end
end
end
assign conformant = (v_unaccounted == '0) &&
(v_ingress_echo == '0) &&
(v_blocked_egress == '0) &&
(v_mask_mismatch == '0) &&
(v_gate_unknown == '0);
endmoduleClassification: synthesizable, and intended to stay in silicon.
What it teaches: that exhaustiveness is checkable by accounting rather than by enumeration. v_unaccounted does not need to know what the missing path is; it observes that frames entered and did not leave, which is true of any unenumerated outcome including ones nobody has thought of. That is the structural answer to Section 16's rejected property: instead of asserting that the outcome is one of a list, assert that every input produced an outcome, and let the count expose a path the list omits.
And v_mask_mismatch is the cross-check between the two halves of the decision. The action says what; the mask says where. A design can get either right while the translation between them is wrong — a filter that produces a non-empty mask, a forward that produces two bits, a flood that produces none. Each of those is a working switch by every other measure.
Deliberately simplified: the n_in > n_out + 16 slack assumes a decision pipeline no deeper than 16. A real monitor tags each frame and reconciles individually, because a fixed slack hides a leak of fewer than sixteen frames indefinitely.
Production implication: conformant here means the decision followed from its inputs and every input produced a decision. It does not mean the frame was delivered — gate 6 can discard it afterwards, and Chapter 12.1 §15 established that discarding is the design working. It also does not mean the table was right, which Chapter 12.2 §16 established is not assertable at all. The monitor's honest claim is narrow: given what the table said and what the port states were, the decision is the one the specification requires — and that is exactly the claim a switch designer is responsible for.
15. One Frame, End to End
Put the eight modules in order and follow a single frame through them, with the cycle budget alongside.
| Cycle | Stage | What happens | Which gate |
|---|---|---|---|
| 0 | receive complete | store-and-forward; FCS known | 1 |
| 0 | port state read | ingress state, one register bit | 2 |
| 0 | classify | group or individual destination | — |
| 1 | issue | lookup_needed — skipped for a group destination | 3 |
| 1–14 | lookup | hit, miss, or deadline | 3 |
| 14 | filter compare | ans_port == ingress_port | 4 |
| 14 | egress state | one bit, indexed by the answer | 5 |
| 15 | mask build | forwarding_mask & all_but_ingress for a flood | — |
| 15 | admission | queue occupancy compare | 6 |
| 16 | release | in issue order, through the ordering guard | — |
Sixteen cycles at 500 MHz is 32 ns, against a 28 ns budget for a fully loaded 24-port gigabit switch. The design as written is 14% over — and the fix is not to make the lookup faster. It is Section 11's arithmetic: every group destination that skips gate 3 returns a slot, and at 15% broadcast and multicast the engine's effective budget rises to 33 ns.
Which is why lookup_needed is not a convenience. It is the difference between a design that meets line rate and one that does not, and it comes from a fact established three chapters ago: Chapter 12.1 §8 showed that broadcast and multicast have a known answer, so consulting the table for them costs the scarcest resource in the switch and returns nothing.
And follow the frame that goes nowhere, because that is the case the telemetry exists for.
| Frame | Gate that vetoed | Egress | What an operator must do |
|---|---|---|---|
| bad FCS | 1 | none | find the failing link — Chapter 7.3 |
| arrived on a blocking port | 2 | none | nothing — correct behaviour during a transition |
| destination shares the ingress segment | 4 | none | nothing — correct behaviour, always |
| table names a blocked port | 5 | none | wait for the entry to age, or check the port's state |
| egress queue full | 6 | none | capacity — Chapter 12.1 §6 |
Two of the five require no action at all, and an operator who cannot tell them from the other three will chase the wrong one.
16. Properties Worth Asserting, and One Worth Refusing
Every property here names a gate, a mask, or the accounting between them. The rejected property names a set, and that is what makes it unfalsifiable.
The lookup and its deadline
// P1. A lookup answers within the deadline OR reports that it did not.
// It never simply fails to answer.
property p_lookup_always_answers;
@(posedge clk) disable iff (!rst_n)
tbl_req |-> ##[1:DEADLINE_CY] ans_valid;
endproperty
a_lookup_answers: assert property (p_lookup_always_answers);
// P2. A timeout is reported as a TIMEOUT, never as a miss. One encoding
// value separates a network problem from a design problem.
property p_timeout_not_a_miss;
@(posedge clk) disable iff (!rst_n)
(ans_valid && (ans_latency_cy >= 4'(DEADLINE_CY)) && !tbl_ack)
|-> (ans == LK_TIMEOUT);
endproperty
a_timeout_distinct: assert property (p_timeout_not_a_miss);
// P3. A hit reports a port; a miss and a timeout do not pretend to.
property p_miss_has_no_port;
@(posedge clk) disable iff (!rst_n)
(ans_valid && (ans != LK_HIT)) |-> (ans_port == '0);
endproperty
a_no_phantom_port: assert property (p_miss_has_no_port);
// P4. The worst-case latency is retained, not averaged away.
property p_worst_latency_monotonic;
@(posedge clk) disable iff (!rst_n)
$stable(rst_n) |-> (worst_latency_cy >= $past(worst_latency_cy));
endproperty
a_worst_retained: assert property (p_worst_latency_monotonic);
// P5. A group destination consumes NO lookup slot -- Section 11's
// throughput lever.
property p_group_skips_lookup;
@(posedge clk) disable iff (!rst_n)
(frame_valid && dst_is_group) |-> !lookup_needed;
endproperty
a_group_no_lookup: assert property (p_group_skips_lookup);The filter rule
// P6. THE FILTER. A hit whose port equals the ingress port produces
// A_FILTER -- never a flood, never a forward.
property p_hit_on_ingress_filters;
@(posedge clk) disable iff (!rst_n)
(ans_valid && (ans == LK_HIT) && (ans_port == ingress_port))
|=> (dec_action == A_FILTER);
endproperty
a_filter_on_self: assert property (p_hit_on_ingress_filters);
// P7. A filter emits NOTHING. The empty mask is reached deliberately.
property p_filter_empty_mask;
@(posedge clk) disable iff (!rst_n)
(mask_valid && (mask_action == A_FILTER)) |-> (egress_mask == '0);
endproperty
a_filter_no_egress: assert property (p_filter_empty_mask);
// P8. A hit on ANOTHER port forwards to exactly one port.
property p_hit_elsewhere_forwards_one;
@(posedge clk) disable iff (!rst_n)
(mask_valid && (mask_action == A_FORWARD)) |-> $onehot(egress_mask);
endproperty
a_forward_one_port: assert property (p_hit_elsewhere_forwards_one);
// P9. A filter is NOT counted as a miss, and a miss is NOT counted as a
// filter. They are opposite conclusions from opposite evidence.
property p_filter_and_miss_distinct;
@(posedge clk) disable iff (!rst_n)
(dec_valid && (dec_action == A_FILTER)) |-> $stable(c_flood_miss);
endproperty
a_filter_not_miss: assert property (p_filter_and_miss_distinct);The absolute invariants
// P10. NEVER out the ingress port -- Chapter 12.1's P9, re-asserted at
// the point the mask is built.
property p_never_egress_on_ingress;
@(posedge clk) disable iff (!rst_n)
mask_valid |-> !egress_mask[ingress_port];
endproperty
a_no_ingress_echo: assert property (p_never_egress_on_ingress);
// P11. NEVER to a port that may not forward, whatever the table says.
property p_never_to_blocked_port;
@(posedge clk) disable iff (!rst_n)
mask_valid |-> ((egress_mask & ~forwarding_mask) == '0);
endproperty
a_no_blocked_egress: assert property (p_never_to_blocked_port);
// P12. A flood is the INTERSECTION of both conditions, not either.
property p_flood_is_intersection;
@(posedge clk) disable iff (!rst_n)
(mask_valid && (mask_action inside {A_FLOOD, A_UNRESOLVED}))
|-> (egress_mask == (forwarding_mask & ~(N_PORTS'(1) << ingress_port)));
endproperty
a_flood_intersection: assert property (p_flood_is_intersection);
// P13. Every non-emitting action produces an EMPTY mask -- no partial
// emission from an action that decided to emit nothing.
property p_non_emitting_actions_empty;
@(posedge clk) disable iff (!rst_n)
(mask_valid && (mask_action inside {A_FILTER, A_BLOCKED_INGRESS,
A_BLOCKED_EGRESS, A_INELIGIBLE}))
|-> (egress_mask == '0);
endproperty
a_non_emitting_empty: assert property (p_non_emitting_actions_empty);Gate ordering and attribution
// P14. Gate 1 precedes everything. An ineligible frame consumes no
// lookup slot.
property p_ineligible_no_lookup;
@(posedge clk) disable iff (!rst_n)
(frame_valid && !frame_eligible) |-> !lookup_needed;
endproperty
a_ineligible_no_lookup: assert property (p_ineligible_no_lookup);
// P15. Gate 2 precedes the lookup for the same reason.
property p_blocked_ingress_no_lookup;
@(posedge clk) disable iff (!rst_n)
(frame_valid && !ingress_forwarding) |-> !lookup_needed;
endproperty
a_blocked_no_lookup: assert property (p_blocked_ingress_no_lookup);
// P16. Exactly ONE gate is credited per decision.
property p_one_gate_per_decision;
@(posedge clk) disable iff (!rst_n)
out_valid |-> (deciding_gate <= 3'd6);
endproperty
a_gate_in_range: assert property (p_one_gate_per_decision);
// P17. Gate 6 does not change the ACTION. The decision was to forward;
// the resource refused it, and both facts survive.
property p_admission_preserves_action;
@(posedge clk) disable iff (!rst_n)
(out_valid && (deciding_gate == 3'd6)) |-> (out_action == A_FORWARD);
endproperty
a_gate6_action: assert property (p_admission_preserves_action);
// P18. A frame blocked at the ingress may STILL be learned from -- gates
// on forwarding and gates on learning are independent.
property p_blocked_ingress_may_learn;
@(posedge clk) disable iff (!rst_n)
(ingress_state == PS_LEARNING) |-> (may_learn && !ingress_forwarding);
endproperty
a_learning_state: assert property (p_blocked_ingress_may_learn);Ordering
// P19. Frames are RELEASED in issue order, whatever order they complete
// in. Ethernet carries no sequence number, so a reorder is undetectable
// and therefore forbidden.
property p_release_in_order;
@(posedge clk) disable iff (!rst_n)
release_valid |=> (release_tag != $past(release_tag));
endproperty
a_in_order_release: assert property (p_release_in_order);
// P20. A completion out of order is HELD, not dropped.
property p_early_completion_held;
@(posedge clk) disable iff (!rst_n)
(done_valid && (done_tag != head_q)) |-> ##[1:DEPTH] release_valid;
endproperty
a_early_held: assert property (p_early_completion_held);
// P21. The reordering the guard prevented is COUNTED. A c_held of zero
// on a pipelined lookup means the guard is not wired up.
property p_held_counted;
@(posedge clk) disable iff (!rst_n)
(done_valid && (done_tag != head_q)) |=> (c_held > $past(c_held));
endproperty
a_held_counted: assert property (p_held_counted);Accounting and exhaustiveness
// P22. THE EXHAUSTIVENESS PROPERTY. Every frame that entered the
// decision produced a decision. This is the constructive replacement for
// the rejected property below: it catches an unenumerated path without
// naming it.
property p_every_frame_decided;
@(posedge clk) disable iff (!rst_n)
frame_in |-> ##[1:MAX_DECISION_CY] out_valid;
endproperty
a_all_frames_decided: assert property (p_every_frame_decided);
// P23. The gate histogram sums to the decision count. A shortfall means
// a decision was reached by a path with no gate credited to it.
property p_gate_accounts_balance;
@(posedge clk) disable iff (!rst_n)
window_valid |-> ((c_gate[0] + c_gate[1] + c_gate[2] + c_gate[3] +
c_gate[4] + c_gate[5] + c_gate[6]) == n_out);
endproperty
a_gates_balance: assert property (p_gate_accounts_balance);
// P24. The mask agrees with the action -- the two halves of the decision
// are cross-checked against each other.
property p_mask_matches_action;
@(posedge clk) disable iff (!rst_n)
out_valid |-> (v_mask_mismatch == $past(v_mask_mismatch));
endproperty
a_mask_action_agree: assert property (p_mask_matches_action);
// P25. ANY timeout is reported. Not a rate -- a presence bit, because a
// design that misses its deadline once misses it.
property p_timeout_presence_reported;
@(posedge clk) disable iff (!rst_n)
(window_valid && (c_gate[3] != '0)) |-> timeout_present;
endproperty
a_timeout_reported: assert property (p_timeout_presence_reported);
// P26. Filtering is attributed to a PORT. The distribution across ports
// is the useful form; the total says nothing.
property p_filter_attributed_to_port;
@(posedge clk) disable iff (!rst_n)
(out_valid && (out_action == A_FILTER))
|=> (filter_by_port[$past(ingress_port)] >
$past(filter_by_port[$past(ingress_port)]));
endproperty
a_filter_by_port: assert property (p_filter_attributed_to_port);
// P27. Conformance means the decision followed from its inputs -- never
// that the frame was delivered, and never that the table was right.
property p_conformant_definition;
@(posedge clk) disable iff (!rst_n)
conformant |-> ((v_unaccounted == '0) && (v_ingress_echo == '0) &&
(v_blocked_egress == '0) && (v_mask_mismatch == '0) &&
(v_gate_unknown == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);17. Verification Scenarios
Fifty scenarios, grouped by the gate they exercise. Every one of them must produce a deciding_gate, and the accounting in P22 and P23 must balance across all of them.
The lookup and its deadline
| # | Scenario | Expected |
|---|---|---|
| 1 | Known unicast destination | LK_HIT, port from the table |
| 2 | Unknown unicast destination | LK_MISS, no port claimed |
| 3 | Table stalls for DEADLINE_CY cycles | LK_TIMEOUT, not LK_MISS |
| 4 | Table answers on the deadline cycle exactly | LK_HIT — the boundary belongs to the answer |
| 5 | Table answers one cycle late | LK_TIMEOUT, and the late answer is discarded |
| 6 | Broadcast destination | no lookup issued, lookup_needed low |
| 7 | Multicast destination | no lookup issued |
| 8 | 1000 hits at 4 cycles, one at 19 | worst_latency_cy = 19, over_budget set |
| 9 | c_timeout after any timeout | non-zero — and c_miss unchanged |
| 10 | Reset | counters and worst_latency_cy cleared |
The filter rule
| # | Scenario | Expected |
|---|---|---|
| 11 | Hit, ans_port ≠ ingress_port | A_FORWARD, one bit in the mask |
| 12 | Hit, ans_port = ingress_port | A_FILTER, empty mask |
| 13 | Same, on a 24-port switch | no frame on any of the other 23 ports |
| 14 | Filter event | c_filter increments, c_flood_miss unchanged |
| 15 | Port with a hub behind it, two stations talking | filter_by_port for that port rises steadily |
| 16 | Port with one station behind it | filter_by_port stays zero |
| 17 | Miss on a port with a shared segment | flood — a miss is not a filter |
| 18 | Filter immediately after a station moves onto the ingress port | filter, from the first frame after the move |
Port state
| # | Scenario | Expected |
|---|---|---|
| 19 | Ingress FORWARDING, egress FORWARDING | forwarded — the only combination that does |
| 20 | Ingress FORWARDING, egress BLOCKING | A_BLOCKED_EGRESS, empty mask |
| 21 | Ingress LEARNING | A_BLOCKED_INGRESS, and may_learn high |
| 22 | Ingress LISTENING | blocked, and may_learn low |
| 23 | Ingress DISABLED | blocked, no learn |
| 24 | Flood with 8 of 24 ports forwarding | mask has 7 bits — forwarding ports minus ingress |
| 25 | Flood with only the ingress port forwarding | empty mask, flood_reduced set |
| 26 | All 25 ingress/egress state combinations | exactly one forwards |
| 27 | Port transitions BLOCKING → LISTENING → LEARNING → FORWARDING | learning enabled one state before forwarding |
Mask construction
| # | Scenario | Expected |
|---|---|---|
| 28 | Flood on a 24-port switch, all forwarding | 23 bits set, the ingress bit clear |
| 29 | Forward | exactly one bit set |
| 30 | Filter | zero bits set |
| 31 | Blocked ingress | zero bits |
| 32 | Blocked egress | zero bits |
| 33 | Unresolved | same mask as a flood, different action code |
| 34 | An action value outside the enumeration | mask empty, mask_valid low — never a flood |
| 35 | Ingress port also blocked, during a flood | ingress bit clear by both conditions |
Ordering
| # | Scenario | Expected |
|---|---|---|
| 36 | Frame 1 misses (14 cy), frame 2 hits (4 cy), same pair | released 1 then 2 |
| 37 | Same | c_held increments — the guard did work |
| 38 | Eight frames completing in reverse order | released in issue order, max_hold_depth = 8 |
| 39 | Pipeline deeper than DEPTH | issue_ready deasserts, no frame lost |
| 40 | All frames completing in order | c_held stays zero, no added latency |
| 41 | c_held zero on a pipelined lookup under mixed traffic | a bug — the guard is not wired to completion |
Accounting and conformance
| # | Scenario | Expected |
|---|---|---|
| 42 | One million frames in | one million decisions out |
| 43 | Gate histogram after any run | sums to the decision count |
| 44 | A frame that enters and never produces a decision | v_unaccounted increments |
| 45 | Egress mask containing the ingress bit | v_ingress_echo increments |
| 46 | Egress mask containing a non-forwarding port | v_blocked_egress increments |
| 47 | A_FILTER with a non-empty mask | v_mask_mismatch increments |
| 48 | A_FORWARD with two bits set | v_mask_mismatch increments |
| 49 | Healthy switch, one million frames | c_gate[0] dominant, c_gate[3] zero |
| 50 | Any single timeout in a window | timeout_present — no rate threshold |
18. Debugging a Forwarding Decision
Every row produces valid links, zero frame errors, and a switch that is passing traffic. The middle column is the hypothesis; the right column is what settles it in one look.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| Flood rate high, table not full | many genuinely unknown stations | c_gate[3] = 0 and c_miss high — a learning-side problem |
| Flood rate high, table not full | lookup engine over budget | c_timeout non-zero — no amount of table tuning helps |
| Duplicate frames on one segment | the filter is not firing | filter_by_port = 0 on a port known to have a shared segment |
| Duplicate frames, one per lookup | a late answer was not suppressed | two ans_valid pulses for one frame — Section 17's row 2 |
| One host unreachable, table says it is on port 7 | port 7 is not forwarding | c_blocked_egress rising, and port 7's state |
| Everything unreachable through one port | that port is not forwarding at the ingress | c_blocked_ingress rising, may_learn may still be high |
| Host unreachable for exactly the transition window | a flood reached only the forwarding ports | flood_reduced set during the window |
| Throughput short of line rate on small frames | the lookup engine is the bottleneck | worst_latency_cy against DEADLINE_CY; check lookup_needed on group frames |
| Occasional out-of-order delivery reported by hosts | the ordering guard is not engaged | c_held = 0 on a pipelined lookup — it should never be |
Throughput loss with no errors, max_hold_depth at DEPTH | the release queue is shallower than the pipeline | issue_ready deasserting under load |
| Frames counted in, fewer counted out | a path with no gate credited | v_unaccounted — an unenumerated outcome exists |
| A host reachable through one switch and not its neighbour | one of them has a stale entry naming a blocked port | c_blocked_egress on the failing switch only, with both tables agreeing |
| Throughput collapses when one port is brought into service | that port floods while its table warms | flood_reduced and c_gate[2] during the LEARNING state — expected, and bounded |
| A filter counted as a miss | the two conclusions were merged | c_filter = 0 while c_flood_miss tracks the shared-segment port's traffic |
| Lookup engine saturated on a network with heavy ARP | broadcast frames are consuming lookup slots | lookup_needed asserting on group destinations — it must not |
| Flood rate climbs only during business hours | genuinely more unknown stations, or more timeouts under load | c_timeout against time — a load-correlated timeout is a budget failure |
| Two decisions recorded for one frame | a late lookup answer was accepted after the deadline | out_valid twice for one frame_in — and the frame is on the wire twice |
| Drop counter permanently non-zero, no complaints | filtering and transition blocking folded into it | deciding_gate histogram — gates 4 and 2 are correct behaviour |
19. Common Misconceptions
1 — "Forwarding is a table lookup."
The wrong model: egress = table[destination], and everything else is plumbing.
What it costs: every failure that is not a table failure gets attributed to the table. A frame that went nowhere because the egress port is blocking, because the ingress port is not forwarding, because the destination shares the ingress segment, or because the lookup did not answer in time all look like "the table was wrong" to an engineer holding this model — and the table was right in every one of those cases.
The corrected model: the decision is six gates, five of which can veto, and the table is one input to the third. A complete answer to "why did this frame go nowhere" names a gate, and only one of the six has anything to do with the table's contents.
2 — "A miss means the destination is not present."
The wrong model: the table is a directory of who exists, so a miss is an absence.
What it costs: it makes discarding on a miss look reasonable, which would silently destroy traffic to every station that has not transmitted recently. It also makes a high miss rate look like a topology problem rather than a learning-side one.
The corrected model: a miss means the switch has no entry, and Chapter 12.2 gave four reasons a present station produces one — it has never transmitted, its entry aged out, the table was full, its entry was flushed. Only the fifth reason is an actual absence, and the switch cannot tell which it is in. Flooding is the response that is correct under all five.
3 — "A hit always means forward."
The wrong model: hit and miss partition the outcomes; hit forwards, miss floods.
What it costs: the ingress-port filter disappears. A hit naming the ingress port is treated as "no useful answer" and floods, sending 23 copies of a frame that has already been delivered and producing a duplicate on the shared segment — which is then blamed on a topology loop.
The corrected model: a hit has two outcomes, and they are opposites. Hit elsewhere → forward. Hit on the ingress port → filter, which is the lookup succeeding completely and the correct action being to emit nothing. And a hit can still be vetoed afterwards by gate 5 or gate 6.
4 — "A lookup that is usually fast is fast enough."
The wrong model: average lookup latency is the design metric.
What it costs: a lookup engine averaging 6 cycles with a 19-cycle worst case is over budget, and the frames that meet the worst case are flooded. At one in a thousand that is 1488 flooded frames per second per gigabit port — invisible in an average, and enough to raise the flood rate into the range where an operator starts changing ageing times.
The corrected model: the budget is per frame, not per second. Chapter 12.1 §12 derived 28 ns because the next frame is 672 ns behind and there are 24 ports. The worst case is what an arriving frame meets when the table is in its unlucky state, and worst_latency_cy is the number that decides whether the design meets its specification.
5 — "If every assertion passes, the decision engine is correct."
The wrong model: a clean assertion run means the enumeration is complete.
What it costs: Section 16's rejected property, exactly. action inside {FORWARD, FLOOD, FILTER} passes on a design that folds every timeout into a flood, because the encoding cannot represent the case. The assertion is satisfied by the bug.
The corrected model: ask of every property could this fail, given the declared widths and enumerations? If not, it is a statement about the type and not about the behaviour. Replace it with accounting — every frame in produces a decision out, and the gate histogram sums to the decision count — which catches an unenumerated path without having to name it.
6 — "A frame that goes nowhere has been dropped."
The wrong model: one counter for frames that did not leave, called drops.
What it costs: five unrelated causes are merged into one number. A frame can go nowhere because it was malformed (gate 1), because its ingress port is not forwarding (gate 2), because its destination shares the ingress segment (gate 4), because the port the table named is blocked (gate 5), or because the egress queue was full (gate 6). Two of those five are correct behaviour requiring no action at all — filtering always is, and ingress blocking is during a transition — and an operator staring at a single drop counter has no way to subtract them. The remaining three need a link fix, a port-state investigation and a capacity change respectively.
The corrected model: the useful record is deciding_gate, not a drop count. "The frame went nowhere" is an observation; "gate 4 vetoed it" is a diagnosis, and the difference between them is one three-bit field carried alongside the action.
20. Interview Reasoning
Q1 — "A frame arrives, the table hits, the port state is forwarding, the queue has room, and the frame is not transmitted. Is this a bug?"
Reason through it. Not necessarily — one gate is unaccounted for in the question. If the port the table named is the port the frame arrived on, the correct action is to filter, and emitting nothing is right: the destination shares a segment with the sender and already has the frame. The strong answer names the filter, explains why flooding instead would send 23 useless copies and produce a duplicate on the shared segment, and then observes that the question's own framing is the misconception — a hit does not imply a forward, because a hit has two opposite outcomes depending on which port it names.
Q2 — "Your switch's flood rate is 30% and the MAC table is a quarter full. Where do you look?"
Reason through it. Two hypotheses produce identical flooding. Either many destinations are genuinely unknown — a learning-side problem — or the lookup engine is missing its deadline and unresolved lookups are being flooded. The counter that separates them is the timeout count: c_timeout non-zero at all means the engine is over budget, and no change to ageing time or table size will help. c_miss high with c_timeout zero is a learning problem, and Chapter 12.2's diagnosis applies. The strong answer notes that a design which folds timeout into miss cannot distinguish these at all, and that this is why the action encoding needs seven values rather than three.
Q3 — "Why does a port in the learning state learn but not forward, and why not the other way round?"
Reason through it. Because the two actions fail in opposite directions. Forwarding to a port that should not carry traffic is actively harmful — it can create a loop, or deliver frames into a segment being taken down. Learning from a port that should not be trusted is passively wrong — an entry is created that ageing will retract, and no frame goes anywhere. So the safe sequence enables the harmless action first, which is why a port walks BLOCKING → LISTENING → LEARNING → FORWARDING and learning is enabled one full state before forwarding. The strong answer draws the general principle: gate the world-changing action and the belief-changing action separately, and enable the belief-changing one first, so the table is warm before the first frame is sent.
Q4 — "A pipelined lookup answers hits in 4 cycles and misses in 14. What correctness problem does that create, and why can't the receiver fix it?"
Reason through it. Two frames from the same source to the same destination can complete out of order: the second frame hits while the first is still missing, so its decision finishes up to 10 cycles — 20 ns at 500 MHz — earlier, and without an in-order release it reaches the wire first. The receiver cannot fix it because an Ethernet frame carries no sequence number — Chapter 5.1's fields are destination, source, EtherType, payload and FCS, and none of them orders one frame against another. A reorder is therefore undetectable, uncorrectable and unreported, absorbed by whatever protocol sits above. The strong answer concludes that the burden is entirely on the switch: ordering is something the switch provides and nothing else can restore, which puts it in the same class as "never duplicate" and "never modify" rather than in the class of performance properties.
Q5 — "Two of your six gates produce 'the frame went nowhere' and require no action from anybody. Which two, and why does that matter?"
Reason through it. Gate 4, the ingress filter — the destination shares the ingress port's segment and already has the frame, so emitting nothing is not just acceptable but required; emitting anything would duplicate it. And gate 2 during a topology transition — a port in BLOCKING, LISTENING or LEARNING correctly refuses to forward, and the state will advance on its own. It matters because a design that collapses all five non-emitting outcomes into one drop counter forces an operator to investigate a number that is partly composed of the switch working correctly. The strong answer names the consequence: a switch with a hub behind one port and a port mid-transition shows a permanent, unexplained drop rate, and the engineer who chases it finds nothing — which trains people to ignore the counter, so that when gate 5 or gate 6 starts contributing, nobody notices.
21. Understanding Check
22. What's Next
This chapter established when a frame is flooded. It has said almost nothing about what flooding costs.
Two of the seven outcomes — A_FLOOD and A_UNRESOLVED — send a frame to every forwarding port except the ingress, and this chapter treated that as the safe answer without examining the bill. Chapter 12.4 — Flooding examines it: what a flood does to every port in the domain, why broadcast is unavoidable rather than merely tolerated, and why a flood is nonetheless the only correct response to not knowing.
Then Chapter 12.5 — The MAC Table builds the structure that gate 3 consults, and it is where this chapter's 28 ns deadline is actually met or missed. CAM structures, hashing, associativity, and what a hash collision does to worst_latency_cy.
And Chapter 12.6 — Store-and-Forward against Cut-Through returns to Chapter 12.1 §10's choice with the full decision path in view, because a cut-through switch must run all six gates before the frame has finished arriving.
Continue learning
Related tutorials
- Related topic
End Systems, Switches and Routers
A device taxonomy is a taxonomy of mutation authority. A repeater changes nothing, a switch changes nothing in the frame but chooses where it goes, and a router destroys the frame and builds a new one — which is what fixes the forwarding boundary between layer two and layer three.
- Related topic
What a Switch Does
One decision per frame multiplied a 24-station network's capacity by 48. It did not remove contention — it moved it from the wire into a queue, where dropping is the mechanism working, not a fault.
- 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.
- Related topic
CSMA/CD, Collision Domains and Slot Time
Slot time is the parameter the whole half-duplex MAC hangs on: it bounds medium acquisition, bounds a collision fragment, and is the retransmission quantum. Deriving it from round-trip propagation plus jam is what fixes Ethernet's minimum frame size — a timing constant wearing a frame-format costume.
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.
