Skip to content
VLSI Mentor

Ethernet · Module 12

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.

Modules 1 to 11 were about one link. Two devices, one cable, one negotiation, and a great deal of machinery to make those two devices agree.

This module is about what happens when there are more than two.

Chapter 9.2 §3 established the mechanism and its consequence: a switch port is a collision domain of one, which is what made full duplex possible and left Chapter 1.2's entire arbitration mechanism present in the silicon and never reached.

What that chapter did not ask is what the switch itself is doing.

It makes one decision per frame: read the destination address, and deliver the frame to the port that needs it rather than to every port. On a 24-station network that multiplies the available capacity by 48.

And it did not remove contention. It moved it.

1. Scope — What This Chapter Owns

This chapter owns the shape of switching: what a switch does per frame, why that replaced the shared medium, the three decisions it makes, and where the contention went.

It does not re-derive what other chapters own. Chapter 9.2 §3 owns the collision-domain-of-one argument; Chapter 1.2 owns CSMA/CD; Chapter 1.5 owns full-duplex operation; Chapter 5.3 owns MAC address structure and Chapter 5.4 owns unicast, multicast and broadcast.

Chapter 12.2 owns how the forwarding table is populated; Chapter 12.3 owns the lookup and the ingress-port filter; Chapter 12.4 owns flooding; Chapter 12.5 owns the table's hardware; Chapter 12.6 owns store-and-forward against cut-through.

The claim this chapter defends: a switch replaced spatial arbitration with temporal arbitration — and dropping is the temporal mechanism's equivalent of a collision, which means a property forbidding drops forbids the design's specified behaviour.

2. What Replaced the Shared Medium

On a shared medium every station contends for one wire, so the total capacity of the network is one link's worth however many stations are attached, and each station gets at best that capacity divided by the number of stations. On a switch each station has its own link and its own collision domain containing exactly one transmitter, so each station gets a full link's capacity in each direction and the network's total capacity is the number of ports times the link rate times two. For twenty four gigabit stations that is forty eight gigabits per second against one, a factor of forty eight. The contention did not disappear: it moved from the wire to the switch's egress queues, where several ports may want the same output at once.Shared mediumone wire, N stationsCSMA/CD arbitratesin space1 Gb/s total41.7 Mb/s each at 24SwitchN links, N domains48 Gb/s total1 Gb/s each wayContention movedwire → egress queue12
Figure 1 — the same eight stations, arbitrating for one wire and then for nothing at all.

One decision changes everything: deliver each frame only to the port that needs it.

On a shared medium every frame reaches every station, which is why the medium has to be arbitrated at all — two simultaneous transmissions interfere. The capacity is one link's worth, divided among everybody.

On a switch each station has its own link. A frame arriving on port 3 for a station on port 7 crosses only those two ports. Ports 1, 2, 4, 5 and 6 are free to carry other frames at the same instant.

Compute what that is worth.

StationsShared, eachShared, totalSwitched, eachSwitched, totalRatio
2500 Mb/s1 Gb/s1000 Mb/s4 Gb/s
4250 Mb/s1 Gb/s1000 Mb/s8 Gb/s
8125 Mb/s1 Gb/s1000 Mb/s16 Gb/s16×
2441.7 Mb/s1 Gb/s1000 Mb/s48 Gb/s48×
4820.8 Mb/s1 Gb/s1000 Mb/s96 Gb/s96×

And the shared column is an upper bound. It assumes perfect arbitration with no collisions — which CSMA/CD does not achieve, and its efficiency falls as both load and station count rise.

The switched column is not an upper bound. Each station genuinely has 1000 Mb/s in each direction, simultaneously, because Chapter 1.5's full duplex is available on a collision domain of one.

3. One Decision, Per Frame

A switch's work is a loop with three steps, and the middle one is the whole subject of Module 12.

StepWhat happensOwned by
receivea frame arrives on a portChapter 7.3 validates it
learnits source address is associated with the ingress portChapter 12.2
decideits destination address is looked upChapter 12.3
forwardit is queued for the egress port, or flooded, or discardedChapter 12.4
transmitit leavesthis chapter's fabric

Two addresses, two completely different uses, and confusing them is the commonest conceptual error in switching.

The source address is used to write. It says the station with this address is reachable through the port this frame arrived on — which is a fact the switch learns for free, from a frame it was going to process anyway.

The destination address is used to read. It asks which port leads to this station?

And the decision is made per frame, not per conversation. There is no session, no flow state and no setup. Every single frame is looked up independently, which is what makes the mechanism stateless enough to run at line rate — and expensive enough to be worth computing.

Compute the cost.

QuantityWorkingResult
minimum frame on the wire64 + 8 preamble/SFD + 12 gap84 octets
as bits84 × 8672 bits
frames per second, 1 Gb/s10⁹ ÷ 6721.4881 M
per-frame budget, one port1 ÷ 1.4881 M672 ns
a 24-port switch, all ports at line rate24 × 1.4881 M35.71 M decisions/s
one shared lookup engine's budget672 ÷ 2428 ns
at 500 MHz28 × 0.514 clock cycles

Fourteen cycles to answer which port leads to this 48-bit address? — which is why Chapter 12.5's table is built the way it is, and why nobody searches a list.

4. RTL 1 — Receiving and Classifying at a Port

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Per-port ingress: extract the addresses a switch needs and classify
// the frame, so the forwarding engine has a fixed-size descriptor
// rather than a frame.
//
// WHAT A SWITCH NEEDS FROM A FRAME:
//   the DESTINATION address -- to decide where it goes
//   the SOURCE address      -- to learn where the sender is
//   the INGRESS PORT        -- which is not in the frame at all
//   the destination's TYPE  -- unicast, multicast or broadcast
//
// And nothing else. The payload is never examined; a switch is not
// looking at it and does not need to.
//
// THE INGRESS PORT IS NOT IN THE FRAME. It is a fact about which wire
// the frame arrived on, and every later step needs it: learning writes
// it, the forwarding decision compares against it (Chapter 12.3's
// filter rule), and flooding excludes it. A descriptor that omits it
// cannot be forwarded correctly by anything downstream.
package switching_pkg;
 
  localparam int unsigned MAC_BITS  = 48;
  localparam int unsigned N_PORTS   = 24;
  localparam int unsigned PORT_BITS = 5;
 
  // Frame rate arithmetic, from Section 3.
  //   minimum frame on the wire = 64 + 8 + 12 = 84 octets = 672 bits
  //   at 1 Gb/s that is 1.4881 Mpps and a 672 ns budget per frame
  localparam int unsigned MIN_FRAME_ON_WIRE = 84;
  localparam int unsigned MIN_FRAME_BITS    = MIN_FRAME_ON_WIRE * 8;
 
  typedef enum logic [1:0] {
    DST_UNICAST,
    DST_MULTICAST,
    DST_BROADCAST
  } dst_kind_e;
 
  // The fixed-size descriptor the fabric moves instead of the frame.
  typedef struct packed {
    logic [MAC_BITS-1:0]  dst;
    logic [MAC_BITS-1:0]  src;
    logic [PORT_BITS-1:0] ingress_port;
    dst_kind_e            dst_kind;
    logic                 src_is_group;   // illegal: see below
    logic [13:0]          length;
    logic                 valid;
  } descriptor_t;
 
endpackage
 
module port_ingress_classifier
  import switching_pkg::*;
