Ethernet · Module 1
Packet Switching
A circuit allocates capacity in advance and guarantees it; a packet network allocates on demand and guarantees nothing. The exchange is measurable in RTL — idle reserved slots against buffered, delayed and occasionally dropped packets — and it is why a packet must describe its own extent and destination.
Chapter 1.1 and Chapter 1.2 built an access method for a shared medium: sense, defer, detect, jam, back off. That machinery exists to arbitrate a resource at the moment of use.
There is an older answer to sharing, and it does the opposite. Arrange the sharing in advance: give each conversation a reserved portion of the medium for its duration, and the arbitration problem disappears because nothing ever contends.
That answer works. Telephone networks were built on it and it delivers guarantees a packet network cannot. Ethernet does not use it.
When several sources must share one link, why chop the data into self-describing units and let them contend, instead of allocating each source a guaranteed share?
The answer is not "packets are more modern". It is a specific, measurable exchange, and this chapter measures it in RTL.
1. Two Ways to Share One Wire
Strip both models to the decision each one makes.
CIRCUIT SWITCHING — allocate in advance
time → | A | B | C | A | B | C | A | B | C |
└─ each source owns its slots whether or not it uses them
PACKET SWITCHING — allocate on demand
time → | A | A | A | | C | | B | A | A | | C | C |
└─ each unit contends when it exists; idle time is available to anyoneBoth diagrams share one wire between three sources. The difference is when the allocation decision is made, and everything else follows from it.
In the circuit model the decision is made once, before data flows. A source is assigned capacity for the duration of its conversation. From then on there is no arbitration, no addressing per unit, no queue, and no possibility that two sources collide — because the schedule already separated them. The receiver knows what is arriving because the arrangement said so.
In the packet model the decision is made per unit, at the moment of transmission. Nothing is reserved. A source with data contends for the link now, and a source with nothing to send occupies nothing. There is no prior arrangement, which means each unit has to be self-describing and the network has to be able to say no.
2. What a Circuit Guarantees, and What It Costs
The circuit model is worth taking seriously, because its guarantees are real and a packet network spends the rest of its life approximating them.
What it guarantees. A fixed rate, for the duration. Bounded and near-constant delay, because there is no queue to wait in. In-order delivery, because there is only one path and one stream. No loss from congestion, because congestion cannot occur — the admission decision already ensured the sum of allocations fits.
Those four properties are exactly what Module 17's time-sensitive networking spends a whole module reconstructing on top of Ethernet, decades later, for traffic that needs them.
What it costs. Three things, and the third is the one that matters here.
Setup. Before any data moves, the allocation has to be made — and torn down afterwards. For a conversation lasting minutes, negligible. For a hundred-byte exchange, the setup dominates the transfer completely.
Admission failure. When the capacity is fully allocated, a new request is refused. Not slowed — refused. A packet network under the same load delivers everyone's traffic more slowly; a circuit network delivers some conversations perfectly and others not at all.
Idle reservation. A reserved slot belongs to its owner whether or not the owner uses it. If source A is silent, A's slot goes out empty. It cannot be lent to B, because lending it would mean an allocation decision at the moment of use, which is the thing the model exists to avoid.
That third cost is not a flaw in an implementation. It is the mechanism working correctly, and it is what Section 4's RTL will make visible as a counter.
3. What a Packet Must Carry Because Nothing Was Arranged
Remove the advance arrangement and the receiver loses everything the arrangement used to tell it. Each loss becomes a field.
| The arrangement used to say | Without it, the unit must carry | In Ethernet |
|---|---|---|
| whose data this is and where it goes | a destination, and usually a source | destination and source address — Module 5 |
| where this unit starts and ends | a delimiter or a length | preamble, start delimiter, and the length/type field |
| what to do with the contents | a type, or a length that implies it | the length/type field |
| that the contents are intact | a check value, because nothing else checks | the frame check sequence |
This is the whole reason a frame has a header. Not convention, not layering aesthetics — a packet travels alone through a network that made no promises about it, so everything needed to handle it has to travel with it.
Two consequences a hardware engineer meets immediately.
Header overhead is the price of self-description, and it is fixed per unit. A frame carrying 40 payload octets spends a large fraction of its wire time on header and check value; one carrying 1500 spends a small fraction. Chapter 8.3 computes this exactly; the structural point is that the overhead does not shrink with the payload, so small transfers are inherently less efficient on a packet network. Circuit switching has no per-unit header at all, because the arrangement is the header, amortised over the whole conversation.
Parsing is now a hardware problem. A circuit receiver takes bits from its slot and is finished. A packet receiver has to find a boundary, decode a length or a type, decide whether the unit is for it, and validate it — at line rate, on every unit. Module 7's receive datapath and Module 19's frame parser exist entirely because of the choice made in this chapter.
4. RTL 1 — A TDM Circuit Allocator
Start with the model Ethernet did not choose, because the waste has to be visible before the alternative is interesting.
// SYNTHESIZABLE. Fixed time-division allocation across N sources: every
// source owns every Nth slot, whether or not it has data.
//
// NOT an Ethernet block. No signalling, no call setup, no admission control.
// It exists to expose the idle-reservation cost as a counter.
module tdm_circuit_allocator #(
parameter int unsigned SOURCES = 4,
localparam int unsigned SRC_W = $clog2(SOURCES)
) (
input logic clk,
input logic rst_n,
// Per-source offered load: bit i high means source i has data this slot.
input logic [SOURCES-1:0] src_has_data,
// The slot's owner and whether the slot carried anything.
output logic [SRC_W-1:0] slot_owner,
output logic slot_used,
output logic slot_wasted,
// Sources that had data and could not send, because it was not their slot.
output logic [SOURCES-1:0] src_blocked
);
logic [SRC_W-1:0] rr_q;
// The schedule is FIXED and data-independent. That is the definition of a
// circuit: the allocation was decided in advance, so nothing observed at
// run time may change it. A round-robin that skipped idle owners would be
// a different machine entirely — it would be making an allocation decision
// at the moment of use, which is Section 5's model, not this one.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) rr_q <= '0;
else if (rr_q == SRC_W'(SOURCES - 1)) rr_q <= '0;
else rr_q <= rr_q + 1'b1;
end
assign slot_owner = rr_q;
assign slot_used = src_has_data[rr_q];
assign slot_wasted = !src_has_data[rr_q];
// A source is blocked when it has data and the slot belongs to someone
// else — even when that someone else is idle and the slot is going out
// empty. This combination is the entire cost of the model, and it is why
// the two outputs above and this one must be observed together.
always_comb begin
src_blocked = '0;
for (int unsigned i = 0; i < SOURCES; i++)
src_blocked[i] = src_has_data[i] && (SRC_W'(i) != rr_q);
end
endmoduleClassification: synthesizable.
What it teaches: that a circuit's guarantee and its waste are the same property seen from two sides. slot_wasted and src_blocked can be high in the same cycle — a slot going out empty while another source has data and is not permitted to use it. No amount of implementation quality removes that; it is what "allocated in advance" means.
Why the schedule must not consult src_has_data. A reader will want to add "skip the owner if it is idle". That single change converts this into a demand-driven allocator, which is Section 5's model with extra steps, and it destroys every guarantee the circuit model exists to provide — the owner can no longer count on its slot being there. The refusal to look at the data is the design, and it is why the always_ff above has no data input.
Deliberately simplified: one slot per source per cycle; no setup or teardown; no admission control, so SOURCES allocations always fit; no per-source rate weighting; no signalling channel.
Production implication: a real TDM system carries a signalling plane for setup and teardown, an admission-control function that refuses new circuits when the sum would exceed capacity, and a frame structure that lets a receiver find slot boundaries — none of which changes the behaviour above, which is why that behaviour is what this model isolates.
5. RTL 2 — A Packet Queue
The other model. No schedule, no ownership; a unit contends when it exists, and the buffer absorbs what the link cannot take right now.
// SYNTHESIZABLE. On-demand allocation: whoever has a unit contends for the
// link now, and the buffer absorbs the excess.
//
// NOT an Ethernet FIFO. No CDC, no frame boundaries, no byte enables.
module packet_queue #(
parameter int unsigned DEPTH = 16,
parameter int unsigned WIDTH = 8,
localparam int unsigned PTR_W = $clog2(DEPTH),
localparam int unsigned CNT_W = $clog2(DEPTH + 1)
) (
input logic clk,
input logic rst_n,
input logic enq_valid,
input logic [WIDTH-1:0] enq_data,
output logic enq_ready,
output logic enq_dropped, // offered while full: the "no"
input logic deq_ready,
output logic deq_valid,
output logic [WIDTH-1:0] deq_data,
output logic [CNT_W-1:0] occupancy,
output logic [CNT_W-1:0] high_water // deepest the queue ever got
);
logic [WIDTH-1:0] mem_q [DEPTH];
logic [PTR_W-1:0] wr_q, rd_q;
logic [CNT_W-1:0] cnt_q, hw_q;
wire full = (cnt_q == CNT_W'(DEPTH));
wire empty = (cnt_q == '0);
// enq_ready does NOT depend on enq_valid — no combinational path from a
// producer's valid back to its own ready. Same handshake discipline the
// rest of the track uses.
assign enq_ready = !full;
assign deq_valid = !empty;
assign deq_data = mem_q[rd_q];
wire do_enq = enq_valid && enq_ready;
wire do_deq = deq_valid && deq_ready;
// THE DROP. A packet network's defining ability is to refuse a unit when
// it cannot hold it. A circuit has no equivalent: it refuses at SETUP and
// never afterwards. Counting this separately from `occupancy` is what
// makes Section 7's comparison honest — a queue that never drops has not
// been loaded hard enough to prove anything.
assign enq_dropped = enq_valid && !enq_ready;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_q <= '0; rd_q <= '0; cnt_q <= '0; hw_q <= '0;
end else begin
if (do_enq) begin
mem_q[wr_q] <= enq_data;
wr_q <= (wr_q == PTR_W'(DEPTH - 1)) ? '0 : wr_q + 1'b1;
end
if (do_deq)
rd_q <= (rd_q == PTR_W'(DEPTH - 1)) ? '0 : rd_q + 1'b1;
// One counter for both directions rather than comparing pointers.
// Pointer comparison cannot distinguish full from empty without an
// extra bit, and the extra bit is exactly this counter with the
// information thrown away.
case ({do_enq, do_deq})
2'b10: cnt_q <= cnt_q + 1'b1;
2'b01: cnt_q <= cnt_q - 1'b1;
default: cnt_q <= cnt_q; // both or neither: no net change
endcase
if (cnt_q > hw_q) hw_q <= cnt_q;
end
end
assign occupancy = cnt_q;
assign high_water = hw_q;
endmoduleClassification: synthesizable.
What it teaches: that the buffer is the mechanism. On-demand allocation only works because something can hold a unit that arrives when the link is busy, and the depth of that something is the design's guess about how bursty the traffic will be. Section 12 develops what happens when the guess is wrong.
The high_water register earns its area. Occupancy tells you the queue's state now; high water tells you how close the design came to dropping. A system that has never dropped a packet and has never exceeded half depth is over-provisioned; one that has never dropped and repeatedly touches full is one burst away from a fault. The two readings are indistinguishable from the drop counter alone, which is why every serious queue exposes both.
Deliberately simplified: single clock, no CDC; fixed-width units with no packet boundaries, so a "packet" here is one beat; drop-on-full at the tail with no drop-from-head policy; no per-source accounting, so it cannot show which source is causing the pressure.
Production implication: a real transmit queue carries whole frames rather than beats, so a drop decision must be taken at the frame boundary or a partial frame is emitted; it tracks per-source or per-priority occupancy so backpressure can be selective, which is what Module 14's priority flow control needs; and its depth is chosen against a stated burst-absorption requirement rather than a round number.
6. Statistical Multiplexing — The Result That Decides It
Here is the argument in one paragraph, and it is the reason the industry moved.
With N sources each active a fraction d of the time, a circuit network must reserve N full shares — because each source needs its share whenever it is active, and the reservation cannot know when that will be. A packet network needs enough capacity for the aggregate, and the aggregate of many independent bursty sources is far smoother than any one of them. Sources are rarely all active at once, so the peak of the sum is much less than the sum of the peaks.
That gap is the statistical multiplexing gain, and it is the entire economic case for packet switching.
Two things make it work, and both fail in identifiable ways.
Independence. The gain comes from sources not being busy simultaneously. Correlated sources — every machine in a rack starting a backup at midnight, or every node in a cluster returning results at the end of a compute step — remove the independence and with it the gain. Module 23's AI-cluster chapter is precisely this failure at scale: synchronised traffic that defeats statistical multiplexing and forces the fabric to be provisioned closer to the sum of the peaks.
Enough sources. With two sources the aggregate is barely smoother than one. With hundreds it is very much smoother. The gain grows with the number of independent contributors, which is why the argument is stronger for a data-centre fabric than for a two-station link.
And the gain is not free. It is bought with exactly three things: a buffer, an unpredictable delay, and the possibility that a unit is discarded. Section 7 measures the first side; Section 11 counts the second.
7. RTL 3 — Measuring the Trade
An argument that ends in "far smoother" needs an instrument. This block runs both models against the same offered load and produces the numbers.
// SYNTHESIZABLE (counters only). Runs the two allocation models against one
// offered load and counts what each wastes.
//
// NOT an Ethernet block. Pure instrumentation for the Section 6 comparison.
module switching_efficiency_counters #(
parameter int unsigned W = 24
) (
input logic clk,
input logic rst_n,
input logic clear,
// Common offered load.
input logic offered, // a unit existed somewhere this cycle
// Circuit model observations, from tdm_circuit_allocator.
input logic tdm_slot_used,
input logic tdm_slot_wasted,
input logic tdm_blocked, // any source blocked this cycle
// Packet model observations, from packet_queue.
input logic pkt_delivered,
input logic pkt_dropped,
output logic [W-1:0] offered_cnt,
output logic [W-1:0] tdm_used_cnt,
output logic [W-1:0] tdm_wasted_cnt,
output logic [W-1:0] tdm_blocked_cnt,
output logic [W-1:0] pkt_delivered_cnt,
output logic [W-1:0] pkt_dropped_cnt
);
// SATURATING, every one of them. A wrapping statistics counter reports a
// small number after a long run and reads as a healthy system — the most
// dangerous possible failure for a counter whose only job is to be read
// by a human drawing a conclusion.
`define SAT_CNT(name) \
always_ff @(posedge clk or negedge rst_n) begin \
if (!rst_n) name``_cnt <= '0; \
else if (clear) name``_cnt <= '0; \
else if (name && name``_cnt != {W{1'b1}}) \
name``_cnt <= name``_cnt + 1'b1; \
end
`SAT_CNT(offered)
`SAT_CNT(pkt_delivered)
`SAT_CNT(pkt_dropped)
// The three circuit-model counters, written out rather than macro-generated
// so the names stay searchable in a waveform viewer.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tdm_used_cnt <= '0; tdm_wasted_cnt <= '0; tdm_blocked_cnt <= '0;
end else if (clear) begin
tdm_used_cnt <= '0; tdm_wasted_cnt <= '0; tdm_blocked_cnt <= '0;
end else begin
if (tdm_slot_used && tdm_used_cnt != {W{1'b1}}) tdm_used_cnt <= tdm_used_cnt + 1'b1;
if (tdm_slot_wasted && tdm_wasted_cnt != {W{1'b1}}) tdm_wasted_cnt <= tdm_wasted_cnt + 1'b1;
if (tdm_blocked && tdm_blocked_cnt != {W{1'b1}}) tdm_blocked_cnt <= tdm_blocked_cnt + 1'b1;
end
end
`undef SAT_CNT
endmoduleClassification: synthesizable; instrumentation only, no datapath.
What it teaches: that the comparison between the two models is a measurement, not an opinion, and that the measurement needs both a waste counter and a loss counter because the two models fail differently. The circuit model never loses a unit and wastes capacity; the packet model never wastes capacity and can lose units. Reading only one counter makes whichever model you already prefer look better.
The reading that settles it. Run a bursty load past both. The circuit model's tdm_wasted_cnt and tdm_blocked_cnt rise together — slots going out empty while other sources have data and may not use them. The packet model's pkt_dropped_cnt stays at zero until the queue is genuinely overloaded, and its delivered count tracks the offered count almost exactly. That simultaneous waste-and-block is the number that decided the industry, and it is two counters wide.
Deliberately simplified: cycle-granular rather than octet-granular; no latency measurement, so it shows throughput and not delay; tdm_blocked collapses a per-source vector to one bit.
Production implication: real statistics are per-port and per-priority, are read atomically as a snapshot so a set of counters describes one instant, and define clear-on-read behaviour explicitly — because two readers that both clear will each see half the events.
8. Burstiness Is the Whole Argument
Section 6 said the gain depends on duty cycle. It is worth making concrete, because it also explains why the answer was different for telephony and is different again for a synchronised compute cluster.
| Traffic | Duty cycle | Better model | Why |
|---|---|---|---|
| Voice call | near 1 | circuit | a reserved slot is used almost always; almost nothing is wasted |
| Terminal session | very low | packet | reserving for a source that transmits in occasional keystrokes wastes nearly everything |
| File transfer | bursty: 1 then 0 | packet | wants the whole link briefly, then nothing at all |
| Synchronised cluster | near 1, and correlated | neither is comfortable | the sum of the peaks is the peak of the sum; statistical gain vanishes |
All duty-cycle figures here are illustrative characterisations, not measured values.
The last row is the interesting one for a semiconductor engineer today. Statistical multiplexing assumes independence, and a cluster whose nodes all finish a computation together and all transmit at once has none. The fabric must then be provisioned near the sum of the peaks — which is close to what a circuit network would have reserved — while still paying the packet network's buffering and loss costs.
This is why deterministic mechanisms keep being added back to Ethernet. Module 17's time-aware shaping is, in effect, a circuit reintroduced on top of a packet network for the traffic that needs the guarantee. The industry did not decide circuits were wrong; it decided they were wrong for bursty traffic, and it has been carefully re-adding them ever since for the traffic that is not.
9. RTL 4 — The Length-Delimited Framer
Section 3 established that a packet must describe its own extent. Here is what that costs in hardware.
// SYNTHESIZABLE. Prefixes a payload with its own length so a receiver can
// find the boundary without a prior arrangement.
//
// NOT the Ethernet frame format: no preamble, no SFD, no addresses, no FCS,
// no length/type disambiguation. Chapter 5.1 owns the real thing.
module packet_framer #(
parameter int unsigned WIDTH = 8,
parameter int unsigned MAX_LEN = 255,
localparam int unsigned LEN_W = $clog2(MAX_LEN + 1)
) (
input logic clk,
input logic rst_n,
// Payload in, with an explicit end marker from the client.
input logic in_valid,
input logic [WIDTH-1:0] in_data,
input logic in_last,
output logic in_ready,
// Framed stream out: one length beat, then the payload beats.
output logic out_valid,
output logic [WIDTH-1:0] out_data,
input logic out_ready,
output logic len_overflow // payload exceeded MAX_LEN
);
typedef enum logic [1:0] {
F_COLLECT = 2'd0, // buffering the payload, counting it
F_LENGTH = 2'd1, // emitting the length beat
F_PAYLOAD = 2'd2 // emitting the buffered payload
} f_state_e;
f_state_e state_q, state_d;
logic [WIDTH-1:0] buf_q [MAX_LEN];
logic [LEN_W-1:0] len_q, rd_q;
logic ovf_q;
// THE STRUCTURAL COST OF SELF-DESCRIPTION, in one line: the length must be
// transmitted BEFORE the payload it describes, and it is not known until
// the payload has ended. So the payload must be BUFFERED WHOLE before the
// first bit goes out. This is store-and-forward, and it is not an
// implementation choice — it is forced by putting a length in a header.
//
// The alternative Ethernet actually took is worth naming here: delimit the
// END instead of declaring the length up front, which allows cut-through.
// Chapter 12.6 shows what that buys a switch.
assign in_ready = (state_q == F_COLLECT) && !ovf_q;
wire do_in = in_valid && in_ready;
wire do_out = out_valid && out_ready;
always_comb begin
state_d = state_q;
case (state_q)
F_COLLECT: if (do_in && in_last) state_d = F_LENGTH;
F_LENGTH: if (do_out) state_d = F_PAYLOAD;
F_PAYLOAD: if (do_out && (rd_q == len_q - 1'b1)) state_d = F_COLLECT;
default: state_d = F_COLLECT;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= F_COLLECT; len_q <= '0; rd_q <= '0; ovf_q <= 1'b0;
end else begin
state_q <= state_d;
if (state_q == F_COLLECT) begin
if (do_in) begin
buf_q[len_q] <= in_data;
if (len_q == LEN_W'(MAX_LEN)) ovf_q <= 1'b1;
else len_q <= len_q + 1'b1;
end
if (state_d == F_LENGTH) rd_q <= '0;
end else if (state_q == F_PAYLOAD && do_out) begin
rd_q <= rd_q + 1'b1;
end
if (state_d == F_COLLECT && state_q == F_PAYLOAD) begin
len_q <= '0; ovf_q <= 1'b0;
end
end
end
assign out_valid = (state_q == F_LENGTH) || (state_q == F_PAYLOAD);
assign out_data = (state_q == F_LENGTH) ? WIDTH'(len_q) : buf_q[rd_q];
assign len_overflow = ovf_q;
endmoduleClassification: synthesizable.
What it teaches: the most transferable idea in this chapter — a length-prefixed format forces store-and-forward. The header cannot be emitted until the payload has ended, so the whole unit must be held. That is a latency cost of one full unit, paid on every unit, and it is the direct consequence of choosing to declare the extent rather than delimit it.
Why this matters far beyond this chapter. Ethernet does not length-prefix its payload in this way; it marks the start and lets the end be signalled. That choice is what makes cut-through switching possible at all — a switch can begin forwarding once it has seen the destination address, without waiting for the frame to end. Chapter 12.6 measures what that is worth, and this module is why the question exists.
Deliberately simplified: one packet in flight; a full-depth buffer sized to MAX_LEN rather than to a realistic distribution; no error path for a payload that ends early; length carried in one beat, so MAX_LEN cannot exceed what WIDTH can express.
Production implication: a real framer streams rather than buffers wherever the format allows it, sizes storage against a length distribution rather than the maximum, and defines behaviour for a client that abandons a payload mid-transfer — which here would leave the state machine in F_COLLECT with a partial buffer and no way to release it.
10. RTL 5 — The Deframer, and Why It Is Harder
Framing is bookkeeping. Deframing is where a packet network's trust problem becomes hardware, because the receiver is decoding a field that arrived from somewhere else and may be wrong.
// SYNTHESIZABLE. Recovers a payload from a length-prefixed stream, and
// treats the declared length as an ASSERTION BY A STRANGER, not as truth.
//
// NOT an Ethernet receiver. Chapter 7.2 owns the real receive datapath.
module packet_deframer #(
parameter int unsigned WIDTH = 8,
parameter int unsigned MAX_LEN = 255,
localparam int unsigned LEN_W = $clog2(MAX_LEN + 1)
) (
input logic clk,
input logic rst_n,
input logic in_valid,
input logic [WIDTH-1:0] in_data,
input logic in_abort, // upstream lost the stream
output logic in_ready,
output logic out_valid,
output logic [WIDTH-1:0] out_data,
output logic out_last,
output logic err_zero_len, // a length of zero
output logic err_too_long, // a length beyond MAX_LEN
output logic err_truncated // the stream ended mid-payload
);
typedef enum logic [1:0] {
D_LENGTH = 2'd0,
D_PAYLOAD = 2'd1,
D_ERROR = 2'd2
} d_state_e;
d_state_e state_q, state_d;
logic [LEN_W-1:0] remaining_q;
logic ez_q, etl_q, etr_q;
assign in_ready = (state_q != D_ERROR);
wire do_in = in_valid && in_ready;
// The length field is data from another machine. Every one of the three
// checks below is a case where believing it costs something specific:
//
// zero -> a packet with no payload; a counter that assumes at least
// one beat under-runs and the state machine hangs.
// too long -> a receiver that trusts it waits forever for beats that
// will never come, and the next real packet is consumed as
// this one's payload. One bad length desynchronises the
// stream permanently.
// truncated -> the payload ended early; the partial unit must be
// discarded rather than delivered short.
wire len_is_zero = do_in && (state_q == D_LENGTH) && (in_data == '0);
wire len_too_big = do_in && (state_q == D_LENGTH) &&
(in_data > WIDTH'(MAX_LEN));
always_comb begin
state_d = state_q;
case (state_q)
D_LENGTH: if (in_abort) state_d = D_ERROR;
else if (len_is_zero || len_too_big) state_d = D_ERROR;
else if (do_in) state_d = D_PAYLOAD;
D_PAYLOAD: if (in_abort) state_d = D_ERROR;
else if (do_in && remaining_q == 1) state_d = D_LENGTH;
D_ERROR: state_d = D_LENGTH; // resynchronise on the next unit
default: state_d = D_LENGTH;
endcase
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= D_LENGTH; remaining_q <= '0;
ez_q <= 1'b0; etl_q <= 1'b0; etr_q <= 1'b0;
end else begin
state_q <= state_d;
ez_q <= len_is_zero;
etl_q <= len_too_big;
// Truncation is detected by the ABORT arriving mid-payload — the one
// error class the length field itself cannot reveal, because a short
// stream and a correct stream look identical until the beats stop.
etr_q <= (state_q == D_PAYLOAD) && in_abort;
if (state_q == D_LENGTH && do_in && !len_is_zero && !len_too_big)
remaining_q <= LEN_W'(in_data);
else if (state_q == D_PAYLOAD && do_in && remaining_q != '0)
remaining_q <= remaining_q - 1'b1;
end
end
assign out_valid = (state_q == D_PAYLOAD) && do_in;
assign out_data = in_data;
assign out_last = out_valid && (remaining_q == 1);
assign err_zero_len = ez_q;
assign err_too_long = etl_q;
assign err_truncated = etr_q;
endmoduleClassification: synthesizable.
What it teaches: that a received header field is an assertion by a stranger. The framer's length was computed from data it held; the deframer's length arrived over a network that made no promises. Every field a packet carries because nothing was arranged in advance is also a field that can be wrong, and the receiver's error handling is the price of self-description just as surely as the header bits are.
Why the too-long check is the important one. A zero length hangs one packet. A too-long length desynchronises the stream permanently: the receiver waits for beats that never arrive, then consumes the next packet's header as this packet's payload, and every subsequent boundary is wrong. One corrupted field costs every packet after it until something forces a resynchronisation. This is exactly why Chapter 7.3's frame-validity rules and Module 6's check value exist, and why D_ERROR here exits to D_LENGTH rather than latching.
Deliberately simplified: no check value, so a corrupted payload passes silently; resynchronisation is optimistic — it assumes the next beat is a length, which a real receiver cannot assume; in_abort stands in for a physical-layer error indication.
Production implication: a real receiver validates with a check value covering the whole unit, resynchronises on an explicit delimiter rather than by guessing, counts each error class separately for the taxonomy Chapter 21.2 builds, and defines whether a partially-received unit is delivered with an error flag or silently discarded.
11. What Packet Switching Costs
The gain in Section 6 is real and so is the bill. Four costs, and each becomes a module later in this track.
| Cost | Why it is inherent | Where the track handles it |
|---|---|---|
| Per-unit overhead | self-description does not shrink with the payload | Chapter 8.3, effective throughput |
| Variable delay | queueing time depends on other traffic, which is not knowable locally | Chapter 8.4, latency decomposition |
| Loss under congestion | on-demand allocation must be able to say no | Module 14, flow control |
| Reordering, in general | independent units may take independent paths | Chapter 15.2, why aggregation hashes per flow |
The third and fourth deserve a note each, because engineers meet them as surprises.
Loss is not a defect. A circuit network refuses at setup; a packet network accepts and may discard later. Both refuse — they differ in when, and the packet network's timing means the refusal appears as a lost unit rather than a failed request. A design that treats every discarded frame as a fault will chase congestion as though it were a bug.
Reordering is possible in general and rare in Ethernet. A single Ethernet link delivers in order, and a switch forwarding a flow out one port keeps it in order. But nothing in packet switching as a model guarantees it, and the moment a design spreads a flow across several paths the guarantee is gone — which is exactly why link aggregation hashes whole flows onto one member rather than striping frames across all of them. Chapter 15.2 owns that, and the reason lives here.
12. Buffering as the Price of the Gain
Statistical multiplexing works because a buffer absorbs the moments when the aggregate exceeds the link. That makes buffer depth a first-class design parameter, and it is a parameter with a bad shape: both directions are wrong.
Too shallow and bursts are dropped that the link could have carried a moment later. The statistical gain is not realised, because absorbing bursts is the gain.
Too deep and units sit in the queue for a long time before being sent. Throughput is fine, latency is terrible, and — worse — a unit may be delivered so late that whatever needed it has already given up, so the link did work for nothing. Deep buffers converting a loss problem into a latency problem is a well-known failure mode in networking equipment.
And depth interacts with everything downstream. Module 14's flow control exists to signal before the buffer fills; Chapter 8.4 decomposes the delay a buffer adds; Chapter 12.5 sizes a switch's tables and buffers together. The parameter chosen here is a parameter three later modules argue about.
13. Waveform — Two Sources, Two Switching Models
The same bursty offered load, through both models. Read what happens at cycles 2 and 3.
Circuit waste against packet absorption
10 cyclesCycle 1 and cycle 3 are the argument. tdm_wasted and a_blocked are high in the same cycle: the slot belongs to B, B has nothing, the slot goes out empty, and A has data it is not permitted to send. The link carried nothing while a source wanted it. No implementation quality fixes this, because the allocation was made before either fact was knowable.
Cycle 5 is the counter-argument, and it is real. B's data arrives and B's slot is there, immediately, with no queueing and no risk of being dropped. That is the guarantee, and it is what the circuit model bought with all the waste above.
The packet rows show the cost as well as the gain. pkt_occ is non-zero from cycle 1 to cycle 8 — every one of those cycles is a unit sitting in a buffer, waiting, adding delay that the circuit model's units never experienced. The packet model did not get something for nothing; it traded a fixed waste for a variable delay.
14. Assertions
Invariants of these models. None is an IEEE requirement; they encode the design contracts stated above.
// SVA over the modules in this chapter.
// SAFETY — P1: the circuit schedule is data-independent. This is the property
// that makes it a circuit at all; a failure means someone "optimised" the
// allocator to skip idle owners, which silently destroys the guarantee.
property p_schedule_ignores_data;
@(posedge clk) disable iff (!rst_n)
##1 (slot_owner == $past((slot_owner == SOURCES-1) ? 0 : slot_owner + 1));
endproperty
a_schedule_fixed : assert property (p_schedule_ignores_data);
// SAFETY — P2: waste and block CAN coincide, and the design must not hide it.
// Written as a coverage-style check rather than a prohibition: if this never
// fires, the testbench never loaded the model hard enough to show the cost.
property p_waste_and_block_observable;
@(posedge clk) disable iff (!rst_n)
(slot_wasted && |src_blocked);
endproperty
c_waste_and_block : cover property (p_waste_and_block_observable);
// CONSERVATION — P3: queue occupancy accounts for every unit. Catches the
// classic dual-pointer bug where full and empty become indistinguishable.
property p_occupancy_conserved;
@(posedge clk) disable iff (!rst_n)
##1 (occupancy == $past(occupancy) + $past(do_enq) - $past(do_deq));
endproperty
a_occupancy_conserved : assert property (p_occupancy_conserved);
// SAFETY — P4: never enqueue into a full queue. The drop path exists so this
// cannot happen; asserting it catches a bypass added for "performance".
property p_no_enq_when_full;
@(posedge clk) disable iff (!rst_n)
(occupancy == DEPTH) |-> !do_enq;
endproperty
a_no_enq_full : assert property (p_no_enq_when_full);
// SAFETY — P5: a drop is reported whenever one occurs. A queue that silently
// discards is worse than one that overflows loudly, because the loss is then
// attributed to the network rather than to the buffer.
property p_drop_reported;
@(posedge clk) disable iff (!rst_n)
(enq_valid && !enq_ready) |-> enq_dropped;
endproperty
a_drop_reported : assert property (p_drop_reported);
// CAUSATION — P6: the framer emits exactly as many payload beats as the
// length it declared. The single most important property in the chapter:
// a mismatch here desynchronises every receiver downstream.
property p_framer_length_matches;
@(posedge clk) disable iff (!rst_n)
(state_q == F_PAYLOAD && do_out && rd_q == len_q - 1)
|=> (state_q == F_COLLECT);
endproperty
a_framer_len_matches : assert property (p_framer_length_matches);
// SAFETY — P7: the deframer never emits more beats than the declared length.
// Catches a decrement that misses a cycle, which would run past the packet
// boundary and consume the next packet's header.
property p_deframer_bounded;
@(posedge clk) disable iff (!rst_n)
out_valid |-> (remaining_q != '0);
endproperty
a_deframer_bounded : assert property (p_deframer_bounded);
// SAFETY — P8: an illegal declared length never enters the payload state.
// Believing a bad length is what desynchronises a stream permanently.
property p_bad_length_rejected;
@(posedge clk) disable iff (!rst_n)
(len_is_zero || len_too_big) |=> (state_q == D_ERROR);
endproperty
a_bad_length_rejected : assert property (p_bad_length_rejected);
// SAFETY — P9: statistics counters saturate rather than wrap. A wrapped
// counter reads as a healthy small number after a long run.
property p_counters_saturate;
@(posedge clk) disable iff (!rst_n)
(pkt_dropped_cnt == {W{1'b1}}) |=> (pkt_dropped_cnt == {W{1'b1}});
endproperty
a_counters_saturate : assert property (p_counters_saturate);
// LIVENESS — P10: a queued unit eventually leaves. ASSUMPTION, stated: the
// sink eventually accepts. Without it this is false for a correct design,
// because a permanently stalled sink is not a bug in the queue.
assume property (@(posedge clk) s_eventually (deq_ready));
property p_queued_unit_departs;
@(posedge clk) disable iff (!rst_n)
do_enq |-> s_eventually (do_deq);
endproperty
a_unit_departs : assert property (p_queued_unit_departs);The property that must not be written
// FALSE for a correct design. Included as a warning, not as a check.
// property p_no_packet_is_ever_dropped;
// @(posedge clk) disable iff (!rst_n)
// enq_valid |-> ##[1:$] deq_valid;
// endpropertyIt reads like the definition of a working network, and it is wrong at the level of the model, not the implementation.
A packet network's ability to drop is a feature. On-demand allocation means capacity is not reserved, which means an arrival can find nothing available, which means the unit must be refused. An implementation that could not drop would have to block its producer forever, which converts a loss problem into a deadlock.
The correct property is the conservation pair, P3 with P5: every unit is either delivered or counted as dropped, and none is silently lost. That is what a packet network actually promises, and it is a much more useful thing to check than an impossible guarantee. Writing the impossible version gets it waived, and the waiver then also covers P5 — which is the one that catches a queue quietly discarding units.
15. Verification
Monitors observe: offered load per source, the TDM owner and its used/wasted/blocked outputs, every queue handshake and its occupancy and high water, the framer's declared length against its emitted beat count, and each deframer error line.
The scoreboard independently predicts the beat sequence out of the framer from the payload in, and the payload out of the deframer from the framed stream in. It must count beats itself rather than reading remaining_q — a checker that reads the design's counter agrees with the design about every decrement bug in it.
Scenarios
- Uniform full load, both models. Every source has data every cycle. Verify the TDM allocator wastes nothing and the queue drops as expected — the case where the circuit model is not worse, which the comparison must be honest about.
- Single active source. One source with data, others idle. Verify
tdm_wastedandsrc_blockedare high together for every foreign slot; verify the queue delivers everything with no drops. The chapter's central measurement. - Bursty independent sources. Randomised on/off with low duty cycle. Verify the delivered count tracks the offered count in the packet model and does not in the circuit model.
- Correlated sources. All sources active in the same cycles. Verify the packet model's drop count rises — the Section 6 failure of statistical multiplexing, made reproducible.
- Queue exactly full, then one more. Verify the drop is reported (P5), occupancy does not advance (P4), and the next dequeue recovers cleanly.
- Queue drained to empty, then dequeue. Verify no underflow and
deq_validlow. - Simultaneous enqueue and dequeue at full and at empty. Both boundaries, both directions at once — where the single-counter update in Section 5 is either right or subtly wrong.
- Framer, minimum payload. One beat. Verify the length beat says 1 and exactly one payload beat follows.
- Framer, maximum payload and one beat beyond. Verify
len_overflowand that no truncated frame is emitted. - Deframer, declared length zero. Verify
err_zero_len, the error state, and clean resynchronisation. - Deframer, declared length beyond
MAX_LEN. Verifyerr_too_longand — the important part — that the next packet is decoded correctly, proving the stream resynchronised. - Deframer, abort mid-payload. Verify
err_truncatedand that no partial payload is delivered as complete. - Back-to-back packets with no gap. Verify the framer's return to
F_COLLECTand the deframer's return toD_LENGTHboth happen in time for the next unit. - Reset in each state of both state machines. Verify no stale length, no partial buffer presented, and a clean first packet afterwards.
Coverage
Cross occupancy against both handshakes at every boundary value: empty, one, DEPTH-1, full. Cover declared lengths of 1, 2, MAX_LEN-1, MAX_LEN, MAX_LEN+1 and 0. Cover the TDM owner index against each source's data state, so the waste-and-block cross in P2 is hit for every source. Cover reset in each state of each state machine.
A directed stimulus for the desynchronisation case
Scenario 11 is the one that matters most and the one randomisation reaches least reliably, because it needs a corrupted field followed by a correct packet.
// NON-SYNTHESIZABLE — directed stimulus. Sends one packet with an illegal
// declared length, then a correct packet, and checks that the SECOND one is
// decoded correctly. That second check is the whole test.
task automatic bad_length_then_good_packet();
// 1. A length the deframer must refuse.
send_beat(.data(WIDTH'(MAX_LEN + 1))); // the illegal length beat
@(posedge clk);
assert (dut.err_too_long)
else $error("illegal length was accepted");
assert (dut.state_q == dut.D_ERROR)
else $error("illegal length did not enter the error state");
// 2. A correct packet immediately afterwards, with no gap. If the design
// resynchronised by guessing rather than by design, this is where it
// consumes the length beat as payload and everything after is wrong.
send_beat(.data(WIDTH'(3))); // length = 3
send_beat(.data(8'hA1));
send_beat(.data(8'hA2));
send_beat(.data(8'hA3));
// 3. The assertion that the whole task exists for.
assert (scoreboard_last_packet == '{8'hA1, 8'hA2, 8'hA3})
else $error("stream did not resynchronise: one bad length corrupted the next packet");
endtaskCall it with MAX_LEN + 1 and again with 0. A design that latches its error state passes the first check and fails the third on both calls; a design that resynchronises correctly passes all three. The third assertion is the executable form of Section 10's argument that a bad length costs every packet after it, not just its own.
16. Debugging — Which Half of the Trade Broke
Packet-network faults divide cleanly by which side of Section 11's bill is being paid wrongly.
| Symptom | Which half | First thing to check |
|---|---|---|
| Throughput below the link rate, no drops | Buffering or scheduling, not loss | Queue occupancy — a queue at zero means nothing is arriving to send |
| Drops rising with load, latency stable | The queue is doing its job at its limit | High water against depth, and the burst size that causes it |
| Latency high, no drops, throughput fine | Too much buffering | Depth against the delay budget; a deep queue trades one problem for another |
| Occasional corrupted or missing packets, no counter moves | Something is discarding silently | Look for a path that drops without incrementing — the defect P5 targets |
| One bad packet followed by many | A boundary was lost | Deframer error counters, and whether resynchronisation actually works |
The last row is the signature worth memorising. A burst of consecutive failures after a single event is almost never a burst of independent faults — it is one desynchronisation and its consequences. In a length-delimited stream that is a bad length field; in Ethernet it is a lost delimiter or a corrupted length/type field, and Chapter 21.2's taxonomy separates the two.
And the fourth row is the one that wastes the most time. A drop that is not counted is attributed to the network, so the investigation goes to cabling and switches while the fault is a buffer inside the design. Every discard path needs a counter, which is why P5 exists and why Section 7 counts drops separately from occupancy.
17. Where Ethernet Sits
Ethernet is a packet-switched network and makes every trade in this chapter, with two choices worth naming now because later modules depend on them.
It delimits rather than length-prefixes. Section 9 showed that declaring a length up front forces store-and-forward. Ethernet marks the start of a frame and lets the end be signalled, so a switch can begin forwarding after reading only the destination address. Chapter 12.6 measures what that buys and what it costs — principally that a cut-through switch forwards a frame before it can know whether the check value is good.
It is best-effort by design. The MAC delivers or reports failure; it never guarantees. Chapter 1.2 showed the transmit side giving up after sixteen attempts; the receive side discards anything that fails validation. Reliability, where it is needed, is built above — which is the layering argument Chapter 2.3 develops.
Everything that follows in this track is a consequence of the model chosen here. The frame has a header because nothing was arranged in advance. Switches have buffers because on-demand allocation needs somewhere to absorb a burst. Flow control exists because a buffer can fill. Time-sensitive networking exists because some traffic wanted the guarantee that was traded away.
18. Common Misconceptions
"Packet switching is more efficient than circuit switching."
The wrong model: one is better; the other was a historical mistake.
What it costs: a design that reaches for packet switching where a circuit is genuinely better — a constant-rate stream with a hard delay bound — and then spends enormous effort rebuilding the guarantee with shaping, priorities and over-provisioning. This is a real and expensive pattern in industrial and automotive systems.
The corrected model: efficiency depends entirely on duty cycle. For a source that transmits continuously, a reserved slot wastes almost nothing and a circuit is efficient. For bursty traffic, reservation wastes nearly everything. Ethernet chose the model that suits bursty computer traffic, and Module 17 exists because some traffic on Ethernet is not bursty and wants the other model back.
"Dropped packets mean something is broken."
The wrong model: a discard is a fault, so a non-zero drop counter means find the bug.
What it costs: time spent hunting a defect that does not exist, and — worse — the real signal being missed. A drop counter rising smoothly with load is the queue working at its limit. A drop counter rising while the link is idle is a genuine fault. Treating both as bugs makes the counter unreadable.
The corrected model: on-demand allocation must be able to refuse, so discarding is the mechanism, not its failure. A circuit network refuses at setup; a packet network accepts and may discard later. What is a fault is a discard that is not counted, because that loss gets attributed to the network instead of to the buffer that caused it.
"A bigger buffer is a better buffer."
The wrong model: drops are bad, buffers prevent drops, therefore more buffer is better.
What it costs: excellent throughput and unusable latency. Units sit queued long enough that the application that wanted them has already timed out and retransmitted, so the link does work whose result is discarded — and the retransmission adds load, which deepens the queue further.
The corrected model: depth is a two-sided parameter. Too shallow forfeits the statistical gain the buffer exists to capture; too deep converts a loss problem into a latency problem and can make the system worse rather than better. The right depth comes from a stated burst-absorption requirement and a stated delay budget, and Section 12's four-way reading of occupancy, high water and drops is how you tell which side you are on.
"Ethernet guarantees in-order delivery, so packet switching does."
The wrong model: frames arrive in order in practice, so ordering is a property of the model.
What it costs: a design that spreads one flow across several paths — several aggregated links, several fabric routes — and is then surprised by reordering, or a receiver written with no reassembly because ordering was assumed.
The corrected model: a single Ethernet link delivers in order and a switch forwarding a flow out one port preserves that, so ordering holds under the common topology. Nothing in packet switching guarantees it, and the guarantee disappears the moment a flow has more than one path. That is precisely why link aggregation hashes whole flows onto one member instead of striping frames — Chapter 15.2 owns the mechanism, and this chapter owns the reason it is necessary.
19. Interview Reasoning
Because computer traffic is bursty, and a reservation made in advance is wasted whenever its owner is idle.
The chain a strong answer walks:
- A circuit allocates capacity before data flows and guarantees it: fixed rate, bounded delay, in-order, no congestion loss.
- The guarantee is paid for by every reserved instant that goes unused, and the reservation cannot be lent out — lending it would be an allocation decision at the moment of use, which is the thing a circuit avoids.
- Computer sources have low duty cycles, so most of every reservation is wasted.
- Packet switching allocates on demand, so idle sources cost nothing and the network is provisioned for the aggregate rather than the sum of the peaks. That gap is the statistical multiplexing gain.
- It is bought with buffers, variable delay and the possibility of loss — and because nothing was arranged in advance, every unit must carry its own destination, its own boundaries and its own check value.
What separates a good answer from a complete one: naming that the circuit model is not obsolete but unsuited to bursty traffic, and that time-sensitive networking is the industry re-adding circuit-like guarantees on top of Ethernet for the traffic that is not bursty.
The follow-up to be ready for: when does statistical multiplexing stop working? When sources stop being independent. A synchronised compute cluster where every node transmits at the same instant has a peak-of-the-sum equal to the sum-of-the-peaks, and the fabric has to be provisioned as though it were a circuit network while still paying the packet network's costs.
20. Understanding Check
21. What's Next
Ethernet moves packets because its traffic is bursty, and this chapter has priced that decision: capacity allocated on demand, a header on every unit because nothing was arranged, a buffer to absorb what the link cannot take now, and the possibility of refusing a unit outright.
What the chapter has not addressed is the medium those packets cross. Chapter 1.1 and Chapter 1.2 assumed one shared wire and built an access method for it. That assumption did not survive.
Chapter 1.4 — From Coax to Twisted Pair to Switched Links traces what each physical step actually changed in the hardware: a shared coax, then repeaters that extended the domain without helping contention, then hubs, then bridges and switches that partitioned the contention region entirely. Each step is a change in the collision-domain budget Chapter 1.2 derived, and the last one dismantles the problem the first two chapters solved.
Chapter 1.5 then covers full duplex, which retires most of that machinery and creates the need for the flow control this chapter's buffers have already hinted at.
The full path is on the Ethernet curriculum index.
Continue learning
Related tutorials
- Related topic
The MAC Layer
Framing, addressing, error detection, sizing, interframe gap and transmit access. Each exists because the medium is unreliable, shared, or both — and knowing which reason applies predicts exactly what full duplex deleted and what it left untouched.
- Related topic
Frame Format Overview
Every field exists to let a receiver make one decision at one moment, and the field order is the order those decisions must be made. The check value comes last because it covers everything before it — which makes every decision taken before it provisional.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
- Related topic
CSMA/CD, Collision Domains and Slot Time
Slot time is the parameter the whole half-duplex MAC hangs on: it bounds medium acquisition, bounds a collision fragment, and is the retransmission quantum. Deriving it from round-trip propagation plus jam is what fixes Ethernet's minimum frame size — a timing constant wearing a frame-format costume.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
