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
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.
| Stations | Shared, each | Shared, total | Switched, each | Switched, total | Ratio |
|---|---|---|---|---|---|
| 2 | 500 Mb/s | 1 Gb/s | 1000 Mb/s | 4 Gb/s | 4× |
| 4 | 250 Mb/s | 1 Gb/s | 1000 Mb/s | 8 Gb/s | 8× |
| 8 | 125 Mb/s | 1 Gb/s | 1000 Mb/s | 16 Gb/s | 16× |
| 24 | 41.7 Mb/s | 1 Gb/s | 1000 Mb/s | 48 Gb/s | 48× |
| 48 | 20.8 Mb/s | 1 Gb/s | 1000 Mb/s | 96 Gb/s | 96× |
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.
| Step | What happens | Owned by |
|---|---|---|
| receive | a frame arrives on a port | Chapter 7.3 validates it |
| learn | its source address is associated with the ingress port | Chapter 12.2 |
| decide | its destination address is looked up | Chapter 12.3 |
| forward | it is queued for the egress port, or flooded, or discarded | Chapter 12.4 |
| transmit | it leaves | this 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.
| Quantity | Working | Result |
|---|---|---|
| minimum frame on the wire | 64 + 8 preamble/SFD + 12 gap | 84 octets |
| as bits | 84 × 8 | 672 bits |
| frames per second, 1 Gb/s | 10⁹ ÷ 672 | 1.4881 M |
| per-frame budget, one port | 1 ÷ 1.4881 M | 672 ns |
| a 24-port switch, all ports at line rate | 24 × 1.4881 M | 35.71 M decisions/s |
| one shared lookup engine's budget | 672 ÷ 24 | 28 ns |
| at 500 MHz | 28 × 0.5 | 14 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
// 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
endmoduleClassification: 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
// 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
endmoduleClassification: 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 ports | Offered | Deliverable | Must be discarded |
|---|---|---|---|
| 2 | 2 Gb/s | 1 Gb/s | 1 Gb/s — 50% |
| 3 | 3 Gb/s | 1 Gb/s | 2 Gb/s — 66.7% |
| 8 | 8 Gb/s | 1 Gb/s | 7 Gb/s — 87.5% |
| 23 | 23 Gb/s | 1 Gb/s | 22 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 oversubscription | Buffer needed |
|---|---|
| 0.1 ms | 12.2 KiB |
| 1 ms | 122 KiB |
| 10 ms | 1.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
// 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
endmoduleClassification: 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.
| Destination | Decision | Consults the table? | Result |
|---|---|---|---|
| broadcast | flood | no | every port except the ingress |
| multicast | flood | no, in a basic switch | every port except the ingress |
| unicast, table hit, different port | forward | yes | one port |
| unicast, table hit, ingress port | filter | yes | no port — discard |
| unicast, table miss | flood | yes, and missed | every 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
// 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
endmoduleClassification: 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.
| Frame | Store-and-forward at 1 Gb/s | Store-and-forward at 10 Gb/s |
|---|---|---|
| 64 octets | 512 bits = 512 ns | 51.2 ns |
| 512 octets | 4096 bits = 4.096 µs | 409.6 ns |
| 1518 octets | 12 144 bits = 12.144 µs | 1.214 µs |
| 9000 octets (jumbo) | 72 000 bits = 72 µs | 7.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 for | At 1 Gb/s | At 10 Gb/s |
|---|---|---|
| destination address, 6 octets | 48 bits = 48 ns | 4.8 ns |
| through EtherType, 14 octets | 112 bits = 112 ns | 11.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.
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
// 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
endmoduleClassification: 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.
| Quantity | 24-port gigabit switch |
|---|---|
| aggregate bandwidth | 24 × 1000 × 2 = 48 Gb/s |
| aggregate frame rate needed | 24 × 1.4881 M = 35.71 Mpps |
| per-port budget, minimum frames | 672 ns |
| shared-engine budget | 672 ÷ 24 = 28 ns |
| at 500 MHz | 14 cycles |
And the two rank a switch differently depending on what crosses it.
| Engine capability | Minimum frames | Maximum frames |
|---|---|---|
| 35.7 Mpps | 35.7 M × 672 = 24 Gb/s | 35.7 M × 12 304 = 439 Gb/s |
| 20 Mpps | 20 M × 672 = 13.4 Gb/s | 246 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
// 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
endmoduleClassification: 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
// 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
endmoduleClassification: 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
// 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
// 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
// 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
// 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
// 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
| # | Scenario | What it must show |
|---|---|---|
| 1 | Unicast destination, individual bit clear | dst_kind = DST_UNICAST, lookup performed |
| 2 | Broadcast destination, all forty-eight bits set | DST_BROADCAST before the group test, flood without lookup |
| 3 | Multicast destination, I/G set, not all-ones | DST_MULTICAST, flood without lookup |
| 4 | The single address 01:00:00:00:00:00 | group bit alone classifies it multicast |
| 5 | The single address FE:FF:FF:FF:FF:FF | I/G clear — unicast, despite forty-seven ones |
| 6 | Group source address | src_is_group set, never learned |
| 7 | Frame with bad FCS | no descriptor — not forwarded, not learned from |
| 8 | Runt, 40 octets | rejected before any descriptor is produced |
| 9 | Frame exactly 64 octets | accepted, descriptor produced |
| 10 | Frame exactly 1518 octets | accepted, descriptor produced |
| 11 | Ingress port field on every descriptor | equals this port's PORT_ID, always |
| 12 | Back-to-back frames at minimum gap | one descriptor each, none merged or lost |
The forwarding decision
| # | Scenario | Expected |
|---|---|---|
| 13 | Unicast hit, egress ≠ ingress | forward, exactly one port in the mask |
| 14 | Unicast hit, egress = ingress | filter — valid answer, empty mask |
| 15 | Unicast miss | flood to every port except ingress |
| 16 | Broadcast | flood, no lookup requested |
| 17 | Multicast | flood, no lookup requested |
| 18 | Learn precedes lookup on the same frame | lookup_request never asserts before the learn |
| 19 | Frame that will be filtered | source still learned |
| 20 | Frame that will be dropped for congestion | source still learned |
| 21 | Flood mask on a 24-port switch | exactly 23 bits set, the ingress bit clear |
| 22 | Two frames, same source, different ports | second updates the table before its own lookup |
| 23 | Lookup latency at the 28 ns budget | forwarding rate sustained at 1.4881 Mpps |
Arbitration, queueing and discard
| # | Scenario | Expected |
|---|---|---|
| 24 | Two ingress ports, one egress, both at line rate | ~50% delivered, ~50% discarded — and both counted |
| 25 | Eight to one at line rate | 87.5% discarded, each drop attributed |
| 26 | Twenty-three to one | 95.7% discarded, switch stays functional |
| 27 | Queue at QUEUE_DEPTH − 1 | frame accepted, no drop |
| 28 | Queue at QUEUE_DEPTH | frame dropped, c_drops increments once |
| 29 | Drop attribution across mixed ingress | drops_by_ingress identifies the heavy talker |
| 30 | Grant with a single requester | that port, pointer advances past it |
| 31 | Grant with all ports requesting | strict rotation, no port served twice in a round |
| 32 | One port requesting continuously | others still served — no monopoly |
| 33 | Requests removed mid-round | pointer does not stall |
| 34 | Grant and drop in the same cycle | never both |
| 35 | Burst of 2:1 for 0.1 ms with a 16 KiB buffer | absorbed, zero drops |
| 36 | Burst of 2:1 for 10 ms with a 16 KiB buffer | drops begin at ~0.13 ms, then steady |
Integrity — these have no acceptable failure
| # | Scenario | Expected |
|---|---|---|
| 37 | Every frame emitted, every port, every case | never on the ingress port |
| 38 | Flood on a 24-port switch | exactly one copy per egress port |
| 39 | Emitted frame's addresses | byte-identical to the received frame |
| 40 | Emitted frame's length | identical to the received length |
| 41 | Frame discarded at a full queue | no partial frame appears on any egress port |
| 42 | Ordering, same source to same destination | delivery order equals arrival order |
Accounting and measurement
| # | Scenario | Expected |
|---|---|---|
| 43 | Line rate of minimum frames | kfps near 1488, binding_limit reports the frame-rate limit |
| 44 | Line rate of maximum frames | mbps near 1000, binding_limit reports the bit-rate limit |
| 45 | Latency under an idle egress | dominant_term = serialisation |
| 46 | Latency under a congested egress | dominant_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.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| Throughput far below line rate, small frames only | forwarding engine cannot sustain 1.4881 Mpps per port | binding_limit reports the frame-rate limit while mbps is far below the link rate |
| Throughput fine on large frames, terrible on small | same cause, seen from the other side | frame-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 small | peak_occupancy reaching QUEUE_DEPTH while average occupancy is near zero |
| Drops on a port at 100% utilisation | arithmetic — the egress is oversubscribed | drops_by_ingress names the ingress port responsible |
| One host slow, everything else fine | that host's traffic is being starved by the arbiter | starvation_suspected, and a >4:1 spread in grants_by_port |
| Duplicate frames on one segment | a frame is being echoed to its ingress port | the P9 assertion, or a capture showing the same frame twice with identical FCS |
| Broadcast storm, CPU load on every host | flooding is correct and the source of the broadcasts is not | c_flooded rising with c_forwarded flat, and a capture identifying the sender |
| Everything floods, nothing is ever forwarded | the table is not being written, so every lookup misses | c_flooded climbing, c_forwarded stuck at zero, learn_request never asserting |
| Latency spikes to milliseconds under load | queueing delay, not switch delay | dominant_term = queueing while lookup_ns and serialise_ns are unchanged |
| Latency high and constant at low load | the lookup is slow, which is a design problem | dominant_term = lookup with queue_ns near zero |
| Corrupt frames arriving at a host several hops away | a cut-through path is propagating them from a bad edge link | FCS errors at the ingress port of the first switch, and none at the ports between |
| A frame that should be delivered locally never arrives | the destination shares the ingress port and the filter is working | c_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 given — lookup_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
Related tutorials
- Related topic
End Systems, Switches and Routers
A device taxonomy is a taxonomy of mutation authority. A repeater changes nothing, a switch changes nothing in the frame but chooses where it goes, and a router destroys the frame and builds a new one — which is what fixes the forwarding boundary between layer two and layer three.
- Related topic
The Receive Path
A transmitter assembles from settled quantities; a receiver discovers, and every decision stays provisional until the check sequence at the very end — so the pipeline either waits for the verdict or acquires the ability to withdraw what it has already delivered.
- Related topic
Serialization Delay
A frame's size divided by the line rate — trivial arithmetic whose significance changes by four orders of magnitude, inverting latency budgets so that at 100 Gb/s one metre of cable outweighs an entire minimum-size frame.
- Related topic
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.
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.
