Skip to content
VLSI Mentor

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:

#GateCan it veto?What its veto means
1is this frame eligible to be forwarded at allyesmalformed — Chapter 7.3 rejected it
2is the ingress port in a forwarding stateyesadministratively or protocol-blocked
3what does the table sayit narrows, it does not vetohit, miss, or no answer in time
4is the answer the ingress portyesthe filter rule — the frame is already there
5is the egress port in a forwarding stateyesthat port must not carry this frame
6is there room on the egressyesChapter 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 contentsChapter 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 hardwareChapter 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 floodingChapter 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 discardChapter 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 missedIs the station present?How long it lasts
it has never transmittedyesforever, until it does
its entry aged out during a quiet periodyesuntil it transmits again
the table was full when it first transmittedyesuntil room appears
its entry was flushed when a port bouncedyesuntil it transmits again
it genuinely is not thereno

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.

#OutcomeEgressWhich gate produced it
1FORWARDone porttable hit, not the ingress port, all gates permit
2FLOODall but ingresstable miss, or a group destination
3FILTERnonegate 4 — the answer is the ingress port
4BLOCKED_INGRESSnonegate 2 — the ingress port is not forwarding
5BLOCKED_EGRESSnone, or a reduced floodgate 5 — that egress must not carry it
6UNRESOLVEDflood, by policygate 3 — the lookup did not answer in time
7DISCARDnonegate 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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.

StepWith the filterWithout it (treating the hit as a miss)
the frame reaches A directlyyes — shared segmentyes
the switch learns B on port 3yesyes
the lookup for A returnsport 3 — the ingress portport 3 — the ingress port
the switch's actionFILTER — emit nothingFLOOD — 23 copies
copies on the shared segmentthe one A already hastwo — A receives a duplicate
copies elsewherenone23 ports carry a frame for a station on none of them
bandwidth cost per framezero23 × frame
what the operator seesnothingduplicate 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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 stateEgress stateFrame forwarded?Learned from?Action
FORWARDINGFORWARDINGyes — the only oneyesA_FORWARD
FORWARDINGanything elsenoyesA_BLOCKED_EGRESS
LEARNINGanythingnoyesA_BLOCKED_INGRESS
LISTENINGanythingnonoA_BLOCKED_INGRESS
BLOCKINGanythingnonoA_BLOCKED_INGRESS
DISABLEDanythingnonoA_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 outcomeChapter 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.

A frame arrives and passes through six gates in series. First, eligibility: a malformed frame is rejected outright. Second, the ingress port state: a port that is not forwarding vetoes the frame regardless of what the table says, although the frame may still be learned from. Third, the table lookup, which narrows the outcome to a hit, a miss, or no answer within the deadline, but does not itself veto. Fourth, the ingress filter: a hit naming the ingress port means the frame has already reached its destination, so nothing is emitted. Fifth, the egress port state: the table's answer may be correct while that port is still forbidden to carry the frame. Sixth, egress admission: a full queue discards. The frame reaches a wire only when all six permit it, and which gate vetoed is the entire diagnosis.1 — Eligible?Chapter 7.3's checks2 — Ingressforwarding?veto, but still learns3 — Lookuphit, miss or no answer4 — Answer isingress?filter — alreadydelivered5 — Egressforwarding?correct answer, forbiddenport6 — Room on egress?Chapter 12.1's discardOn the wireall six permitted12
Figure 1 — six gates in series, five of which can veto; the frame reaches a wire only when every one of them permits it, and the reason it did not is the diagnosis.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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.

GateTypical costWhy
1 — eligibility0already resolved by the receiver, store-and-forward
2 — ingress state0one bit of a register read in parallel with everything
3 — the lookup4 to 14a memory access plus collision handling — Chapter 12.5
4 — ingress filter0one comparator, folded into the lookup return stage
5 — egress state0one bit, indexed by the lookup's answer
6 — admission1a 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 engineone engine per port
lookups per second24 × 1.4881 M = 35.71 M1.4881 M each
budget per lookup1 ÷ 35.71 M = 28 ns1 ÷ 1.4881 M = 672 ns
at 500 MHz14 cycles336 cycles
search logicone copy24 copies
the tableone memory, one portone memory, 24 read ports — or 24 copies
a learn while a lookup runsone write against one readone 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
 
endmodule

Classification: 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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);
 
endmodule

Classification: 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.