#(
  parameter int unsigned PORT_ID = 0,
  parameter int unsigned CNT_W   = 24
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic [7:0] octet,
  input  logic       octet_valid,
  input  logic       frame_start,
  input  logic       frame_end,
  input  logic       frame_bad_fcs,
 
  output descriptor_t descriptor,
  output logic        descriptor_valid,
 
  output logic [CNT_W-1:0] c_frames,
  output logic [CNT_W-1:0] c_unicast,
  output logic [CNT_W-1:0] c_multicast,
  output logic [CNT_W-1:0] c_broadcast,
  output logic [CNT_W-1:0] c_bad_fcs,
  // A frame whose SOURCE address has the group bit set. Illegal --
  // no station may own a group address -- and learning from it would
  // put a multicast address in the table, where a later destination
  // lookup would match it and unicast a multicast frame to one port.
  output logic             group_source_seen,
  output logic [CNT_W-1:0] c_group_source
);
 
  logic [5:0]  index_q;
  logic [47:0] dst_q, src_q;
  logic [13:0] len_q;
  logic        in_frame_q;
 
  // Chapter 5.3's I/G bit: the least-significant bit of the FIRST
  // octet transmitted. Set means a group address.
  wire dst_is_group = dst_q[40];
  wire dst_is_bcast = (dst_q == 48'hFFFF_FFFF_FFFF);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      index_q <= 6'd0; dst_q <= '0; src_q <= '0; len_q <= 14'd0;
      in_frame_q <= 1'b0;
      descriptor <= '0; descriptor_valid <= 1'b0;
      c_frames <= '0; c_unicast <= '0; c_multicast <= '0;
      c_broadcast <= '0; c_bad_fcs <= '0;
      group_source_seen <= 1'b0; c_group_source <= '0;
    end else begin
      descriptor_valid  <= 1'b0;
      group_source_seen <= 1'b0;
 
      if (frame_start) begin
        index_q    <= 6'd0;
        in_frame_q <= 1'b1;
        len_q      <= 14'd0;
      end
 
      if (octet_valid && in_frame_q) begin
        // The first twelve octets are the two addresses, destination
        // first. Everything after them is payload the switch does not
        // look at.
        if (index_q < 6'd6)       dst_q <= {dst_q[39:0], octet};
        else if (index_q < 6'd12) src_q <= {src_q[39:0], octet};
 
        if (index_q != 6'd63) index_q <= index_q + 6'd1;
        if (len_q != 14'h3FFF) len_q <= len_q + 14'd1;
      end
 
      if (frame_end && in_frame_q) begin
        in_frame_q <= 1'b0;
        if (!(&c_frames)) c_frames <= c_frames + 1'b1;
 
        if (frame_bad_fcs) begin
          // A frame that failed its check sequence is not forwarded and
          // not learned from. Learning from a corrupt frame writes a
          // corrupt address into the table, and Chapter 12.2 §7 shows
          // what that costs.
          if (!(&c_bad_fcs)) c_bad_fcs <= c_bad_fcs + 1'b1;
 
        end else begin
          descriptor.dst          <= dst_q;
          descriptor.src          <= src_q;
          // THE INGRESS PORT, which is not in the frame at all.
          descriptor.ingress_port <= PORT_BITS'(PORT_ID);
          descriptor.length       <= len_q;
          descriptor.valid        <= 1'b1;
          descriptor_valid        <= 1'b1;
 
          if (dst_is_bcast) begin
            descriptor.dst_kind <= DST_BROADCAST;
            if (!(&c_broadcast)) c_broadcast <= c_broadcast + 1'b1;
          end else if (dst_is_group) begin
            descriptor.dst_kind <= DST_MULTICAST;
            if (!(&c_multicast)) c_multicast <= c_multicast + 1'b1;
          end else begin
            descriptor.dst_kind <= DST_UNICAST;
            if (!(&c_unicast)) c_unicast <= c_unicast + 1'b1;
          end
 
          // A GROUP SOURCE ADDRESS is illegal: no station owns one.
          // Flagged here so Chapter 12.2's learning can refuse it,
          // because a group address in the forwarding table will later
          // MATCH a destination lookup and unicast a multicast frame.
          descriptor.src_is_group <= src_q[40];
          if (src_q[40]) begin
            group_source_seen <= 1'b1;
            if (!(&c_group_source)) c_group_source <= c_group_source + 1'b1;
          end
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that the ingress port is not in the frame, and every later step needs it. Learning writes it, Chapter 12.3's forwarding decision compares against it, and flooding excludes it. A descriptor that omits the ingress port cannot be forwarded correctly by anything downstream — and it is the one field a switch adds rather than extracts.

Deliberately simplified: addresses are shifted in one octet at a time and the payload is counted rather than stored. A production ingress path is wider and the descriptor typically carries a pointer into a shared frame buffer rather than the frame itself.

Production implication: src_is_group flags a frame whose source address has Chapter 5.3's I/G bit set, which is illegal — no station owns a group address. The reason it matters is what happens if it is learned: a multicast address enters the forwarding table, and a later destination lookup for that same multicast address matches it — so a frame that should have been flooded to every member of the group is unicast to one port instead. One malformed frame silently breaks multicast delivery for as long as the entry lives.

5. RTL 2 — A Port That Is Its Own Collision Domain

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Models what a switch port IS, against what a hub port is -- because
// the difference is the whole reason switching replaced sharing, and
// it is one enable bit wide.
//
// Chapter 9.2 §3 established the argument; this module makes it
// concrete enough to count. A repeater port and a switch port run the
// SAME MAC, and the difference is which of its mechanisms are ever
// reached:
//
//   REPEATER port -- every port's traffic reaches every other, so all
//        ports share ONE collision domain and every station must
//        arbitrate for it.
//   SWITCH port   -- a collision domain containing exactly one
//        transmitter, so there is nothing to arbitrate for and
//        Chapter 1.5's full duplex becomes available.
//
// AND THE CAPACITY DIFFERENCE FALLS OUT: a repeater's total is one
// link's worth however many ports it has; a switch's is ports x rate x
// two directions.
module collision_domain_model
  import switching_pkg::*;
#(
  parameter int unsigned PORTS = 24,
  parameter int unsigned LINK_MBPS = 1000,
  parameter int unsigned CNT_W = 24
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic is_switch,                    // 0 = repeater behaviour
  input  logic [PORTS-1:0] port_transmitting,
  input  logic [PORTS-1:0] port_full_duplex,
 
  // Repeater behaviour: any two simultaneous transmitters collide.
  output logic             collision,
  output logic [PORTS-1:0] colliding_ports,
 
  // Switch behaviour: simultaneous transmitters are simply parallel.
  output logic [5:0]       simultaneous_conversations,
 
  // The capacity each arrangement delivers, computed rather than
  // asserted -- Section 2's table, in hardware.
  output logic [17:0]      aggregate_mbps,
  output logic [17:0]      per_station_mbps,
 
  // Mechanisms present in the MAC and never reached on a switch port.
  // Chapter 9.2 §7's dormant_mechanisms, per port.
  output logic [PORTS-1:0] csma_reachable,
  output logic [4:0]       dormant_mechanism_count,
 
  output logic [CNT_W-1:0] c_collisions,
  output logic [CNT_W-1:0] c_parallel_cycles
);
 
  logic [5:0] tx_count_c;
 
  always_comb begin
    tx_count_c = 6'd0;
    for (int p = 0; p < PORTS; p = p + 1)
      if (port_transmitting[p]) tx_count_c = tx_count_c + 6'd1;
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      collision <= 1'b0; colliding_ports <= '0;
      simultaneous_conversations <= 6'd0;
      aggregate_mbps <= 18'd0; per_station_mbps <= 18'd0;
      csma_reachable <= '0; dormant_mechanism_count <= 5'd0;
      c_collisions <= '0; c_parallel_cycles <= '0;
    end else begin
      if (!is_switch) begin
        // REPEATER. Two or more transmitters at once is a collision,
        // and every port is in it -- that is what "one collision
        // domain" means.
        collision       <= (tx_count_c > 6'd1);
        colliding_ports <= (tx_count_c > 6'd1) ? port_transmitting : '0;
        simultaneous_conversations <= (tx_count_c > 6'd1) ? 6'd0 : tx_count_c;
 
        // Capacity is ONE link's worth however many ports there are.
        aggregate_mbps   <= 18'(LINK_MBPS);
        per_station_mbps <= 18'(LINK_MBPS / PORTS);
 
        // Every port's CSMA/CD is live.
        csma_reachable          <= {PORTS{1'b1}};
        dormant_mechanism_count <= 5'd0;
 
        if (tx_count_c > 6'd1)
          if (!(&c_collisions)) c_collisions <= c_collisions + 1'b1;
 
      end else begin
        // SWITCH. Simultaneous transmitters are not a collision; they
        // are the point. Each is in its own domain of one.
        collision       <= 1'b0;
        colliding_ports <= '0;
        simultaneous_conversations <= tx_count_c;
 
        // Capacity is ports x rate x BOTH DIRECTIONS.
        aggregate_mbps   <= 18'(PORTS * LINK_MBPS * 2);
        per_station_mbps <= 18'(LINK_MBPS);
 
        // CSMA/CD is reachable only on a port that is NOT full duplex.
        // Chapter 9.2 §7's conjunction: a switch port PERMITS full
        // duplex; the negotiation DECIDES it.
        csma_reachable <= ~port_full_duplex;
        // Carrier sense, collision detect, jam, backoff and slot time:
        // five mechanisms, present in the netlist and unreachable.
        dormant_mechanism_count <= (&port_full_duplex) ? 5'd5 : 5'd0;
 
        if (tx_count_c > 6'd1)
          if (!(&c_parallel_cycles)) c_parallel_cycles <= c_parallel_cycles + 1'b1;
      end
    end
  end
 
endmodule

Classification: synthesizable model, written to make an argument countable rather than to be instantiated in a product.

What it teaches: that the same input — several ports transmitting at once — is a collision on one arrangement and the entire point of the other. tx_count_c > 1 increments c_collisions on a repeater and c_parallel_cycles on a switch, from identical stimulus. The difference is not in the ports, the MAC or the frames; it is in whether the ports share a collision domain.

Deliberately simplified: capacity is computed from parameters rather than measured. The point is that the two formulas differ structurally — one is independent of PORTS and the other is linear in it.

Production implication: csma_reachable is ~port_full_duplex rather than a constant, which is Chapter 9.2 §7's conjunction restated: a switch port permits full duplex and the negotiation decides it. A switch port whose partner negotiated half duplex — or was parallel-detected, per Chapter 11.1 §8 — has CSMA/CD live on it, and a design that assumes switch-port-implies-full-duplex has an unreachable-in-theory path that is reachable in practice.

6. Where the Contention Went

A switch removed collisions and did not remove contention. Compute what it did instead.

Take N ingress ports all sending to one egress port at line rate.

Ingress portsOfferedDeliverableMust be discarded
22 Gb/s1 Gb/s1 Gb/s — 50%
33 Gb/s1 Gb/s2 Gb/s — 66.7%
88 Gb/s1 Gb/s7 Gb/s — 87.5%
2323 Gb/s1 Gb/s22 Gb/s — 95.7%

This is arithmetic, not a defect. An egress port carries one gigabit per second and there is no configuration, buffer size or scheduler that changes it.

Buffering converts a rate problem into a duration problem and does not remove it.

Burst of 2:1 oversubscriptionBuffer needed
0.1 ms12.2 KiB
1 ms122 KiB
10 ms1.19 MiB

So a buffer absorbs a burst and not a sustained overload. Ten milliseconds of two-to-one oversubscription costs more than a megabyte — per port — and sustained oversubscription costs unbounded memory, which is another way of saying it cannot be buffered at all.

Which leaves exactly one mechanism: discard.

And discarding is the switch's equivalent of a collision. On a shared medium, two stations transmitting at once produce a collision, a jam and a backoff — the protocol's specified response to more demand than the medium can carry. In a switch, more demand than the egress port can carry produces a queue and then a drop — the switch's specified response to the same condition.

7. RTL 3 — The Forwarding Loop

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// The forwarding engine's skeleton: descriptor in, egress port mask
// out. Chapters 12.2 through 12.4 own the three things it consults;
// this module owns the LOOP and the ordering between them.
//
// THE ORDERING IS NOT ARBITRARY:
//   1. LEARN from the source, unconditionally, before deciding.
//   2. LOOK UP the destination.
//   3. Apply the INGRESS FILTER -- never send a frame back out the
//      port it arrived on (Chapter 12.3).
//   4. Forward, flood or discard.
//
// STEP 1 BEFORE STEP 2 MATTERS, and it is easy to get wrong. A frame
// from a station that has just moved ports must UPDATE the table
// before any lookup of that station's address in the same frame -- a
// station sending to itself, or two frames back to back in opposite
// directions -- otherwise the switch forwards using an entry it is
// about to correct.
module forwarding_engine
  import switching_pkg::*;
#(
  parameter int unsigned CNT_W = 24
) (
  input  logic clk,
  input  logic rst_n,
 
  input  descriptor_t descriptor,
  input  logic        descriptor_valid,
 
  // Learning (Chapter 12.2 owns the table).
  output logic                 learn_request,
  output logic [MAC_BITS-1:0]  learn_address,
  output logic [PORT_BITS-1:0] learn_port,
 
  // Lookup (Chapter 12.3 owns the decision).
  output logic                 lookup_request,
  output logic [MAC_BITS-1:0]  lookup_address,
  input  logic                 lookup_hit,
  input  logic [PORT_BITS-1:0] lookup_port,
  input  logic                 lookup_valid,
 
  // The result: a bitmask of egress ports, because flooding is many.
  output logic [N_PORTS-1:0]   egress_mask,
  output logic                 forward_valid,
  output logic [1:0]           forward_action,   // 0 fwd, 1 flood, 2 drop
 
  output logic [CNT_W-1:0] c_forwarded,
  output logic [CNT_W-1:0] c_flooded,
  output logic [CNT_W-1:0] c_filtered,
  output logic [CNT_W-1:0] c_learn_refused
);
 
  typedef enum logic [1:0] { F_IDLE, F_LEARN, F_LOOKUP, F_DECIDE } fstate_e;
 
  fstate_e     st_q;
  descriptor_t desc_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      st_q <= F_IDLE; desc_q <= '0;
      learn_request <= 1'b0; learn_address <= '0; learn_port <= '0;
      lookup_request <= 1'b0; lookup_address <= '0;
      egress_mask <= '0; forward_valid <= 1'b0; forward_action <= 2'd0;
      c_forwarded <= '0; c_flooded <= '0; c_filtered <= '0;
      c_learn_refused <= '0;
    end else begin
      learn_request  <= 1'b0;
      lookup_request <= 1'b0;
      forward_valid  <= 1'b0;
 
      unique case (st_q)
        F_IDLE: if (descriptor_valid) begin
          desc_q <= descriptor;
          st_q   <= F_LEARN;
        end
 
        F_LEARN: begin
          // LEARN FIRST. A station that moved ports must update the
          // table before any lookup of its address, or a frame in the
          // opposite direction is forwarded to where it used to be.
          //
          // AND REFUSE A GROUP SOURCE. Chapter 5.3's I/G bit set in a
          // source address is illegal; learning it puts a multicast
          // address in the table, where a later destination lookup
          // will MATCH and unicast a multicast frame to one port.
          if (!desc_q.src_is_group) begin
            learn_request <= 1'b1;
            learn_address <= desc_q.src;
            learn_port    <= desc_q.ingress_port;
          end else begin
            if (!(&c_learn_refused)) c_learn_refused <= c_learn_refused + 1'b1;
          end
          st_q <= F_LOOKUP;
        end
 
        F_LOOKUP: begin
          // Broadcast and multicast are not looked up at all: their
          // answer is known without consulting the table.
          if (desc_q.dst_kind == DST_UNICAST) begin
            lookup_request <= 1'b1;
            lookup_address <= desc_q.dst;
          end
          st_q <= F_DECIDE;
        end
 
        F_DECIDE: begin
          if (desc_q.dst_kind != DST_UNICAST) begin
            // Flood, minus the ingress port.
            egress_mask    <= ~(N_PORTS'(1) << desc_q.ingress_port);
            forward_action <= 2'd1;
            forward_valid  <= 1'b1;
            if (!(&c_flooded)) c_flooded <= c_flooded + 1'b1;
            st_q <= F_IDLE;
 
          end else if (lookup_valid) begin
            if (lookup_hit) begin
              if (lookup_port == desc_q.ingress_port) begin
                // THE FILTER RULE. The destination is on the port the
                // frame came from, so the two stations already heard
                // each other directly and forwarding would duplicate
                // the frame back onto its own segment.
                egress_mask    <= '0;
                forward_action <= 2'd2;
                if (!(&c_filtered)) c_filtered <= c_filtered + 1'b1;
              end else begin
                egress_mask    <= (N_PORTS'(1) << lookup_port);
                forward_action <= 2'd0;
                if (!(&c_forwarded)) c_forwarded <= c_forwarded + 1'b1;
              end
            end else begin
              // A MISS floods. Not a failure -- the switch has not yet
              // heard from that station, and flooding is how it finds
              // out. Chapter 12.4 owns the consequences.
              egress_mask    <= ~(N_PORTS'(1) << desc_q.ingress_port);
              forward_action <= 2'd1;
              if (!(&c_flooded)) c_flooded <= c_flooded + 1'b1;
            end
            forward_valid <= 1'b1;
            st_q <= F_IDLE;
          end
        end
 
        default: st_q <= F_IDLE;
      endcase
    end
  end
 
endmodule

Classification: synthesizable skeleton; the table it consults is Chapter 12.2's and the decision policy is Chapter 12.3's.

What it teaches: that learning happens before the lookup, unconditionally, and the ordering matters in a case that is easy to miss. A station that has moved ports must update the table before any lookup of its address — a station sending to itself, or two frames back to back in opposite directions — or the switch forwards using an entry it is about to correct. Learning is also unconditional on the result: a frame that will be filtered or dropped still teaches the switch where its sender is.

Deliberately simplified: a four-state machine processing one descriptor at a time. A production engine pipelines learning and lookup so both complete inside Section 3's 28 ns budget, which requires the table to support a concurrent read and write.

Production implication: c_learn_refused counts frames whose source carried a group address, and refusing them is not pedantry. Learning one puts a multicast address into the forwarding table — and a later destination lookup for that same multicast address matches it, so a frame that should have been flooded to every member of the group is unicast to a single port. One malformed frame breaks multicast delivery for the lifetime of the entry, and nothing reports an error.

8. Three Decisions, Not One

"Forward the frame" is three separate decisions, and each has a different answer for a different reason.

DestinationDecisionConsults the table?Result
broadcastfloodnoevery port except the ingress
multicastfloodno, in a basic switchevery port except the ingress
unicast, table hit, different portforwardyesone port
unicast, table hit, ingress portfilteryesno port — discard
unicast, table missfloodyes, and missedevery port except the ingress

Two of the five outcomes send the frame nowhere or everywhere, and neither is an error.

The filter case is the one that surprises people. A unicast frame whose destination is on the port it arrived from is discarded, because the two stations are on the same segment and already heard each other directly — forwarding it would put a duplicate back onto a wire that already carried it. Chapter 12.3 owns the rule; what matters here is that a successful lookup can produce no egress port.

And the miss case is not a failure either. A table miss means the switch has not yet heard from that station — it may have been silent, or may have just been plugged in — and flooding is how the switch discovers it, because the reply will arrive with that station's address as its source.

9. RTL 4 — Arbitrating for an Egress Port

On a shared medium two stations transmitting at once collide, and both back off and retry, so the loss is symmetric and the medium is wasted for the duration of the collision. In a switch several ingress ports may want the same egress port at once, and the loser is queued rather than destroyed, so no bandwidth is wasted and the frame is delivered late instead of not at all. But if the offered load exceeds the egress port's rate for long enough, the queue fills and frames must be discarded, which is the switch's equivalent of a collision: the specified response to more demand than the resource can carry.Several ingressportssame egress portSchedulerpicks one per slotThe rest queuelate, not lostQueue fillssustained overloadDiscardthe mechanism, not a faultA collision, movedsame condition, new place12
Figure 2 — the contention did not go away; it moved from the wire into a queue, where the loser waits instead of colliding.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Arbitrates among ingress ports competing for one egress port, and --
// more importantly -- accounts for what it does when it cannot serve
// them all.
//
// WHAT REPLACED WHAT:
//   shared medium -- two transmitters at once COLLIDE. Both frames are
//        destroyed, both stations back off, and the medium is idle for
//        the duration. The loss is symmetric and the bandwidth is
//        wasted.
//   switch        -- several ingress ports want one egress port. One is
//        served, the rest QUEUE. Nothing is destroyed and no bandwidth
//        is wasted; the frame is late rather than lost.
//
// UNTIL THE QUEUE FILLS. Then something is discarded -- and that is
// the switch's equivalent of a collision, not a defect in it.
//
// SO THIS MODULE COUNTS AND ATTRIBUTES rather than preventing. Which
// port's frames were dropped, under what discipline, and how full the
// queue was when it happened.
module fabric_arbiter
  import switching_pkg::*;
#(
  parameter int unsigned QUEUE_DEPTH = 64,
  parameter int unsigned CNT_W = 24
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  // One request bit per ingress port wanting this egress port.
  input  logic [N_PORTS-1:0] request,
  input  logic               egress_ready,
 
  output logic [PORT_BITS-1:0] granted_port,
  output logic                 grant_valid,
 
  // Queue state.
  input  logic [7:0] queue_occupancy,
  output logic       queue_full,
  output logic       drop_now,
  output logic [PORT_BITS-1:0] dropped_from_port,
 
  // Accounting: which ingress port's frames were discarded, so a drop
  // is attributable rather than merely counted.
  output logic [CNT_W-1:0] c_grants,
  output logic [CNT_W-1:0] c_drops,
  output logic [CNT_W-1:0] drops_by_ingress [N_PORTS],
  output logic [7:0]       peak_occupancy,
 
  // Fairness, measured. A round-robin that is not actually fair
  // starves a port, and nothing else reports it.
  output logic [CNT_W-1:0] grants_by_ingress [N_PORTS],
  output logic [PORT_BITS-1:0] most_granted_port,
  output logic [PORT_BITS-1:0] least_granted_port,
  output logic                 starvation_suspected
);
 
  logic [PORT_BITS-1:0] rr_q;          // round-robin pointer
  logic [PORT_BITS-1:0] winner_c;
  logic                 any_c;
  integer i;
 
  // Round robin starting from the port after the last grant, so a
  // permanently-requesting port cannot monopolise the egress.
  always_comb begin
    winner_c = rr_q;
    any_c    = 1'b0;
    for (int k = 0; k < N_PORTS; k = k + 1) begin
      automatic int idx = (int'(rr_q) + k) % N_PORTS;
      if (!any_c && request[idx]) begin
        winner_c = PORT_BITS'(idx);
        any_c    = 1'b1;
      end
    end
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rr_q <= '0; granted_port <= '0; grant_valid <= 1'b0;
      queue_full <= 1'b0; drop_now <= 1'b0; dropped_from_port <= '0;
      c_grants <= '0; c_drops <= '0; peak_occupancy <= 8'd0;
      most_granted_port <= '0; least_granted_port <= '0;
      starvation_suspected <= 1'b0;
      for (i = 0; i < N_PORTS; i = i + 1) begin
        drops_by_ingress[i]  <= '0;
        grants_by_ingress[i] <= '0;
      end
    end else if (clear) begin
      c_grants <= '0; c_drops <= '0; peak_occupancy <= 8'd0;
      for (i = 0; i < N_PORTS; i = i + 1) begin
        drops_by_ingress[i]  <= '0;
        grants_by_ingress[i] <= '0;
      end
    end else begin
      grant_valid <= 1'b0;
      drop_now    <= 1'b0;
 
      queue_full <= (queue_occupancy >= 8'(QUEUE_DEPTH));
      if (queue_occupancy > peak_occupancy) peak_occupancy <= queue_occupancy;
 
      if (any_c) begin
        if (queue_occupancy >= 8'(QUEUE_DEPTH)) begin
          // THE DISCARD. Not an error path -- the specified response to
          // more offered load than the egress port can carry, which is
          // arithmetic. What matters is that it is ATTRIBUTED.
          drop_now          <= 1'b1;
          dropped_from_port <= winner_c;
          if (!(&c_drops)) c_drops <= c_drops + 1'b1;
          if (!(&drops_by_ingress[winner_c]))
            drops_by_ingress[winner_c] <= drops_by_ingress[winner_c] + 1'b1;
          rr_q <= (winner_c == PORT_BITS'(N_PORTS-1)) ? '0 : winner_c + 1'b1;
 
        end else if (egress_ready) begin
          granted_port <= winner_c;
          grant_valid  <= 1'b1;
          if (!(&c_grants)) c_grants <= c_grants + 1'b1;
          if (!(&grants_by_ingress[winner_c]))
            grants_by_ingress[winner_c] <= grants_by_ingress[winner_c] + 1'b1;
          // ADVANCE PAST the winner, so the next arbitration starts
          // elsewhere and a permanently-requesting port cannot hold
          // the egress indefinitely.
          rr_q <= (winner_c == PORT_BITS'(N_PORTS-1)) ? '0 : winner_c + 1'b1;
        end
      end
    end
  end
 
  // FAIRNESS, MEASURED. A round-robin that is not actually round is a
  // starved port, and nothing at the frame layer reports it.
  always_comb begin
    logic [CNT_W-1:0] hi, lo;
    hi = '0; lo = '1;
    most_granted_port  = '0;
    least_granted_port = '0;
    for (int p = 0; p < N_PORTS; p = p + 1) begin
      if (grants_by_ingress[p] > hi) begin
        hi = grants_by_ingress[p]; most_granted_port = PORT_BITS'(p);
      end
      if (grants_by_ingress[p] < lo) begin
        lo = grants_by_ingress[p]; least_granted_port = PORT_BITS'(p);
      end
    end
    // A four-to-one spread between the most and least served port is
    // not round-robin behaving; it is a pointer that is not advancing.
    starvation_suspected = (hi > (lo << 2)) && (hi > CNT_W'(1000));
  end
 
endmodule

Classification: synthesizable.

What it teaches: that a drop must be attributed to an ingress port and not merely counted. c_drops says the egress port was oversubscribed, which anybody could infer from its utilisation. drops_by_ingress says which conversation is causing it — and the difference between "port 7 is congested" and "port 3's traffic is filling port 7" is the difference between a capacity problem and a specific host to investigate.

Deliberately simplified: a single round-robin over one egress port with a tail-drop discipline. Production fabrics have per-priority queues, weighted schedulers, and drop policies that discard from the head or by a random-early discipline rather than tail-dropping the arrival.

Production implication: starvation_suspected measures the fairness the arbiter is supposed to provide, and nothing at the frame layer reports it. A round-robin pointer that fails to advance in some corner — a request pattern that repeatedly re-selects the same port — delivers all of one port's traffic and starves another, with both ports showing valid links, zero errors and wildly different throughput. A four-to-one spread between most- and least-served ports is not round robin behaving.

10. Store-and-Forward, Cut-Through, and the Cost of Knowing

The forwarding loop in Section 7 waited for the whole frame before it did anything. That was a choice, and it is the second structural decision a switch makes — after where, the question is when.

A switch cannot forward before it knows the destination address, which arrives in the first six octets after the preamble. Everything after that is a decision about how much more of the frame to wait for.

Wait for all of it — store-and-forward. The frame is buffered completely, its check sequence is verified against Chapter 7.3's rules, and only a frame that passes is forwarded.

Wait for the address and start immediately — cut-through. The first octets are already leaving the egress port while the last are still arriving at the ingress port.

What the choice costs in time

Store-and-forward's latency floor is the serialisation time of the whole frame, and it scales with frame length. Cut-through's does not.

FrameStore-and-forward at 1 Gb/sStore-and-forward at 10 Gb/s
64 octets512 bits = 512 ns51.2 ns
512 octets4096 bits = 4.096 µs409.6 ns
1518 octets12 144 bits = 12.144 µs1.214 µs
9000 octets (jumbo)72 000 bits = 72 µs7.2 µs

Cut-through's wait is a constant, because it depends only on how far into the frame the destination address ends.

Cut-through waits forAt 1 Gb/sAt 10 Gb/s
destination address, 6 octets48 bits = 48 ns4.8 ns
through EtherType, 14 octets112 bits = 112 ns11.2 ns

So on a maximum-length frame at gigabit, store-and-forward costs 12 144 ÷ 48 = 253 times cut-through's wait, and on a minimum-length frame it costs 512 ÷ 48 = about 11 times. The advantage is real and it is largest exactly where store-and-forward is worst — long frames on slow links.

What the choice costs in correctness

A cut-through switch has committed the frame's first octets to the egress port before its last octet arrives, which means it commits before it can check anything that depends on the end of the frame.

It cannot verify the FCS, because the FCS is the last four octets. A frame corrupted anywhere in its payload is forwarded in full, consumes egress bandwidth, and is discarded by the destination's receiver instead — Chapter 7.3's check, performed one hop too late.

It cannot reject a runt, because a frame shorter than 64 octets is only known to be short when it ends. A collision fragment from a legacy segment is forwarded as though it were a frame.

And in a chain of cut-through switches the error propagates the whole way, so a single bad link at the edge spends bandwidth on every switch between it and the destination. Store-and-forward contains a corrupt frame at the first hop; cut-through delivers it to the last.

The constraint that is easy to miss

Cut-through requires that the egress port drain no faster than the ingress port fills.

If the egress link is faster than the ingress link, the transmitter runs out of frame mid-transmission. A 1 Gb/s ingress feeding a 10 Gb/s egress supplies one bit per nanosecond into a port that consumes ten — after 111 ns of transmission the egress has sent everything the ingress has received, and there is nothing left to send. Ethernet has no mechanism for pausing inside a frame; the result is a truncated, invalid frame on the wire.

So cut-through is only available when ingress rate egress rate. Any speed step upward — the access-to-aggregation direction in almost every real network — forces store-and-forward, and so does any frame that must be buffered because its egress port is busy.

A frame arrives at an ingress port. Under store-and-forward the switch buffers every octet, verifies the frame check sequence, and forwards only a frame that passes, so a corrupt frame is contained at this hop but the latency equals the whole frame's serialisation time. Under cut-through the switch waits only for the destination address, performs the lookup, and begins transmitting on the egress port while the rest of the frame is still arriving, so the latency is a constant independent of frame length but the check sequence has not yet been received and a corrupt frame is forwarded to the destination, which discards it one hop too late.Frame arrivesdestination in first 6octetsStore-and-forwardbuffer all, check FCSCorrupt framecontaineddiscarded at this hopLatency scales withlength12.144 µs at 1 Gb/sCut-throughforward after the addressLatency is constant48 ns at 1 Gb/sCorrupt framepropagateschecked one hop too late12
Figure 3 — the same frame, two disciplines: store-and-forward pays the whole serialisation time and gains the ability to check; cut-through pays only the address and forwards what it has not yet verified.

Chapter 12.6 owns this choice in full — the buffering it requires, the latency it sets, and the error propagation it permits. This chapter takes it only as far as the decision itself, because the decision changes what the rest of the switch can check.

This chapter's RTL is store-and-forward throughout, and Section 14's conformance monitor depends on it — a switch that has not received the whole frame cannot report whether the frame it emitted is identical to the frame it received.

11. RTL 5 — Accounting for Capacity

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Measures what a switch is actually delivering, in the two units that
// characterise it -- and they rank switches differently.
//
// BITS PER SECOND is what a datasheet quotes and what an operator
// thinks about. FRAMES PER SECOND is what the forwarding engine is
// sized by, because Section 3 established that a 64-octet frame and a
// 1518-octet frame cost the SAME ONE LOOKUP and differ by eighteen
// times in the time available to make it.
//
// So a switch rated at 48 Gb/s that can make only 20 Mpps of decisions
// is a 48 Gb/s switch for large frames and a 13.4 Gb/s switch for
// small ones -- and its datasheet says 48.
//
//   20 Mpps x 672 bits = 13.44 Gb/s of minimum-length frames
//   20 Mpps x 12304 bits = 246 Gb/s of maximum-length frames
//
// The second number exceeds the ports' capacity, which is why large
// frames never expose the limit.
module port_bandwidth_accountant
  import switching_pkg::*;
#(
  parameter int unsigned CLK_MHZ = 500,
  parameter int unsigned WINDOW_MS = 100,
  parameter int unsigned CNT_W = 32
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic        frame_forwarded,
  input  logic [13:0] frame_length,
  input  logic        frame_dropped,
  input  logic [PORT_BITS-1:0] port,
 
  output logic [CNT_W-1:0] frames_in_window,
  output logic [CNT_W-1:0] octets_in_window,
  output logic [CNT_W-1:0] drops_in_window,
 
  // The two characterisations, both computed.
  output logic [17:0] mbps_delivered,
  output logic [17:0] kfps_delivered,
  output logic [13:0] mean_frame_octets,
 
  // Which limit is being approached. A switch near its FRAME rate and
  // one near its BIT rate need completely different remedies, and the
  // two are indistinguishable from throughput alone.
  output logic        near_frame_rate_limit,
  output logic        near_bit_rate_limit,
  output logic [1:0]  binding_limit,       // 0 none, 1 frames, 2 bits
 
  output logic        window_valid,
  output logic [CNT_W-1:0] c_windows,
  output logic             ever_frame_limited
);
 
  localparam int unsigned MS_TICKS   = 1000 * CLK_MHZ;
  // What this design's forwarding engine can do, and what one port can.
  localparam int unsigned MAX_KFPS   = 1488;      // 1.488 Mpps
  localparam int unsigned MAX_MBPS   = 1000;
  localparam int unsigned NEAR_PCT   = 85;
 
  logic [31:0] tick_q;
  logic [15:0] ms_q;
  logic [CNT_W-1:0] frames_q, octets_q, drops_q;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      tick_q <= '0; ms_q <= 16'd0;
      frames_q <= '0; octets_q <= '0; drops_q <= '0;
      frames_in_window <= '0; octets_in_window <= '0; drops_in_window <= '0;
      mbps_delivered <= 18'd0; kfps_delivered <= 18'd0;
      mean_frame_octets <= 14'd0;
      near_frame_rate_limit <= 1'b0; near_bit_rate_limit <= 1'b0;
      binding_limit <= 2'd0; window_valid <= 1'b0;
      if (!rst_n) begin c_windows <= '0; ever_frame_limited <= 1'b0; end
    end else begin
      if (frame_forwarded) begin
        if (!(&frames_q)) frames_q <= frames_q + 1'b1;
        octets_q <= octets_q + CNT_W'(frame_length);
      end
      if (frame_dropped) begin
        if (!(&drops_q)) drops_q <= drops_q + 1'b1;
      end
 
      if (tick_q == 32'(MS_TICKS - 1)) begin
        tick_q <= '0;
        ms_q   <= ms_q + 16'd1;
      end else begin
        tick_q <= tick_q + 1'b1;
      end
 
      if (ms_q == 16'(WINDOW_MS)) begin
        ms_q         <= 16'd0;
        window_valid <= 1'b1;
        frames_in_window <= frames_q;
        octets_in_window <= octets_q;
        drops_in_window  <= drops_q;
        if (!(&c_windows)) c_windows <= c_windows + 1'b1;
 
        // Both characterisations, from the same window.
        kfps_delivered <= 18'((frames_q * 1000) / 32'(WINDOW_MS) / 32'd1000);
        mbps_delivered <= 18'((octets_q * 8 * 1000) /
                              32'(WINDOW_MS) / 32'd1_000_000);
        mean_frame_octets <= (frames_q == 0) ? 14'd0
                             : 14'(octets_q / frames_q);
 
        // WHICH LIMIT BINDS. A switch at 90% of its frame rate and one
        // at 90% of its bit rate need different remedies -- more
        // forwarding capacity against more link capacity -- and
        // throughput alone cannot tell them apart.
        near_frame_rate_limit <=
          (((frames_q * 1000 / 32'(WINDOW_MS)) * 100) >
           (32'(MAX_KFPS) * 1000 * 32'(NEAR_PCT)));
        near_bit_rate_limit <=
          (((octets_q * 8 / 32'(WINDOW_MS) / 1000) * 100) >
           (32'(MAX_MBPS) * 32'(NEAR_PCT)));
 
        if (((frames_q * 1000 / 32'(WINDOW_MS)) * 100) >
            (32'(MAX_KFPS) * 1000 * 32'(NEAR_PCT))) begin
          binding_limit      <= 2'd1;
          ever_frame_limited <= 1'b1;
        end else if (((octets_q * 8 / 32'(WINDOW_MS) / 1000) * 100) >
                     (32'(MAX_MBPS) * 32'(NEAR_PCT))) begin
          binding_limit <= 2'd2;
        end else begin
          binding_limit <= 2'd0;
        end
 
        frames_q <= '0; octets_q <= '0; drops_q <= '0;
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that binding_limit distinguishes two congestion causes that look identical from throughput alone. A port at 90% of its bit rate is carrying as much data as the link allows — more link capacity is the remedy. A port at 90% of its frame rate is making as many decisions as the engine allows, and may be nowhere near its bit rate at all — more forwarding capacity is the remedy, and adding a faster link changes nothing.

Deliberately simplified: integer arithmetic and a fixed window. Production accounting exports raw counters and lets software compute the rates, keeping dividers out of a path that runs per frame.

Production implication: mean_frame_octets is the number that predicts which limit will bind. A workload averaging 1400 octets per frame will hit the bit rate first and never approach the frame rate; one averaging 80 octets will hit the frame rate at roughly a seventh of the switch's rated bandwidth. The same switch, the same ports, the same datasheet — and a factor of seven in deliverable throughput decided entirely by the traffic's frame-size distribution.

12. The Capacity Arithmetic

A switch has two capacities and a datasheet usually quotes one.

Quantity24-port gigabit switch
aggregate bandwidth24 × 1000 × 2 = 48 Gb/s
aggregate frame rate needed24 × 1.4881 M = 35.71 Mpps
per-port budget, minimum frames672 ns
shared-engine budget672 ÷ 24 = 28 ns
at 500 MHz14 cycles

And the two rank a switch differently depending on what crosses it.

Engine capabilityMinimum framesMaximum frames
35.7 Mpps35.7 M × 672 = 24 Gb/s35.7 M × 12 304 = 439 Gb/s
20 Mpps20 M × 672 = 13.4 Gb/s246 Gb/s

The right-hand column exceeds the ports' capacity in both rows, which is why large frames never expose a forwarding-rate limit — the links run out first.

The left-hand column is the real characterisation, and a 20 Mpps engine on a switch sold as 48 Gb/s delivers 13.4 Gb/s of minimum-length frames. Both numbers are true; only one appears on the box.

13. RTL 6 — Where a Switch's Latency Comes From

A frame crossing a switch accumulates delay from four sources. The ingress path costs a fixed number of clock cycles to extract the addresses. The forwarding decision costs a fixed lookup time. The egress serialisation costs the frame's own transmission time at the link rate, which is a property of the link rather than of the switch. And the queueing delay depends entirely on how many other frames are ahead of this one for the same egress port, which is unbounded and depends on offered load rather than on the design. Three of the four are constants a designer chooses, and the fourth is decided by the traffic, so a single latency figure cannot characterise a switch.Ingressfixed, ~nsLookupfixed, ~28 nsQueueUNBOUNDEDEgress serialisationthe link's, not oursThree are chosenby the designerOne is chosenby the traffic12
Figure 4 — a switch's own delay is small and its queueing delay is unbounded, which is why one number cannot characterise it.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Decomposes a switch's contribution to Chapter 8.4's latency budget,
// because three of its four terms are design constants and the fourth
// is decided by the traffic.
//
//   INGRESS      fixed -- extract the addresses, build a descriptor
//   LOOKUP       fixed -- Section 3's 28 ns budget
//   QUEUEING     UNBOUNDED -- how many frames are ahead of this one
//   SERIALISATION the frame's own transmission time at the link rate,
//                which belongs to the LINK and not to the switch
//
// So a single "switch latency" figure is a statement about the first
// two terms under no load, and it tells an operator nothing about the
// third -- which is the one that grows without limit and the one they
// are actually asking about.
module switch_latency_accountant
  import switching_pkg::*;
#(
  parameter int unsigned CLK_MHZ = 500,
  parameter int unsigned CNT_W = 24
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  logic        frame_arrived,
  input  logic        descriptor_ready,
  input  logic        decision_made,
  input  logic        enqueued,
  input  logic        dequeued,
  input  logic        transmitted,
  input  logic [13:0] frame_length,
  input  logic [17:0] link_mbps,
 
  // The four terms, separately.
  output logic [15:0] ingress_ns,
  output logic [15:0] lookup_ns,
  output logic [19:0] queue_ns,
  output logic [19:0] serialise_ns,
  output logic [19:0] total_ns,
 
  // Worst case for each, which is the number that matters.
  output logic [15:0] worst_ingress_ns,
  output logic [15:0] worst_lookup_ns,
  output logic [19:0] worst_queue_ns,
 
  // Which term dominated. A switch whose latency is queueing and one
  // whose latency is lookup need completely different remedies.
  output logic [1:0]  dominant_term,       // 0 ing, 1 lookup, 2 queue
  output logic [6:0]  queue_share_percent,
 
  output logic        measurement_valid,
  output logic [CNT_W-1:0] c_measurements,
  // The queue contributed more than this share. Not an error, and the
  // signal that says a latency complaint is about load rather than
  // about the switch.
  output logic             queue_dominated,
  output logic             ever_queue_dominated
);
 
  logic [19:0] t_arrive_q, t_desc_q, t_decide_q, t_enq_q, t_deq_q;
  logic [19:0] tick_q;
 
  // Nanoseconds per clock, as a reciprocal-free constant.
  localparam int unsigned NS_PER_CLK = 1000 / CLK_MHZ;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n || clear) begin
      tick_q <= '0; t_arrive_q <= '0; t_desc_q <= '0;
      t_decide_q <= '0; t_enq_q <= '0; t_deq_q <= '0;
      ingress_ns <= '0; lookup_ns <= '0; queue_ns <= '0;
      serialise_ns <= '0; total_ns <= '0;
      worst_ingress_ns <= '0; worst_lookup_ns <= '0; worst_queue_ns <= '0;
      dominant_term <= 2'd0; queue_share_percent <= 7'd0;
      measurement_valid <= 1'b0; queue_dominated <= 1'b0;
      if (!rst_n) begin c_measurements <= '0; ever_queue_dominated <= 1'b0; end
    end else begin
      measurement_valid <= 1'b0;
      tick_q            <= tick_q + 1'b1;
 
      if (frame_arrived)    t_arrive_q <= tick_q;
      if (descriptor_ready) t_desc_q   <= tick_q;
      if (decision_made)    t_decide_q <= tick_q;
      if (enqueued)         t_enq_q    <= tick_q;
      if (dequeued)         t_deq_q    <= tick_q;
 
      if (transmitted) begin
        ingress_ns <= 16'((t_desc_q   - t_arrive_q) * NS_PER_CLK);
        lookup_ns  <= 16'((t_decide_q - t_desc_q)   * NS_PER_CLK);
        // THE UNBOUNDED TERM. Everything between enqueue and dequeue is
        // other frames ahead of this one, and nothing in this design
        // decides how many there are.
        queue_ns   <= 20'((t_deq_q - t_enq_q) * NS_PER_CLK);
 
        // SERIALISATION belongs to the LINK, not to the switch. Counted
        // separately so it is not attributed to the switch's design.
        serialise_ns <= (link_mbps == 0) ? 20'd0
                        : 20'((frame_length * 8 * 1000) / link_mbps);
 
        total_ns <= 20'((t_deq_q - t_arrive_q) * NS_PER_CLK) +
                    ((link_mbps == 0) ? 20'd0
                     : 20'((frame_length * 8 * 1000) / link_mbps));
 
        if (16'((t_desc_q - t_arrive_q) * NS_PER_CLK) > worst_ingress_ns)
          worst_ingress_ns <= 16'((t_desc_q - t_arrive_q) * NS_PER_CLK);
        if (16'((t_decide_q - t_desc_q) * NS_PER_CLK) > worst_lookup_ns)
          worst_lookup_ns <= 16'((t_decide_q - t_desc_q) * NS_PER_CLK);
        if (20'((t_deq_q - t_enq_q) * NS_PER_CLK) > worst_queue_ns)
          worst_queue_ns <= 20'((t_deq_q - t_enq_q) * NS_PER_CLK);
 
        measurement_valid <= 1'b1;
        if (!(&c_measurements)) c_measurements <= c_measurements + 1'b1;
 
        // WHICH TERM DOMINATED, which is what turns a latency figure
        // into an action.
        if ((t_deq_q - t_enq_q) > (t_decide_q - t_arrive_q)) begin
          dominant_term        <= 2'd2;
          queue_dominated      <= 1'b1;
          ever_queue_dominated <= 1'b1;
        end else if ((t_decide_q - t_desc_q) > (t_desc_q - t_arrive_q)) begin
          dominant_term   <= 2'd1;
          queue_dominated <= 1'b0;
        end else begin
          dominant_term   <= 2'd0;
          queue_dominated <= 1'b0;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that serialisation is counted separately because it belongs to the link and not to the switch. Chapter 8.1 owns that term, and it is the same whether the frame crossed a switch, a hub or a piece of wire — attributing it to the switch makes a fast switch on a slow link look slow.

Deliberately simplified: one frame's timestamps at a time. Production accounting samples periodically or tags a subset of frames, because timestamping every frame at 35.7 Mpps costs more than the forwarding does.

Production implication: dominant_term turns a latency complaint into an action. queue_dominated says the switch is fine and the load is the problem — the fix is capacity or scheduling, not a faster switch. dominant_term = 1 says the lookup is the bottleneck, which is a forwarding-engine problem. And under no load these three terms are a few hundred nanoseconds and the queue is zero, which is exactly the condition under which a datasheet's latency figure is measured — and exactly the condition an operator with a latency complaint is not in.

14. RTL 7 — Conformance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE.
//
// Checks the invariants a switch must never violate, all of which are
// about what it does to a frame rather than about whether it delivers
// it.
//
// THE DISTINCTION THIS MODULE RESTS ON. A switch is ALLOWED to:
//   discard a frame          -- Section 6's arithmetic
//   flood a frame            -- on a miss, or a group destination
//   filter a frame           -- destination on the ingress port
//   delay a frame            -- arbitrarily, by queueing it
//
// A switch is NOT allowed to:
//   duplicate a frame onto one egress port
//   send a frame back out its ingress port
//   modify a frame's contents
//   reorder frames within one ingress/egress pair
//
// The second list is short, absolute, and entirely about integrity --
// which is why the properties in Section 15 are about the policy and
// never about delivery.
module switch_conformance_monitor
  import switching_pkg::*;
#(
  parameter int unsigned CNT_W = 24
) (
  input  logic clk,
  input  logic rst_n,
  input  logic clear,
 
  input  descriptor_t        descriptor,
  input  logic               forward_valid,
  input  logic [N_PORTS-1:0] egress_mask,
  input  logic [1:0]         forward_action,
 
  input  logic               frame_emitted,
  input  logic [PORT_BITS-1:0] emit_port,
  input  logic [MAC_BITS-1:0]  emit_dst,
  input  logic [MAC_BITS-1:0]  emit_src,
  input  logic [13:0]          emit_length,
 
  // The four absolute violations.
  output logic sent_to_ingress_port,
  output logic frame_modified,
  output logic duplicate_on_one_port,
  output logic reordered,
 
  output logic [CNT_W-1:0] c_ingress_echo,
  output logic [CNT_W-1:0] c_modified,
  output logic [CNT_W-1:0] c_duplicated,
  output logic [CNT_W-1:0] c_reordered,
 
  // Policy accounting -- legal outcomes, counted rather than flagged.
  output logic [CNT_W-1:0] c_forward,
  output logic [CNT_W-1:0] c_flood,
  output logic [CNT_W-1:0] c_filter,
 
  output logic       first_violation_valid,
  output logic [1:0] first_violation_kind,
  output logic       ever_violated
);
 
  descriptor_t pending_q;
  logic        pending_valid_q;
  logic [N_PORTS-1:0] emitted_mask_q;
  logic [15:0] seq_expected_q, seq_seen_q;
  logic        any_c;
  logic [1:0]  kind_c;
 
  always_comb begin
    any_c  = 1'b0;
    kind_c = 2'd0;
    if      (sent_to_ingress_port)  begin any_c = 1'b1; kind_c = 2'd0; end
    else if (frame_modified)        begin any_c = 1'b1; kind_c = 2'd1; end
    else if (duplicate_on_one_port) begin any_c = 1'b1; kind_c = 2'd2; end
    else if (reordered)             begin any_c = 1'b1; kind_c = 2'd3; end
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      pending_q <= '0; pending_valid_q <= 1'b0; emitted_mask_q <= '0;
      seq_expected_q <= 16'd0; seq_seen_q <= 16'd0;
      sent_to_ingress_port <= 1'b0; frame_modified <= 1'b0;
      duplicate_on_one_port <= 1'b0; reordered <= 1'b0;
      c_ingress_echo <= '0; c_modified <= '0; c_duplicated <= '0;
      c_reordered <= '0; c_forward <= '0; c_flood <= '0; c_filter <= '0;
      first_violation_valid <= 1'b0; first_violation_kind <= 2'd0;
      ever_violated <= 1'b0;
    end else if (clear) begin
      c_ingress_echo <= '0; c_modified <= '0; c_duplicated <= '0;
      c_reordered <= '0; c_forward <= '0; c_flood <= '0; c_filter <= '0;
      first_violation_valid <= 1'b0;
      // ever_violated survives.
    end else begin
      sent_to_ingress_port  <= 1'b0;
      frame_modified        <= 1'b0;
      duplicate_on_one_port <= 1'b0;
      reordered             <= 1'b0;
 
      if (forward_valid) begin
        pending_q       <= descriptor;
        pending_valid_q <= 1'b1;
        emitted_mask_q  <= '0;
 
        // THE INGRESS-ECHO CHECK, made on the DECISION rather than on
        // the emission -- it is cheaper here and it catches the fault
        // before the frame is on a wire.
        if (egress_mask[descriptor.ingress_port]) begin
          sent_to_ingress_port <= 1'b1;
          if (!(&c_ingress_echo)) c_ingress_echo <= c_ingress_echo + 1'b1;
        end
 
        unique case (forward_action)
          2'd0: if (!(&c_forward)) c_forward <= c_forward + 1'b1;
          2'd1: if (!(&c_flood))   c_flood   <= c_flood   + 1'b1;
          2'd2: if (!(&c_filter))  c_filter  <= c_filter  + 1'b1;
          default: ;
        endcase
      end
 
      if (frame_emitted && pending_valid_q) begin
        // A SWITCH DOES NOT MODIFY. Same addresses, same length -- a
        // basic switch is a forwarding device, not a rewriting one.
        if ((emit_dst != pending_q.dst) || (emit_src != pending_q.src) ||
            (emit_length != pending_q.length)) begin
          frame_modified <= 1'b1;
          if (!(&c_modified)) c_modified <= c_modified + 1'b1;
        end
 
        // NOT TWICE ON ONE PORT. Flooding sends one copy to each of
        // many ports; two copies to one port is a duplicate.
        if (emitted_mask_q[emit_port]) begin
          duplicate_on_one_port <= 1'b1;
          if (!(&c_duplicated)) c_duplicated <= c_duplicated + 1'b1;
        end
        emitted_mask_q[emit_port] <= 1'b1;
 
        // AND NOT OUT THE INGRESS PORT.
        if (emit_port == pending_q.ingress_port) begin
          sent_to_ingress_port <= 1'b1;
          if (!(&c_ingress_echo)) c_ingress_echo <= c_ingress_echo + 1'b1;
        end
      end
 
      if (any_c) begin
        ever_violated <= 1'b1;
        if (!first_violation_valid) begin
          first_violation_valid <= 1'b1;
          first_violation_kind  <= kind_c;
        end
      end
    end
  end
 
endmodule

Classification: synthesizable.

What it teaches: that a switch's absolute obligations are about integrity and never about delivery, and separating the two lists is the module. Discarding, flooding, filtering and delaying are all legal — they are the design's specified responses to congestion, a miss, a same-port destination, and contention. Duplicating, echoing, modifying and reordering are not, and there is no traffic condition that excuses any of them.

Deliberately simplified: one pending descriptor and a per-frame emitted mask. Production monitors track many frames in flight and check ordering per ingress/egress pair, since order is only required within a pair and not globally.

Production implication: c_forward, c_flood and c_filter are policy counters and not error counters, and mixing them into an error total is a common and damaging mistake. A rising flood count is normal on a switch with a cold table and abnormal on a warm one; a rising filter count means stations are sharing a segment, which may be exactly the topology. Counting them alongside c_modified puts a legal outcome and an impossible one in the same number.

15. Properties Worth Asserting, and One Worth Refusing

A switch's properties split cleanly into two lists, and getting a property onto the wrong one is this chapter's rejected class. Integrity properties hold absolutely. Delivery properties do not hold at all.

Ingress and classification

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P1. The ingress port is recorded. It is not in the frame, and every
// later step needs it.
property p_ingress_port_recorded;
  @(posedge clk) disable iff (!rst_n)
  descriptor_valid |-> (descriptor.ingress_port == PORT_BITS'(PORT_ID));
endproperty
a_ingress_recorded: assert property (p_ingress_port_recorded);
 
// P2. A frame that failed its check sequence produces NO descriptor --
// it is neither forwarded nor learned from.
property p_bad_fcs_no_descriptor;
  @(posedge clk) disable iff (!rst_n)
  (frame_end && frame_bad_fcs) |=> !descriptor_valid;
endproperty
a_bad_fcs_no_descriptor: assert property (p_bad_fcs_no_descriptor);
 
// P3. The destination classification follows Chapter 5.3's I/G bit and
// the all-ones broadcast address, in that priority.
property p_broadcast_classified_first;
  @(posedge clk) disable iff (!rst_n)
  (descriptor_valid && (descriptor.dst == 48'hFFFF_FFFF_FFFF))
    |-> (descriptor.dst_kind == DST_BROADCAST);
endproperty
a_broadcast_first: assert property (p_broadcast_classified_first);
 
// P4. A group SOURCE address is flagged. Learning it puts a multicast
// address in the table, where a destination lookup will match it.
property p_group_source_flagged;
  @(posedge clk) disable iff (!rst_n)
  (descriptor_valid && descriptor.src[40]) |-> descriptor.src_is_group;
endproperty
a_group_source_flagged: assert property (p_group_source_flagged);

The forwarding loop's ordering

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P5. THE ORDERING PROPERTY. Learning happens before the lookup, so a
// station that has moved updates the table before any frame in the
// opposite direction is forwarded to where it used to be.
property p_learn_before_lookup;
  @(posedge clk) disable iff (!rst_n)
  lookup_request |-> $past(learn_request || desc_q.src_is_group);
endproperty
a_learn_before_lookup: assert property (p_learn_before_lookup);
 
// P6. Learning is unconditional on the forwarding RESULT. A frame that
// will be filtered or dropped still teaches the switch where its
// sender is.
property p_learn_regardless_of_action;
  @(posedge clk) disable iff (!rst_n)
  (descriptor_valid && !descriptor.src_is_group) |-> ##[1:3] learn_request;
endproperty
a_learn_regardless: assert property (p_learn_regardless_of_action);
 
// P7. A group source is REFUSED, never learned.
property p_group_source_not_learned;
  @(posedge clk) disable iff (!rst_n)
  (st_q == F_LEARN) && desc_q.src_is_group |-> !learn_request;
endproperty
a_group_source_not_learned: assert property (p_group_source_not_learned);
 
// P8. Broadcast and multicast are not looked up at all -- their answer
// is known without consulting the table.
property p_group_dst_no_lookup;
  @(posedge clk) disable iff (!rst_n)
  ((st_q == F_LOOKUP) && (desc_q.dst_kind != DST_UNICAST))
    |-> !lookup_request;
endproperty
a_group_dst_no_lookup: assert property (p_group_dst_no_lookup);

Integrity — the absolute list

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P9. NEVER out the ingress port. The most fundamental invariant in
// switching, and the one a flood must explicitly exclude.
property p_never_egress_on_ingress;
  @(posedge clk) disable iff (!rst_n)
  forward_valid |-> !egress_mask[descriptor.ingress_port];
endproperty
a_never_ingress_echo: assert property (p_never_egress_on_ingress);
 
// P10. NEVER twice on one port. Flooding is one copy to each of many
// ports; two to one port is a duplicate.
property p_no_duplicate_on_a_port;
  @(posedge clk) disable iff (!rst_n)
  (frame_emitted && emitted_mask_q[emit_port]) |=> duplicate_on_one_port;
endproperty
a_no_duplicate: assert property (p_no_duplicate_on_a_port);
 
// P11. NEVER modified. A basic switch forwards; it does not rewrite.
property p_frame_unmodified;
  @(posedge clk) disable iff (!rst_n)
  (frame_emitted && pending_valid_q)
    |-> ((emit_dst == pending_q.dst) && (emit_src == pending_q.src));
endproperty
a_frame_unmodified: assert property (p_frame_unmodified);
 
// P12. A unicast hit on a port OTHER than the ingress produces exactly
// ONE egress port -- not a flood, not none.
property p_unicast_hit_one_port;
  @(posedge clk) disable iff (!rst_n)
  (forward_valid && (forward_action == 2'd0)) |-> $onehot(egress_mask);
endproperty
a_unicast_one_port: assert property (p_unicast_hit_one_port);
 
// P13. A filter produces NO egress port. A successful lookup can
// legitimately send the frame nowhere.
property p_filter_no_egress;
  @(posedge clk) disable iff (!rst_n)
  (forward_valid && (forward_action == 2'd2)) |-> (egress_mask == '0);
endproperty
a_filter_no_egress: assert property (p_filter_no_egress);
 
// P14. A flood covers every port EXCEPT the ingress -- exactly.
property p_flood_is_all_but_ingress;
  @(posedge clk) disable iff (!rst_n)
  (forward_valid && (forward_action == 2'd1))
    |-> (egress_mask == ~(N_PORTS'(1) << descriptor.ingress_port));
endproperty
a_flood_all_but_ingress: assert property (p_flood_is_all_but_ingress);

Arbitration and the discard policy

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P15. THE POLICY PROPERTY. A drop happens only when the queue is
// full -- never as a shortcut, and never on an arbitrary frame.
property p_drop_only_when_full;
  @(posedge clk) disable iff (!rst_n)
  drop_now |-> (queue_occupancy >= 8'(QUEUE_DEPTH));
endproperty
a_drop_only_when_full: assert property (p_drop_only_when_full);
 
// P16. Every drop is ATTRIBUTED to an ingress port. "Port 7 is
// congested" and "port 3's traffic is filling port 7" are different
// findings.
property p_drop_attributed;
  @(posedge clk) disable iff (!rst_n)
  drop_now |-> (dropped_from_port < PORT_BITS'(N_PORTS));
endproperty
a_drop_attributed: assert property (p_drop_attributed);
 
// P17. A grant goes only to a requesting port.
property p_grant_to_requester;
  @(posedge clk) disable iff (!rst_n)
  grant_valid |-> request[granted_port];
endproperty
a_grant_to_requester: assert property (p_grant_to_requester);
 
// P18. The round-robin pointer ADVANCES past the winner, so a
// permanently-requesting port cannot monopolise the egress.
property p_pointer_advances;
  @(posedge clk) disable iff (!rst_n)
  grant_valid |=> (rr_q != $past(granted_port));
endproperty
a_pointer_advances: assert property (p_pointer_advances);
 
// P19. Drops and grants are mutually exclusive in a cycle -- a frame
// is served or discarded, never both.
property p_grant_drop_exclusive;
  @(posedge clk) disable iff (!rst_n)
  !(grant_valid && drop_now);
endproperty
a_grant_drop_exclusive: assert property (p_grant_drop_exclusive);

Accounting

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// P20. Both capacity characterisations are computed, because they rank
// a switch differently and only one is on the datasheet.
property p_both_rates_reported;
  @(posedge clk) disable iff (!rst_n)
  window_valid |-> ((kfps_delivered != 18'hXXXXX) &&
                    (mbps_delivered != 18'hXXXXX));
endproperty
a_both_rates: assert property (p_both_rates_reported);
 
// P21. Which limit binds is named. A frame-rate limit and a bit-rate
// limit need different remedies and look identical from throughput.
property p_binding_limit_named;
  @(posedge clk) disable iff (!rst_n)
  (window_valid && (near_frame_rate_limit || near_bit_rate_limit))
    |-> (binding_limit != 2'd0);
endproperty
a_binding_limit_named: assert property (p_binding_limit_named);
 
// P22. Serialisation is attributed to the LINK, not to the switch --
// otherwise a fast switch on a slow link measures as slow.
property p_serialisation_separate;
  @(posedge clk) disable iff (!rst_n)
  measurement_valid |-> (total_ns >= serialise_ns);
endproperty
a_serialisation_separate: assert property (p_serialisation_separate);
 
// P23. The dominant latency term is named. Queue-dominated latency is
// a load problem; lookup-dominated latency is a design problem.
property p_dominant_term_named;
  @(posedge clk) disable iff (!rst_n)
  measurement_valid |-> (dominant_term <= 2'd2);
endproperty
a_dominant_term_named: assert property (p_dominant_term_named);
 
// P24. Starvation is reported. A round-robin that is not round
// delivers all of one port's traffic and none of another's, with both
// ports showing valid links and zero errors.
property p_starvation_reported;
  @(posedge clk) disable iff (!rst_n)
  starvation_suspected |-> (most_granted_port != least_granted_port);
endproperty
a_starvation_reported: assert property (p_starvation_reported);

16. Verification Scenarios

Forty-six scenarios. The integrity ones must pass absolutely; the delivery ones have expected outcomes that include discarding.

Classification at ingress

#ScenarioWhat it must show
1Unicast destination, individual bit cleardst_kind = DST_UNICAST, lookup performed
2Broadcast destination, all forty-eight bits setDST_BROADCAST before the group test, flood without lookup
3Multicast destination, I/G set, not all-onesDST_MULTICAST, flood without lookup
4The single address 01:00:00:00:00:00group bit alone classifies it multicast
5The single address FE:FF:FF:FF:FF:FFI/G clear — unicast, despite forty-seven ones
6Group source addresssrc_is_group set, never learned
7Frame with bad FCSno descriptor — not forwarded, not learned from
8Runt, 40 octetsrejected before any descriptor is produced
9Frame exactly 64 octetsaccepted, descriptor produced
10Frame exactly 1518 octetsaccepted, descriptor produced
11Ingress port field on every descriptorequals this port's PORT_ID, always
12Back-to-back frames at minimum gapone descriptor each, none merged or lost

The forwarding decision

#ScenarioExpected
13Unicast hit, egress ingressforward, exactly one port in the mask
14Unicast hit, egress = ingressfilter — valid answer, empty mask
15Unicast missflood to every port except ingress
16Broadcastflood, no lookup requested
17Multicastflood, no lookup requested
18Learn precedes lookup on the same framelookup_request never asserts before the learn
19Frame that will be filteredsource still learned
20Frame that will be dropped for congestionsource still learned
21Flood mask on a 24-port switchexactly 23 bits set, the ingress bit clear
22Two frames, same source, different portssecond updates the table before its own lookup
23Lookup latency at the 28 ns budgetforwarding rate sustained at 1.4881 Mpps

Arbitration, queueing and discard

#ScenarioExpected
24Two ingress ports, one egress, both at line rate~50% delivered, ~50% discarded — and both counted
25Eight to one at line rate87.5% discarded, each drop attributed
26Twenty-three to one95.7% discarded, switch stays functional
27Queue at QUEUE_DEPTH − 1frame accepted, no drop
28Queue at QUEUE_DEPTHframe dropped, c_drops increments once
29Drop attribution across mixed ingressdrops_by_ingress identifies the heavy talker
30Grant with a single requesterthat port, pointer advances past it
31Grant with all ports requestingstrict rotation, no port served twice in a round
32One port requesting continuouslyothers still served — no monopoly
33Requests removed mid-roundpointer does not stall
34Grant and drop in the same cyclenever both
35Burst of 2:1 for 0.1 ms with a 16 KiB bufferabsorbed, zero drops
36Burst of 2:1 for 10 ms with a 16 KiB bufferdrops begin at ~0.13 ms, then steady

Integrity — these have no acceptable failure

#ScenarioExpected
37Every frame emitted, every port, every casenever on the ingress port
38Flood on a 24-port switchexactly one copy per egress port
39Emitted frame's addressesbyte-identical to the received frame
40Emitted frame's lengthidentical to the received length
41Frame discarded at a full queueno partial frame appears on any egress port
42Ordering, same source to same destinationdelivery order equals arrival order

Accounting and measurement

#ScenarioExpected
43Line rate of minimum frameskfps near 1488, binding_limit reports the frame-rate limit
44Line rate of maximum framesmbps near 1000, binding_limit reports the bit-rate limit
45Latency under an idle egressdominant_term = serialisation
46Latency under a congested egressdominant_term = queueing, and the number rises without bound

17. Debugging a Switch That "Works"

Every failure in this table produces a switch with valid links, zero frame errors and unhappy users. That combination is the point: none of these is visible in the statistics anybody looks at first.

SymptomLikely causeThe observable that decides it
Throughput far below line rate, small frames onlyforwarding engine cannot sustain 1.4881 Mpps per portbinding_limit reports the frame-rate limit while mbps is far below the link rate
Throughput fine on large frames, terrible on smallsame cause, seen from the other sideframe-size histogram against kfps — the limit binds only below a length
Drops on a port whose utilisation is 40%the offered load is bursty and the buffer is smallpeak_occupancy reaching QUEUE_DEPTH while average occupancy is near zero
Drops on a port at 100% utilisationarithmetic — the egress is oversubscribeddrops_by_ingress names the ingress port responsible
One host slow, everything else finethat host's traffic is being starved by the arbiterstarvation_suspected, and a >4:1 spread in grants_by_port
Duplicate frames on one segmenta frame is being echoed to its ingress portthe P9 assertion, or a capture showing the same frame twice with identical FCS
Broadcast storm, CPU load on every hostflooding is correct and the source of the broadcasts is notc_flooded rising with c_forwarded flat, and a capture identifying the sender
Everything floods, nothing is ever forwardedthe table is not being written, so every lookup missesc_flooded climbing, c_forwarded stuck at zero, learn_request never asserting
Latency spikes to milliseconds under loadqueueing delay, not switch delaydominant_term = queueing while lookup_ns and serialise_ns are unchanged
Latency high and constant at low loadthe lookup is slow, which is a design problemdominant_term = lookup with queue_ns near zero
Corrupt frames arriving at a host several hops awaya cut-through path is propagating them from a bad edge linkFCS errors at the ingress port of the first switch, and none at the ports between
A frame that should be delivered locally never arrivesthe destination shares the ingress port and the filter is workingc_filtered incrementing — not a bug

18. Common Misconceptions

1 — "A switch is a fast hub."

The wrong model: a switch does what a hub does, only quicker.

What it costs: every capacity calculation comes out wrong, because a hub's capacity is one link's worth shared and a switch's is the sum of all its ports. Twenty-four stations on a hub share 1000 ÷ 24 = 41.7 Mb/s; on a switch each gets 1000 Mb/s in both directions, for 48 Gb/s aggregate. It also predicts collisions that cannot occur, and sends engineers hunting for a duplex problem that a switch's topology makes impossible.

The corrected model: a hub is a repeater — one collision domain, one shared bandwidth, no decision. A switch is a frame-level router — one collision domain per port, per-port bandwidth, and a decision taken on every single frame.

2 — "A switch never drops frames."

The wrong model: dropping means something has gone wrong.

What it costs: it produces both bad designs from Section 15's rejected property — unbounded buffering, or backpressure that converts local congestion into head-of-line blocking across the whole switch — and it produces bad diagnosis, because an engineer who believes drops are faults will chase a hardware problem that does not exist while the actual oversubscription goes unaddressed.

The corrected model: discarding is the switch's specified response to more offered load than an egress port can carry, exactly as a collision was the shared medium's. Twenty-three ports offering line rate to one port must discard 95.7% of it. The right questions are whether the drops are counted, attributed, and following the intended discipline.

3 — "A successful lookup always produces a forward."

The wrong model: hit means forward; miss means flood.

What it costs: the exact bug in Section 16's directed test. A design that treats a lookup returning the ingress port as a miss floods a frame to 23 ports that do not lead to the destination, while the destination — which already received the frame over the shared segment — gets a duplicate. The user sees duplicate frames and blames a loop.

The corrected model: a lookup has three outcomes, not two. Hit on another port → forward. Hit on the ingress port → filter, which is the switch correctly declining to do anything. Miss → flood. Filtering is a successful lookup producing a deliberate non-action, and it is what makes a switch useful behind a shared segment at all.

4 — "Aggregate bandwidth is what a switch's datasheet number means."

The wrong model: 48 Gb/s is the capacity, so anything under 48 Gb/s will pass.

What it costs: a switch sized on bandwidth alone fails on small frames. Sustaining 48 Gb/s of minimum-length frames requires 35.71 Mpps, and a 20 Mpps engine delivers only 20 M × 672 = 13.4 Gb/s of them — 28% of the number on the box. A network of voice, control or acknowledgement traffic is all minimum-length frames, and it will hit a wall the bandwidth figure said was nowhere near.

The corrected model: a switch has two capacities — bits per second and frames per second — and which one binds depends entirely on frame size. The frame-rate figure is the one that is usually missing and usually the one that matters.

5 — "Cut-through is faster, so it is better."

The wrong model: cut-through's lower latency makes it the superior discipline.

What it costs: corrupt frames propagate hop by hop instead of being contained at the first switch, runts are forwarded, and a speed step upward forces store-and-forward anyway. And the latency saving is available only while the egress port is idle — under the load where latency actually matters, the frame queues, and a queued frame has been stored.

The corrected model: cut-through trades error containment for a saving of serialisation time only, and end-to-end latency is serialisation + lookup + queueing. It earns its cost only in networks engineered to keep the queueing term at zero — deliberately underloaded storage and trading fabrics. Everywhere else the load profile cancels the benefit and keeps the cost.

19. Interview Reasoning

Q1 — "A 24-port gigabit switch is advertised as 48 Gb/s non-blocking. A customer reports that it collapses under their workload, which is voice traffic. Nothing is faulty. Explain."

Reason through it. 48 Gb/s is 24 × 1000 × 2, correct for full-duplex ports. But delivering that as minimum-length frames requires 24 × 1.4881 M = 35.71 Mpps, and voice traffic is essentially all small frames. If the forwarding engine sustains 20 Mpps, the switch delivers 20 M × 672 bits = 13.4 Gb/s of this workload — 28% of its advertised figure, with every port showing a valid link and no errors anywhere. The strong answer names the two capacities, computes the frame rate, and observes that the datasheet quoted the one the workload does not stress.

Q2 — "Your switch RTL passes every assertion. A reviewer asks why there is no assertion that frames are not dropped. What do you say?"

Reason through it. Because that assertion is false on any switch with three or more ports. Two ingress ports at line rate to one egress port offer 2 Gb/s to a 1 Gb/s port; the excess must be discarded, and no buffer, scheduler or configuration changes it. The property would describe a switch that is never oversubscribed, not a switch that works. And a design built to satisfy it either buffers without bound or applies backpressure to the ingress — which blocks conversations bound for idle ports, a strictly worse pathology. The strong answer then supplies the replacements: drops occur only on a full queue, every drop is attributed to an ingress port, and no frame is duplicated or corrupted by the discard path.

Q3 — "Two hosts sit behind a hub on switch port 3. One sends to the other. What does the switch do, and what would a naïve implementation do?"

Reason through it. The switch learns the source on port 3, looks up the destination, and hits on port 3 — the ingress port. The correct action is to filter: emit nothing. The destination already received the frame directly over the shared segment. A naïve implementation treats "the answer is the ingress port" as no answer and floods, sending copies to all 23 other ports — none of which lead to the destination — while the destination receives a duplicate of a frame it already has. The strong answer identifies filtering as a third outcome distinct from both hit-and-forward and miss-and-flood, and names the visible symptom: duplicate frames on one segment, with a switch showing zero errors.

Q4 — "A user reports intermittent slowness. Switch latency measures 3 µs at 2 a.m. and 4 ms at 11 a.m. Which component changed?"

Reason through it. Switch latency is serialisation + lookup + queueing. Serialisation is a property of the link and the frame length — 12.144 µs for a maximum frame at gigabit — and does not vary with time of day. Lookup is a fixed pipeline depth and does not vary either. A thousand-fold change can only be queueing delay, which is unbounded by construction and rises with offered load. The strong answer draws the operational conclusion: this is not a switch defect and no firmware change fixes it. The egress port is oversubscribed during business hours, and the remedies are capacity — a faster uplink, a different topology — or prioritisation, so that latency-sensitive traffic is not queued behind bulk transfers.

20. Understanding Check

21. What's Next

This chapter established that a switch takes one decision per frame and that the decision needs a table. It has not said where the table's contents come from.

Every module here treated the learning table as givenlookup_hit and lookup_port arrived from somewhere, and learn_request went somewhere. That somewhere is the mechanism that makes switching possible without any configuration at all, and it is built from a single observation: a frame's source address proves where that station is.

Chapter 12.2 — Source-Address Learning builds it: how an entry is created, why it must expire, what happens when a station moves, and why a table that never forgets is worse than no table at all.

Chapter 12.3 — The Forwarding Decision returns to the lookup with the table's real behaviour in hand, including what a miss actually costs and why flooding on a miss is the only safe answer.

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.