Ethernet · Module 19
The Transmit Frame Assembler
The interframe gap is 9 to 12 octets held to a mean of 12 by a bounded deficit, so a transmitter that emits exactly 12 is wrong on 75% of frame sizes.
Chapter 19.2 received frames at offsets it discovered. This chapter emits them at offsets it chooses, and the choice is constrained in a way that is easy to get exactly backwards.
Chapter 8.3 §17 and Chapter 5.9 established that the interframe gap is not twelve octets. It is a mean of twelve, held by a bounded deficit, with individual gaps as short as nine.
And on a wide interface it cannot be twelve, because a frame must begin on a lane boundary.
| Frame end, mod 4 | Shortest legal gap | Shortfall | The gap one lane later |
|---|---|---|---|
| 0 | 12 | 0 | 16 |
| 1 | 11 | 1 | 15 |
| 2 | 10 | 2 | 14 |
| 3 | 9 | 3 | 13 |
The shortfalls are 0, 1, 2 and 3 — exactly Chapter 5.9 §5's published deficit bound, which is not a coincidence: the bound is the alignment granularity minus one.
So a transmitter sending 1 518-octet frames back to back emits gaps of 10 and 14, alternating, and one sending 1 519-octet frames emits 9, 13, 13, 13 repeating. Both hold a mean of exactly 12.
| Frame size | Gap sequence | Mean |
|---|---|---|
| 64 | 12, 12, 12, … | 12.000 |
| 1 517 | 11, 11, 11, 15, … | 12.000 |
| 1 518 | 10, 14, 10, 14, … | 12.000 |
| 1 519 | 9, 13, 13, 13, … | 12.000 |
Which means a transmitter that emits exactly twelve octets every time is wrong on 75% of frame sizes — it places the next frame off-lane — and an assertion that the gap is at least twelve fails on entirely legal traffic. Section 19 is about that assertion.
This chapter builds the assembler: Chapter 5.1's padding, Chapter 6.2's check value appended across a word boundary, the deficit that holds the gap's mean, and Chapter 19.1 §5's commit point — the instant after which a stall corrupts a frame that is already on the wire.
1. Scope, and the Output That Cannot Be Recalled
Chapter 19.2's output was advisory. A consumer waited, and nothing had left the building. This chapter's output is on a cable.
| Chapter 19.2's parser | this chapter's assembler | |
|---|---|---|
| output | offsets and flags | octets on a wire |
| a wrong result | a consumer declines | a corrupted frame at the far end |
| latency is | free — pipeline it | free until commit; fatal after |
| may stall | yes | before commit only — Chapter 19.1 §5 |
Row four is the chapter's structural fact and Section 9 derives where commit sits.
| Section | Establishes |
|---|---|
| 2 | padding to Chapter 5.1's floor |
| 4 | the check value across a word boundary |
| 6 | the gap as an average, and the deficit that holds it |
| 9 | choosing the start offset, and the commit point |
| 11 | where commit sits against Chapter 18.4 §7's threshold |
| 13 | what the assembler must never do |
| 16 | back-to-back transmission at 0.31 beats of gap |
What this chapter does not build: the CRC engine itself is Chapter 19.4 — this chapter appends a value that block computes. The FIFOs are Chapter 19.5 and the memory interface Chapter 19.6. Those forward references are bold and unlinked because those chapters are not yet published.
2. Padding to the Floor
Chapter 5.1 §9 fixed the payload's floor at 46 octets and the frame's minimum at 64 including the check value. This section is what a transmitter does about it.
The arithmetic is one clamp.
| Payload offered | Padded to | Frame before FCS | With FCS | Pad octets |
|---|---|---|---|---|
| 0 | 46 | 60 | 64 | 46 |
| 14 | 46 | 60 | 64 | 32 |
| 45 | 46 | 60 | 64 | 1 |
| 46 | 46 | 60 | 64 | 0 |
| 47 | 47 | 61 | 65 | 0 |
| 1 500 | 1 500 | 1 514 | 1 518 | 0 |
And the pad's content is the part worth stating, because two answers are defensible and one of them leaks.
| Pad with | Argument | Problem |
|---|---|---|
| zeros | deterministic; testable | none |
| whatever is in the buffer | free — no write | LEAKS adjacent memory onto the wire |
| a fixed pattern | testable, distinctive | none |
Row two is a real implementation and it is a disclosure path. A transmitter that pads by simply not masking the octets beyond the payload emits up to 45 octets of whatever followed the payload in the host buffer — which is Chapter 18.4 §4's stale-buffer exposure arriving from a different direction, and it happens on every frame with a payload below 46.
How much traffic that is depends entirely on the application, and the honest answer is that it is not rare: TCP acknowledgements, ARP replies, and most control-plane messages are below 46 octets of payload.
So the pad must be written, and writing it costs the one thing Section 9 is about.
| Mask the octets | Write zeros into the FIFO | |
|---|---|---|
| where | at the datapath, per beat | at the FIFO, before commit |
| cost | 46 byte-enables | a beat of write bandwidth |
| latency | none | none — the frame was short anyway |
Masking at the datapath is the usual answer and it is free: the byte-enables already exist for Chapter 19.1 §3's partial final beat, and padding reuses them.
One more consequence of the floor is worth naming because it interacts with Section 6. A padded frame is always 64 octets on the wire including the check value — 64 mod 4 is 0, so Section 6's table says its gap is exactly 12 and its shortfall is zero. Every minimum-size frame is perfectly lane-aligned, which is why a run of them produces the trivial gap sequence and Section 20's directed test has to use other sizes.
3. RTL 1 — The Pad Inserter
The clamp from Section 2, placed where the byte-enables already are.
// -----------------------------------------------------------------------
// txasm_pkg -- the transmit frame assembler.
// -----------------------------------------------------------------------
package txasm_pkg;
localparam int DP_BYTES = 64; // 512-bit at 100 Gb/s
localparam int LANE_B = 4; // 5.9's alignment granularity
localparam int IFG_NOM = 12; // 8.3's nominal gap
localparam int IFG_MIN = 9; // 5.9 section 5
localparam int DEF_MAX = LANE_B - 1; // 5.9's deficit bound: 0..3
localparam int PAYLOAD_MIN = 46; // 5.1
localparam int FRAME_MIN = 64; // 5.1, including the FCS
localparam int FCS_B = 4;
typedef struct packed {
logic [DP_BYTES*8-1:0] data;
logic [$clog2(DP_BYTES+1)-1:0] bytes;
logic sof;
logic eof;
logic [7:0] frame_id; // 19.1 section 3
} tbeat_t;
// Where the assembler is in a frame's life. `committed` is the
// state after which 19.1 section 5's rule applies: a stall here
// corrupts a frame already on the wire.
typedef enum logic [2:0] {
A_IDLE, A_FILL, A_PAD, A_FCS, A_COMMITTED, A_GAP
} astate_e;
endpackage// -----------------------------------------------------------------------
// pad_inserter -- clamps the payload to 5.1's floor by writing the
// pad rather than by leaving it.
//
// Section 2: leaving the octets beyond the payload unmasked emits
// whatever followed them in the host buffer. The pad must be
// written, and the byte-enables that 19.1 section 3's partial final
// beat already needs are where it is written.
// -----------------------------------------------------------------------
module pad_inserter
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic in_valid,
input logic [DP_BYTES*8-1:0] in_data,
input logic [$clog2(DP_BYTES+1)-1:0] in_bytes,
input logic in_eof,
input logic [15:0] frame_bytes_so_far,
input logic [7:0] cfg_pad_value,
output logic out_valid,
output logic [DP_BYTES*8-1:0] out_data,
output logic [$clog2(DP_BYTES+1)-1:0] out_bytes,
output logic out_eof,
output logic [15:0] padded_length,
output logic [31:0] c_frames,
output logic [31:0] c_padded,
output logic [31:0] c_pad_octets,
output logic [15:0] worst_pad,
output logic pad_leaked // must never assert
);
// The frame before the FCS must reach FRAME_MIN - FCS_B = 60.
localparam int PRE_FCS_MIN = FRAME_MIN - FCS_B;
wire [15:0] total_pre_fcs = frame_bytes_so_far + {9'b0, in_bytes};
wire short = in_eof && (total_pre_fcs < 16'(PRE_FCS_MIN));
wire [15:0] pad_needed = short ? (16'(PRE_FCS_MIN) - total_pre_fcs) : 16'd0;
// The pad is written into the beat, not merely left. Every octet
// beyond in_bytes and within the padded length takes cfg_pad_value.
logic [DP_BYTES*8-1:0] padded;
always_comb begin
int i;
padded = in_data;
for (i = 0; i < DP_BYTES; i++)
if ((i >= int'(in_bytes)) &&
(i < int'(in_bytes) + int'(pad_needed)))
padded[i*8 +: 8] = cfg_pad_value;
end
assign out_valid = in_valid;
assign out_data = padded;
assign out_bytes = in_bytes + $clog2(DP_BYTES+1)'(pad_needed);
assign out_eof = in_eof;
assign padded_length = short ? 16'(PRE_FCS_MIN) : total_pre_fcs;
// If any octet inside the padded length still holds input data
// beyond in_bytes, the pad was not written. This is the section 2
// disclosure path, expressed as a signal so it can be asserted.
logic leak;
always_comb begin
int i;
leak = 1'b0;
for (i = 0; i < DP_BYTES; i++)
if ((i >= int'(in_bytes)) && (i < int'(out_bytes)) &&
(padded[i*8 +: 8] != cfg_pad_value))
leak = 1'b1;
end
assign pad_leaked = in_valid && in_eof && leak;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_frames <= '0; c_padded <= '0; c_pad_octets <= '0; worst_pad <= '0;
end else if (in_valid && in_eof) begin
c_frames <= c_frames + 1;
if (short) begin
c_padded <= c_padded + 1;
c_pad_octets <= c_pad_octets + {16'b0, pad_needed};
if (pad_needed > worst_pad) worst_pad <= pad_needed;
end
end
end
endmoduleClassification: a byte-enable-driven clamp with an explicit leak check.
What it teaches: that pad_leaked is an invariant rather than a counter, because the failure it catches is a disclosure and not a defect. A frame whose pad was left unwritten is well formed, has a valid check value, and carries up to 45 octets of the host's memory to whoever is listening. Chapter 18.4 §4 made the same argument about a stale buffer pointer; this is the same exposure at a smaller scale and a far higher frequency, because every frame with a payload below 46 octets is affected.
And it teaches that the pad value should be configurable rather than hard-wired to zero. A distinctive pattern — 0x5A, say — makes a leak visible in a capture: an unpadded frame shows real data where the pattern should be. A zero pad makes the leak invisible, because memory that happens to be zero looks exactly like a correct pad.
Deliberately simplified: the pad loop and the leak loop are both combinational over 64 octets and evaluated every cycle, which is 128 byte comparisons in one path; a production design derives the byte-enables arithmetically. frame_bytes_so_far arrives as an input, so the running length is tracked elsewhere. And the pad is assumed to fit in the final beat — a payload of zero needs 46 octets of pad, which fits in a 64-octet beat, but on a narrower datapath it would span two and the loop would be wrong.
Production implication: c_padded against c_frames is the fraction of frames below the floor, and it is a direct measure of how much this block matters on a given deployment. A port carrying bulk data pads almost nothing; one carrying acknowledgements, ARP and control-plane messages pads most frames — and it is the second kind of port where a leak would be continuous rather than occasional.
4. The Check Value Across a Word Boundary
Chapter 6.4 §3 identified the final partial word as one of the two places parallel CRC designs go wrong. This section is the transmit-side half of it, which is a different problem: not computing over a partial word but appending four octets that may not fit in one.
The check value is four octets and it follows the frame's last data octet. On a 64-octet beat, it fits in the final beat unless that beat already holds 61 or more octets.
| Pre-FCS length mod 64 | Octets left in the final beat | FCS fits? |
|---|---|---|
| 0 | 64 | yes — a fresh beat |
| 1 to 60 | 63 down to 4 | yes |
| 61 | 3 | NO — 1 octet spills |
| 62 | 2 | NO — 2 spill |
| 63 | 1 | NO — 3 spill |
Three residues out of sixty-four, which over the legal frame sizes is:
| Value | |
|---|---|
| pre-FCS sizes from 60 to 1 514 | 1 455 |
| of which the FCS spans two beats | 69 |
| fraction | 4.74% |
So one frame in twenty-one needs an extra beat purely to carry between one and three octets of check value, and the cost of that beat is real:
| FCS fits | FCS spans | |
|---|---|---|
| beats for a 1 518-octet frame | 24 | 25 |
| the extra beat carries | — | 1 to 3 octets |
| efficiency of that beat | — | 1.6% to 4.7% |
And there is a second consequence that is easy to miss and is worse than the wasted beat.
The spilling beat is the frame's last beat, so it is the beat that carries eof — and it arrives one cycle after the beat that carried the last data octet. A design that asserts eof on the data's final beat and then emits another beat has emitted a beat after the end of the frame, which downstream blocks read as the start of the next one.
eof on the data beat | eof on the FCS beat | |
|---|---|---|
beats after eof | one — a protocol violation | none |
| the frame's length | understated by 1 to 3 | correct |
| Chapter 19.1 §3's dual-frame case | misaligned | correct |
Which makes the rule short: eof belongs on the beat carrying the check value's last octet, never on the beat carrying the payload's.
And the same rule has a receive-side twin worth noting. Chapter 19.2's parser measures a frame from sof to eof — so a transmitter that marks eof early produces frames the far end's parser measures four octets short, which Chapter 7.3's validity check reads as a runt on a 64-octet frame. A transmit bug diagnosed as a receive error, one hop away.
5. RTL 2 — The Check-Value Appender
Section 4's spill, handled by emitting the beat rather than by avoiding it.
// -----------------------------------------------------------------------
// crc_appender -- places 6.2's check value after the frame's last
// data octet, emitting an extra beat when it does not fit.
//
// The value itself is Chapter 19.4's; this block places it. The only
// difficulty is section 4's: `eof` belongs on the beat carrying the
// check value's LAST octet, and on 4.74% of frame sizes that is a
// beat after the payload ended.
// -----------------------------------------------------------------------
module crc_appender
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic in_valid,
input logic [DP_BYTES*8-1:0] in_data,
input logic [$clog2(DP_BYTES+1)-1:0] in_bytes,
input logic in_eof, // end of PAYLOAD
input logic [31:0] fcs, // from Chapter 19.4
output logic out_valid,
output logic [DP_BYTES*8-1:0] out_data,
output logic [$clog2(DP_BYTES+1)-1:0] out_bytes,
output logic out_eof, // end of FRAME
input logic out_ready,
output logic [31:0] c_frames,
output logic [31:0] c_fcs_spans,
output logic [31:0] c_extra_beats,
output logic eof_before_fcs // must never assert
);
logic spilling;
logic [1:0] spill_bytes;
logic [31:0] fcs_q;
wire [6:0] room = 7'(DP_BYTES) - {2'b0, in_bytes};
wire fits = (room >= 7'(FCS_B));
// The FCS is little-endian on the wire in 6.2's convention, so
// octet 0 of the appended four is fcs[7:0]. Getting this backwards
// produces a frame that fails at every receiver and is the single
// most common bring-up error on a transmit path.
wire [FCS_B*8-1:0] fcs_bytes = {fcs[31:24], fcs[23:16],
fcs[15:8], fcs[7:0]};
logic [DP_BYTES*8-1:0] merged;
always_comb begin
int i;
merged = in_data;
for (i = 0; i < FCS_B; i++)
if ((int'(in_bytes) + i) < DP_BYTES)
merged[(int'(in_bytes) + i)*8 +: 8] = fcs_bytes[i*8 +: 8];
end
// The spill beat carries the octets that did not fit, at offset 0.
logic [DP_BYTES*8-1:0] spill_beat;
always_comb begin
int i;
spill_beat = '0;
for (i = 0; i < FCS_B; i++)
if ((int'(in_bytes) + i) >= DP_BYTES)
spill_beat[(int'(in_bytes) + i - DP_BYTES)*8 +: 8] =
fcs_bytes[i*8 +: 8];
end
assign out_valid = in_valid || spilling;
assign out_data = spilling ? spill_beat : merged;
assign out_bytes = spilling ? {5'b0, spill_bytes}
: (fits ? (in_bytes + $clog2(DP_BYTES+1)'(FCS_B))
: $clog2(DP_BYTES+1)'(DP_BYTES));
// Section 4's rule. eof is on the beat carrying the check value's
// last octet -- the data beat if it fits, the spill beat if not.
assign out_eof = spilling || (in_valid && in_eof && fits);
// The violation: eof on a beat that does not carry the last FCS
// octet. A downstream block would then read the spill beat as the
// start of the next frame.
assign eof_before_fcs = in_valid && in_eof && !fits && out_eof && !spilling;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
spilling <= 1'b0; spill_bytes <= '0; fcs_q <= '0;
c_frames <= '0; c_fcs_spans <= '0; c_extra_beats <= '0;
end else begin
if (in_valid && in_eof && out_ready) begin
c_frames <= c_frames + 1;
if (!fits) begin
spilling <= 1'b1;
spill_bytes <= 2'(7'(FCS_B) - room);
fcs_q <= fcs;
c_fcs_spans <= c_fcs_spans + 1;
end
end
if (spilling && out_ready) begin
spilling <= 1'b0;
c_extra_beats <= c_extra_beats + 1;
end
end
end
endmoduleClassification: a merge-and-spill appender with the end-of-frame marker placed on the check value's last octet.
What it teaches: that the octet order of the appended check value is a convention, not an arithmetic fact, and getting it backwards produces a frame every receiver rejects. Chapter 6.2 fixed four conventions — initial value, reflection of input, reflection of output, final complement — and the on-wire octet order is a fifth thing on top of them. A transmitter with the arithmetic perfect and the octet order reversed fails at 100% of receivers, which is at least a loud failure.
And it teaches that eof_before_fcs catches the spill case's real bug. Section 4: a design that marks eof on the payload's last beat and then emits a spill beat has put a beat after the end of a frame, which Chapter 19.1 §3's dual-frame logic downstream reads as the start of the next. The signal is one comparison and it fires on 4.74% of frame sizes — so it is not a corner case, and a design that does not check it fails on one frame in twenty-one.
Deliberately simplified: the merge and spill loops are combinational over four octets at a variable offset, which is two 64-way byte-position decodes — modest, and a production design derives them from the byte-enables. The FCS arrives as a 32-bit input with no handshake, so a value arriving a cycle late is silently wrong. And spill_bytes is 2 bits, which is exactly right for a spill of 1 to 3 and would need widening on a design where the check value could span more than two beats — which cannot happen at 64 octets per beat and can at 4.
Production implication: c_fcs_spans against c_frames should be near 4.74% on uniformly distributed frame sizes and zero on a port carrying only minimum-size frames — because 64 mod 64 is 0 and a fresh beat has room. A regression reporting zero has not exercised the spill path, which is Section 20's directed test: the path exists on one frame in twenty-one and a minimum-size stress test meets it never.
6. The Gap Is an Average
This section is the chapter's central fact and it contradicts the number every engineer remembers.
Chapter 8.3's efficiency arithmetic uses twelve octets of interframe gap and is right to. Chapter 5.9 is where the twelve comes from and it is a mean rather than a minimum.
Chapter 5.9 §5: the standard fixes an average and lets individual gaps fall below it, provided a bounded deficit is carried and repaid. The shortest permitted single gap is 9 octets and the deficit bound is 0 to 3.
On a wide interface the mean cannot be held any other way, because a frame must begin on a lane boundary. With a 4-octet lane:
| Frame end, mod 4 | Largest legal gap ≤ 12 | Shortfall | One lane later |
|---|---|---|---|
| 0 | 12 | 0 | 16 |
| 1 | 11 | 1 | 15 |
| 2 | 10 | 2 | 14 |
| 3 | 9 | 3 | 13 |
The shortfalls are exactly 0 to 3, which is exactly Chapter 5.9's deficit bound — and that is not a coincidence but an identity: the bound is the granularity minus one.
The mechanism is then forced. Take the short gap while the deficit permits; take the long one to repay.
| Frame size | mod 4 | Short gap | Long gap | Sequence | Mean |
|---|---|---|---|---|---|
| 64 | 0 | 12 | 16 | 12, 12, 12, … | 12.000 |
| 1 517 | 1 | 11 | 15 | 11, 11, 11, 15, … | 12.000 |
| 1 518 | 2 | 10 | 14 | 10, 14, 10, 14, … | 12.000 |
| 1 519 | 3 | 9 | 13 | 9, 13, 13, 13, … | 12.000 |
Every row holds the mean at exactly twelve and no row emits twelve on more than one frame size.
Which has three consequences and each is a section of this chapter.
First, a transmitter that emits exactly twelve every time is wrong. It places the next frame off-lane on three quarters of frame sizes — and a PHY cannot start a frame off-lane, so the result is either a misaligned frame or a gap that silently grows to the next lane.
Second, a receiver measuring the gap must not treat nine as an error. Chapter 5.9 §7's argument: a gap of nine is legal and a receiver that discards on it discards legal traffic.
Third — and this is Section 19's subject — an assertion that the gap is at least twelve is false on legal traffic. A run of 1 518-octet frames emits a gap of ten on alternate frames, and the property fails while the design is correct.
| Property | On 1 518-octet frames |
|---|---|
gap >= 12 | fails every other frame |
gap >= 9 | passes |
mean(gap) == 12 | passes |
deficit <= 3 | passes |
Rows two to four are the requirement and row one is what everybody writes.
7. RTL 3 — The Gap Enforcer
Section 6's table, as a two-choice decision made once per frame.
// -----------------------------------------------------------------------
// txasm_ifg_enforcer -- chooses this frame's gap from the two the
// alignment permits.
//
// Section 6: with a LANE_B-octet granularity there are exactly two
// candidate gaps -- the largest legal one at or below the nominal,
// and that one plus a lane. The deficit decides which.
// -----------------------------------------------------------------------
module txasm_ifg_enforcer
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic frame_end,
input logic [15:0] frame_end_pos, // octets since a lane
input logic [2:0] deficit, // from section 8
input logic cfg_dic_enable,
output logic [4:0] gap_octets,
output logic gap_valid,
output logic took_short,
output logic [2:0] shortfall,
output logic [2:0] excess,
output logic [31:0] c_gaps,
output logic [31:0] c_short,
output logic [31:0] c_long,
output logic [31:0] c_gap_hist [8],
output logic gap_below_min // must never assert
);
// The next frame must start on a lane boundary, so the gap is
// congruent to (-frame_end_pos) mod LANE_B. The largest such value
// at or below the nominal is the short choice.
wire [1:0] residue = 2'((LANE_B - frame_end_pos[1:0]) % LANE_B);
// short = the largest g <= IFG_NOM with g mod LANE_B == residue
wire [4:0] short_gap = 5'(IFG_NOM) - 5'(frame_end_pos[1:0]);
wire [4:0] long_gap = short_gap + 5'(LANE_B);
assign shortfall = 3'(5'(IFG_NOM) - short_gap);
assign excess = 3'(long_gap - 5'(IFG_NOM));
// Take the short gap while the deficit can absorb the shortfall;
// otherwise take the long one and repay. Disabling the mechanism
// forces the long gap always, which is legal and wastes bandwidth.
wire can_short = cfg_dic_enable &&
((deficit + shortfall) <= 3'(DEF_MAX));
assign took_short = can_short;
assign gap_octets = can_short ? short_gap : long_gap;
assign gap_valid = frame_end;
// 5.9 section 5: nine is the floor. A gap below it is a protocol
// violation the receiver may not tolerate.
assign gap_below_min = gap_valid && (gap_octets < 5'(IFG_MIN));
always_ff @(posedge clk or negedge rst_n) begin
int i;
if (!rst_n) begin
c_gaps <= '0; c_short <= '0; c_long <= '0;
for (i = 0; i < 8; i++) c_gap_hist[i] <= '0;
end else if (gap_valid) begin
c_gaps <= c_gaps + 1;
if (took_short) c_short <= c_short + 1;
else c_long <= c_long + 1;
// Buckets 0..7 cover gaps 9..16, which is the whole legal set.
if ((gap_octets >= 5'd9) && (gap_octets <= 5'd16))
c_gap_hist[gap_octets - 5'd9] <= c_gap_hist[gap_octets - 5'd9] + 1;
end
end
endmoduleClassification: a two-candidate selection driven by a bounded accumulator, with a histogram over the whole legal set.
What it teaches: that there are exactly two candidate gaps and not a range, which is what makes the decision a mux rather than an arithmetic unit. The lane boundary fixes the gap's residue; the nominal fixes which multiple; so the only freedom is one lane up or not. A design that computes a gap from a target and then rounds has done the same arithmetic in a more expensive way and will round to the same two values.
And it teaches that c_gap_hist over buckets 9 to 16 covers the entire legal set, which makes it a complete description of the transmitter's behaviour in eight counters. A port sending 1 518-octet frames shows two populated buckets — 10 and 14, in equal numbers. One sending mixed sizes shows all eight. A port showing a single bucket at 12 is either sending only lane-aligned frames or has the mechanism disabled.
Deliberately simplified: short_gap is computed as IFG_NOM - (frame_end_pos mod 4), which is correct for LANE_B = 4 and IFG_NOM = 12 and does not generalise — a parameterised version needs the modulo explicitly. residue is computed and unused, left in the listing because it names the quantity the arithmetic is standing in for. And gap_below_min can only fire if the parameters are inconsistent, which is why it is an assertion rather than a counter.
Production implication: c_short against c_long is the deficit mechanism's duty cycle, and its value is a direct function of the frame-size distribution. All-short means every frame is lane-aligned — frame_end_pos mod 4 == 0, which is 64-octet and 9 000-octet frames. Alternating means a shortfall of 2. Three short to one long means a shortfall of 1, and one short to three long means a shortfall of 3. So the ratio reads back the traffic's length distribution mod 4, which is a curious and occasionally useful thing for a counter to tell you.
8. RTL 4 — The Deficit Accumulator
The bounded accumulator Chapter 5.9 §11 identified as a general pattern, instantiated for the gap.
// -----------------------------------------------------------------------
// deficit_accumulator -- tracks how much gap has been borrowed and
// refuses to borrow beyond the bound.
//
// 5.9 section 11: the general form is a bounded accumulator --
// track what you owe, spend when the quantisation is favourable,
// refuse to borrow beyond the bound. 4.4's elastic buffer is the
// same structure applied to frequency; this is it applied to
// alignment.
// -----------------------------------------------------------------------
module deficit_accumulator
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic gap_valid,
input logic took_short,
input logic [2:0] shortfall,
input logic [2:0] excess,
input logic link_down,
output logic [2:0] deficit,
output logic [31:0] c_borrowed,
output logic [31:0] c_repaid,
output logic [2:0] peak_deficit,
output logic [31:0] c_mean_gap_num,
output logic [31:0] c_mean_gap_den,
output logic deficit_out_of_bound // must never assert
);
// The invariant 5.9 section 5 publishes and section 11 generalises:
// 0 <= deficit <= DEF_MAX. An unbounded accumulator eventually
// violates the requirement it exists to protect.
assign deficit_out_of_bound = (deficit > 3'(DEF_MAX));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
deficit <= '0; peak_deficit <= '0;
c_borrowed <= '0; c_repaid <= '0;
c_mean_gap_num <= '0; c_mean_gap_den <= '0;
end else if (link_down) begin
// A link event ends the accounting. Carrying a deficit across
// a down/up cycle would repay it against a partner that never
// received the borrowing -- harmless, and meaningless.
deficit <= '0;
end else if (gap_valid) begin
if (took_short) begin
deficit <= deficit + shortfall;
c_borrowed <= c_borrowed + {29'b0, shortfall};
end else begin
deficit <= (deficit >= excess) ? (deficit - excess) : 3'd0;
c_repaid <= c_repaid + {29'b0, excess};
end
c_mean_gap_num <= c_mean_gap_num +
(took_short ? (32'(IFG_NOM) - {29'b0, shortfall})
: (32'(IFG_NOM) + {29'b0, excess}));
c_mean_gap_den <= c_mean_gap_den + 1;
if ((deficit + (took_short ? shortfall : 3'd0)) > peak_deficit)
peak_deficit <= deficit + (took_short ? shortfall : 3'd0);
end
end
endmoduleClassification: a saturating bounded accumulator with borrow and repay accounting.
What it teaches: that the reset on link_down is a correctness decision rather than tidiness. A deficit carried across a link event would be repaid against a partner that never saw the borrowing — the link went down, the far end reset its own accounting, and repaying into a fresh conversation makes the first few gaps longer for no reason. It is harmless and it is meaningless, and Chapter 5.9 §11's general form — a bounded accumulator — says nothing about when to clear it, which is why every instance has to decide.
And it teaches that c_mean_gap_num / c_mean_gap_den is the requirement, measured. Chapter 5.9 fixes an average; this is the average. A port whose measured mean is 12.000 is conformant whatever any individual gap was — and a port whose mean is 12.4 is wasting 3.3% of its interframe bandwidth by taking the long gap more often than the deficit requires.
Deliberately simplified: the accumulator saturates at zero on repayment rather than going negative, which loses the information that a long gap was taken when nothing was owed — a production design either prevents that in Section 7 or tracks a signed value. peak_deficit is computed with the same expression twice in one always block. And the mean accumulators are 32 bits and wrap, so a long-running port's mean drifts from its recent behaviour.
Production implication: deficit_out_of_bound must be wired to something loud, because a deficit above the bound means the transmitter has borrowed gap it will not repay — and the far end's receiver is being given gaps shorter than the standard permits. Chapter 5.9 §8 established that the discharge opportunity is what bounds the accumulation; a deficit that grows without bound is the transmitter consuming its partner's margin, and the symptom appears at the partner as Chapter 4.4's elastic buffer overrunning — hours or days later, and diagnosed there.
9. Choosing the Start Offset, and the Commit Point
Chapter 19.2 §11 established that a receiver discovers its start offset. A transmitter chooses it — and Section 6 showed the choice is between two lanes. This section is the other decision made at the same instant: when transmission becomes irreversible.
Chapter 19.1 §5's rule: a transmit stage may stall before commit and not after. So the commit point is where the assembler's latency stops being free, and everything upstream of it is allowed to be slow.
| Upstream of commit | Downstream of commit |
|---|---|
| the ring walk — Chapter 18.4 §3 | the PHY interface |
| the gather — Chapter 18.4 §5 | — |
| padding — Section 3 | — |
| the check value — Section 5 | — |
| may stall | MUST NOT |
And the commit point's placement is Chapter 18.4 §7's decision arriving in the assembler.
On store-and-forward, commit is when the whole frame is in the FIFO. There is then no way to underrun: every octet is already held, and Section 11 is where that sits against the FIFO's fill threshold.
On cut-through, commit is when the fill threshold is reached — and Chapter 18.4 §9 derived when that is possible:
| Worst memory stall | Lead needed at 100 Gb/s | Against a 1 518-octet frame |
|---|---|---|
| 100 ns | 1 250 octets | possible |
| 300 ns | 3 750 octets | IMPOSSIBLE |
| 800 ns | 10 000 octets | impossible |
So at 100 Gb/s with any realistic memory system the commit point is at the end of the frame, and the assembler is store-and-forward whether it is configured that way or not.
Which makes the assembler's latency budget generous in a way that is worth stating, because it is the opposite of Chapter 19.2's.
| Chapter 19.2's parser | this assembler | |
|---|---|---|
| latency available | the pipeline's depth | the frame's own transmission time |
| at 100 Gb/s, 1 518 octets | 5 cycles | 24 beats |
| what constrains it | throughput | nothing, before commit |
Twenty-four beats is a lot of time to pad a frame and append four octets, and the assembler uses almost none of it.
Now the start offset, which is the choice Section 6 left.
The transmitter picks the next frame's start by picking the gap, and the two are the same decision. But there is a second consideration the gap arithmetic does not capture: which start offset makes the receiver's life easier.
| Start offset chosen | What the far end's parser sees |
|---|---|
| a lane boundary — forced | one of 16 positions on a 64-octet beat |
| offset 0 of a beat | Chapter 19.2 §6's one-beat parse |
| anything else | a two-beat parse for tagged frames |
And the transmitter cannot choose row two, because the gap is bounded to 9–16 and the frame's length decides the rest. A frame ending at offset 40 of a beat, with a gap of 12, starts the next at offset 52 — and no legal gap reaches 64.
Which is worth being explicit about, because it is a tempting optimisation that does not exist: a transmitter cannot pad the gap to align frames on beat boundaries for the receiver's benefit. The gap's ceiling is 16 octets and a beat is 64. Chapter 19.2's barrel shift is not avoidable by cooperation.
10. RTL 5 — The Commit Point
One state transition, and everything about the transmit path's safety hangs on it.
// -----------------------------------------------------------------------
// commit_point -- the instant after which a stall corrupts a frame
// already on the wire.
//
// 19.1 section 5: a transmit stage may stall before commit and not
// after. This block is where that line is drawn, and 18.4 section 7's
// fill threshold is what decides where to draw it.
// -----------------------------------------------------------------------
module commit_point
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic cfg_store_and_forward,
input logic [15:0] cfg_fill_threshold,
input logic [15:0] cfg_required_lead, // 18.4 section 7's figure
input logic frame_present,
input logic frame_complete, // the whole frame is held
input logic [15:0] fifo_occupancy,
input logic phy_ready,
input logic frame_last_octet,
output astate_e state,
output logic committed,
output logic may_start,
output logic cut_through_refused,
output logic [31:0] c_commits,
output logic [31:0] c_cut_through,
output logic [31:0] c_store_forward,
output logic [15:0] worst_occupancy_at_commit,
output logic stalled_after_commit // must never assert
);
// 18.4 section 7: a cut-through threshold below the required lead
// is a design that will underrun. The refusal falls back rather
// than erroring, because store-and-forward costs latency and
// loses nothing.
assign cut_through_refused = !cfg_store_and_forward &&
(cfg_fill_threshold < cfg_required_lead);
wire threshold_met = (fifo_occupancy >= cfg_fill_threshold);
assign may_start = frame_present && phy_ready &&
(frame_complete ||
(!cfg_store_and_forward && !cut_through_refused &&
threshold_met));
assign committed = (state == A_COMMITTED);
// The invariant. Once committed, the PHY must take every beat.
assign stalled_after_commit = committed && !phy_ready && !frame_last_octet;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= A_IDLE;
c_commits <= '0; c_cut_through <= '0; c_store_forward <= '0;
worst_occupancy_at_commit <= 16'hFFFF;
end else begin
unique case (state)
A_IDLE:
if (may_start) begin
state <= A_COMMITTED;
c_commits <= c_commits + 1;
if (frame_complete) c_store_forward <= c_store_forward + 1;
else c_cut_through <= c_cut_through + 1;
if (fifo_occupancy < worst_occupancy_at_commit)
worst_occupancy_at_commit <= fifo_occupancy;
end
A_COMMITTED:
if (frame_last_octet) state <= A_GAP;
A_GAP:
state <= A_IDLE;
default: state <= A_IDLE;
endcase
end
end
endmoduleClassification: a three-state commit machine with a configuration safety check.
What it teaches: that worst_occupancy_at_commit is a minimum rather than a maximum, and it is the transmit path's margin measured. Every other watermark in this track records how full something got; this records how empty the FIFO was when the design committed to an irreversible action. A port whose minimum is 64 octets has committed with one beat in hand — and Chapter 18.4 §14's c_min_occupancy is the same measurement taken during transmission rather than at its start.
And it teaches why the refusal falls back rather than erroring. Cut-through is a latency optimisation. Falling back to store-and-forward costs latency and loses nothing; erroring costs the link. Chapter 17.3 §13 made the same argument about a capability whose failure corrupts data and whose absence only costs performance — the default must be off and the evidence must be positive.
Deliberately simplified: the state machine has no error path, so a frame abandoned mid-transmission — a bus error on the gather — has nowhere to go; a real design adds an abort state that drives Chapter 18.4 §8's underrun guard. cfg_required_lead is supplied by the integrator rather than derived, which Chapter 18.4 §7 established is the honest arrangement and is only as good as the figure. And A_GAP occupies exactly one cycle, where Section 7's gap is 9 to 16 octets — which on a 64-octet beat is less than one beat and is therefore correct, and would not be on a narrower datapath.
Production implication: c_cut_through against c_commits is the fraction of frames that started before they were complete, and on a 100 Gb/s port it should be zero. Section 9: the required lead exceeds the largest frame at any realistic memory latency, so cut-through is refused and every commit is store-and-forward. A non-zero count at 100 Gb/s means cfg_required_lead was configured optimistically, and Chapter 18.4 §12's c_underrun_completions is where the consequence appears.
11. Where Commit Sits Against the FIFO's Threshold
Section 10 made the commit point configurable. This section is where the configuration must sit, and the answer comes from two chapters that did not know about each other.
Chapter 18.4 §7 derived the required lead: stall × rate / 8 octets. Section 10 refuses a threshold below it. So the constraint is one inequality:
cfg_fill_threshold ≥ cfg_required_lead, andcfg_required_lead = S × r / 8.
| Line rate | 100 ns stall | 300 ns | 800 ns |
|---|---|---|---|
| 1 Gb/s | 12.5 octets | 37.5 | 100 |
| 10 Gb/s | 125 | 375 | 1 000 |
| 25 Gb/s | 312.5 | 937.5 | 2 500 |
| 100 Gb/s | 1 250 | 3 750 | 10 000 |
And the threshold cannot exceed the frame, because there is nothing else to accumulate. So cut-through survives only where the lead fits:
| Stall | Survives up to |
|---|---|
| 100 ns | 121.44 Gb/s |
| 300 ns | 40.48 Gb/s |
| 800 ns | 15.18 Gb/s |
Which is Chapter 18.4 §9's table and it is the same table because it is the same inequality.
What this chapter adds is the other end of the range, which Chapter 18.4 did not need: the threshold must also leave room for Section 5's spill beat.
A frame whose check value spans two beats needs one more beat than its payload implies — Section 4's 4.74% — and that beat is emitted after the commit. So a design that sets the threshold to exactly the frame's payload length commits with no room for the spill, and the spill beat is produced from a FIFO that the commit assumed was complete.
| Threshold set to | Spill beat |
|---|---|
| the payload's length | not covered |
| the payload plus 4 | covered |
| the whole frame including FCS | covered — store-and-forward |
Row two is the correct minimum and row one is the natural mistake, because "the frame is complete" is a statement about the payload in most designs and the check value is appended later.
And there is a third constraint that comes from Section 7 and binds at low rates.
The gap is 9 to 16 octets and the commit machine spends one cycle in A_GAP. On a 512-bit datapath that is fine — 16 octets is a quarter of a beat. On a 64-bit datapath a 16-octet gap is two beats, and on GMII it is sixteen cycles — so A_GAP must be a counter rather than a single state, and a design written for 100 Gb/s and ported down has a gap of one cycle where sixteen are required.
| Datapath | A 16-octet gap is | A_GAP must be |
|---|---|---|
| 8 bits (GMII) | 16 cycles | a counter |
| 64 bits | 2 cycles | a counter |
| 512 bits | 0.25 cycles | one cycle |
Which is Chapter 19.1 §4's slack table read from the transmit side: the gap is twenty cycles of work at 1 Gb/s and a third of a cycle at 100, and a design that assumes either one breaks at the other.
12. RTL 6 — The Assembler Datapath
The five blocks in one pipeline, and the block exists to show where the handshakes are.
// -----------------------------------------------------------------------
// txasm_datapath -- pad, append, commit, gap, in order.
//
// The handshake discipline is 19.1 section 5's: ready propagates
// backward up to the commit point and no further. Downstream of
// commit there is no ready, because there is nothing that may
// refuse.
// -----------------------------------------------------------------------
module txasm_datapath
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
// From 18.4 section 5's gather engine.
input logic src_valid,
output logic src_ready,
input tbeat_t src_beat,
// To the PHY. NOTE: no ready -- 19.1 section 5.
output logic phy_valid,
output tbeat_t phy_beat,
output logic [4:0] phy_gap,
input logic committed,
input logic pad_leaked,
input logic eof_before_fcs,
input logic [4:0] gap_octets,
output logic [31:0] c_beats,
output logic [31:0] c_stall_cycles,
output logic ready_after_commit // must never assert
);
logic occupied;
// Backpressure exists only before commit. After it, `src_ready` is
// irrelevant because the frame is already held -- store-and-forward
// guarantees it -- and the PHY takes every beat unconditionally.
assign src_ready = !committed && (!occupied || 1'b1);
// The violation: offering backpressure to the source while a frame
// is on the wire. On a store-and-forward design this cannot lose
// data, and on a cut-through one it is an underrun.
assign ready_after_commit = committed && !src_ready;
assign phy_valid = occupied;
assign phy_gap = gap_octets;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
occupied <= 1'b0; phy_beat <= '0;
c_beats <= '0; c_stall_cycles <= '0;
end else begin
if (src_valid && src_ready) begin
phy_beat <= src_beat;
occupied <= 1'b1;
c_beats <= c_beats + 1;
end else begin
occupied <= 1'b0;
if (src_valid) c_stall_cycles <= c_stall_cycles + 1;
end
end
end
endmoduleClassification: a single-register pipeline stage whose backpressure is gated on the commit state.
What it teaches: that the PHY interface has no ready and that absence is the design. Chapter 19.1 §10 made the same argument about the receive path and enforced it with a generate; here the enforcement is that the port simply does not exist. A design that adds one has given the PHY the ability to stall a frame in flight, which is Chapter 18.4 §6's underrun by a different route.
And it teaches that ready_after_commit is the signal Chapter 19.1 §5 named and this block can finally check. That chapter's stalled_after_commit was about a stage refusing to accept; this is about the assembler refusing to supply — the same failure from the other end, and on a store-and-forward design it cannot lose data because the frame is already held.
Deliberately simplified: src_ready is written as !committed && (!occupied || 1'b1), which reduces to !committed — the listing keeps the occupancy term to show where a skid buffer would go and a linter will object. There is no skid buffer, so a beat arriving while src_ready is low is dropped rather than held. And phy_gap is presented as a static value alongside every beat, where a real interface signals the gap once at the frame's end.
Production implication: c_stall_cycles on the assembler should be zero after commit and non-zero before it, and the split is the useful reading. Stalls before commit are Chapter 18.4 §16's pipeline depth being insufficient — the gather engine has not kept up — and cost throughput. Stalls after commit are the invariant violation. A single counter cannot distinguish them, which is why the commit state is an input to this block rather than something it infers.
13. What the Assembler Must Never Do
Three things, and each has a signal in one of the preceding blocks.
| # | Must never | Signal | Consequence if it does |
|---|---|---|---|
| 1 | emit an unwritten pad | pad_leaked | up to 45 octets of host memory on the wire |
| 2 | mark eof before the check value's last octet | eof_before_fcs | a beat after the frame; the far end misframes |
| 3 | stall after commit | ready_after_commit | an underrun — a corrupted frame in flight |
And a fourth that has no signal, which is the section's point.
The assembler must never emit a gap below Chapter 5.9's floor of nine octets — and Section 7's gap_below_min checks it. But it must also never emit a mean above twelve, and no per-frame signal can detect that.
| Per-frame | Over a run | |
|---|---|---|
| gap ≥ 9 | checkable | — |
| deficit ≤ 3 | checkable | — |
| mean gap = 12 | NOT checkable | checkable |
A mean is a property of a sequence and cannot be evaluated on an instance, which is Section 19's rejected class and is also a design consideration: the only mechanism that guarantees the mean is the bounded deficit, and the only check is Section 8's accumulator staying inside its bound.
Which gives the relationship between the three, and it is worth stating because it is the chapter's structural argument:
The deficit's bound is what makes the per-frame check sufficient. Because the deficit never exceeds 3 and every borrowing is repaid, the mean is 12 — so checking
deficit ≤ 3on every frame proves a property of the sequence.
A bounded accumulator turns a sequence property into an instance property, and that is what Chapter 5.9 §11 meant by calling it a general pattern. Chapter 4.4's elastic buffer does the same for frequency: an unbounded accumulation is unverifiable and a bounded one is checkable every cycle.
And the failure mode both share is the same: remove the discharge opportunity and the bound stops holding. Chapter 4.4 §8: a MAC that closes the interframe gap leaves nowhere to insert or delete, and the elastic buffer overruns hours later. This chapter's assembler is the MAC that would do that — so Section 7's refusal to emit a gap below nine is not only a conformance check; it is what keeps the far end's elastic buffer bounded.
14. RTL 7 — Assembler Telemetry
The counters, and one of them measures a requirement rather than a behaviour.
// -----------------------------------------------------------------------
// txasm_telemetry -- the assembler's observable state.
// -----------------------------------------------------------------------
module txasm_telemetry
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic frame_done,
input logic [15:0] frame_bytes,
input logic was_padded,
input logic [15:0] pad_octets,
input logic fcs_spanned,
input logic [4:0] gap_octets,
input logic took_short,
input logic [2:0] deficit,
input logic committed_cut_through,
input logic [15:0] occupancy_at_commit,
output logic [31:0] c_frames,
output logic [31:0] c_padded,
output logic [31:0] c_fcs_spans,
output logic [31:0] c_short_gaps,
output logic [31:0] c_cut_through,
output logic [31:0] c_gap_sum,
output logic [2:0] peak_deficit,
output logic [15:0] min_occupancy_at_commit,
output logic [15:0] mean_gap_x100,
output logic [15:0] pad_pct,
output logic [15:0] span_pct
);
always_comb begin
mean_gap_x100 = (c_frames == '0) ? 16'd0
: 16'((c_gap_sum * 32'd100) / c_frames);
pad_pct = (c_frames == '0) ? 16'd0
: 16'((c_padded * 32'd100) / c_frames);
span_pct = (c_frames == '0) ? 16'd0
: 16'((c_fcs_spans * 32'd100) / c_frames);
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_frames <= '0; c_padded <= '0; c_fcs_spans <= '0;
c_short_gaps <= '0; c_cut_through <= '0; c_gap_sum <= '0;
peak_deficit <= '0; min_occupancy_at_commit <= 16'hFFFF;
end else if (frame_done) begin
c_frames <= c_frames + 1;
c_gap_sum <= c_gap_sum + {27'b0, gap_octets};
if (was_padded) c_padded <= c_padded + 1;
if (fcs_spanned) c_fcs_spans <= c_fcs_spans + 1;
if (took_short) c_short_gaps <= c_short_gaps + 1;
if (committed_cut_through) c_cut_through <= c_cut_through + 1;
if (deficit > peak_deficit) peak_deficit <= deficit;
if (occupancy_at_commit < min_occupancy_at_commit)
min_occupancy_at_commit <= occupancy_at_commit;
end
end
endmoduleClassification: a per-frame accountant whose headline output is a mean rather than a count.
What it teaches: that mean_gap_x100 is the standard's requirement, measured, and it is the only counter in Modules 18 and 19 of which that is true. Every other number in these chapters measures a behaviour — how often something happened, how close something came. Chapter 5.9 fixes an average and this is the average: a port reading 1200 is exactly conformant, and one reading 1240 is wasting 3.3% of its interframe bandwidth by taking the long gap more often than the deficit requires.
And it teaches that span_pct should sit near 4.74% and its value reads back the traffic's length distribution. Section 4: the check value spans a beat when the pre-FCS length mod 64 is 61, 62 or 63. A port sending only minimum-size frames reports zero; one sending uniformly distributed sizes reports 4.74%; a port reporting 12% is sending sizes concentrated near those residues, which is unusual and worth knowing.
Deliberately simplified: three combinational divides. c_gap_sum is 32 bits and wraps after about 358 million frames at a mean of 12 — which at 100 Gb/s with minimum-size frames is 2.4 seconds, so a production design either widens it or resets on read. And min_occupancy_at_commit is initialised to 0xFFFF, which reads as 65 535 on a port that has never transmitted.
Production implication: mean_gap_x100 against 1200 is a conformance statement a port can make about itself, and it is the number to quote when a partner reports a gap violation. Chapter 5.9 §7 established that a receiver may not conclude much from an individual gap it measured; a transmitter that can report its own mean settles the argument — and a partner measuring gaps of 9 and complaining is measuring a legal sequence.
15. RTL 8 — The Assembler Conformance Monitor
The verdicts, and two of the five are about conformance to a standard rather than to a design intent.
// -----------------------------------------------------------------------
// txasm_conformance_monitor -- assembler verdicts.
// -----------------------------------------------------------------------
module txasm_conformance_monitor
import txasm_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [31:0] c_frames,
input logic [31:0] c_fcs_spans,
input logic [31:0] c_cut_through,
input logic [15:0] mean_gap_x100,
input logic [15:0] span_pct,
input logic [2:0] peak_deficit,
input logic [15:0] min_occupancy_at_commit,
input logic [15:0] cfg_required_lead,
input logic pad_leaked,
input logic eof_before_fcs,
input logic ready_after_commit,
input logic stalled_after_commit,
input logic gap_below_min,
input logic deficit_out_of_bound,
input logic cut_through_refused,
output logic assembler_ok,
output logic disclosure_fault,
output logic framing_fault,
output logic gap_nonconformant,
output logic commit_margin_thin,
output logic cut_through_misconfigured,
output logic spill_path_untested,
output logic none_of_the_above
);
// Section 13's first row: a pad that was not written.
assign disclosure_fault = pad_leaked;
// Section 13's second and third rows.
assign framing_fault = eof_before_fcs | ready_after_commit |
stalled_after_commit;
// 5.9's two conformance requirements: no gap below 9, and a mean
// of 12. The second is a sequence property and is checkable only
// because the deficit is bounded -- section 13.
assign gap_nonconformant = gap_below_min | deficit_out_of_bound |
((c_frames > 32'd10000) &&
((mean_gap_x100 < 16'd1190) ||
(mean_gap_x100 > 16'd1210)));
// Section 10: committing with less than the required lead in hand.
assign commit_margin_thin = (c_frames > 32'd1000) &&
(min_occupancy_at_commit < cfg_required_lead);
assign cut_through_misconfigured = cut_through_refused;
// Section 4: the spill path exists on 4.74% of frame sizes and a
// run that never meets it has not tested it.
assign spill_path_untested = (c_frames > 32'd1_000_000) &&
(c_fcs_spans == 32'd0);
assign assembler_ok = !disclosure_fault && !framing_fault &&
!gap_nonconformant;
assign none_of_the_above = assembler_ok && !commit_margin_thin &&
!cut_through_misconfigured &&
!spill_path_untested;
// ---- properties -------------------------------------------------
p_no_pad_leak:
assert property (@(posedge clk) disable iff (!rst_n)
!pad_leaked)
else $error("a frame was padded with unwritten buffer contents");
p_eof_on_fcs:
assert property (@(posedge clk) disable iff (!rst_n)
!eof_before_fcs)
else $error("end of frame was marked before the check value's last octet");
p_no_stall_after_commit:
assert property (@(posedge clk) disable iff (!rst_n)
!stalled_after_commit && !ready_after_commit)
else $error("the assembler stalled after committing to transmit");
p_gap_at_least_nine:
assert property (@(posedge clk) disable iff (!rst_n)
!gap_below_min)
else $error("a gap below 5.9's nine-octet floor was emitted");
p_deficit_bounded:
assert property (@(posedge clk) disable iff (!rst_n)
!deficit_out_of_bound)
else $error("the interframe-gap deficit exceeded its bound");
endmoduleClassification: a verdict generator separating design faults from standards conformance from coverage.
What it teaches: that gap_nonconformant is the only verdict in Modules 18 and 19 that is about a standard rather than about a system. Every other conformance monitor in these chapters checks a design against its own contracts — with the driver, the bus, the memory system. This one checks a design against IEEE 802.3, and the check has two halves: an instance bound (nine octets) and a sequence property (a mean of twelve), with the second made checkable by the first.
And it teaches that spill_path_untested is a coverage verdict on a path that exists on one frame in twenty-one. Chapter 19.1 §14's dual_frame_untested and Chapter 19.2 §14's offsets_covered are the same idea; this is the third instance in two chapters, which suggests it is worth making a habit of.
Deliberately simplified: the mean's tolerance is ±0.1 octets expressed as literals, which a production monitor takes from registers — and the right tolerance depends on how many frames have been counted. commit_margin_thin compares against cfg_required_lead even on a store-and-forward port, where the comparison is vacuous because the whole frame is held. And the five verdicts are not mutually exclusive, so a badly broken assembler asserts several.
Production implication: none_of_the_above for the ninth time across Modules 18 and 19, and it now covers a standards claim. A port asserting it has no disclosure path, no framing violation, a conformant gap sequence, an adequate commit margin, a sane cut-through configuration, and a spill path that has been exercised. That last clause is the one a verification lead cares about and the first is the one a security review does — which is Section 17's point about who reads which counter, arriving in the monitor.
16. Back-to-Back Transmission at 0.31 Beats of Gap
Chapter 19.1 §4 established that the gap is 0.31 beats at 512 bits. This section is what that does to the transmit path's timing, and it is the mirror of Chapter 19.2 §6.
The receive side discovers that a beat holds two frames. The transmit side causes it.
| Receive | Transmit | |
|---|---|---|
| the dual-frame beat | arrives | is emitted |
| the start offset | discovered | chosen — Section 9 |
| the cost | a barrel shift | a mux on the outgoing beat |
And the transmit-side cost is smaller, which is worth noticing. The assembler knows where it is placing the next frame — it chose the gap — so the beat that carries the end of one frame and the start of another is assembled from two known sources at a known offset. There is no shift to compute; there is a merge at an offset the design already has.
| Chapter 19.2's aligner | this assembler's merge | |
|---|---|---|
| the offset | an input, any of 64 | an output, chosen |
| structure | a 64-way barrel shift | a 64-way byte-enable merge |
| cost | 1 024 byte-muxes, two stages | 64 byte-enables and an OR |
Row three is a factor of sixteen and it is entirely because knowing the offset is cheaper than discovering it.
And it is worth asking what the alternative costs, because "never emit a beat holding two frames" is a tempting simplification and it is an expensive one.
Refusing means every frame starts at octet 0 of a beat. The preamble and start-frame delimiter are 8 octets and the frame is L, so the frame occupies ceil((8 + L) / 64) beats — and the remainder of the last beat is idle, which must still leave at least Chapter 5.9's nine octets of gap or another whole beat is needed.
| Frame | Wire octets per frame, shared beats | Beat-aligned | Penalty |
|---|---|---|---|
| 64 | 84 | 128 | 52.38% |
| 512 | 532 | 576 | 8.27% |
| 1 518 | 1 538 | 1 600 | 4.03% |
Row one is the one that decides it. A minimum-size frame plus its preamble is 72 octets, which is 8 octets past a beat boundary, so beat alignment rounds it to two beats — 128 octets of wire time for 84 octets of work, and the port's efficiency falls from 76.19% to 50.00%.
Row three shows why the temptation survives in slow designs: at maximum size the penalty is 4%, and a 1 Gb/s port at 1 500-octet frames barely notices it. It is the short-frame case that makes the simplification untenable, and short frames are exactly the case a 100 Gb/s MAC is specified against — Chapter 19.1 §4's 1.312-cycle budget is a minimum-size-frame budget.
So the transmit path emits dual-frame beats, and the merge above is how.
Now the timing, which is where the gap's 0.31 beats binds.
At 100 Gb/s with minimum-size frames, Chapter 19.1 §4's slot is 1.312 cycles. The assembler must, per frame: pad, append four octets, choose a gap, update a deficit, and commit.
| Work | Cycles if done per frame |
|---|---|
| pad — a byte-enable mask | combinational |
| append — a merge | combinational |
| choose the gap | combinational — a two-way mux |
| update the deficit | one cycle |
| commit | one cycle |
Two sequential cycles against a 1.312-cycle budget, which is Chapter 19.2 §6's deficit arriving in the assembler — and the answer is the same: pipeline across frames.
But there is a complication the parser did not have. The deficit is a sequential dependency between consecutive frames: frame n+1's gap choice depends on the deficit after frame n. A pipeline cannot hide a loop-carried dependency.
| The parser's per-frame work | the assembler's deficit | |
|---|---|---|
| depends on | this frame only | the previous frame's choice |
| pipelinable | yes | NO — it is a recurrence |
| cycles available | the pipeline depth | 1.312 |
So the deficit update must complete in one cycle, and it does: a 3-bit add, a compare against 3, and a two-way mux. That is one level of logic, and it is the only part of the assembler that cannot be pipelined.
Which is a useful thing to know about any design with an accumulator in it: the accumulator is the pipeline's floor. Everything else can be deepened; a recurrence cannot, and its width is what decides whether the floor is reachable. Three bits is reachable at 195 MHz; thirty-two would not be.
17. What the Assembler Assumes
Six assumptions, and three of them are about a component the assembler cannot see.
| # | The assumption | Owned by | If false |
|---|---|---|---|
| 1 | the check value is correct | Chapter 19.4 | every receiver discards the frame |
| 2 | the frame is complete before commit | Chapter 18.4 §7's threshold | an underrun |
| 3 | the worst-case stall figure is honest | the integrator | a threshold that cannot work |
| 4 | the PHY takes every beat after commit | the PHY | an underrun |
| 5 | the lane granularity is 4 octets | the interface | the gap arithmetic is wrong |
| 6 | the payload length is what the descriptor said | the driver | padding or truncation |
Row three is Chapter 18.4 §7's again and it is the one that ships wrong. Section 10's refusal compares the configured threshold against a configured lead; both are numbers somebody typed. An optimistic stall figure produces a threshold that passes the check and underruns in the field, and Chapter 18.4 §12's c_underrun_completions is where it appears.
Row five is the assumption this chapter introduced and it is worth being explicit that it is a parameter rather than a fact. Section 6 derived the gap set 12 from a 4-octet lane and Chapter 5.9's deficit bound of 0 to 3 — and the derivation runs backwards: the bound is the granularity minus one. A design on an interface with a different granularity has a different gap set and a different bound, and a design that hard-codes 4 while the interface is something else emits gaps the PHY cannot place.
Row six is the driver's and hardware can check part of it. A descriptor claiming 1 400 octets whose buffer chain supplies 1 200 produces a frame the assembler pads to 1 400 — with 200 octets of Section 3's pad value, which is at least visible. A descriptor claiming 1 200 whose chain supplies 1 400 truncates, which is not.
| Claim exceeds supply | Supply exceeds claim | |
|---|---|---|
| the assembler | pads the difference | truncates |
| visible? | yes — the pad pattern | no |
| the far end | a frame with a valid FCS and padding | a frame with a valid FCS and missing data |
Both produce a valid check value over the wrong contents, which is Chapter 18.7 §5's unprotected-path argument arriving at the transmit side: the FCS is computed over whatever the assembler emitted, so it certifies the mistake.
And the assembler's contribution is one comparison and it is worth having. A length check — the octets gathered against the length the descriptor claimed — catches both rows and costs a counter and a compare. Neither Chapter 18.4 nor this chapter's RTL includes it, which is a gap worth naming rather than glossing.
18. The Cost, Accounted
Eight blocks, and the assembler is the cheapest datapath block in Module 19.
| Block | Approximate cost | Dominated by |
|---|---|---|
pad_inserter | ~150 flops + 128 byte comparisons | the leak check |
crc_appender | ~250 flops + 2 byte-position decodes | the merge and spill |
txasm_ifg_enforcer | ~120 flops + 8 histogram counters | the histogram |
deficit_accumulator | ~90 flops | trivial, and the pipeline's floor |
commit_point | ~80 flops | the state machine |
txasm_datapath | ~560 flops | the beat register |
txasm_telemetry | ~400 flops | counters |
txasm_conformance_monitor | ~130 flops | comparators |
About 1 780 flops — against Chapter 19.2's 4 800 — and the difference is Section 16's: the assembler knows the offset it is placing a frame at, and the parser has to discover it.
| Chapter 19.2's parser | this assembler | |
|---|---|---|
| alignment structure | 1 024 byte-muxes, two stages | 64 byte-enables |
| tagged result store | ~2 400 flops | none — no cross-frame results |
| total | ~4 800 flops | ~1 780 |
Row two is the larger part of the difference and its absence has a reason. The parser produces per-frame results consumed later, so it needs Chapter 19.1 §6's tagged store. The assembler's outputs go on the wire in the cycle they are produced — there is nothing to store, because nothing consumes an assembler result out of band.
The one structure that could not be made smaller is Section 8's accumulator, and it is the smallest block in the table:
| Flops | Pipelinable? | |
|---|---|---|
| the deficit | ~90 | NO — a recurrence |
| everything else | ~1 690 | yes |
Ninety flops that cannot be pipelined against 1 690 that can, which is Section 16's argument in a cost table: a recurrence is the pipeline's floor and its width decides whether the floor is low enough.
And no memory. The FIFO is Chapter 19.5's and was counted in Chapter 18.1 §18 — 9 KiB at a 9 000-octet MTU — and this chapter adds zero bytes.
Module 19's running total, with two chapters built:
| Chapter | Logic | Memory |
|---|---|---|
| Chapter 19.2 — the parser | ~4 800 flops + 1 024 byte-muxes | none |
| this chapter — the assembler | ~1 780 flops | none |
| subtotal | ~6 580 flops | none |
| Chapter 19.1 §18's estimate, as first published | ~11 300 flops + 32 768 XOR terms | 41 KiB of FIFO |
Two chapters and 58% of that flop estimate, with the CRC engine, the FIFOs, the memory interface and the counters still to come — and Chapter 19.1 §18 placed the CRC's two matrices at 32 768 XOR terms separately from the flop count. Chapter 19.4 generated them and corrected it to 39 445, 20.4% higher, which is what §18 now carries.
19. Properties Worth Asserting, and One Worth Refusing
The assembler's properties split between design intent and standards conformance, and the rejected one is a conformance property written as though the standard said something it does not.
Padding.
// Every frame reaches 5.1's floor before the check value.
p_padded_to_floor:
assert property (@(posedge clk) disable iff (!rst_n)
(in_valid && in_eof) |-> (padded_length >= 16'(FRAME_MIN - FCS_B)))
else $error("a frame was emitted below the 60-octet pre-FCS floor");
// The pad is WRITTEN, never left.
p_pad_is_written:
assert property (@(posedge clk) disable iff (!rst_n)
!pad_leaked)
else $error("pad octets carried buffer contents");
// A frame at or above the floor is not padded.
p_no_needless_pad:
assert property (@(posedge clk) disable iff (!rst_n)
(in_valid && in_eof &&
(frame_bytes_so_far + {9'b0, in_bytes} >= 16'(FRAME_MIN - FCS_B)))
|-> (pad_needed == 16'd0))
else $error("a frame at or above the floor was padded");
// The padded length never exceeds the input plus the maximum pad.
p_pad_bounded:
assert property (@(posedge clk) disable iff (!rst_n)
(in_valid && in_eof) |-> (pad_needed <= 16'd46))
else $error("more than 46 octets of pad were inserted");Check-value append.
// End of frame is on the beat carrying the check value's last octet.
p_eof_on_last_fcs_octet:
assert property (@(posedge clk) disable iff (!rst_n)
!eof_before_fcs)
else $error("end of frame preceded the check value's last octet");
// A spill beat carries between one and three octets.
p_spill_bounded:
assert property (@(posedge clk) disable iff (!rst_n)
(spilling && out_ready) |->
((out_bytes >= $clog2(DP_BYTES+1)'(1)) &&
(out_bytes <= $clog2(DP_BYTES+1)'(3))))
else $error("a spill beat carried an impossible number of octets");
// A spill beat follows exactly one data beat.
p_one_spill_per_frame:
assert property (@(posedge clk) disable iff (!rst_n)
(spilling && out_ready) |=> !spilling)
else $error("two consecutive spill beats");
// The check value fits when the final beat has four or more octets free.
p_fits_when_room:
assert property (@(posedge clk) disable iff (!rst_n)
(in_valid && in_eof && (room >= 7'(FCS_B))) |-> out_eof)
else $error("a check value that fitted produced a spill");
// Nothing follows the end of a frame until the gap has elapsed.
p_nothing_after_eof:
assert property (@(posedge clk) disable iff (!rst_n)
out_eof |=> !out_valid)
else $error("a beat was emitted immediately after end of frame");The gap and the deficit.
// 5.9's floor. Nine octets, never fewer.
p_gap_at_least_nine:
assert property (@(posedge clk) disable iff (!rst_n)
gap_valid |-> (gap_octets >= 5'(IFG_MIN)))
else $error("a gap below nine octets was emitted");
// The gap never exceeds the nominal plus one lane.
p_gap_at_most_sixteen:
assert property (@(posedge clk) disable iff (!rst_n)
gap_valid |-> (gap_octets <= 5'(IFG_NOM + LANE_B)))
else $error("a gap exceeded the nominal plus one lane");
// Exactly two candidates, and the chosen one is a lane apart.
p_two_candidates:
assert property (@(posedge clk) disable iff (!rst_n)
gap_valid |-> ((gap_octets == short_gap) || (gap_octets == long_gap)))
else $error("a gap that was neither candidate");
// The deficit stays inside 5.9's bound.
p_deficit_bounded:
assert property (@(posedge clk) disable iff (!rst_n)
deficit <= 3'(DEF_MAX))
else $error("the deficit exceeded its bound");
// A short gap is only taken when the deficit can absorb it.
p_short_requires_headroom:
assert property (@(posedge clk) disable iff (!rst_n)
took_short |-> ((deficit + shortfall) <= 3'(DEF_MAX)))
else $error("a short gap was taken beyond the deficit bound");
// A long gap repays.
p_long_repays:
assert property (@(posedge clk) disable iff (!rst_n)
(gap_valid && !took_short) |=> (deficit <= $past(deficit)))
else $error("a long gap did not reduce the deficit");
// The deficit is cleared on a link event.
p_link_clears_deficit:
assert property (@(posedge clk) disable iff (!rst_n)
link_down |=> (deficit == 3'd0))
else $error("the deficit survived a link event");Commit.
// Store-and-forward commits only on a complete frame.
p_sf_commits_complete:
assert property (@(posedge clk) disable iff (!rst_n)
(may_start && cfg_store_and_forward) |-> frame_complete)
else $error("store-and-forward committed on an incomplete frame");
// Cut-through commits only when the threshold is met and safe.
p_ct_commits_safe:
assert property (@(posedge clk) disable iff (!rst_n)
(may_start && !frame_complete) |->
(threshold_met && !cut_through_refused))
else $error("cut-through committed below the threshold or while unsafe");
// Committed means no stall, from either side.
p_no_stall_after_commit:
assert property (@(posedge clk) disable iff (!rst_n)
committed |-> (phy_ready || frame_last_octet))
else $error("the PHY stalled a committed frame");
p_no_backpressure_after_commit:
assert property (@(posedge clk) disable iff (!rst_n)
!ready_after_commit)
else $error("the assembler refused its source after committing");
// Commit is entered once per frame.
p_one_commit_per_frame:
assert property (@(posedge clk) disable iff (!rst_n)
(state == A_COMMITTED) |=>
((state == A_COMMITTED) || (state == A_GAP)))
else $error("the commit state was left other than through the gap");
// An unsafe cut-through configuration is refused, not obeyed.
p_unsafe_refused:
assert property (@(posedge clk) disable iff (!rst_n)
(cfg_fill_threshold < cfg_required_lead) |->
(cut_through_refused || cfg_store_and_forward))
else $error("an unsafe cut-through threshold was accepted");Telemetry.
// Counters are monotonic.
p_counters_monotonic:
assert property (@(posedge clk) disable iff (!rst_n)
##1 ((c_frames >= $past(c_frames)) &&
(c_fcs_spans >= $past(c_fcs_spans))))
else $error("an assembler counter decreased");
// Spans never exceed frames.
p_spans_le_frames:
assert property (@(posedge clk) disable iff (!rst_n)
c_fcs_spans <= c_frames)
else $error("more spilling frames than frames");
// The minimum occupancy at commit only falls.
p_min_occ_monotonic:
assert property (@(posedge clk) disable iff (!rst_n)
##1 (min_occupancy_at_commit <= $past(min_occupancy_at_commit)))
else $error("the minimum occupancy at commit increased");
// The gap histogram covers the whole legal set and nothing else.
p_gap_hist_in_range:
assert property (@(posedge clk) disable iff (!rst_n)
gap_valid |-> ((gap_octets >= 5'd9) && (gap_octets <= 5'd16)))
else $error("a gap outside the legal set was counted");20. Verification Scenarios
Fifty-seven scenarios, plus a six-run directed test whose whole content is the frame size.
Padding — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 1 | a 46-octet payload | no pad; 60 pre-FCS |
| 2 | a 45-octet payload | 1 octet of pad |
| 3 | a 0-octet payload | 46 octets of pad |
| 4 | a 47-octet payload | no pad; 61 pre-FCS |
| 5 | the pad's contents | cfg_pad_value, every octet |
| 6 | an unmasked pad | pad_leaked |
| 7 | a 1 500-octet payload | no pad |
| 8 | a pad spanning into a second beat | not possible at 64 octets |
| 9 | cfg_pad_value = 0x5A | visible in a capture |
Check-value append — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 10 | pre-FCS length mod 64 = 0 | FCS in a fresh beat |
| 11 | mod 64 = 60 | fits exactly |
| 12 | mod 64 = 61 | 1 octet spills |
| 13 | mod 64 = 62 | 2 spill |
| 14 | mod 64 = 63 | 3 spill |
| 15 | eof on the data beat when spilling | eof_before_fcs |
| 16 | the octet order of the appended value | matches Chapter 6.2's convention |
| 17 | the octet order reversed | every receiver discards |
| 18 | two spill beats in a row | property fires |
| 19 | a beat after eof | property fires |
The gap and the deficit — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 20 | 64-octet frames | gap 12 always; deficit 0 |
| 21 | 1 517-octet frames | 11, 11, 11, 15 repeating |
| 22 | 1 518-octet frames | 10, 14 alternating |
| 23 | 1 519-octet frames | 9, 13, 13, 13 repeating |
| 24 | the mean over 10 000 frames | 12.000 in all four |
| 25 | a gap of 8 | gap_below_min |
| 26 | the deficit reaching 3 | the next gap is long |
| 27 | the deficit reaching 4 | deficit_out_of_bound |
| 28 | DIC disabled | the long gap always; mean above 12 |
| 29 | a link-down event | the deficit clears |
| 30 | 9 000-octet frames | gap 12; mod 4 is 0 |
| 31 | the histogram over mixed sizes | all eight buckets |
Commit — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 32 | store-and-forward, complete frame | commits |
| 33 | store-and-forward, incomplete | does not commit |
| 34 | cut-through, threshold met, lead adequate | commits early |
| 35 | cut-through, threshold below the lead | cut_through_refused; falls back |
| 36 | 100 Gb/s, 300 ns stall | lead 3 750 > 1 518; always refused |
| 37 | the PHY stalling after commit | stalled_after_commit |
| 38 | the source refused after commit | ready_after_commit |
| 39 | minimum occupancy at commit recorded | the margin |
| 40 | commit entered twice for one frame | property fires |
Datapath and timing — 8 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 41 | back-to-back frames sharing a beat | a merge, not a shift |
| 42 | the deficit update in one cycle | it must — a recurrence |
| 43 | a stall before commit | legal; counted |
| 44 | A_GAP on a 512-bit datapath | one cycle suffices |
| 45 | A_GAP on GMII | sixteen cycles needed |
| 46 | the gap's two candidates | a lane apart |
| 47 | a lane granularity of 8 | the gap set changes; the bound would be 7 |
| 48 | a frame abandoned mid-gather | no abort path — the known gap |
Verdicts — 9 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 49 | everything nominal | none_of_the_above |
| 50 | a pad leak | disclosure_fault, not assembler_ok |
| 51 | eof early | framing_fault |
| 52 | a mean of 12.4 | gap_nonconformant |
| 53 | a mean of 12.0 with gaps of 9 | conformant |
| 54 | commit below the required lead | commit_margin_thin |
| 55 | a million frames, no spill | spill_path_untested |
| 56 | an unsafe cut-through threshold | cut_through_misconfigured |
| 57 | the gap histogram, one bucket | all frames lane-aligned |
The directed test — six runs random stimulus will not produce.
Both of this chapter's structural paths are selected by the frame's length modulo a small number, and a random size distribution hits them in proportion rather than deliberately.
| Path | Selected by | Fraction of sizes |
|---|---|---|
| the check value spilling | pre-FCS length mod 64 ∈ 63 | 4.74% |
| a short gap | frame length mod 4 ≠ 0 | 75% |
| the deficit reaching its bound | mod 4 = 3, four frames running | a sequence, not a size |
Row three is the one a random generator will not produce, because it needs the same size repeated — a run of 1 519-octet frames — and a generator randomising the size breaks the sequence before the deficit fills.
Construct it. Six runs, one variable: the frame size.
| Run | Frame size | mod 4 | Expected gaps | Deficit | Spill? |
|---|---|---|---|---|---|
| A | 64, repeated | 0 | 12, 12, 12, … | never rises | no |
| B | 1 517, repeated | 1 | 11, 11, 11, 15 | 0→1→2→3→0 | no |
| C | 1 518, repeated | 2 | 10, 14, 10, 14 | 0→2→0→2 | no |
| D | 1 519, repeated | 3 | 9, 13, 13, 13 | 0→3→0 | no |
| E | 1 475, repeated | 3 | 9, 13, 13, 13 | 0→3→0 | YES — 1471 mod 64 = 63 |
| F | random 64…1518 | mixed | all eight | rarely fills | 4.7% of frames |
Run A is the obvious stress test and it exercises nothing: the gap is always 12, the deficit never rises, the check value never spills. A design with a broken deficit accumulator and a broken spill path passes run A completely.
Run D is the deficit's worst case. A shortfall of 3 per frame with a bound of 3 means every second frame must take the long gap, so the mechanism is exercised at its limit on every pair. And run B is the slowest cycle — a shortfall of 1 takes three frames to fill the bound, so the repayment happens once in four.
Run E is the only run that exercises both paths at once, and its size had to be searched for: 1 475 octets on the wire is 1 471 pre-FCS, and 1 471 mod 64 is 63 — so the check value spills three octets — while 1 475 mod 4 is 3, so the gap is 9 and the deficit fills.
And there are only twenty-three such sizes between 64 and 1 518 — the ones congruent to 3 modulo 64 — which is 1.6% of the range, and is why the combination has to be constructed rather than stumbled upon.
The oracle, in four parts:
| Check | Runs A–F | What A alone shows |
|---|---|---|
| mean gap over the run | 12.000 in every run | 12.000 — but trivially |
gap_below_min | never | never — no short gaps at all |
deficit_out_of_bound | never | never — the deficit never moves |
c_fcs_spans | non-zero in E and F | ZERO |
Row four is the coverage finding and row three is the correctness one. Run A's deficit never leaves zero, so an accumulator that fails to saturate, fails to repay, or fails to clear on a link event is untested — and the design passes.
And row one is the property that matters and it is satisfied by every run including the broken ones, which is why the per-run mean is a necessary check and not a sufficient one: a design that always takes the long gap has a mean of 12 plus the excess, and one that always takes the short gap has a mean below 12 — but a design that never faces the choice has a mean of exactly 12 while proving nothing.
21. Debugging an Assembler
Three complaints, and one of them is a property failure rather than a design failure.
Complaint 1 — "the far end reports interframe-gap violations."
| Check | If yes | Meaning |
|---|---|---|
mean_gap_x100 at 1200? | conformant | the partner's check is wrong |
gap_below_min ever? | a gap below nine | ours |
deficit_out_of_bound ever? | borrowing without repaying | ours |
is the partner asserting gap >= 12? | Section 19's class 83 | theirs |
Row four is the usual answer and it is worth having the evidence for. Chapter 5.9 §7 established that a receiver cannot conclude much from a single measured gap; a transmitter reporting a mean of exactly 12.000 with no gap below nine is conformant, and a partner complaining about gaps of 10 is measuring a legal sequence.
Complaint 2 — "one frame in twenty is discarded at the far end."
| Check | If yes | Meaning |
|---|---|---|
| does it correlate with frame size? | the spill path | Section 4 |
eof_before_fcs ever? | eof marked early | the far end misframes |
c_fcs_spans non-zero? | the path is being used | and may be wrong |
| is the check value's octet order right? | Chapter 6.2's convention | a reversal fails 100%, not 5% |
Row one's correlation is the tell and the fraction is the confirmation. The check value spans a beat on 4.74% of frame sizes — one in twenty-one — so a discard rate near 5% that tracks the size distribution is the spill path, and a rate of 100% is row four.
Complaint 3 — "transmit underruns at high rates only."
| Check | If yes | Meaning |
|---|---|---|
c_cut_through non-zero at 100 Gb/s? | cut-through engaged | it should be refused — Section 11 |
cfg_required_lead realistic? | the integrator's figure | optimistic if cut-through engaged |
min_occupancy_at_commit small? | committing with no margin | commit_margin_thin |
| does forcing store-and-forward fix it? | confirms | and costs 121 ns |
Row four is the test and the cost is worth quoting when proposing it. Store-and-forward at 100 Gb/s adds 121.44 ns to a maximum-size frame — Chapter 18.4 §9 — which against Chapter 17.1's per-hop budget is negligible, and it removes the underrun entirely.
Complaint 4 — "the first frame after a link flap is late, and only the first."
| Check | If yes | Meaning |
|---|---|---|
| is the first gap 13, 14 or 15? | a stale deficit | the reset missed the accumulator |
| does it clear on the second frame? | confirms | one repayment, then normal |
deficit_out_of_bound at reset? | the accumulator holds 1 to 3 | through a reset |
| is the reset synchronous to the transmit clock? | Chapter 19.1 §13 | or it is a domain bug, not this one |
Row one is the whole diagnosis and the fix is one line, but the reasoning is worth stating: the mean is a property of a continuous transmission, so a deficit carried across a link-down event repays a debt to a link that no longer exists. The cost is three octets of delay on one frame — negligible — which is exactly why it survives to production. Row four matters because Chapter 19.1 §13's reset sequencer releases the transmit domain on its own clock, and an accumulator reset from the wrong domain reads as this bug intermittently.
And the two symptoms this chapter is systematically blamed for:
| Symptom | Blamed on | Usually is |
|---|---|---|
| gap violations reported by a partner | our transmitter | the partner asserting an instance bound on an average |
| a 5% discard rate at the far end | the link | eof marked before the check value's last octet |
| a 3-octet hiccup after every link flap | the PHY's autonegotiation | a deficit that survived the reset |
| short frames arriving with plausible-looking garbage | the peer's stack | a pad counted but never written |
22. Misconceptions
Misconception 1 — "the interframe gap is twelve octets."
The wrong model: the standard specifies twelve; emit twelve.
What it costs: frames placed off-lane on 75% of frame sizes, because the next frame must begin on a lane boundary and twelve only reaches one when the frame's length is a multiple of four. The PHY then misaligns the frame or silently stretches the gap.
The corrected model: Chapter 5.9 §5 specifies an average of twelve with a floor of nine and a deficit bounded at 0 to 3. The gap set is 12 with 16 for repayment, and which one this frame takes is decided by its length mod 4 and the deficit. Section 6.
Misconception 2 — "padding means leaving the extra octets alone."
The wrong model: the frame is short, so send it and let the receiver see 60 octets.
What it costs: up to 45 octets of whatever followed the payload in the host buffer, on every frame below the floor — and control-plane traffic, acknowledgements and ARP are all below it. The frame is well formed and its check value is valid, so nothing downstream notices.
The corrected model: the pad must be written, using the byte-enables Chapter 19.1 §3's partial final beat already needs. A distinctive pad value makes a leak visible in a capture; a zero pad makes it invisible. Sections 2, 3.
Misconception 3 — "the check value always fits in the last beat."
The wrong model: append four octets to the final beat.
What it costs: a frame whose final beat already holds 61, 62 or 63 octets needs one more beat, and a design that does not emit it truncates the check value. That is 4.74% of frame sizes — one in twenty-one.
The corrected model: the append may spill, and eof belongs on the beat carrying the check value's last octet. A design that marks eof on the payload's final beat and then emits a spill has put a beat after the end of a frame, which downstream reads as the start of the next. Sections 4, 5.
Misconception 4 — "the assembler is fast, so latency does not matter."
The wrong model: padding and appending are combinational; the block is nearly free.
What it costs: nothing, and that is the point worth internalising — the constraint is not latency but the commit point. Chapter 19.1 §5: a transmit stage may stall before commit and not after, so everything upstream of commit has twenty-four beats of slack on a maximum-size frame and everything downstream has none.
The corrected model: place the commit point deliberately. At 100 Gb/s with any realistic memory latency it is at the end of the frame — Chapter 18.4 §9's arithmetic — so the assembler is store-and-forward whether it is configured that way or not. Sections 9, 11.
Misconception 5 — "the deficit is bookkeeping; pipeline it like everything else."
The wrong model: the deficit update is one of several per-frame operations and can be deepened if timing demands.
What it costs: a design that cannot close timing and cannot be fixed by adding stages, because frame n+1's gap depends on the deficit after frame n — a loop-carried dependency, and a pipeline cannot hide a recurrence.
The corrected model: the accumulator is the pipeline's floor. Everything else in the assembler is combinational or pipelinable; the deficit must complete in one cycle, and it does because it is a 3-bit add, a compare and a mux. Three bits is reachable at 195 MHz; thirty-two would not be. Sections 8, 16.
Misconception 6 — "assert that the gap is at least twelve."
The wrong model: the standard says twelve; assert it.
What it costs: a property that fails on legal traffic — every other frame on a 1 518-octet stream — and a team that responds by "fixing" the design to always emit twelve has broken conformance to satisfy an incorrect check.
The corrected model: three properties. The instance floor the standard does specify (nine), the accumulator bound that makes the mean hold (3), and the mean itself over a run. The middle one is what turns a sequence property into an instance property, which is Chapter 5.9 §11's general pattern arriving in verification. Section 19.
23. Interview Questions
Question 1 — "A frame is 46 octets of payload short of the minimum. What does the assembler do, and what is the cost of doing it the obvious way?"
What the answer should establish: the pad goes in the payload, below the check value, so the check value covers it; the pad must be written, not merely counted; and the byte-enables that write it are the ones Chapter 19.1 §3's partial final beat already provides. The cost of the obvious way — extending the length field and leaving the octets alone — is a leak of up to 45 octets of adjacent host memory on every short frame, invisible downstream because the frame is well formed. A strong answer adds that short frames are disproportionately control traffic, so the leak follows the acknowledgements.
Question 2 — "Where does the four-octet check value go when the final beat is already 62 octets full?"
What the answer should establish: into a twenty-fifth beat, and eof moves with it. The number to have is 4.74% — 69 of the 1 455 legal pre-FCS lengths — and the failure mode to name is eof on the payload's last beat with a spill beat after it, which the next block reads as the start of a frame. A strong answer notices that the spill beat's byte-enables are 1, 2 or 3 octets wide, so the path exercises the same partial-beat machinery as a short frame's tail.
Question 3 — "The interframe gap is twelve octets. Your datapath is 512 bits. How many beats is the gap?"
What the answer should establish: 0.3125 — twelve octets in a sixty-four-octet beat — which means the gap is not a number of beats at all; it is a position within a beat, and a design that rounds it to one beat is 3.2× over the standard and has thrown away 2.44% of the line. A strong answer goes to the real mechanism: the gap is an average of twelve, and the instance is whichever of 12 lands the next frame on a lane boundary, with the shortfall carried in a deficit bounded at 3.
Question 4 — "Prove that the deficit accumulator cannot grow without bound."
What the answer should establish: the deficit takes the value (4 − L mod 4) mod 4's complement — it is exactly the shortfall this frame's length forced — so it is in 3 by construction, not by an argument about traffic. Each frame sets it; no frame adds to it. A strong answer states the consequence for verification: the bound is an invariant, so it is assertable per frame, and it is what turns Chapter 5.9's sequence-level average into something a simulation can check on frame one.
Question 5 — "Your assembler runs at 195 MHz and misses timing by 400 ps on one path. Which path is it, and why can you not pipeline it?"
What the answer should establish: the deficit update, because frame n+1's gap depends on the deficit after frame n — a loop-carried dependency, and pipelining cannot hide a recurrence. A strong answer says what can be done instead: precompute all four next-deficit values and select, since the recurrence is a 3-bit state; and observes that the reason it closes at all is that three bits is small — the same structure on a 32-bit accumulator would not.
Question 6 — "At 100 Gb/s, should this block be cut-through or store-and-forward?"
What the answer should establish: store-and-forward, and not as a preference. Chapter 18.4 §9 derived the feasibility limit: with a 300 ns memory stall, cut-through transmit survives to 40.48 Gb/s on a maximum-size frame and 1.707 Gb/s on a minimum-size one. At 100 Gb/s it is infeasible at every frame size, so the commit point sits at the end of the frame. A strong answer quotes the cost of the decision — 121.44 ns for a 1 518-octet frame — and puts it against a per-hop budget to show it is affordable, which is the argument that actually settles the question.
24. Questions and Answers
25. What's Next
The assembler hands two things downstream and takes one from upstream, and each is a chapter.
The check value it appends is not computed here. Section 5 treated crc_appender as a consumer of a running remainder and said nothing about how the remainder is produced at 512 bits per cycle, or what happens when a beat holds the end of one frame and the start of the next — Chapter 19.1 §3's dual-frame case, which the check engine sees as two independent accumulations in one beat. Chapter 19.4 builds that engine, integrates Chapter 6.4's matrix formulation, and prices Chapter 17.3's mCRC against it.
The frame data it consumes comes from a buffer this chapter treated as a fill level. Section 11's commit point needs an occupancy, and Section 9's threshold came from Chapter 18.4 §7 without asking how deep the buffer has to be to hold it — a question that turns out to be decided by the 100 ppm clock tolerance rather than by any latency. Chapter 19.5 sizes it, and finds the elastic-buffer depth and the almost-full threshold come from different arguments entirely.
And the parser's mirror is already live. Chapter 19.2 took the same datapath in the other direction, where the alignment this chapter chooses is instead imposed — a useful contrast to hold, because it is the difference between a barrel shifter and a deficit accumulator.
Continue learning
Related tutorials
- Related topic
Integrating the CRC Engine
The 512-bit CRC matrix is 8 512 XOR terms rather than the dense bound's 16 384, and a beat that holds two frames needs two accumulators — which linearity caps at two, not three.
- 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
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.
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.
