Ethernet · Module 7
The Transmit Path
Framing, padding, check append and interframe gap in one datapath — and their order is forced rather than chosen, because the pad is inside the covered range and the gap is outside the frame. Plus the one event the order cannot help with.
Modules 5 and 6 built the pieces one at a time. Chapter 5.6 built padding, Chapter 5.8 placed the check-sequence append point, Chapter 5.9 built the interframe gap, and Chapter 6.4 built an engine fast enough to run at line rate.
This chapter puts them in one datapath, and the interesting claim is that their order is not a choice.
It looks like a choice. A transmit path is a pipeline, pipelines have stages, and stage order is usually an engineering decision balanced against timing and area. Here it is forced — each ordering constraint follows from something already established, and every wrong order produces a specific, identifiable, and usually silent failure.
Two constraints do all the work:
- Padding must precede the check sequence, because Chapter 5.8 established that the pad is inside the covered range.
- The check sequence must precede the gap, because the gap is outside the frame and the check is the frame's last field.
And a third that is not about order at all but about which quantity is used: the length field carries the client's octet count, not the padded one — Chapter 5.6 §5's most damaging bug, which produces a frame that is internally consistent and delivers pad as data.
1. Scope — What This Chapter Owns
This chapter owns the assembly: the order of operations in a transmit datapath, why each ordering constraint is forced, how back-pressure interacts with a frame that has already begun, what happens on an underrun, and how to verify that the frames leaving the interface are well formed.
It does not re-derive the pieces. Chapter 5.6 owns the pad and the floor; Chapter 5.8 owns what the check covers and where it is appended; Chapter 5.9 owns the gap and its deficit; Chapter 6.4 owns the engine. Each appears here as a block with an interface.
It does not own the client interface above it — descriptor rings, DMA and buffer management are a host-interface subject — nor the transmit arbitration that decides which frame goes next, which is queueing.
And it does not own the receive path, which is Chapter 7.2 and which is not symmetric with this one: a transmitter knows what it is building, and a receiver is discovering it.
The question this chapter answers that its neighbours do not: given all the pieces, what constrains the order they are applied in — and what exactly breaks when the order is wrong?
2. The Order, and Why Each Step Is Where It Is
Work the constraints one at a time, because each one names its source.
The preamble is outside the covered range, so the check engine must not start until after the start delimiter. Chapter 5.8 §2 gave the reason: the preamble is consumed and regenerated at every hop, so a check computed over it would be invalid on arrival everywhere.
The pad is inside the covered range, so it must exist before the check is computed. Chapter 5.8 §3 gave that reason too: excluding the pad would create a blind region across most of the shortest frames on the link. This is the constraint people get wrong, because padding feels like a finishing touch applied to a completed frame.
The check sequence is the frame's last field, established in Chapter 5.1 — so nothing that belongs to the frame may follow it.
The gap is not part of the frame at all, so it follows the check. And Chapter 5.9 added a constraint the pipeline must respect rather than merely obey: the gap's duration is enforced by a state that has no early exit, so the transmit path cannot start the next frame's preamble on demand.
Which leaves exactly one genuine degree of freedom in the whole pipeline: whether the check is computed as the data flows or in a separate pass after the frame is assembled. Section 6 shows why the first is the only workable answer at line rate, and what it costs.
3. What Each Wrong Order Breaks
Take them in order of how hard they are to find, which is roughly the reverse of how bad they are.
Check sequence computed before padding. The pad is appended after the four check octets, so the frame's field order is wrong and the check covers only the client data. Every receiver computes over data-plus-pad and disagrees. Every padded frame is rejected by every peer — which is total, immediate, and therefore the easiest of the four to find. It also fails Chapter 5.1's structural requirement that the check sequence is last.
Interframe gap started before the check octets are emitted. The four octets land inside what should be idle. A receiver sees the frame end four octets early — a truncation, Chapter 6.3 §8's case — and then sees four stray octets that may or may not delimit as a runt. Two symptoms from one bug, and the runt count is the misleading one because it points at a peer.
Next preamble started as soon as the check octets are out. The gap is shortened, potentially below Chapter 5.9's permitted floor. The symptom depends on the peer, because receiver gap tolerance varies — so the design works against the peer it was developed with and fails intermittently against others, which is the worst debugging shape in this list.
Length computed after padding. No ordering is visibly wrong; the frame is well formed, the check is correct, and the field order is right. The length field simply reports the wrong quantity, and a conforming receiver delivers up to 43 octets of pad to its client as data. Chapter 5.6 §14 built the directed test for it, and it is worth repeating why nothing else finds it: the frame is internally consistent, so every structural check passes.
Read the last two rows of the figure together. The three ordering errors produce visible failures — rejected frames, truncations, intermittent drops. The quantity error produces a frame that is accepted and wrong. The pipeline's order is the easy part to get right and the easy part to verify; which value feeds which field is neither.
4. RTL 1 — The Ordered Datapath
// SYNTHESIZABLE.
//
// The transmit datapath's control, with the stage order of Section 2
// expressed as a state graph.
//
// Two things are computed at frame start and never recomputed:
//
// the LENGTH to emit -- the CLIENT's octet count
// the PAD target -- the client-data region's size
//
// They come from one decision (Chapter 5.6 §5) so they cannot disagree.
// A design that recomputes either later has two representations of one
// boundary and no mechanism keeping them equal.
package mac_tx_pkg;
import frame_size_pkg::*;
typedef enum logic [3:0] {
S_IDLE,
S_PREAMBLE, // outside the covered range
S_SFD, // outside the covered range
S_HEADER, // covered range begins here
S_DATA, // covered
S_PAD, // covered -- BEFORE the check, per Section 2
S_FCS, // the frame's last field
S_GAP, // outside the frame -- AFTER the check
S_ABORT // underrun: finish the frame and mark it (Section 9)
} tx_state_e;
endpackage
module mac_tx_controller
import mac_tx_pkg::*;
import frame_size_pkg::*;
import crc32_pkg::*;
(
input logic clk,
input logic rst_n,
// From the client.
input logic req_valid,
input logic [10:0] req_octets, // client data only
input logic req_is_length_form,
input logic cli_valid,
input logic [7:0] cli_data,
input logic cli_last,
// From the gap enforcer of Chapter 5.9. The transmit path may not
// start a frame until this permits it, and it is an INPUT rather than
// something this module counts, so the gap's floor stays structural.
input logic gap_permits_start,
// To the interface.
output logic tx_valid,
output logic [7:0] tx_data,
output logic tx_last,
// To the CRC engine. Note crc_enable is high for exactly the covered
// range -- Chapter 5.8's boundary, expressed as one signal.
output logic crc_start,
output logic crc_enable,
output logic [7:0] crc_data,
input logic [31:0] crc_fcs,
output tx_state_e state,
output logic frame_aborted,
output logic [10:0] emitted_length
);
tx_state_e state_q;
logic [3:0] cnt_q; // preamble octets, FCS octets
logic [10:0] sent_q; // octets of the client-data region sent
logic [10:0] target_q; // that region's size
logic [10:0] length_q; // the value the length field will carry
logic [31:0] fcs_q;
assign state = state_q;
assign emitted_length = length_q;
// ONE decision, taken at frame start. The pad count and the length are
// both read off it, so they cannot drift (Chapter 5.6 §5).
wire [10:0] region_target = (req_octets < 11'(MIN_CLIENT))
? 11'(MIN_CLIENT) : req_octets;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state_q <= S_IDLE;
cnt_q <= '0;
sent_q <= '0;
target_q <= '0;
length_q <= '0;
fcs_q <= '0;
tx_valid <= 1'b0;
tx_data <= '0;
tx_last <= 1'b0;
crc_start <= 1'b0;
crc_enable <= 1'b0;
crc_data <= '0;
frame_aborted <= 1'b0;
end else begin
tx_valid <= 1'b0;
tx_last <= 1'b0;
crc_start <= 1'b0;
crc_enable <= 1'b0;
case (state_q)
S_IDLE: if (req_valid && gap_permits_start) begin
state_q <= S_PREAMBLE;
cnt_q <= '0;
sent_q <= '0;
target_q <= region_target;
// The CLIENT's count, never the target. Section 3's fourth row.
length_q <= req_octets;
frame_aborted <= 1'b0;
end
// Seven preamble octets. crc_enable stays LOW: outside the
// covered range (Chapter 5.8 §2).
S_PREAMBLE: begin
tx_valid <= 1'b1;
tx_data <= 8'h55;
if (cnt_q == 4'd6) begin state_q <= S_SFD; cnt_q <= '0; end
else cnt_q <= cnt_q + 1'b1;
end
S_SFD: begin
tx_valid <= 1'b1;
tx_data <= 8'hD5;
// The engine is started HERE, so the first covered octet is the
// first address octet and not the delimiter.
crc_start <= 1'b1;
state_q <= S_HEADER;
cnt_q <= '0;
end
// Addresses and length/type. Covered.
S_HEADER: if (cli_valid) begin
tx_valid <= 1'b1;
tx_data <= cli_data;
crc_enable <= 1'b1;
crc_data <= cli_data;
if (cnt_q == 4'd13) begin state_q <= S_DATA; cnt_q <= '0; end
else cnt_q <= cnt_q + 1'b1;
end
S_DATA: begin
if (cli_valid) begin
tx_valid <= 1'b1;
tx_data <= cli_data;
crc_enable <= 1'b1;
crc_data <= cli_data;
sent_q <= sent_q + 1'b1;
if (cli_last) begin
// Pad if short, otherwise straight to the check. The two
// exits are the only ones, and neither can reach S_GAP.
state_q <= ((sent_q + 1'b1) < target_q) ? S_PAD : S_FCS;
cnt_q <= '0;
end
end else if (tx_needs_octet_now) begin
// UNDERRUN. The frame cannot be un-started, so it is finished
// and marked. Section 9 owns what "marked" means.
state_q <= S_ABORT;
end
end
// Pad is COVERED, so crc_enable is high here too. This is the
// constraint of Section 2 expressed in one line.
S_PAD: begin
tx_valid <= 1'b1;
tx_data <= 8'h00;
crc_enable <= 1'b1;
crc_data <= 8'h00;
sent_q <= sent_q + 1'b1;
if ((sent_q + 1'b1) >= target_q) begin
state_q <= S_FCS;
cnt_q <= '0;
fcs_q <= crc_fcs;
end
end
// The check sequence. crc_enable is LOW: the field does not cover
// itself (Chapter 5.8 §3).
S_FCS: begin
tx_valid <= 1'b1;
tx_data <= fcs_q[8*cnt_q +: 8];
if (cnt_q == 4'd3) begin
tx_last <= 1'b1;
state_q <= S_GAP;
cnt_q <= '0;
end else begin
cnt_q <= cnt_q + 1'b1;
end
end
// The gap is entered unconditionally after the check and left
// only when the enforcer permits -- so the floor is not this
// module's to get wrong (Chapter 5.9 §9).
S_GAP: if (gap_permits_start) state_q <= S_IDLE;
S_ABORT: begin
frame_aborted <= 1'b1;
state_q <= S_GAP;
end
default: state_q <= S_IDLE;
endcase
end
end
endmoduleClassification: synthesizable.
What it teaches: that crc_enable is the covered range, expressed as one signal across the whole state graph. It is low in S_PREAMBLE and S_SFD, high in S_HEADER, S_DATA and S_PAD, and low again in S_FCS. Reading those five values in order is Chapter 5.8's Figure 1, and a design in which they do not match that figure has an incorrect covered range regardless of how good its engine is.
Deliberately simplified: one octet per clock, a fixed 14-octet header, and no tag insertion. A tagged frame moves the length/type field (Chapter 5.5 §8) and changes the client-data floor (Chapter 5.6 §9), which adds states rather than changing any ordering constraint.
Production implication: gap_permits_start is an input rather than a counter inside this module, and that is deliberate. Chapter 5.9 §9 made the gap floor structural by putting it in a state with no early exit; importing it as a permission signal keeps that guarantee intact. A transmit controller that counts the gap itself has taken a structural guarantee and turned it into a computed one, which is the specific downgrade that chapter argued against.
5. RTL 2 — Back-Pressure Without Losing the Frame
// SYNTHESIZABLE.
//
// The assembler between a bursty client and an interface that cannot be
// stalled.
//
// The two directions are not symmetric and conflating them is the bug:
//
// client -> assembler : may stall. The client is a memory system and
// stalls whenever it feels like it. The assembler absorbs this.
//
// assembler -> wire : may NOT stall. Once the preamble is out, the
// interface consumes an octet per cycle until the frame ends.
// There is no back-pressure signal in that direction because
// there is no such thing as pausing a frame mid-transmission.
//
// So the assembler's job is to guarantee that, from the moment it starts
// a frame, it can supply an octet every cycle until the end -- and its
// only tool is refusing to start until it can.
module tx_assembler
#(
parameter int unsigned DEPTH = 64,
// Octets that must be buffered before a frame may start. The whole
// underrun defence lives in this number.
parameter int unsigned START_THRESHOLD = 32
) (
input logic clk,
input logic rst_n,
// Client side: may stall freely.
input logic cli_valid,
input logic [7:0] cli_data,
input logic cli_last,
output logic cli_ready,
// Wire side: consumes on demand, cannot be stalled.
input logic wire_take,
output logic wire_valid,
output logic [7:0] wire_data,
output logic wire_last,
// To the controller: it is safe to begin a frame.
output logic safe_to_start,
// The frame is in flight and the buffer has run dry.
output logic underrun
);
logic [7:0] mem_q [DEPTH];
logic [7:0] wr_q, rd_q;
logic [8:0] occ_q;
logic in_frame_q;
logic last_seen_q;
assign cli_ready = (occ_q != 9'(DEPTH));
// A frame may start when either the whole frame is buffered -- the
// client's last octet has arrived -- or enough is buffered to cover
// the client's worst-case stall. The first condition is what a
// store-and-forward transmitter uses and it removes underrun entirely.
assign safe_to_start = last_seen_q || (occ_q >= 9'(START_THRESHOLD));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_q <= '0; rd_q <= '0; occ_q <= '0;
in_frame_q <= 1'b0; last_seen_q <= 1'b0;
wire_valid <= 1'b0; wire_data <= '0; wire_last <= 1'b0;
underrun <= 1'b0;
end else begin
wire_valid <= 1'b0;
wire_last <= 1'b0;
underrun <= 1'b0;
if (cli_valid && cli_ready) begin
mem_q[wr_q] <= cli_data;
wr_q <= wr_q + 1'b1;
occ_q <= occ_q + 1'b1;
if (cli_last) last_seen_q <= 1'b1;
end
if (wire_take) begin
if (occ_q != '0) begin
wire_valid <= 1'b1;
wire_data <= mem_q[rd_q];
rd_q <= rd_q + 1'b1;
occ_q <= occ_q - 1'b1;
if (last_seen_q && (occ_q == 9'd1)) begin
wire_last <= 1'b1;
last_seen_q <= 1'b0;
in_frame_q <= 1'b0;
end else begin
in_frame_q <= 1'b1;
end
end else if (in_frame_q) begin
// The wire wants an octet, the buffer is empty, and a frame is
// in flight. There is no stall available in this direction, so
// the frame is already unfixable -- report it and let the
// controller mark it (Section 9).
underrun <= 1'b1;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the only defence against an underrun is the decision not to start. Once the preamble is out there is no back-pressure toward the wire, no way to pause, and no way to withdraw the frame. safe_to_start is therefore the entire mechanism, and every other underrun measure is damage control.
Deliberately simplified: a single threshold. Real designs derive it from the client's worst-case stall and the frame's remaining length, and a store-and-forward transmitter sets it to "the whole frame", which removes underrun entirely at the cost of a frame's worth of latency and buffer.
Production implication: underrun is reported by the assembler and acted on by the controller, rather than handled locally. The assembler cannot fix it — it has no access to the check engine and cannot mark the frame — and a design that lets the assembler paper over the condition by inserting filler octets has produced a frame that is well formed and carries wrong data. The component that detects a fault is often not the one that can respond to it, and wiring the report out is the difference between a marked frame and silent corruption.
6. Computing the Check As the Data Flows
Section 2 identified one genuine degree of freedom in the pipeline: whether the check sequence is computed as the data flows past or in a separate pass over an assembled frame. It is worth working through, because it is the only place in this chapter where a design chooses rather than obeys.
The two-pass approach is simpler and it is not available. Assemble the frame in a buffer, run the engine over it, append the result, transmit. The control logic is trivial — no state machine has to coordinate the engine with the datapath — and the check is computed over a frame that is already complete, so padding, the length field and the covered range are all settled before the engine starts.
What it costs is a frame time of latency and a frame of buffer, per transmission. At 1518 octets and 1 Gb/s that is about 12 microseconds and 1.5 kilobytes; at ten ports it is fifteen kilobytes of buffer whose only purpose is to let the check be computed conveniently.
And it does not scale in the direction that matters. The buffer is per-port and the latency is per-frame, so both grow with port count while the saving stays constant.
So the check is computed on the fly, and the cost of that decision is a coordination problem: the engine's enable must track the covered range exactly, cycle by cycle, through a state machine that is also generating padding and counting octets. Every ordering constraint in Section 2 becomes a constraint on crc_enable's timing rather than on the arrangement of buffers.
Which is why Section 4's controller looks the way it does. crc_enable is not a separate mechanism bolted onto the datapath; it is a per-state output of the same machine that drives tx_valid, so the two cannot drift apart.
7. Underrun — Finish It and Mark It
A frame that has started cannot be withdrawn. Its preamble and header are on the wire, a receiver is already synchronised to it, and there is no abort symbol at this layer.
So there are exactly four things a transmitter can do, and three of them are wrong.
| Response | What the receiver sees | Verdict |
|---|---|---|
| stop transmitting | a truncated frame | wrong — indistinguishable from a link fault |
| pad the shortfall and finish normally | a valid frame with wrong data | worst — accepted and corrupt |
| finish with a deliberately wrong check | a frame that fails its check | correct |
| assert that underrun never happens | whatever the design does anyway | the rejected property of Section 11 |
Row two is the one that must be argued against explicitly, because it is superficially attractive: the frame is the right length, the check is correct, nothing is malformed. And it delivers filler to the peer's client as data, with no error anywhere in either device — the same silent-corruption shape as Chapter 5.6's length-field bug, arriving by a different route.
Row one is wrong for a subtler reason. A truncated frame is what a link fault produces, so a transmitter that truncates on underrun is generating link-fault symptoms from a host-interface problem. Chapter 5.6 §8's classifier would call it a fragment — bad check sequence, below the floor — and send the investigation to the cable.
Row three is correct and it has a specific implementation: finish the frame to the right length and emit a check sequence that is deliberately wrong. Chapter 5.8 §9 already established the convention — the bitwise inverse of the correct value — and established why the exact inverse matters: it is astronomically unlikely to arise from corruption, so a receiver testing for it learns that an upstream device already knew this frame was bad.
Which makes the underrun response and the cut-through stomp the same mechanism used for the same reason: propagating a diagnosis through a protocol that has no field to carry one.
8. RTL 3 — The Underrun Handler
// SYNTHESIZABLE.
//
// Finishes a starved frame to the correct length and marks it, and
// records enough locally that the fault is attributable HERE rather than
// only at the far end.
//
// The three outputs are three different audiences:
//
// force_bad_fcs -> the receiver, which must reject the frame
// c_underrun -> whoever operates this device, who must fix it
// first_at_octet -> whoever debugs it, because WHERE it starved names
// the subsystem that starved
module tx_underrun_handler
import frame_size_pkg::*;
#(
parameter int unsigned CNT_W = 32
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic frame_start,
input logic underrun,
input logic [13:0] octets_sent,
input logic [13:0] octets_expected,
// To the datapath: keep transmitting to the correct length, with
// filler, so the frame is the right SIZE -- and then invert the check
// sequence so it is rejected. Length correct, check deliberately wrong.
output logic fill_active,
output logic [7:0] fill_data,
output logic force_bad_fcs,
output logic [CNT_W-1:0] c_underrun,
output logic underrun_seen, // sticky
output logic [13:0] first_at_octet,
output logic [13:0] first_frame_length,
// Deepest into a frame that a starvation has ever occurred. Survives
// clear: it bounds how much buffering would have been enough.
output logic [13:0] worst_at_octet
);
logic active_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
active_q <= 1'b0;
fill_active <= 1'b0;
fill_data <= '0;
force_bad_fcs <= 1'b0;
c_underrun <= '0;
underrun_seen <= 1'b0;
first_at_octet <= '0;
first_frame_length <= '0;
worst_at_octet <= '0;
end else begin
if (clear) begin
c_underrun <= '0;
// The context deliberately survives. A count says how often; the
// octet position says which subsystem, and only one of those can
// be reconstructed later.
end
if (frame_start) begin
active_q <= 1'b0;
fill_active <= 1'b0;
force_bad_fcs <= 1'b0;
end else if (underrun && !active_q) begin
active_q <= 1'b1;
// Fill to the CORRECT length. A frame that is the right size and
// fails its check is unambiguous; a short frame is a fragment and
// points at the link (Section 7).
fill_active <= 1'b1;
fill_data <= 8'h00;
// And mark it. The inversion is Chapter 5.8 §9's convention, so a
// receiver that tests for it learns this frame was known-bad
// rather than corrupted in transit.
force_bad_fcs <= 1'b1;
if (!(&c_underrun)) c_underrun <= c_underrun + 1'b1;
underrun_seen <= 1'b1;
if (octets_sent > worst_at_octet) worst_at_octet <= octets_sent;
if (c_underrun == '0) begin
first_at_octet <= octets_sent;
first_frame_length <= octets_expected;
end
end
end
end
endmoduleClassification: synthesizable.
What it teaches: that the frame is filled to the correct length and marked, and both halves matter. Filling without marking produces a valid frame with filler in it — Section 7's worst row. Marking without filling produces a short frame, which is a fragment and points at the link. Only both together produce a frame that is unambiguously "this transmitter knew this was bad".
Deliberately simplified: the filler is zeros. Its value is irrelevant because the frame will be rejected — and Chapter 5.6 §13's rejected property applies here too, so nothing downstream should assume anything about it.
Production implication: worst_at_octet bounds the buffering that would have prevented every observed underrun, and it is the one number in this module that leads to a fix. A starvation at octet 12 of a frame means the start threshold was too low; one at octet 1400 means the client's sustained bandwidth is short, and those have nothing in common. A count alone distinguishes neither, which is why the position survives a counter clear and the count does not.
9. RTL 4 — Deliberate Error Insertion, and Keeping It Out of Production
// SYNTHESIZABLE TEST FEATURE.
//
// Emits deliberately malformed frames so that a receive path's error
// handling can be exercised against real traffic.
//
// Every error class Chapters 5 and 6 defined is reachable from here, and
// each is generated by ONE mechanism rather than by a special path, so
// the normal datapath is exercised even while producing bad frames.
//
// The guard is the design content. A test feature that can be enabled in
// production is a way to emit corrupt traffic onto a live network, and
// "it defaults to off" is not a guard -- a register defaults to whatever
// software last wrote.
module tx_error_insertion
import frame_size_pkg::*;
#(
// Tied off at integration. A production build ties this low and the
// whole feature synthesises away; a test build ties it high. It is NOT
// a software-writable bit, which is the point.
parameter bit ERROR_INSERTION_PRESENT = 1'b0
) (
input logic clk,
input logic rst_n,
input logic enable, // ignored unless PRESENT
input logic [2:0] error_class,
input logic frame_start,
input logic [13:0] octet_index,
input logic [13:0] frame_length,
output logic corrupt_octet, // flip a bit in this octet
output logic force_bad_fcs,
output logic force_short, // end the frame early: a fragment
output logic force_long, // overrun the ceiling: a giant
output logic force_short_gap, // Chapter 5.9's floor, violated
output logic insertion_active
);
localparam logic [2:0] E_NONE = 3'd0;
localparam logic [2:0] E_BIT_FLIP = 3'd1; // one payload bit
localparam logic [2:0] E_BAD_FCS = 3'd2; // Chapter 5.8's mismatch
localparam logic [2:0] E_STOMP = 3'd3; // the exact inverse
localparam logic [2:0] E_FRAGMENT = 3'd4; // below the floor
localparam logic [2:0] E_GIANT = 3'd5; // above the ceiling
localparam logic [2:0] E_SHORT_GAP = 3'd6; // below the gap floor
wire armed = ERROR_INSERTION_PRESENT && enable;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
corrupt_octet <= 1'b0;
force_bad_fcs <= 1'b0;
force_short <= 1'b0;
force_long <= 1'b0;
force_short_gap <= 1'b0;
insertion_active <= 1'b0;
end else begin
corrupt_octet <= 1'b0;
force_short <= 1'b0;
force_short_gap <= 1'b0;
if (frame_start) begin
force_bad_fcs <= 1'b0;
force_long <= 1'b0;
insertion_active <= armed && (error_class != E_NONE);
end else if (armed) begin
case (error_class)
// A bit flipped mid-payload. Inside the covered range, so the
// check catches it -- which is what makes it a useful stimulus
// rather than an interesting one.
E_BIT_FLIP: if (octet_index == (frame_length >> 1)) corrupt_octet <= 1'b1;
E_BAD_FCS: force_bad_fcs <= 1'b1;
E_STOMP: force_bad_fcs <= 1'b1; // datapath inverts instead
E_FRAGMENT: if (octet_index == 14'd40) force_short <= 1'b1;
E_GIANT: force_long <= 1'b1;
E_SHORT_GAP: force_short_gap <= 1'b1;
default: ;
endcase
end
end
end
// The guard, restated where a reviewer will see it. A feature that can
// emit malformed frames onto a live network must be ABSENT from the
// netlist, not merely disabled in it -- so the check is on the
// parameter, and a production build proves it by elaboration.
`ifndef TEST_BUILD
// synopsys translate_off
a_insertion_absent_in_production: assert final (ERROR_INSERTION_PRESENT == 1'b0)
else $fatal(1, "error insertion is present in a non-test build");
// synopsys translate_on
`endif
endmoduleClassification: synthesizable test feature.
What it teaches: that a test feature's guard belongs in the parameter, not in the register. enable is a runtime signal and a runtime signal is whatever software last wrote — a driver bug, a stale register image after a warm reset, or a debug script left running. ERROR_INSERTION_PRESENT is tied at integration, so a production build has no logic that can emit a corrupt frame, and the question stops being one of policy.
Deliberately simplified: the error positions are fixed. A verification-oriented version parameterises them so a sweep can place a bit flip at every octet, which is how the covered-range boundaries of Chapter 5.8 get exercised at their edges.
Production implication: E_STOMP and E_BAD_FCS are separate classes even though both produce a failing check, and the distinction is exactly Chapter 5.8 §9's. A stomp is the exact inverse and tells a receiver the frame was already known-bad; an ordinary bad check is indistinguishable from corruption on the link. A receive path must be tested against both, because a receiver that treats them identically discards the strongest evidence available about where a fault is.
10. RTL 5 — Checking What Actually Left the Interface
// SYNTHESIZABLE MONITOR.
//
// Watches the octet stream at the INTERFACE and checks that every frame
// leaving this device is well formed.
//
// Placement is the design decision. A monitor on the controller's state
// confirms the state machine sequenced correctly -- which it did, because
// it was written to. A monitor at the interface confirms what actually
// LEFT, so it also catches everything between: a serialiser dropping an
// octet, a clock-domain crossing losing a transfer, a physical-layer
// wrapper inserting or removing idle.
module tx_conformance_monitor
import frame_size_pkg::*;
import ifg_pkg::*;
#(
parameter int unsigned CNT_W = 32,
parameter int unsigned MAX_FRAME = 1518
) (
input logic clk,
input logic rst_n,
input logic clear,
input logic tx_valid,
input logic [7:0] tx_data,
input logic tx_last,
input logic in_preamble,
output logic [CNT_W-1:0] c_frames,
output logic [CNT_W-1:0] c_short_preamble,
output logic [CNT_W-1:0] c_undersize, // below the floor
output logic [CNT_W-1:0] c_oversize, // above the ceiling
output logic [CNT_W-1:0] c_short_gap, // below Chapter 5.9's floor
output logic any_violation, // sticky
output logic [13:0] first_bad_length
);
logic [13:0] len_q;
logic [7:0] pre_q;
logic [7:0] gap_q;
logic in_frame_q;
function automatic logic [CNT_W-1:0] bump(input logic [CNT_W-1:0] v);
bump = (&v) ? v : (v + 1'b1);
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
len_q <= '0; pre_q <= '0; gap_q <= '0; in_frame_q <= 1'b0;
c_frames <= '0; c_short_preamble <= '0; c_undersize <= '0;
c_oversize <= '0; c_short_gap <= '0;
any_violation <= 1'b0; first_bad_length <= '0;
end else begin
if (clear) begin
c_frames <= '0; c_short_preamble <= '0; c_undersize <= '0;
c_oversize <= '0; c_short_gap <= '0;
// any_violation and first_bad_length deliberately survive: a
// malformed frame leaving this device is never routine.
end
if (tx_valid) begin
gap_q <= '0;
if (in_preamble) begin
pre_q <= pre_q + 1'b1;
in_frame_q <= 1'b0;
end else begin
len_q <= in_frame_q ? (len_q + 1'b1) : 14'd1;
in_frame_q <= 1'b1;
if (!in_frame_q) begin
// First octet after the delimiter: the preamble just ended,
// so its length can be judged now.
if (pre_q != 8'd8) begin
c_short_preamble <= bump(c_short_preamble);
any_violation <= 1'b1;
end
pre_q <= '0;
end
end
if (tx_last) begin
automatic logic [13:0] final_len = in_frame_q ? (len_q + 1'b1) : 14'd1;
c_frames <= bump(c_frames);
in_frame_q <= 1'b0;
// The two size bounds, checked on what actually left rather
// than on what the controller intended.
if (final_len < 14'(MIN_FRAME_OCTETS)) begin
c_undersize <= bump(c_undersize);
any_violation <= 1'b1;
if (first_bad_length == '0) first_bad_length <= final_len;
end
if (final_len > 14'(MAX_FRAME)) begin
c_oversize <= bump(c_oversize);
any_violation <= 1'b1;
if (first_bad_length == '0) first_bad_length <= final_len;
end
end
end else begin
// Idle. Counting it here means the gap is measured on the wire,
// which is the only place it is real.
gap_q <= (gap_q == 8'hFF) ? gap_q : (gap_q + 1'b1);
if (tx_valid_next_cycle && (gap_q < 8'(MIN_SINGLE_GAP))) begin
c_short_gap <= bump(c_short_gap);
any_violation <= 1'b1;
end
end
end
end
endmoduleClassification: synthesizable monitor.
What it teaches: that a conformance monitor's placement determines what it can find, and the obvious placement is the useless one. A monitor watching the controller's state confirms that the state machine sequenced correctly — which it did, because the same engineer wrote both. A monitor at the interface confirms what left the device, so it also covers the serialiser, the clock-domain crossing, and the physical-layer wrapper, none of which the controller can see.
Deliberately simplified: it checks structure and not content — sizes, preamble length, gap — and does not verify the check sequence, which would require a second engine. A production monitor usually adds one, because Chapter 6.2 §10's argument applies: a transmit engine and a receive engine that agree with each other and not with the standard pass every internal test.
Production implication: the gap is measured in the idle branch, from the interface's own valid signal, rather than taken from the controller's gap state. Chapter 5.9 §10 showed that the gap on the wire is not the gap the controller emitted — clock compensation and physical-layer processing change it downstream — and a monitor reading the controller's intent would report a conforming gap while a short one went out.
11. Assertions — Ordering, Quantities and the Response to Starvation
// ---------------------------------------------------------------------
// P1 -- THE COVERED RANGE, as a property of crc_enable across the state
// graph. This one property is Chapter 5.8's Figure 1 (Section 4).
// ---------------------------------------------------------------------
property p_crc_enable_matches_covered_range;
@(posedge clk) disable iff (!rst_n)
crc_enable |-> (state inside {S_HEADER, S_DATA, S_PAD});
endproperty
a_crc_enable_matches_covered_range: assert property (p_crc_enable_matches_covered_range);
// ---------------------------------------------------------------------
// P2 -- The preamble and delimiter are never covered.
// ---------------------------------------------------------------------
property p_preamble_never_covered;
@(posedge clk) disable iff (!rst_n)
(state inside {S_PREAMBLE, S_SFD}) |-> !crc_enable;
endproperty
a_preamble_never_covered: assert property (p_preamble_never_covered);
// ---------------------------------------------------------------------
// P3 -- The check sequence never covers itself.
// ---------------------------------------------------------------------
property p_fcs_never_covered;
@(posedge clk) disable iff (!rst_n)
(state == S_FCS) |-> !crc_enable;
endproperty
a_fcs_never_covered: assert property (p_fcs_never_covered);
// ---------------------------------------------------------------------
// P4 -- THE FIRST ORDERING CONSTRAINT. Padding, when it happens, happens
// before the check sequence -- there is no path from S_FCS to S_PAD.
// ---------------------------------------------------------------------
property p_pad_precedes_fcs;
@(posedge clk) disable iff (!rst_n)
(state == S_FCS) |=> (state != S_PAD);
endproperty
a_pad_precedes_fcs: assert property (p_pad_precedes_fcs);
// ---------------------------------------------------------------------
// P5 -- THE SECOND ORDERING CONSTRAINT. The gap is only ever entered
// from the check sequence or an abort, never from data or pad.
// ---------------------------------------------------------------------
property p_gap_only_after_fcs;
@(posedge clk) disable iff (!rst_n)
(state == S_GAP) |-> ($past(state) inside {S_FCS, S_ABORT, S_GAP});
endproperty
a_gap_only_after_fcs: assert property (p_gap_only_after_fcs);
// ---------------------------------------------------------------------
// P6 -- THE QUANTITY, not an ordering. The length field carries the
// CLIENT's octet count. Section 3's fourth row, and the one failure that
// produces a well-formed frame.
// ---------------------------------------------------------------------
property p_length_is_client_count;
@(posedge clk) disable iff (!rst_n)
(state == S_HEADER) |-> (emitted_length == $past(req_octets, header_offset));
endproperty
a_length_is_client_count: assert property (p_length_is_client_count)
else $error("length field carries the padded size, not the client's");
// ---------------------------------------------------------------------
// P7 -- The client-data region always reaches the floor.
// ---------------------------------------------------------------------
property p_region_reaches_floor;
@(posedge clk) disable iff (!rst_n)
(state == S_FCS) |-> (sent_q >= 11'(MIN_CLIENT));
endproperty
a_region_reaches_floor: assert property (p_region_reaches_floor);
// ---------------------------------------------------------------------
// P8 -- A frame is only started when the gap enforcer permits it. The
// floor stays structural because this module asks rather than counts.
// ---------------------------------------------------------------------
property p_start_requires_gap_permission;
@(posedge clk) disable iff (!rst_n)
($past(state) == S_IDLE && state == S_PREAMBLE) |-> $past(gap_permits_start);
endproperty
a_start_requires_gap_permission: assert property (p_start_requires_gap_permission);
// ---------------------------------------------------------------------
// P9 -- A frame is only started when the assembler says it can be
// finished. The entire underrun defence (Section 5).
// ---------------------------------------------------------------------
property p_start_requires_safe_to_start;
@(posedge clk) disable iff (!rst_n)
($past(state) == S_IDLE && state == S_PREAMBLE) |-> $past(safe_to_start);
endproperty
a_start_requires_safe_to_start: assert property (p_start_requires_safe_to_start);
// ---------------------------------------------------------------------
// P10 -- THE RESPONSE TO STARVATION. An underrun always produces a
// marked frame. Note the shape: it constrains what the design DOES when
// the event happens, and does not forbid the event (Section 11).
// ---------------------------------------------------------------------
property p_underrun_marks_the_frame;
@(posedge clk) disable iff (!rst_n)
underrun |-> ##[1:$] (force_bad_fcs until_with tx_last);
endproperty
a_underrun_marks_the_frame: assert property (p_underrun_marks_the_frame)
else $error("a starved frame was transmitted without being marked");
// ---------------------------------------------------------------------
// P11 -- A starved frame is filled to the correct LENGTH as well as
// marked. Marking without filling produces a fragment, which points at
// the link (Section 7).
// ---------------------------------------------------------------------
property p_underrun_frame_is_full_length;
@(posedge clk) disable iff (!rst_n)
(frame_aborted && tx_last) |-> (frame_octets >= 14'(MIN_FRAME_OCTETS));
endproperty
a_underrun_frame_is_full_length: assert property (p_underrun_frame_is_full_length);
// ---------------------------------------------------------------------
// P12 -- A starved frame is NEVER emitted with a valid check sequence.
// The prohibition on Section 7's second row -- fill and finish normally.
// ---------------------------------------------------------------------
property p_underrun_never_valid;
@(posedge clk) disable iff (!rst_n)
(frame_aborted && tx_last) |-> force_bad_fcs;
endproperty
a_underrun_never_valid: assert property (p_underrun_never_valid);
// ---------------------------------------------------------------------
// P13 -- An underrun is counted locally. The receiver's rejection tells
// the far end; only this counter tells whoever can fix it.
// ---------------------------------------------------------------------
property p_underrun_counted_locally;
@(posedge clk) disable iff (!rst_n)
$rose(underrun) |=> (c_underrun == $past(c_underrun) + 1);
endproperty
a_underrun_counted_locally: assert property (p_underrun_counted_locally);
// ---------------------------------------------------------------------
// P14 -- First-cause context survives a counter clear.
// ---------------------------------------------------------------------
property p_underrun_context_survives_clear;
@(posedge clk) disable iff (!rst_n)
clear |=> ($stable(first_at_octet) && $stable(worst_at_octet) &&
$stable(underrun_seen));
endproperty
a_underrun_context_survives_clear: assert property (p_underrun_context_survives_clear);
// ---------------------------------------------------------------------
// P15 -- Error insertion is absent, not merely disabled, unless the
// build is a test build.
// ---------------------------------------------------------------------
property p_no_insertion_when_absent;
@(posedge clk) disable iff (!rst_n)
(!ERROR_INSERTION_PRESENT) |-> !insertion_active;
endproperty
a_no_insertion_when_absent: assert property (p_no_insertion_when_absent);
// ---------------------------------------------------------------------
// P16 -- Every frame leaving the interface is within the size bounds,
// checked on the WIRE rather than on the controller's intent.
// ---------------------------------------------------------------------
property p_wire_frames_in_bounds;
@(posedge clk) disable iff (!rst_n)
(tx_valid && tx_last && !frame_aborted)
|-> ((frame_octets >= 14'(MIN_FRAME_OCTETS)) && (frame_octets <= 14'(MAX_FRAME)));
endproperty
a_wire_frames_in_bounds: assert property (p_wire_frames_in_bounds);
// ---------------------------------------------------------------------
// P17 -- The preamble is exactly eight octets including the delimiter.
// ---------------------------------------------------------------------
property p_preamble_length;
@(posedge clk) disable iff (!rst_n)
$rose(in_frame_q) |-> ($past(pre_q) == 8'd8);
endproperty
a_preamble_length: assert property (p_preamble_length);
// ---------------------------------------------------------------------
// P18 -- COVERAGE. A padded frame, an unpadded frame, and an underrun.
// A run without the third has not exercised Section 8 at all.
// ---------------------------------------------------------------------
c_padded_frame: cover property (@(posedge clk) disable iff (!rst_n) (state == S_PAD));
c_unpadded_frame: cover property (@(posedge clk) disable iff (!rst_n) (state == S_DATA ##1 state == S_FCS));
c_underrun_seen: cover property (@(posedge clk) disable iff (!rst_n) underrun);12. Verification — Twenty-Four Scenarios and a Starvation Nothing Else Produces
| # | Scenario | Stimulus | What must be observed |
|---|---|---|---|
| 1 | Minimum-size frame | 46 octets of client data | no padding; frame is 64 octets |
| 2 | Short frame | 3 octets of client data | S_PAD entered; 43 pad octets; emitted_length = 3 (P6) |
| 3 | One below the floor | 45 octets | exactly one pad octet |
| 4 | One above the floor | 47 octets | no padding; S_DATA goes straight to S_FCS |
| 5 | Maximum-size frame | 1500 octets | no padding; frame is 1518 octets |
| 6 | Covered range | any frame | crc_enable matches header, data and pad only (P1, P2, P3) |
| 7 | Pad is covered | a padded frame | crc_enable high throughout S_PAD |
| 8 | Check sequence not covered | any frame | crc_enable low throughout S_FCS (P3) |
| 9 | Ordering: pad before FCS | any padded frame | no transition from S_FCS to S_PAD (P4) |
| 10 | Ordering: FCS before gap | any frame | S_GAP entered only from S_FCS or S_ABORT (P5) |
| 11 | Length after padding | inject the mutation | P6 fires; the frame is otherwise well formed |
| 12 | Check before padding | inject the mutation | every padded frame rejected by a reference receiver |
| 13 | Gap started early | inject the mutation | reference receiver reports truncation plus a runt |
| 14 | Preamble too early | force the gap short | c_short_gap on the interface monitor |
| 15 | Client stalls before start | no data buffered | frame does not start; safe_to_start low (P9) |
| 16 | Client stalls mid-frame, recovers | brief stall, buffer non-empty | frame completes normally; no underrun |
| 17 | Client starves mid-frame | buffer empties in flight | underrun; frame filled and marked (P10, P11, P12) |
| 18 | Starved frame length | the same stimulus | frame reaches the floor — a fragment would point at the link |
| 19 | Starved frame is counted | the same stimulus | c_underrun increments; first_at_octet captured (P13) |
| 20 | Context survives clear | assert clear after an underrun | count zeroes; context and worst_at_octet survive (P14) |
| 21 | Error insertion absent | production build | insertion_active never asserts (P15) |
| 22 | Error insertion, bad FCS | test build, E_BAD_FCS | a receiver reports a mismatch |
| 23 | Error insertion, stomp | test build, E_STOMP | a receiver reports stomped, not a mismatch |
| 24 | Interface conformance | 10 000 frames | no size, preamble or gap violations on the wire (P16, P17) |
13. Debugging — Which Ordering, Which Quantity
Symptom — every padded frame is rejected by every peer, and full-size frames are fine.
The pad is outside the covered range: either the check is computed before padding, or crc_enable is low during S_PAD. The correlation with frame size is the diagnosis — only frames short enough to need padding are affected, so a link carrying mostly maximum-size frames looks healthy until a burst of small ones arrives.
Symptom — a peer's application receives extra bytes on small transfers.
The length field is carrying the padded size. Not an ordering fault — the frame is well formed, the check is correct, and no counter anywhere increments. Check emitted_length against the client's octet count directly at the transmitter, and note the signature: the excess is exactly the difference between the client's count and 46, and it appears only on transfers below 46 octets.
Symptom — a peer reports truncated frames and runts at roughly equal rates.
One bug, two symptoms: the gap is being started before the check octets are emitted, so the frame ends four octets early and the four octets land in the idle period where they may delimit as a runt. The equal rates are the tell — a genuine link fault does not produce truncations and runts in a one-to-one ratio.
Symptom — frames are dropped by some peers and not others, intermittently.
Gap tolerance, which varies between receivers (Chapter 5.9 §10). Read c_short_gap from the interface monitor rather than the controller's gap state, because clock compensation and physical-layer processing change the gap downstream of the controller and the controller's intent is not what went out.
Symptom — the far end reports check-sequence failures and this device reports nothing.
Check c_underrun first. An underrun marks the frame, so the far end sees a failing check — and if this device has no local counter, the fault is a host-interface problem being reported as a network problem on somebody else's equipment. first_at_octet then names the subsystem: an underrun at octet 12 is a start-threshold problem, and one at octet 1400 is a sustained-bandwidth problem.
Symptom — underruns rise under load and the link is not saturated.
The client side is not keeping up, and the position tells you where to look. Compare worst_at_octet against the buffer depth: if the deepest starvation is within the buffer's capacity, the fill rate is the problem; if it is far beyond, the client's sustained bandwidth is short and no threshold will fix it.
Symptom — malformed frames appear on a production link and nobody enabled anything.
Check whether ERROR_INSERTION_PRESENT was tied high in the build. A runtime enable is not a guard — it is whatever software last wrote, including after a warm reset that left a stale register image — which is why the parameter exists and why P15 checks the absence rather than the disable.
14. Common Misconceptions
"The transmit pipeline's stage order is an engineering choice."
The wrong model: stages can be reordered for timing or area like any other pipeline.
What it costs: plausible optimisations that produce broken frames. Padding after the check is attractive because the pad is cheap; starting the next preamble as soon as the check octets are out saves cycles where frame rate matters most. Both produce frames peers reject.
The corrected model: two constraints fix the whole order and both are consequences established elsewhere — the pad is inside the covered range (Chapter 5.8), and the gap is outside the frame. A proposed reordering is a contradiction of a stated fact, not a trade-off.
"Padding is a finishing touch applied to a completed frame."
The wrong model: pad last, because it is filler.
What it costs: the pad ends up outside the covered range, and every padded frame is rejected by every peer. It also puts octets after the check sequence, which contradicts Chapter 5.1's field order.
The corrected model: the pad is covered, so it is part of what the check is computed over and must exist before the computation finishes. In the state graph, crc_enable is high throughout S_PAD — and reading crc_enable across the whole graph is the covered range.
"An underrun means we should truncate the frame."
The wrong model: stop transmitting what you cannot finish.
What it costs: a truncated frame is what a link fault produces, so a host-interface problem generates link-fault symptoms — a fragment, below the floor, with a bad check — and the investigation goes to the cable.
The corrected model: finish the frame to the correct length and mark it with a deliberately wrong check sequence, using Chapter 5.8 §9's inversion so the receiver learns the frame was known-bad rather than corrupted in transit. And count it locally, because the receiver's rejection tells the far end and nothing tells the end that can fix it.
"Filling the shortfall and finishing normally is the graceful option."
The wrong model: the frame is the right size with a correct check, so nothing is malformed.
What it costs: the worst outcome available. The frame is accepted and delivers filler to the peer's client as data, with no error anywhere in either device.
The corrected model: a frame whose contents are wrong must not carry a valid check sequence. Gracefulness is exactly the wrong goal here — the design's job on an unrecoverable event is to make the failure loud, not to make it look normal.
"A monitor on the controller's state verifies the transmit path."
The wrong model: checking that the state machine sequenced correctly checks the output.
What it costs: everything between the controller and the interface is unverified — the serialiser, the clock-domain crossing, the physical-layer wrapper — and those are exactly where an octet gets dropped or the gap gets shortened.
The corrected model: the monitor watches what actually left. And the gap in particular must be measured on the wire, because Chapter 5.9 §10 showed the gap the controller emitted is not the gap that goes out.
15. Interview Reasoning
"What order does a MAC transmit path do things in, and why?"
The weak answer lists stages. The answer that ends the topic gives the two constraints that force the list: the pad is inside the covered range so it must precede the check, and the gap is outside the frame so it must follow it. Everything else is a consequence. The payoff is that a proposed reordering is not a trade-off but a contradiction — and naming which fact it contradicts is what a design review is for.
"What happens if you pad after computing the CRC?"
Every padded frame is rejected by every peer, because the pad is outside the covered range and the receiver computes over data-plus-pad. The strong answer adds the diagnostic shape: the failure correlates with frame size, so a link carrying mostly large frames looks healthy until small ones arrive — and it also contradicts the frame's field order, since octets now follow the check sequence.
"Your transmitter runs out of data mid-frame. What do you do?"
The frame cannot be withdrawn — its preamble is already gone. There are four options and three are wrong: truncating produces link-fault symptoms from a host-interface problem; filling and finishing normally produces a valid frame carrying filler, which is the worst outcome; and asserting it never happens forbids the response rather than the event. The correct answer is to fill to the right length and mark the frame with a deliberately wrong check, then count it locally with the octet position — because the far end's rejection is not visible to whoever can fix it.
"Would you assert that the transmit FIFO never underruns?"
No, and the reason is that the design cannot prevent it — the client side is a memory system with its own timing, and safe_to_start reduces the probability rather than eliminating it. The complete answer names the danger: the cheapest way to make that assertion pass is the fill-and-finish behaviour that corrupts data silently. Assert instead that an underrun always produces a marked, full-length frame, that it is never emitted with a valid check, and that it is counted locally with first-cause context.
16. Understanding Check
Because the pad is inside the covered range.
Chapter 5.8 §3 established that the check covers the addresses, the length/type field, the client data and the pad — and gave the reason: excluding the pad would create a region in which corruption is invisible across most of the shortest frames on the link.
So the pad must exist before the computation finishes, which fixes its position in the pipeline. In the state graph, crc_enable is high throughout S_PAD, exactly as it is in S_HEADER and S_DATA.
Getting it wrong is loud. Compute the check first and the pad lands after the four check octets — the field order is wrong and the check covers only the client data, so every receiver disagrees and every padded frame is rejected.
The useful diagnostic property is that it correlates with size. Only frames short enough to need padding are affected, so a link carrying mostly maximum-size frames looks healthy until a burst of small ones arrives — which is a very different signature from a physical fault.
17. What's Next
The claim this chapter defended: the transmit path's order is derived, not chosen, and every wrong order contradicts something already established.
Two constraints fix the whole pipeline — the pad is inside the covered range, so it precedes the check; the gap is outside the frame, so it follows it. Everything else is a consequence, which means a proposed reordering is a contradiction rather than a trade-off, and a design review's job is to name which fact it contradicts. And separately from any ordering, the length field carries the client's octet count, which is the one failure that produces a frame every structural check accepts and that delivers pad as data.
Then the event the order cannot help with. A started frame cannot be withdrawn, there is no back-pressure toward the wire, and the only defence is the decision not to start. When starvation happens anyway, the frame is finished to the correct length and marked — because truncating produces link-fault symptoms from a host-interface fault, and filling-and-finishing produces a valid frame carrying filler. And it is counted locally, with the octet position, because the far end's rejection is invisible to the end that can fix it.
Chapter 7.2 — The Receive Path is the mirror, and it is not symmetric. A transmitter knows what it is building. A receiver is discovering the frame as it arrives, and every discovery is provisional until the check sequence at the very end — which Chapter 5.1 established is last by construction.
So the receive path faces a question the transmit path never does: whether to begin delivering a frame it may have to withdraw. Start early and latency falls, but a frame that fails its check has already been partly handed over. Wait for the check and the frame is certainly good, at the cost of holding it entirely. 7.2 works through both, and through the accounting that attributes every discarded frame to the stage that discarded it.
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
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
Ethernet System Architecture
Client, MAC, reconciliation sublayer, PCS, PMA, PMD, medium — six blocks whose port lists are the real content. Each contract has two halves: what a layer delivers, and what it is forbidden to know about its neighbours, which is why one MAC outlived every physical layer.
- Related topic
Where the MAC Ends and the PHY Begins
The MAC/PHY boundary is generated by one rule: a responsibility belongs to the side that can detect its own failure. That rule decides every case — and it explains why each side is blind to the other's failures, which is what makes a contract violation invisible from both sides.
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.
