Ethernet · Module 2
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.
Chapter 2.5 and Chapter 2.6 described two blocks of logic. Neither chapter said where those blocks physically live, and that omission is now the problem: a MAC and a PHY are the same silicon in a network card, in a switch port, and in a router port, yet the three devices behave completely differently.
The usual taxonomy is by port count and price. That taxonomy is useless. It cannot tell you what happens to a frame, and every question an engineer actually has — why does my capture show the wrong destination address, why did the broadcast not arrive, why is a switch not a hub with more sockets — is a question about what happens to a frame.
Which device may change a frame, which may only choose where it goes, and which may change nothing at all?
1. Scope — What This Chapter Owns and What It Does Not
This chapter closes Module 2 by placing the layers of Chapter 2.3 into the devices built from them. It is deliberately not the switching module.
This chapter owns: the mutation-authority ladder, the role of a NIC as the boundary where frames begin and end, the reason a switch is categorically not a repeater, the forwarding boundary between layer two and layer three, what happens to addresses when that boundary is crossed, and how a device classifies an arriving frame into a disposition.
This chapter does not own: MAC learning tables and their ageing, spanning tree, VLANs, queueing and scheduling, congestion management, or any routing protocol. Those get their own chapters. What is built here is the classification decision that every one of them assumes.
The test for whether a detail belongs here: does an RTL or DV engineer need it to reason about where their block sits? Learning-table ageing does not pass that test. Knowing that a switch must terminate the MAC on ingress does — because it tells you a switch port contains a receive MAC with a real FCS check, and therefore has a drop path you must verify.
2. The Four Levels of Mutation Authority
| Device | Terminates PHY | Terminates MAC | Terminates frame | May change | Domain effect |
|---|---|---|---|---|---|
| Repeater / hub | regenerates signal only | no | no | nothing | one collision domain, one broadcast domain |
| Switch / bridge | yes | yes | no | egress port selection only | splits collision domains; one broadcast domain |
| Router | yes | yes | yes | destination MAC, source MAC, FCS | splits broadcast domains |
| End system | yes | yes | yes — creates or consumes | everything, as the origin | is a member of one |
Read the table down the "terminates" columns. Each device terminates strictly more than the one above it, and the mutation authority in the next column follows from exactly that. A device cannot change what it has not terminated, and it need not terminate what it does not change.
The repeater does not terminate the MAC, so it has no idea where the frame ends, cannot check the FCS, and cannot make a per-frame decision. It amplifies and retimes. Every port sees every bit, which is why every port shares one collision domain — and why Chapter 1.2's slot-time arithmetic constrained the diameter of a repeatered network.
The switch terminates the MAC, so it has the frame, has checked the FCS, and can decide. But it does not terminate the frame's meaning: it does not open the payload, and it must hand onward exactly the octets it received. That is the constraint Chapter 2.4 called payload opacity, and a switch is the device that honours it most strictly.
The router terminates the frame. It opens the payload, reads addressing that Ethernet knows nothing about, and — this is the part that surprises people — discards the Ethernet header entirely. What continues is the payload. What carries it onward is a new frame.
3. The End System — Where Frames Are Born and Die
An end system is a host: a server, a workstation, an embedded controller. Its network interface is the NIC, and the NIC contains exactly the two blocks the previous two chapters described.
The bottom row is the point of the figure. Across this entire path exactly one frame exists. It is constructed in NIC A, it is not modified by the switch, and it is destroyed in NIC B. Section 9 changes that picture by inserting a router, and the number of frames becomes two.
What the NIC's MAC does on transmit is the six responsibilities of Chapter 2.5: choose the addresses, mark the frame, pad it to the minimum, compute the FCS, and enforce the interframe gap. On receive it does the reverse plus one decision that transmit never makes — deciding whether this frame is addressed to it at all.
That receive decision is the first piece of RTL.
4. RTL 1 — The Address Filter, or "Is This Frame Mine?"
Every receiving MAC on a shared or switched medium sees frames it must ignore. The address filter is the block that decides, and it is more subtle than comparing six octets, because there are four separate reasons a frame may be accepted.
// SYNTHESIZABLE. The receive-side address filter in an end system's MAC.
//
// The subtlety this module exists to teach: "is this frame mine?" is not one
// comparison. It is four independent reasons to accept, and a NIC that
// collapses them into a single accept bit cannot tell an operator whether it
// is receiving broadcast storms, unsubscribed multicast, or genuine traffic.
module mac_address_filter #(
parameter int unsigned MCAST_ENTRIES = 8
) (
input logic clk,
input logic rst_n,
// Frame header, presented for one cycle when hdr_valid is high.
input logic hdr_valid,
input logic [47:0] da, // destination address
input logic [47:0] sa, // source address, for the loop check
// Station configuration.
input logic [47:0] local_mac,
input logic promiscuous, // accept everything (a capture port)
input logic [47:0] mcast_table [MCAST_ENTRIES],
input logic [MCAST_ENTRIES-1:0] mcast_valid,
// Exactly one of these five is high when hdr_valid is high.
output logic accept_unicast, // addressed to this station
output logic accept_broadcast, // addressed to every station
output logic accept_mcast, // a group this station subscribed to
output logic accept_promisc, // accepted only because of capture mode
output logic reject,
// A frame whose SOURCE address is our own came back to us. On a correctly
// wired network this cannot happen; it is evidence of a loop or a mirror
// port, and it is worth a sticky flag rather than a silent drop.
output logic own_source_seen
);
// IEEE 802.3 defines the least significant bit of the FIRST transmitted
// octet as the individual/group bit. In the 48-bit vector convention used
// here, that octet is da[47:40] and the bit is da[40].
localparam int unsigned IG_BIT = 40;
logic is_group;
logic is_broadcast;
logic is_local_exact;
logic mcast_hit;
always_comb begin
is_group = da[IG_BIT];
is_broadcast = (da == 48'hFFFF_FFFF_FFFF);
is_local_exact = (da == local_mac);
// A group address that is not broadcast is a multicast address, and it
// is accepted only if this station asked for it. Accepting all multicast
// is a real configuration, but it is a DIFFERENT mode from subscription
// and must not be confused with it.
mcast_hit = 1'b0;
for (int unsigned i = 0; i < MCAST_ENTRIES; i++) begin
if (mcast_valid[i] && (da == mcast_table[i])) mcast_hit = 1'b1;
end
end
// Priority matters, and this order is deliberate. Exact match wins over
// promiscuous so that a capture port still reports its own traffic as its
// own; broadcast wins over multicast because the broadcast address IS a
// group address and would otherwise fall into the multicast branch.
always_comb begin
accept_unicast = 1'b0;
accept_broadcast = 1'b0;
accept_mcast = 1'b0;
accept_promisc = 1'b0;
reject = 1'b0;
if (hdr_valid) begin
if (!is_group && is_local_exact) accept_unicast = 1'b1;
else if (is_broadcast) accept_broadcast = 1'b1;
else if (is_group && mcast_hit) accept_mcast = 1'b1;
else if (promiscuous) accept_promisc = 1'b1;
else reject = 1'b1;
end
end
// Sticky, because the event is rare and the observer is not watching.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) own_source_seen <= 1'b0;
else if (hdr_valid && (sa == local_mac)) own_source_seen <= 1'b1;
end
endmoduleClassification: synthesizable.
What it teaches: that acceptance has reasons, not just a verdict. The five outputs are mutually exclusive by construction, and Section 13 asserts that. The accept_promisc output exists so an operator can tell "this NIC is receiving traffic" from "this NIC is receiving traffic for itself" — an operational distinction that a single accept bit destroys.
Deliberately simplified: the multicast table is an exact-match array. Production NICs use a hash into a 64-bit or 512-bit filter vector, which admits false positives that software then filters out. The hash is an area optimisation, not a semantic change, and it does not alter the five-way decision.
Production implication: the individual/group bit is a bit position in a transmitted octet, and getting the endianness of a 48-bit address vector wrong is one of the most common integration bugs in Ethernet RTL. It is silent — unicast still works — and it only appears when multicast is first tested. Fix the convention at the interface and assert it.
Later ownership: VLAN-aware filtering and the interaction with promiscuous mode belong to the switching module.
5. The Switch — What It Must Terminate to Do Its Job
A switch's job sounds trivial: read the destination address, send the frame out the right port. The work is in what it must terminate before it is allowed to read anything.
Ingress, in order:
- The PHY terminates. The switch port recovers a clock, achieves block synchronisation, and delivers bits — everything Chapter 2.6 described. A repeater does none of this at the frame level; it retimes and re-drives.
- The MAC terminates. The switch locates the frame boundaries, checks the FCS, and enforces the length limits. A frame that fails here is dropped and never touches another port.
- Only now are the addresses trustworthy enough to make a decision on.
That ordering is the reason a switch is a fundamentally different device. It has committed to believing the frame before it forwards it, and belief requires a check, and a check requires a drop path.
Egress, in order: the frame is queued for the chosen port, the transmit MAC re-applies the interframe gap, and the transmit PHY re-serialises it. The frame's octets are unchanged; its signal is entirely new.
| What the switch re-originates | What the switch preserves |
|---|---|
| the electrical or optical signal | every octet of the frame |
| the transmit clock | the destination and source addresses |
| the interframe gap | the type or length field |
| the queueing position | the FCS value |
The FCS is preserved, not recomputed. That is the strongest single statement of the switch's mutation authority: if nothing in the frame changed, the check value that covers it cannot change either. The moment a device recomputes the FCS, it has admitted it changed something — and Section 8's router does exactly that.
6. RTL 2 — The Ingress Descriptor
Before a switch or router can classify a frame, it must summarise it. Real forwarding pipelines do not carry the frame's header alongside the frame; they extract a small fixed-width descriptor once, at ingress, and carry that through the pipeline while the frame body sits in a buffer.
// SYNTHESIZABLE. Extracts the forwarding-relevant summary of an arriving
// frame, once, at ingress.
//
// The architectural lesson: the pipeline that decides is NOT the pipeline
// that carries data. Descriptors are narrow and travel with the decision;
// bodies are wide and stay in a buffer until a port is chosen.
package fwd_pkg;
// The destination-address class, decided once so that no later stage has
// to re-parse the address. Every downstream decision keys off this.
typedef enum logic [1:0] {
DA_UNICAST_REMOTE = 2'd0, // a unicast address that is not ours
DA_UNICAST_LOCAL = 2'd1, // a unicast address belonging to this device
DA_BROADCAST = 2'd2,
DA_MULTICAST = 2'd3
} da_class_e;
endpackage
module ingress_descriptor
import fwd_pkg::*;
#(
parameter int unsigned PORTS = 8,
parameter int unsigned PORT_W = $clog2(PORTS),
parameter int unsigned DOMAIN_W = 4
) (
input logic clk,
input logic rst_n,
input logic hdr_valid,
input logic [PORT_W-1:0] ingress_port,
input logic [DOMAIN_W-1:0] ingress_domain, // forwarding domain of the port
input logic [47:0] da,
input logic [47:0] sa,
input logic [15:0] type_or_len,
input logic fcs_ok, // from the ingress receive MAC
input logic length_ok,
// The device's own address, used to recognise frames aimed AT this device
// rather than through it. A pure layer-two switch still needs this for its
// own management traffic; a router needs it for everything it will route.
input logic [47:0] device_mac,
output logic desc_valid,
output logic [PORT_W-1:0] desc_ingress_port,
output logic [DOMAIN_W-1:0] desc_domain,
output da_class_e desc_da_class,
output logic [15:0] desc_type,
output logic desc_frame_ok, // FCS and length both good
output logic [47:0] desc_sa // carried for address learning
);
localparam int unsigned IG_BIT = 40;
da_class_e da_class_c;
always_comb begin
if (da == 48'hFFFF_FFFF_FFFF) da_class_c = DA_BROADCAST;
else if (da[IG_BIT]) da_class_c = DA_MULTICAST;
else if (da == device_mac) da_class_c = DA_UNICAST_LOCAL;
else da_class_c = DA_UNICAST_REMOTE;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
desc_valid <= 1'b0;
desc_ingress_port <= '0;
desc_domain <= '0;
desc_da_class <= DA_UNICAST_REMOTE;
desc_type <= '0;
desc_frame_ok <= 1'b0;
desc_sa <= '0;
end else begin
desc_valid <= hdr_valid;
if (hdr_valid) begin
desc_ingress_port <= ingress_port;
desc_domain <= ingress_domain;
desc_da_class <= da_class_c;
desc_type <= type_or_len;
desc_frame_ok <= fcs_ok && length_ok;
desc_sa <= sa;
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that address classification happens once, and that everything downstream consumes an enum rather than re-comparing 48-bit values. A pipeline that re-parses the destination address at three stages will eventually disagree with itself, and the disagreement is invisible until a corner case — typically the broadcast address, which is simultaneously a group address and an exact value.
Deliberately simplified: desc_frame_ok collapses two independent failures into one bit for the classifier's benefit, but the causes stay separate in Section 11's counters. Collapsing a cause into a verdict is acceptable when a separate observability path preserves the cause; it is a defect when it does not.
Production implication: ingress_domain is the field that makes VLANs, bridge domains and virtual routing instances possible later. It costs four bits here and nothing in complexity, and retrofitting it into a pipeline that assumed one global forwarding domain is a rewrite. Carry the domain from the first design.
Later ownership: the switching module owns what populates ingress_domain and how tagged frames change it.
7. RTL 3 — The Forwarding Classifier
Now the decision. Given a descriptor, what happens to this frame? A classifier that returns a single "forward" bit is useless; the interesting outputs are the ones that are not forwarding.
// SYNTHESIZABLE. Turns an ingress descriptor into exactly one disposition.
//
// The five dispositions are exhaustive and mutually exclusive, and that
// property is the point of the module. A frame that matches no case is a
// design error, not a silently dropped frame -- so the default is an
// explicit DROP with a cause, never a fall-through.
module l2_classifier
import fwd_pkg::*;
#(
parameter int unsigned PORTS = 8,
parameter int unsigned PORT_W = $clog2(PORTS)
) (
input logic clk,
input logic rst_n,
input logic desc_valid,
input da_class_e desc_da_class,
input logic desc_frame_ok,
input logic [PORT_W-1:0] desc_ingress_port,
// Result of the address lookup, supplied by the (unmodelled) MAC table.
input logic lookup_hit,
input logic [PORT_W-1:0] lookup_port,
// Is this device permitted to route? A pure bridge is not, and must treat
// a frame addressed to itself as management traffic rather than routable.
input logic routing_enabled,
// Exactly one of these is asserted when out_valid is high.
output logic out_valid,
output logic disp_local, // consume here: our own management
output logic disp_forward, // send out one known port
output logic disp_flood, // send out every port but ingress
output logic disp_to_l3, // hand upward for routing
output logic disp_drop,
output logic [PORT_W-1:0] egress_port, // meaningful only for disp_forward
// Why a frame was dropped. Never collapse this into one bit: the three
// causes point at three different subsystems during a debug session.
output logic drop_bad_frame, // FCS or length -> look at the PHY
output logic drop_same_port, // lookup pointed back at ingress
output logic drop_no_domain // domain rules forbade it
);
logic hit_is_ingress;
assign hit_is_ingress = lookup_hit && (lookup_port == desc_ingress_port);
always_comb begin
out_valid = desc_valid;
disp_local = 1'b0;
disp_forward = 1'b0;
disp_flood = 1'b0;
disp_to_l3 = 1'b0;
disp_drop = 1'b0;
egress_port = lookup_port;
drop_bad_frame = 1'b0;
drop_same_port = 1'b0;
drop_no_domain = 1'b0;
if (desc_valid) begin
if (!desc_frame_ok) begin
// A frame the ingress MAC did not believe is not classified at all.
// This is the containment property that separates a switch from a
// repeater, and it must come FIRST -- before any address is trusted.
disp_drop = 1'b1;
drop_bad_frame = 1'b1;
end else begin
unique case (desc_da_class)
DA_UNICAST_LOCAL: begin
// Addressed to this device. A router hands it upward; a bridge
// consumes it as its own management traffic. Same address, two
// entirely different meanings -- decided by configuration, not
// by anything in the frame.
if (routing_enabled) disp_to_l3 = 1'b1;
else disp_local = 1'b1;
end
DA_UNICAST_REMOTE: begin
if (!lookup_hit) begin
// NOT an error. An unknown unicast destination is flooded,
// because the only way to learn where a station is, is to
// reach it. Section 13 explains why the obvious assertion
// against this is wrong.
disp_flood = 1'b1;
end else if (hit_is_ingress) begin
// The table says the destination is back where the frame came
// from. Forwarding it there would be a reflection.
disp_drop = 1'b1;
drop_same_port = 1'b1;
end else begin
disp_forward = 1'b1;
end
end
DA_BROADCAST, DA_MULTICAST: begin
// Both go to every port in the domain except the ingress port.
// Multicast pruning is a later optimisation, and treating it as
// flooding here is the correct conservative default.
disp_flood = 1'b1;
end
default: begin
disp_drop = 1'b1;
drop_no_domain = 1'b1;
end
endcase
end
end
end
endmoduleClassification: synthesizable teaching model.
What it teaches: three things a first design usually gets wrong. First, the FCS check gates everything — no address is trusted before the frame is believed. Second, an unknown unicast is flooded, not dropped; flooding is how a bridge learns, and treating it as an error breaks the network. Third, the same destination address means "management traffic" on a bridge and "route this" on a router, and nothing in the frame distinguishes them — only the device's configuration does.
Deliberately simplified: no MAC table, no learning, no ageing, no VLAN membership check, no spanning-tree port state. Each of those adds inputs to this decision without changing its shape, which is exactly why the shape is worth learning first.
Production implication: the three separate drop causes are not luxury. During an outage, drop_bad_frame sends you to the PHY and Chapter 2.6's descent, drop_same_port sends you to the topology, and drop_no_domain sends you to configuration. A single drop counter tells you a frame is missing and nothing about who owns the next measurement.
Later ownership: the switching module owns learning, ageing, spanning tree and VLAN membership.
8. The Router Boundary — Where the Frame Is Destroyed
Everything so far preserved the frame. A router does not.
When a frame's destination address is the router's own MAC — DA_UNICAST_LOCAL with routing_enabled in Section 7 — the router has been explicitly asked to take the frame apart. The sending host chose that address deliberately: it determined the ultimate destination was not on its own network, and addressed the frame to its gateway instead.
That is the fact everything else follows from. The host addressed the frame to the router on purpose. The router is not intercepting anything.
Read the two solid messages together. Four different MAC addresses appear across one delivery, and none of the four is shared between the two frames. Meanwhile the payload — the thing the application cared about — was not touched by either device.
| Property | Frame 1 | Frame 2 |
|---|---|---|
| destination MAC | the router's ingress port | Host B |
| source MAC | Host A | the router's egress port |
| FCS | computed by Host A | recomputed by the router |
| the payload inside | identical | identical |
| the payload's own destination address | Host B | Host B |
The FCS row is the diagnostic one. The router recomputes the check value because it changed the header, and a device that recomputes an FCS has by definition destroyed the original frame. Section 5's switch never does this. That single row separates the two devices more sharply than any amount of prose about layers.
9. Which Addresses Change and Which Do Not
Make it concrete. Host A at 10.0.1.5 sends to Host B at 10.0.2.9, through a router whose two interfaces are 10.0.1.1 and 10.0.2.1.
Host A's reasoning, before it builds anything: the destination 10.0.2.9 is not on my own network. I cannot reach it directly. I must hand it to my gateway.
Frame 1, on the first network:
| Field | Value | Why |
|---|---|---|
| Ethernet destination | the router's 10.0.1.1 interface MAC | A is asking the router to forward |
| Ethernet source | Host A's MAC | A is the transmitter on this link |
| payload destination address | 10.0.2.9 | the ultimate destination, unchanged end to end |
| payload source address | 10.0.1.5 | the original source, unchanged end to end |
Frame 2, on the second network:
| Field | Value | Why |
|---|---|---|
| Ethernet destination | Host B's MAC | B is the final recipient on this link |
| Ethernet source | the router's 10.0.2.1 interface MAC | the router is the transmitter on this link |
| payload destination address | 10.0.2.9 | still unchanged |
| payload source address | 10.0.1.5 | still unchanged |
The rule, in one line: layer-two addresses name the two ends of a single link and change at every router; the payload's own addresses name the two ends of the whole conversation and do not.
A consequence worth stating explicitly: because the router builds a fresh frame for the second link, that frame is subject to the second link's own rules — its own minimum and maximum sizes, its own interframe gap, its own PHY. The two links need not run at the same speed, use the same medium, or even use the same maximum frame size. This is why a router can join a copper link to a fibre link, and it is the same argument Chapter 2.3 made about layering: the boundary exists so the two sides can differ.
10. RTL 4 — The Layer-Two to Layer-Three Handoff
Section 7's classifier produced disp_to_l3. This module is what sits behind it: the block that accepts a frame the device has been asked to route, and states precisely what must be rebuilt.
// SYNTHESIZABLE (architectural teaching model). The layer-two side of a
// routing boundary: it does not decide WHERE a frame goes, it states what
// must be rebuilt once something else has decided.
//
// The next-hop decision is an INPUT here. That is the honest boundary: this
// chapter owns the Ethernet consequences of routing, not routing.
module l3_handoff #(
parameter int unsigned PORTS = 8,
parameter int unsigned PORT_W = $clog2(PORTS)
) (
input logic clk,
input logic rst_n,
// From the classifier: this frame was addressed to us and we route.
input logic to_l3_valid,
input logic [PORT_W-1:0] ingress_port,
// Supplied by the routing layer, which this module does not model.
input logic nexthop_valid, // a next hop was resolved
input logic nexthop_unknown, // no route exists
input logic nexthop_unresolved,// route exists, MAC not yet known
input logic [PORT_W-1:0] nexthop_port,
input logic [47:0] nexthop_mac,
input logic [47:0] egress_if_mac, // OUR MAC on the egress port
// The rebuild instruction handed to the egress transmit MAC.
output logic rebuild_valid,
output logic [PORT_W-1:0] rebuild_port,
output logic [47:0] rebuild_da, // becomes the next hop's MAC
output logic [47:0] rebuild_sa, // becomes OUR egress MAC
output logic rebuild_recompute_fcs,
// A routed frame whose egress port is its ingress port. At layer two this
// is a reflection and Section 7 drops it. Here it is LEGAL and common:
// two networks can share one physical interface. Exposed, not suppressed,
// because a design that silently applies the layer-two rule here breaks
// same-interface routing in a way that is very hard to find.
output logic hairpin,
// Why a routable frame produced no egress. Separated because the two
// causes have different owners and different time constants.
output logic no_route, // configuration or protocol
output logic unresolved_nexthop // address resolution pending
);
always_comb begin
rebuild_valid = 1'b0;
rebuild_port = nexthop_port;
rebuild_da = nexthop_mac;
rebuild_sa = egress_if_mac;
// ALWAYS true on this path. The header changed, so the check value that
// covers it must change. A router that forwards the original FCS emits
// frames every receiver will discard -- and the symptom is a silent,
// total loss of routed traffic while local traffic works perfectly.
rebuild_recompute_fcs = 1'b1;
hairpin = 1'b0;
no_route = 1'b0;
unresolved_nexthop = 1'b0;
if (to_l3_valid) begin
if (nexthop_unknown) no_route = 1'b1;
else if (nexthop_unresolved) unresolved_nexthop = 1'b1;
else if (nexthop_valid) begin
rebuild_valid = 1'b1;
hairpin = (nexthop_port == ingress_port);
end
end
end
endmoduleClassification: synthesizable architectural model, with the routing decision explicitly outside it.
What it teaches: the rewrite contract. Exactly three things change — destination address, source address, check value — and rebuild_recompute_fcs is tied high rather than made conditional, because on this path it is never optional. Making it an input would invite a design where someone eventually drives it low.
Deliberately simplified: everything about routing. No table, no prefix match, no hop-count decrement, and no fragmentation. Every one of those is genuinely outside Ethernet's concern, and modelling them here would teach the wrong ownership.
Production implication: hairpin is the field that matters most in practice. Layer two has an absolute rule — never send a frame out the port it arrived on — and an engineer who carries that rule up to the routing path will silently break every network where two subnets share one physical interface. The rule is layer-two-specific, and the boundary between the two layers is exactly where it stops applying. Section 14 makes this a directed test, because random stimulus will not find it.
Later ownership: address resolution, next-hop tables and prefix matching belong to a routing chapter, not to Module 2.
11. RTL 5 — Disposition Counters as Architecture
Every module above produced a categorised outcome. This one turns those categories into something an operator can read at three in the morning.
// SYNTHESIZABLE INSTRUMENTATION. Not in the datapath.
//
// The design rule this module exists to demonstrate: a counter is only
// useful if the set of counters is EXHAUSTIVE. If accepted frames can exceed
// the sum of the dispositions, the counters cannot answer "where did my
// frame go?" -- and answering that question is their entire purpose.
module disposition_counters #(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic frame_seen, // a frame arrived at the classifier
input logic disp_local,
input logic disp_forward,
input logic disp_flood,
input logic disp_to_l3,
input logic disp_drop,
input logic drop_bad_frame,
input logic drop_same_port,
input logic drop_no_domain,
input logic no_route,
input logic unresolved_nexthop,
input logic hairpin,
input logic clear, // explicit; counters never self-clear
output logic [CNT_W-1:0] c_seen,
output logic [CNT_W-1:0] c_local,
output logic [CNT_W-1:0] c_forward,
output logic [CNT_W-1:0] c_flood,
output logic [CNT_W-1:0] c_to_l3,
output logic [CNT_W-1:0] c_drop,
output logic [CNT_W-1:0] c_drop_bad_frame,
output logic [CNT_W-1:0] c_drop_same_port,
output logic [CNT_W-1:0] c_drop_no_domain,
output logic [CNT_W-1:0] c_no_route,
output logic [CNT_W-1:0] c_unresolved,
output logic [CNT_W-1:0] c_hairpin,
// The FIRST drop cause since reset, held. During an incident the counters
// are already large and every cause is nonzero; the question that matters
// is which one moved first, and a rate meter cannot answer it.
output logic [1:0] first_drop_cause,
output logic first_drop_valid
);
// Saturating, not wrapping. A wrapped counter read twice can show a
// DECREASE, and an operator who sees that stops trusting every counter on
// the device. Sticking at maximum is unambiguous and cheap.
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v,
input logic en);
bump = (en && !(&v)) ? (v + 1'b1) : v;
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || clear) begin
c_seen <= '0; c_local <= '0;
c_forward <= '0; c_flood <= '0;
c_to_l3 <= '0; c_drop <= '0;
c_drop_bad_frame <= '0; c_drop_same_port <= '0;
c_drop_no_domain <= '0; c_no_route <= '0;
c_unresolved <= '0; c_hairpin <= '0;
first_drop_cause <= 2'd0;
first_drop_valid <= 1'b0;
end else begin
c_seen <= bump(c_seen, frame_seen);
c_local <= bump(c_local, disp_local);
c_forward <= bump(c_forward, disp_forward);
c_flood <= bump(c_flood, disp_flood);
c_to_l3 <= bump(c_to_l3, disp_to_l3);
c_drop <= bump(c_drop, disp_drop);
c_drop_bad_frame <= bump(c_drop_bad_frame, drop_bad_frame);
c_drop_same_port <= bump(c_drop_same_port, drop_same_port);
c_drop_no_domain <= bump(c_drop_no_domain, drop_no_domain);
c_no_route <= bump(c_no_route, no_route);
c_unresolved <= bump(c_unresolved, unresolved_nexthop);
// Counted, never treated as an error -- see Section 10.
c_hairpin <= bump(c_hairpin, hairpin);
if (!first_drop_valid && disp_drop) begin
first_drop_valid <= 1'b1;
if (drop_bad_frame) first_drop_cause <= 2'd0;
else if (drop_same_port) first_drop_cause <= 2'd1;
else first_drop_cause <= 2'd2;
end
end
end
endmoduleClassification: synthesizable instrumentation.
What it teaches: that observability is a closure property. Because the five dispositions are exhaustive, c_seen equals their sum — and that identity is what makes the counter set diagnostic rather than merely informative. An operator can subtract and know that nothing went anywhere unaccounted for. Section 13 asserts the identity, and if it ever fails the counters are lying, which is worse than having none.
Deliberately simplified: no per-port breakdown, no rate windows, no histogram of frame sizes. All three are standard in production and none changes the closure argument.
Production implication: the sticky first-cause register costs two flip-flops and repeatedly saves hours. During a live incident every counter is large and every cause is nonzero; ordering is the only information left, and a device that did not record it has thrown away the one thing that was still recoverable.
Later ownership: per-port and per-queue statistics belong with the switching and queueing chapters.
12. Collision Domain Against Broadcast Domain
Two terms that sound similar, describe different things, and are separated by different devices. The mutation-authority ladder settles both.
| Collision domain | Broadcast domain | |
|---|---|---|
| What it is | the set of ports where two simultaneous transmissions interfere | the set of ports a broadcast frame reaches |
| What it costs | wasted medium time, retransmission | wasted bandwidth on every member |
| Split by | a switch — each port is its own | a router — it does not forward broadcast |
| Not split by | a repeater — every port is in one | a switch — it floods broadcast to all ports |
| Made irrelevant by | full duplex (Chapter 1.5) | nothing; it is inherent to layer two |
The asymmetry is the lesson. Switching solved the collision problem so completely that Chapter 1.2's machinery became unreachable logic. It did nothing whatsoever for the broadcast problem — a switch floods broadcast by design, which Section 7's classifier shows directly.
So a network of a thousand hosts on one switched fabric has a thousand collision domains and one broadcast domain, and every broadcast still costs every host an interrupt. Only a router boundary changes that, and it changes it because a router destroys the frame rather than forwarding it — the same fact as Section 8, arriving from a different direction.
13. Assertions
Every property below is a property of these teaching models. None is an IEEE 802.3 requirement, and the distinction matters: 802.3 specifies frame format and access behaviour, while forwarding behaviour is specified by the bridging standards and by implementation choice.
// ─── Mutual exclusion: the classifier's core structural guarantee ───────────
// Catches: a refactor that adds a disposition case without making it
// exclusive, producing a frame both forwarded and dropped. Silent in
// simulation until two downstream blocks disagree.
property p_disposition_onehot;
@(posedge clk) disable iff (!rst_n)
out_valid |-> $onehot({disp_local, disp_forward, disp_flood,
disp_to_l3, disp_drop});
endproperty
// ─── Conservation: every arriving frame is accounted for ───────────────────
// Catches: an added case that falls through, leaving a frame with no
// disposition. Without this, a lost frame looks like a fabric problem.
property p_every_frame_disposed;
@(posedge clk) disable iff (!rst_n)
desc_valid |=> out_valid;
endproperty
// ─── Safety: an unbelieved frame never reaches a decision ──────────────────
// This is the property that distinguishes a switch from a repeater, and it
// must hold before any address is examined. Catches: a reordering that lets
// the address lookup run in parallel with the FCS check and act on it.
property p_bad_frame_always_dropped;
@(posedge clk) disable iff (!rst_n)
(desc_valid && !desc_frame_ok) |-> (disp_drop && drop_bad_frame);
endproperty
// ─── Safety: no reflection at layer two ────────────────────────────────────
// Catches: an egress selection that can return the ingress port, which
// produces a duplicate on the segment and, with a second bridge, a loop.
property p_never_forward_to_ingress;
@(posedge clk) disable iff (!rst_n)
disp_forward |-> (egress_port != desc_ingress_port);
endproperty
// ─── Causation: the routing path is entered only when asked ────────────────
// Catches: a device routing frames not addressed to it -- which is
// interception, works in a lab, and fails every security review.
property p_l3_only_for_local_da;
@(posedge clk) disable iff (!rst_n)
disp_to_l3 |-> (desc_da_class == DA_UNICAST_LOCAL) && routing_enabled;
endproperty
// ─── Safety: broadcast is never treated as a unicast destination ───────────
// Catches: the classic bug where the broadcast address, being a group
// address AND an exact value, falls into the wrong branch.
property p_broadcast_floods;
@(posedge clk) disable iff (!rst_n)
(out_valid && desc_frame_ok && (desc_da_class == DA_BROADCAST))
|-> disp_flood;
endproperty
// ─── Safety on the rewrite contract ────────────────────────────────────────
// Catches: a rebuild path that forwards the original check value. The
// symptom is total loss of routed traffic while local traffic is perfect,
// which sends debuggers to the routing layer for hours.
property p_rebuild_always_recomputes_fcs;
@(posedge clk) disable iff (!rst_n)
rebuild_valid |-> rebuild_recompute_fcs;
endproperty
// ─── Safety: a rebuilt frame is sourced from the egress interface ──────────
// Catches: sourcing from the ingress interface MAC, which works on a
// single-interface router and breaks on every multi-interface one.
property p_rebuild_sa_is_egress_if;
@(posedge clk) disable iff (!rst_n)
rebuild_valid |-> (rebuild_sa == egress_if_mac);
endproperty
// ─── Conservation on the instrumentation ───────────────────────────────────
// Catches: counters that stop closing, which means they have started lying.
// A lying counter is worse than an absent one.
property p_counters_close;
@(posedge clk) disable iff (!rst_n)
(!(&c_seen)) |->
(c_seen == c_local + c_forward + c_flood + c_to_l3 + c_drop);
endproperty
// ─── Stability: the descriptor does not move under the decision ────────────
// Catches: a descriptor register updated while a multi-cycle lookup is in
// flight, so the decision applies to one frame and the data to another.
property p_descriptor_stable_during_decision;
@(posedge clk) disable iff (!rst_n)
(desc_valid && !hdr_valid) |=> $stable({desc_da_class, desc_ingress_port});
endproperty
// ─── Bounded response ──────────────────────────────────────────────────────
// Catches: a lookup that can stall indefinitely, turning a forwarding
// decision into an unbounded latency.
property p_decision_is_bounded;
@(posedge clk) disable iff (!rst_n)
desc_valid |-> ##[1:2] out_valid;
endproperty14. Verification
Scenarios
- Unicast to a known port. The nominal case. Verify one
disp_forward, the egress port matching the lookup, andc_forwardadvancing by exactly one. - Unicast to an unknown destination. Verify
disp_flood, not a drop. This is the case the rejected property would have broken. - Unicast whose lookup points back at the ingress port. Verify
disp_dropwithdrop_same_port, and that no egress transaction is generated at all. - Broadcast. Verify
disp_floodand specifically that the frame did not fall into the multicast branch — the broadcast address satisfies both tests, and the priority ordering is the only thing separating them. - Multicast to a subscribed group, and to an unsubscribed one. Two runs against Section 4's filter. Verify
accept_mcastin the first andrejectin the second, withaccept_promisclow in both. - The same multicast frame with promiscuous mode on. Verify the acceptance reason changes to
accept_promiscwhile the frame is still accepted. A design that reportsaccept_mcasthere has lost the operational distinction the module exists for. - Frame with a bad FCS whose destination is a known port. The containment test. Verify
disp_dropwithdrop_bad_frameand — the part that actually matters — that no lookup result was acted on. A design that checks the FCS in parallel with the lookup and forwards on the lookup passes a naive version of this test. - Frame addressed to the device's own MAC, routing disabled. Verify
disp_local. - The identical frame, routing enabled. Verify
disp_to_l3. Same stimulus, different configuration, different correct answer — which is the point of running them as a pair. - Routed frame with no resolvable route. Verify
no_route, norebuild_valid, and thatc_no_routerather thanc_drop_bad_frameadvanced. - Routed frame with a known route but unresolved next-hop address. Verify
unresolved_nexthopand that it is counted separately fromno_route. These have different owners and different time constants: one is configuration, one resolves on its own within milliseconds. - Reset asserted mid-frame. Verify no disposition escapes, the descriptor returns to its reset value, and no counter advances.
- Back-to-back frames at the minimum interframe gap. Verify the descriptor stability property holds — this is the scenario that catches a descriptor register overwritten while a decision is in flight.
- Counter saturation. Drive one counter to its maximum and verify it holds rather than wrapping, and that
p_counters_closeis correctly excluded once saturated. - Sticky first-cause ordering. Inject a
drop_same_portfirst, then a thousanddrop_bad_frameevents. Verifyfirst_drop_causestill reports the same-port drop. A design that overwrites it reports the loudest cause instead of the first, which is precisely backwards. - Own source address received. Verify
own_source_seensets and stays set. Rare, so a level rather than a pulse.
What the checker must own
- A reference model that reproduces the five-way disposition from the descriptor alone. It is a dozen lines and it catches every priority-ordering regression.
- A conservation check running continuously, not just at end of test: frames in must equal the sum of dispositions at every sample point, because a mismatch that self-corrects is still a bug.
- Coverage crosses of
da_classagainstlookup_hitagainstrouting_enabledagainstframe_ok. The combination(DA_UNICAST_LOCAL, routing_enabled=0)and(DA_UNICAST_LOCAL, routing_enabled=1)are two different bins and both must be hit; a run that only ever configured one of them has verified half the module.
15. Debugging — Let the Disposition Choose the Next Measurement
The symptom that brings people to this chapter: a host can reach everything on its own network and nothing beyond it.
The temptation is to start at the routing layer, because the failure is described in routing terms. The disposition counters give a better first move, because they say which device owns the next measurement before anyone has formed a theory.
Step 1 — read c_seen at the switch port facing the host. If it is not advancing, the frame never arrived and this is a link problem: descend into Chapter 2.6's PHY method and stop reasoning about routing entirely.
Step 2 — read the switch's disposition split. The frame is present, so exactly one counter is moving, and each answer names a different next measurement:
| Counter advancing | What it means | Next measurement |
|---|---|---|
c_drop_bad_frame | the frame is arriving corrupted | the PHY and the channel — Chapter 2.6 |
c_flood | the switch does not know where the router's MAC is | is the router transmitting at all? A silent router is never learned |
c_forward | the switch is doing its job | move to the router and repeat from Step 1 |
c_drop_same_port | the topology is not what you think | physical wiring, then the learning table |
Step 3 — at the router, read c_to_l3. If it is not advancing while the switch reports c_forward, the frame reached the router and was not addressed to it. That means the host built the frame wrong — it did not believe the destination was off-network — and the problem is in the host's configuration, two devices away from where the symptom appeared.
Step 4 — c_to_l3 advances but nothing egresses. Now no_route and unresolved_nexthop separate cleanly. no_route is configuration and will not resolve on its own. unresolved_nexthop is address resolution in progress and should clear within milliseconds; if it does not, the next hop is unreachable, and you have just moved the whole investigation one hop onward with evidence rather than a guess.
Step 5 — everything counts correctly and the destination still receives nothing. Now capture on the far side, and filter on the payload's source address, never on the origin host's MAC. Section 9 explains why the MAC filter is guaranteed to show nothing.
The method, stated once: each disposition counter is a claim about which device is responsible, and reading it costs a single query. Forming a theory first costs an hour and is usually wrong, because the symptom appears at the endpoint while the cause is somewhere along the path.
16. Common Misconceptions
"A switch is a hub with more ports."
The wrong model: both devices take a frame in one port and send it out others, so the difference is scale.
What it costs: you expect collision behaviour to persist and look for CSMA/CD faults on a full-duplex network where that logic is unreachable. You assume a corrupted frame reaches the far end and debug the receiver. You size buffers as though no store-and-forward exists, then cannot explain the latency you measure.
The corrected model: a hub does not terminate the MAC and therefore has no idea a frame exists; a switch terminates the MAC on every port, checks the FCS, and drops what it does not believe. That is a categorical difference in mutation authority, and it is why a switch contains errors while a hub spreads them.
"The destination MAC address in a frame is the destination host's MAC address."
The wrong model: the frame is addressed to where it is going.
What it costs: every capture filter you write on the far side of a router returns nothing, and you conclude traffic is being lost when it is being correctly re-addressed. You build address-resolution logic that tries to resolve off-network destinations, which cannot succeed. You size a MAC table for every host you can reach rather than for every host on the local network — an error of orders of magnitude.
The corrected model: the destination MAC names the far end of this one link. When the ultimate destination is off-network it is the router's, deliberately chosen by the sender. Only the payload's own addressing survives end to end.
"A router is just a bigger, smarter switch."
The wrong model: the same forwarding operation with a larger table.
What it costs: you expect the frame to survive, so you trace the wrong addresses. You expect broadcast to cross a router boundary, so you cannot explain why discovery protocols stop at it. You estimate routing throughput from switching numbers, missing that the router rebuilds a header and recomputes an FCS per frame.
The corrected model: a switch preserves the frame and selects a port. A router destroys the frame and builds a new one for a different link — which is exactly why it can join a copper link to a fibre one and why it splits broadcast domains.
"Flooding an unknown unicast is a bug."
The wrong model: a forwarding device that does not know where a destination is should drop the frame.
What it costs: you write the assertion in Section 13's warning, waive it forever when it fires on every cold start, or "fix" the design and produce a bridge that can never learn about a station that has not spoken first. Silent devices — many embedded controllers, anything that only ever replies — become permanently unreachable.
The corrected model: flooding is the learning mechanism. It reaches the station, prompts a reply, and the reply's source address teaches the bridge where it is. The right measurement is that flooding is rare in steady state, which is coverage over a window rather than a per-frame assertion.
"Collision domains and broadcast domains are two names for the same thing."
The wrong model: one boundary separates hosts from each other.
What it costs: you deploy switches to fix broadcast load and nothing improves, because a switch floods broadcast by design. You size a host's receive path from unicast estimates and under-provision it by the entire broadcast load — which scales with the domain, not with anything the host does.
The corrected model: a switch splits collision domains and does nothing to broadcast domains; only a router splits those. A thousand hosts on one switched fabric have a thousand collision domains and one broadcast domain.
17. Interview Reasoning
"Walk me through what happens to a frame between two hosts on different subnets."
The answer that ends the topic names two frames and four MAC addresses, and identifies the FCS recomputation as the proof that the first frame was destroyed. Naming the payload's addresses as the only fields that survive end to end shows the boundary is understood rather than memorised.
"Why does a switch need a receive MAC on every port?"
Because it must believe the frame before it acts on it, and belief means checking the FCS. That answer also delivers the error-containment property and the existence of a drop path — which is why the follow-up is usually about cut-through, where the trade is latency against containment and the frame is still not modified.
"How would you find out why traffic to another subnet is failing?"
Anyone can list the layers. What distinguishes a strong answer is naming a first measurement and what each of its outcomes would mean. Section 15's disposition split is that answer, and the reason it is strong is that it identifies the responsible device before any theory is formed.
18. Understanding Check
Four levels, defined by what each device terminates.
- A repeater terminates nothing above the signal. It cannot see a frame, so it cannot change or even check one. Every port shares one collision domain.
- A switch terminates the PHY and the MAC, checks the FCS, and chooses an egress port. It changes no octet of the frame — the FCS is preserved, not recomputed, which is the proof.
- A router terminates the frame itself. It discards the Ethernet header, uses the payload's own addressing to choose a next hop, and builds a new frame with different addresses and a recomputed FCS.
- An end system creates frames and consumes them. It is where a payload enters and leaves the network.
Why this ordering rather than any other: a device cannot change what it has not terminated. The mutation authority in each row is a direct consequence of the termination in the row before it, which is why the ladder is worth learning as a ladder rather than as four unrelated definitions.
The follow-up to be ready for: does cut-through switching break this? No. A cut-through switch still does not modify the frame; it only moves the FCS check later, trading error containment for latency. The taxonomy holds and only the drop timing changes.
19. What's Next
Module 2 set out to describe Ethernet's architecture, and it ends by placing that architecture into hardware. The organising idea of this chapter — that a device taxonomy is a taxonomy of what a device may change — resolves the layer-two/layer-three boundary that Chapter 2.4 first identified, and it does so without appealing to a layer diagram.
A repeater changes nothing. A switch selects a port and preserves every octet. A router destroys the frame and builds a new one, and its recomputed FCS is the evidence. An end system is where frames begin and end.
Module 3 now takes the PHY properly. Chapter 2.6 named the three sublayers and their responsibilities; Module 3 asks what the physical channel actually does and why each sublayer is built the way it is.
Chapter 3.1 — Copper Ethernet starts with the channel most links use, and with the question that makes copper interesting: why a modern twisted-pair PHY is a signal-processing engine rather than a serialiser attached to a cable driver.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
What a Switch Does
One decision per frame multiplied a 24-station network's capacity by 48. It did not remove contention — it moved it from the wire into a queue, where dropping is the mechanism working, not a fault.
- Related topic
The 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.
- Related topic
CXL 2.0 Switching
CXL 2.0 put one switch between a host and its memory. This chapter builds address routing, the single-level constraint, the round-trip latency cost, the shared upstream port, port binding, hot-removal, buffering, error sourcing, fan-out limits and the assembled switch.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