CycleStageWhat happensWhich gate
0receive completestore-and-forward; FCS known1
0port state readingress state, one register bit2
0classifygroup or individual destination
1issuelookup_needed — skipped for a group destination3
1–14lookuphit, miss, or deadline3
14filter compareans_port == ingress_port4
14egress stateone bit, indexed by the answer5
15mask buildforwarding_mask & all_but_ingress for a flood
15admissionqueue occupancy compare6
16releasein 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.

A decision produces one of seven outcomes. Forward sends the frame to a single egress port. Flood sends it to every forwarding port except the ingress. Filter emits nothing because the destination shares the ingress port's segment and already has the frame. Blocked ingress emits nothing because the arriving port is not in a forwarding state. Blocked egress emits nothing because the port the table named is not permitted to carry traffic. Unresolved floods the frame because the lookup did not answer within its deadline, and is counted separately from a miss so that an over-budget lookup engine can be distinguished from a network with many unknown stations. Discard emits nothing because the egress queue was full. Four of these seven produce no frame on any wire, for four unrelated reasons, and only the reason is actionable.FORWARDone port — the usefulcaseFLOODmiss — the switch doesnot knowUNRESOLVEDno answer in 28 nsFILTERhit on the ingress portBLOCKED_INGRESSport not forwardingBLOCKED_EGRESSright answer, forbiddenportDISCARDqueue fullINELIGIBLEnever forwardable12
Figure 2 — seven outcomes, four of which mean 'the frame went nowhere' for four unrelated reasons; collapsing them is what makes a switch undiagnosable.

And follow the frame that goes nowhere, because that is the case the telemetry exists for.

FrameGate that vetoedEgressWhat an operator must do
bad FCS1nonefind the failing link — Chapter 7.3
arrived on a blocking port2nonenothing — correct behaviour during a transition
destination shares the ingress segment4nonenothing — correct behaviour, always
table names a blocked port5nonewait for the entry to age, or check the port's state
egress queue full6nonecapacity — 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.

Two frames arrive from the same source for the same destination. The first frame's lookup misses and takes fourteen cycles. The second frame's lookup hits and takes four cycles, so its decision completes ten cycles earlier, which is twenty nanoseconds at five hundred megahertz. Without an ordering guard the second frame reaches the wire first and the receiver is handed the two frames in the wrong order, which it cannot detect because an Ethernet frame carries no sequence number. An in-order release queue holds the early completion until the earlier frame's decision finishes, so the frames leave in the order they arrived at the cost of a bounded delay.Frame 1 — miss14 cyclesFrame 2 — hit4 cyclesFrame 2 decidesfirst20 ns earlierIn-order releasehold until frame 1 isdoneFrame 1 decidesthe head of the queueOrder preserved1 then 2, on the wireUndetectable ifreorderedno sequence number exists12
Figure 3 — a variable-latency lookup lets a later frame's decision finish first; Ethernet cannot detect the reorder, so the switch must not create it.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

#ScenarioExpected
1Known unicast destinationLK_HIT, port from the table
2Unknown unicast destinationLK_MISS, no port claimed
3Table stalls for DEADLINE_CY cyclesLK_TIMEOUT, not LK_MISS
4Table answers on the deadline cycle exactlyLK_HIT — the boundary belongs to the answer
5Table answers one cycle lateLK_TIMEOUT, and the late answer is discarded
6Broadcast destinationno lookup issued, lookup_needed low
7Multicast destinationno lookup issued
81000 hits at 4 cycles, one at 19worst_latency_cy = 19, over_budget set
9c_timeout after any timeoutnon-zero — and c_miss unchanged
10Resetcounters and worst_latency_cy cleared

The filter rule

#ScenarioExpected
11Hit, ans_port ≠ ingress_portA_FORWARD, one bit in the mask
12Hit, ans_port = ingress_portA_FILTER, empty mask
13Same, on a 24-port switchno frame on any of the other 23 ports
14Filter eventc_filter increments, c_flood_miss unchanged
15Port with a hub behind it, two stations talkingfilter_by_port for that port rises steadily
16Port with one station behind itfilter_by_port stays zero
17Miss on a port with a shared segmentflood — a miss is not a filter
18Filter immediately after a station moves onto the ingress portfilter, from the first frame after the move

Port state

#ScenarioExpected
19Ingress FORWARDING, egress FORWARDINGforwarded — the only combination that does
20Ingress FORWARDING, egress BLOCKINGA_BLOCKED_EGRESS, empty mask
21Ingress LEARNINGA_BLOCKED_INGRESS, and may_learn high
22Ingress LISTENINGblocked, and may_learn low
23Ingress DISABLEDblocked, no learn
24Flood with 8 of 24 ports forwardingmask has 7 bits — forwarding ports minus ingress
25Flood with only the ingress port forwardingempty mask, flood_reduced set
26All 25 ingress/egress state combinationsexactly one forwards
27Port transitions BLOCKING → LISTENING → LEARNING → FORWARDINGlearning enabled one state before forwarding

Mask construction

#ScenarioExpected
28Flood on a 24-port switch, all forwarding23 bits set, the ingress bit clear
29Forwardexactly one bit set
30Filterzero bits set
31Blocked ingresszero bits
32Blocked egresszero bits
33Unresolvedsame mask as a flood, different action code
34An action value outside the enumerationmask empty, mask_valid low — never a flood
35Ingress port also blocked, during a floodingress bit clear by both conditions

Ordering

#ScenarioExpected
36Frame 1 misses (14 cy), frame 2 hits (4 cy), same pairreleased 1 then 2
37Samec_held increments — the guard did work
38Eight frames completing in reverse orderreleased in issue order, max_hold_depth = 8
39Pipeline deeper than DEPTHissue_ready deasserts, no frame lost
40All frames completing in orderc_held stays zero, no added latency
41c_held zero on a pipelined lookup under mixed traffica bug — the guard is not wired to completion

Accounting and conformance

#ScenarioExpected
42One million frames inone million decisions out
43Gate histogram after any runsums to the decision count
44A frame that enters and never produces a decisionv_unaccounted increments
45Egress mask containing the ingress bitv_ingress_echo increments
46Egress mask containing a non-forwarding portv_blocked_egress increments
47A_FILTER with a non-empty maskv_mask_mismatch increments
48A_FORWARD with two bits setv_mask_mismatch increments
49Healthy switch, one million framesc_gate[0] dominant, c_gate[3] zero
50Any single timeout in a windowtimeout_presentno 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.

SymptomLikely causeThe observable that decides it
Flood rate high, table not fullmany genuinely unknown stationsc_gate[3] = 0 and c_miss high — a learning-side problem
Flood rate high, table not fulllookup engine over budgetc_timeout non-zero — no amount of table tuning helps
Duplicate frames on one segmentthe filter is not firingfilter_by_port = 0 on a port known to have a shared segment
Duplicate frames, one per lookupa late answer was not suppressedtwo ans_valid pulses for one frame — Section 17's row 2
One host unreachable, table says it is on port 7port 7 is not forwardingc_blocked_egress rising, and port 7's state
Everything unreachable through one portthat port is not forwarding at the ingressc_blocked_ingress rising, may_learn may still be high
Host unreachable for exactly the transition windowa flood reached only the forwarding portsflood_reduced set during the window
Throughput short of line rate on small framesthe lookup engine is the bottleneckworst_latency_cy against DEADLINE_CY; check lookup_needed on group frames
Occasional out-of-order delivery reported by hoststhe ordering guard is not engagedc_held = 0 on a pipelined lookup — it should never be
Throughput loss with no errors, max_hold_depth at DEPTHthe release queue is shallower than the pipelineissue_ready deasserting under load
Frames counted in, fewer counted outa path with no gate creditedv_unaccounted — an unenumerated outcome exists
A host reachable through one switch and not its neighbourone of them has a stale entry naming a blocked portc_blocked_egress on the failing switch only, with both tables agreeing
Throughput collapses when one port is brought into servicethat port floods while its table warmsflood_reduced and c_gate[2] during the LEARNING state — expected, and bounded
A filter counted as a missthe two conclusions were mergedc_filter = 0 while c_flood_miss tracks the shared-segment port's traffic
Lookup engine saturated on a network with heavy ARPbroadcast frames are consuming lookup slotslookup_needed asserting on group destinations — it must not
Flood rate climbs only during business hoursgenuinely more unknown stations, or more timeouts under loadc_timeout against time — a load-correlated timeout is a budget failure
Two decisions recorded for one framea late lookup answer was accepted after the deadlineout_valid twice for one frame_in — and the frame is on the wire twice
Drop counter permanently non-zero, no complaintsfiltering and transition blocking folded into itdeciding_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 numberChapter 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

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.