Ethernet · Module 20
Packet Generation
A weight is a per-frame marginal, so it reaches a frame's own properties and nothing else — and 48 of the parser's 64 alignment offsets are unreachable from the transmit side at any weight.
Module 19 built a MAC and, in doing so, left five lists of cases that its own chapters proved random stimulus does not reach. This chapter builds the generator, and the first thing it has to establish is that the five lists are not one problem.
| The case | Chapter | Reached by a weight? |
|---|---|---|
| 64 partial-word residue classes | Chapter 19.4 §4 | yes — 303.6 frames uniform |
| 23 sizes that spill AND fill the deficit | Chapter 19.3 §20 | yes — and one weight buys 31.6× |
| runts under 16 octets | Chapter 19.7 §7 | yes, once illegal sizes are enabled |
| a specific dual-frame beat offset | Chapter 19.4 §7 | PARTLY — 16 of 64, and only 16 |
| four simultaneous read reorderings | Chapter 19.6 §21 | NO — at any weight |
Rows four and five are the chapter. A constrained-random weight is a marginal distribution over one frame's own properties, and two of the five cases are not properties of a frame at all: one is a relationship between a frame and everything that came before it, and one belongs to a different agent entirely.
No amount of weighting reaches them, and the useful question is not how to weight harder but what kind of thing each case is.
1. Scope, and Five Lists Module 19 Left Behind
Chapter 19.1 through Chapter 19.7 each ended by naming a case its own verification would not produce. This chapter's job is to produce all of them, and its finding is that they need three different mechanisms.
What this chapter owns: the frame item and its constraints; the size distribution and the weights that reach the marginal cases; the phase tracker that makes the joint cases addressable; the injection point that decides which alignment offsets exist at all; the interconnect stressor for the cases that are not about frames; and the directed sequences for what none of the above reaches.
What it does not own: the checking — Chapter 20.3's scoreboards — the assertions — Chapter 20.2 — or the coverage model that says whether any of this worked, which is Chapter 20.4. This chapter produces stimulus and measures only whether it produced what it intended.
And it does not own the design. Every number here is quoted from Module 19 and re-derived; the generator is correct if it reaches the cases those chapters identified, and nothing in it depends on the MAC being right.
One boundary is worth stating early, because it changes the architecture. A generator can be attached at two places:
| Injection point | Drives | Controls |
|---|---|---|
| the transmit descriptor ring | Chapter 18.4's DMA | frame contents; NOT the wire alignment |
| the receive wire | Chapter 19.2's parser directly | everything, including alignment |
Row one cannot produce 48 of the parser's 64 alignment offsets and Section 9 is why. Chapter 19.3 §6's gap mechanism lane-aligns every transmitted frame, so a generator that drives transmit and loops the wire back sees only start offsets that are multiples of four. That is a property of the design under test, not of the generator, and it is the clearest example in this chapter of a case that a weight cannot reach because the stimulus path removes it.
2. The Three Classes of Reachability
A constrained-random generator produces one item at a time from a distribution over that item's fields. That sentence contains the whole taxonomy.
| Class | The case is a property of | A weight can | Example |
|---|---|---|---|
| A — marginal | one frame, by itself | target it directly | size mod 64 |
| B — joint | a frame AND its predecessors | shift a distribution, not hit a target | the beat phase |
| C — foreign | a different agent | nothing at all | interconnect reordering |
Class A is what constrained random was built for and it is most of the work. A frame's length, its address type, its VLAN tags, its EtherType, whether its FCS is corrupted — all are fields of the item, all are constrainable, and a weight moves the distribution wherever it is wanted. Section 4.
Class B is where generators quietly fail. The beat phase — which octet of a 64-octet beat a frame starts on — is a function of every preceding frame's length and gap, accumulated modulo 64. A weight on this frame's length shifts the phase distribution and cannot select a phase, because the phase was decided before this item was randomised. Section 7.
Class C is where generators fail loudly and then get blamed. Chapter 19.6 §21's four-simultaneous reordering is a property of the interconnect model: it happens when four of the eight outstanding read responses arrive before the one the design is waiting for. Nothing about the frames decides that. Section 11.
Put numbers on the three and the difference stops being philosophical.
| Class A | Class B | Class C | |
|---|---|---|---|
| example target | all 23 spill sizes | a start offset of 37 | 4 responses buffered |
| uniform random | 5 433 frames | never — see Section 9 | 1 in 5 166, at q = 0.05 |
| with the best weight | 172 frames | unchanged | unchanged |
| what does reach it | the weight | a different injection point | a knob on another agent |
Row three is the finding. A weight buys 31.6× on the class A case and nothing on the other two — and a verification plan that responds to poor coverage by tuning weights will converge on the class A numbers and stall on the others, which is the shape of a coverage closure that plateaus at 90% and stays there.
And row four is the chapter's structure. Three classes, three mechanisms, and Sections 5, 10 and 12 build one each.
3. RTL 1 — The Frame Item
// ---------------------------------------------------------------------
// pktgen_pkg -- the frame item and the constraints that reach Module
// 19's class A cases. Sections 3 through 6.
//
// Every field here is a property of ONE frame, which is exactly the set
// a weight can target. The fields that are NOT here -- the beat phase,
// the interconnect's reordering -- are Sections 7 and 11, and they are
// absent from the item because they are not properties of it.
// ---------------------------------------------------------------------
package pktgen_pkg;
localparam int DATA_B = 64;
localparam int MIN_FRAME = 64;
localparam int MAX_FRAME = 1518;
localparam int JUMBO_MAX = 9000;
localparam int FCS_OCTETS = 4;
typedef enum logic [1:0] {
ADDR_UNICAST = 2'd0, ADDR_MULTICAST = 2'd1, ADDR_BROADCAST = 2'd2
} addr_kind_e;
typedef enum logic [1:0] {
ERR_NONE = 2'd0, ERR_FCS = 2'd1, ERR_ALIGN = 2'd2, ERR_TRUNCATE = 2'd3
} err_kind_e;
class eth_frame;
rand int unsigned length; // octets on the wire, excluding preamble
rand int unsigned tags; // 0, 1 or 2 -- Chapter 13.2
rand addr_kind_e addr_kind;
rand err_kind_e err_kind;
rand bit allow_illegal; // runts and giants -- Chapter 19.7
// Knobs. A weight is a per-frame marginal and these are all of them.
int unsigned w_min = 10; // exactly MIN_FRAME
int unsigned w_max = 10; // exactly MAX_FRAME
int unsigned w_spill = 50; // Chapter 19.3 Section 20's 23 sizes
int unsigned w_uniform = 25; // anything legal
int unsigned w_runt = 5; // Chapter 19.7 Section 7
int unsigned cfg_mtu = MAX_FRAME;
// Chapter 19.3 Section 20: a size whose pre-FCS length is 63 modulo 64
// spills the check value across a beat AND whose wire length is 3
// modulo 4 fills the deficit. Both conditions reduce to
// length == 3 (mod 64), and there are 23 such sizes between 64 and
// 1518 -- 1.6% of the range. Section 6 prices the weight.
constraint c_spill_set {
solve err_kind before length;
(w_spill > 0) -> soft (length % 64 == 3);
}
constraint c_legal {
allow_illegal == 0 -> length inside {[MIN_FRAME:cfg_mtu]};
allow_illegal == 1 -> length inside {[5:JUMBO_MAX]};
}
// Chapter 13.2's tags move every field after them, which is what
// Chapter 19.2 Section 4's offsets depend on. Tagging is a marginal
// property and therefore weightable.
constraint c_tags { tags inside {[0:2]}; }
constraint c_room_for_tags {
length >= MIN_FRAME + 4 * tags || allow_illegal == 1;
}
// The distribution. This is the whole of what a weight can express:
// a marginal over this frame's own length.
constraint c_distribution {
length dist {
MIN_FRAME := w_min,
cfg_mtu := w_max,
[MIN_FRAME+1 : cfg_mtu-1] :/ w_uniform,
[5 : MIN_FRAME-1] :/ w_runt
};
}
function string describe();
return $sformatf("len=%0d tags=%0d addr=%s err=%s resid=%0d",
length, tags, addr_kind.name(), err_kind.name(),
(length - FCS_OCTETS) % 64);
endfunction
endclass
endpackageClassification: the stimulus item, and the boundary of what constrained random can address.
What it teaches: that the item's field list is the reachability taxonomy. Every class A case from Section 2 is a field here; no class B or class C case is, and that is not an omission — the beat phase is not a property of a frame and cannot be constrained on one. A generator whose item carries a start_offset field has lied about where the offset comes from, and the constraint solver will happily solve for a value the injection path then discards.
And it teaches that c_spill_set collapses two conditions into one. Chapter 19.3 §20 needed a size whose pre-FCS length is 63 mod 64 and whose wire length is 3 mod 4. Both reduce to length ≡ 3 (mod 64), which is a single constraint over 23 values — and writing it as two constraints produces the same solutions more slowly and reads as if there were two independent conditions.
Deliberately simplified: c_spill_set uses soft, so it yields to c_distribution whenever the distribution's weights are incompatible — which is what makes the two coexist and is also how a mis-set w_spill silently stops reaching the 23 sizes. allow_illegal gates runts through the length range rather than through a separate constraint block, so a run with it clear never generates a frame under 64 and the Chapter 19.7 §7 cases are absent without any warning. And the payload contents are not modelled at all, which matters for Chapter 19.2's parser and is Chapter 20.3's problem.
Production implication: describe() prints the residue, and that is the field a triage engineer actually needs. A failure log that says len=1475 requires somebody to compute (1475 − 4) mod 64 = 63 before the failure means anything; a log that says resid=63 names Chapter 19.4 §4's spill path directly. The cheapest thing a generator can do for debugging is print the derived quantity the design branches on, not the quantity the generator randomised.
4. Marginal Cases: What a Weight Can Do
Class A is the easy class and it is worth measuring rather than assuming, because the measurements are the argument for the other two sections.
Chapter 19.4 §4's sixty-four residue classes, under a uniform length distribution:
| Value | |
|---|---|
| classes | 64 |
| legal pre-FCS lengths | 1 455 |
| each class's share | about 1/64 |
| coupon collection, expected | 64 × H(64) = 303.6 frames |
Three hundred frames, which is nothing — and it is why Chapter 19.4 §21's scenario 52 says a 1 000-frame random run "usually" covers all 64. The word doing the work there is "usually", and it is the difference between a distribution and a target.
Chapter 19.3 §20's twenty-three sizes are harder by a factor of eighteen.
| Uniform | With w_spill at 50% | |
|---|---|---|
| target sizes | 23 | 23 |
| probability per frame | 23 / 1 455 = 1.58% | 50% |
| coupon collection | 5 433 frames | 172 frames |
| speedup | — | 31.6× |
Row four is what a weight buys, and 31.6× is the whole of it. It is a real saving and it is bounded: the weight cannot do better than putting all the probability on the target set, at which point the cost is coupon collection over 23 items and nothing further is available.
Chapter 19.7 §7's runts are a knob rather than a weight.
allow_illegal = 0 | allow_illegal = 1, w_runt = 5% | |
|---|---|---|
| sizes below 64 | none, ever | 43 of them |
| frames to cover all 43 | infinite | 3 741 |
| what changed | — | a boolean, not a weight |
Row three is the distinction that matters for a verification plan. The runt cases are class A — a runt is a property of one frame — but they are behind a gate that a weight cannot open, because the legal-length constraint excludes them entirely. A coverage report showing zero runt coverage is not a weighting problem and no amount of weight tuning moves it, which is the same symptom as a class B or C failure and a completely different cause.
Which gives the first of this chapter's three diagnostic questions.
If coverage of a case is zero rather than low, ask whether a constraint excludes it before asking about weights.
5. RTL 2 — The Size Distribution
// ---------------------------------------------------------------------
// size_distribution -- the weights, made observable. Sections 4, 5 and 6.
//
// A weight that is set and never measured is a hope. This block counts
// what the generator actually produced against what the weights asked
// for, because the most common generator failure is a soft constraint
// that lost to a hard one and a distribution that silently collapsed.
// ---------------------------------------------------------------------
module size_distribution
import pktgen_pkg::*;
#(
parameter int TARGET_SPILL_PCT = 50,
parameter int TOLERANCE_PCT = 5
) (
input logic clk,
input logic rst_n,
input logic frame_valid,
input logic [15:0] frame_length,
input logic allow_illegal,
output logic [31:0] c_frames,
output logic [31:0] c_spill_sizes,
output logic [31:0] c_min,
output logic [31:0] c_max,
output logic [31:0] c_runts,
output logic [63:0] residues_seen,
output logic [6:0] residues_count,
output logic [15:0] spill_pct_x10,
output logic distribution_collapsed,
output logic residues_incomplete
);
logic [5:0] resid;
int unsigned popcnt;
// Chapter 19.4 Section 4's class: the pre-FCS length modulo 64.
assign resid = 6'((frame_length - 16'(FCS_OCTETS)) % 16'd64);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_frames <= '0; c_spill_sizes <= '0; c_min <= '0; c_max <= '0;
c_runts <= '0; residues_seen <= '0;
end else if (frame_valid) begin
c_frames <= c_frames + 1;
residues_seen[resid] <= 1'b1;
// Chapter 19.3 Section 20's set: length congruent to 3 modulo 64.
// 23 sizes between 64 and 1518.
if ((frame_length % 16'd64) == 16'd3) c_spill_sizes <= c_spill_sizes + 1;
if (frame_length == 16'(MIN_FRAME)) c_min <= c_min + 1;
if (frame_length == 16'(MAX_FRAME)) c_max <= c_max + 1;
if (frame_length < 16'(MIN_FRAME)) c_runts <= c_runts + 1;
end
end
assign spill_pct_x10 = (c_frames == 0) ? 16'd0
: 16'((c_spill_sizes * 32'd1000) / c_frames);
// THE check this block exists for. A soft constraint that lost leaves
// the weight set and the distribution uniform, and the only evidence
// is that the realised share is 1.6% instead of 50%.
assign distribution_collapsed =
(c_frames > 32'd1000) &&
(spill_pct_x10 < 16'((TARGET_SPILL_PCT - TOLERANCE_PCT) * 10));
always_comb begin
popcnt = 0;
for (int i = 0; i < 64; i++) if (residues_seen[i]) popcnt++;
residues_count = 7'(popcnt);
end
assign residues_incomplete = (c_frames > 32'd2000) && (residues_count < 7'd64);
endmoduleClassification: a stimulus monitor, and the only block in this chapter that checks the generator rather than the design.
What it teaches: that distribution_collapsed catches the most common generator bug and nothing else does. A soft constraint that conflicts with a hard one is silently dropped by the solver — no error, no warning, no $display — and the generator continues producing a uniform distribution while w_spill sits at 50 in a configuration file. The realised share is 1.6% instead of 50%, and the only way to know is to count.
And it teaches that measuring the realised distribution is cheap and almost never done. Five counters and a division. A verification environment that tunes weights without measuring them is tuning an input to a system whose output it is not observing, which is a description nobody would accept about the design under test and is routine about the testbench.
Deliberately simplified: resid computes a modulo by 64 as a bit-slice, which is correct because 64 is a power of two and would not generalise to a datapath of 48 octets. distribution_collapsed tests only the spill weight, where a complete version checks every weight against its target with a per-weight tolerance. And the block is a module observing a stream of lengths, so it works equally on generated stimulus and on a capture from a real link — which is what makes it useful for Chapter 19.7 §15's traffic profiling as well.
Production implication: residues_count is the number to put on the regression's summary line, not in a coverage database. Chapter 19.4 §16's residue_classes_untested is the design-side verdict; this is the stimulus-side one, and having both distinguishes "the generator did not produce it" from "the design did not exercise it." Those are different bugs with the same coverage hole, and separating them is worth two counters in two different blocks.
6. The 31.6× a Single Weight Buys, and Its Ceiling
Section 4 measured the speedup. This section is what it costs elsewhere, because a weight is a redistribution and the probability it moves comes from somewhere.
Setting w_spill to 50% puts half the frames on 23 sizes. The other half covers the remaining 1 432.
| Uniform | w_spill = 50% | |
|---|---|---|
| the 23 spill sizes, each | 0.069% | 2.17% |
| the other 1 432, each | 0.069% | 0.035% |
| coupon collection over the 23 | 5 433 frames | 172 frames |
| coupon collection over the 1 432 | 11 233 frames | 22 466 frames |
Row four is the cost and it is exactly a factor of two. The frames that were reaching the general population are now reaching the target set, so general size coverage takes twice as long. That is usually the right trade — the 1 432 ordinary sizes are not individually interesting — but it is a trade, and a plan that sets six weights to 50% has halved the general population six times.
And the ceiling is worth stating because it bounds what weight tuning can ever achieve.
w_spill | Share on the 23 | Frames to cover them |
|---|---|---|
| 0% | 1.58% | 5 433 |
| 50% | 50% | 172 |
| 90% | 90% | 95 |
| 100% | 100% | 86 |
From 50% to 100% the saving is 86 frames — half a per cent of the uniform figure — and the cost is that no other size is generated at all. So the useful range of the weight is essentially its first step, and a team that spends a week tuning weights from 50 to 90 has bought 77 frames of simulation.
Which gives this chapter's second diagnostic question.
Before tuning a weight, compute the coupon-collection bound at 100%. If the current number is close to it, the weight is finished.
And the reason the question matters is that class B and class C cases look exactly like an untuned weight. A coverage hole that will not close has three possible causes — an excluding constraint, a weight that is not at its ceiling, and a case that is not a marginal at all — and only the second responds to tuning. Sections 7 and 11 are the other two.
7. Joint Cases: The Beat Phase and the Deficit
Class B is where a generator's model of the world stops matching the design's, and the beat phase is the clearest example because it is an accumulator.
Chapter 19.2 §3 established that a frame's header can begin at any of 64 offsets within a beat, and that the offset decides how many beats the parse spans. The offset is not random and it is not a property of the frame:
offset(n+1) = (offset(n) + preamble + length(n) + gap(n)) mod 64It is a running sum modulo 64, and every term in it belongs to a previous frame.
| Decided by | When | |
|---|---|---|
length(n) | the generator | when frame n was randomised |
gap(n) | Chapter 19.3 §6's deficit | after frame n's length is known |
offset(n+1) | the accumulation | before frame n+1 is randomised |
Row three is the problem in one line: the offset of the frame being generated was decided before the generator was asked for it. A constraint on offset in the item class has nothing to solve for — the value is already determined by the stream — and a solver given such a constraint either ignores it or produces an item the injection path cannot honour.
What a weight can do is shift the distribution, and it is worth seeing how weakly.
| Length distribution | Offset distribution |
|---|---|
| all frames 64 octets, gap 12 | period 84; offsets cycle through 16 values |
| all frames 65 octets, gap 11 | period 84 again — the deficit compensates |
| uniform lengths | approximately uniform over the reachable offsets |
| any distribution | never selects a single offset |
Row two is the part that surprises. Chapter 19.3 §6's deficit chooses the gap to land the next frame on a four-octet lane boundary, so changing the length by one changes the gap by one in the opposite direction and the period is unchanged. The mechanism that makes the interframe gap conformant also makes the offset insensitive to the generator's length choices, which is the least convenient possible interaction.
And the deficit itself is the same shape. Chapter 19.3 §8's accumulator holds 0 to 3 and its value after frame n is a function of every preceding length. A test that needs the deficit at 3 needs a run of frames whose lengths are 3 modulo 4 — Chapter 19.3 §20's run D — which is a sequence, not a distribution.
| Target | Class | Reached by |
|---|---|---|
| a size ≡ 3 mod 64 | A | a weight — Section 4 |
| the deficit at 3 | B | four consecutive such sizes — a sequence |
| a start offset of 37 | B, and unreachable | Section 9 |
Row two is the general answer for class B: a sequence, not a weight. A constrained-random generator can emit a scripted run as easily as a random one, and the machinery for it already exists — a sequence that sets length deterministically for four frames and then returns control to the random stream. What it cannot do is arrive at the state by weighting.
Which gives the third diagnostic question.
If a case depends on a running quantity, a weight shifts its distribution and a sequence sets its value. Ask which one the coverage bin needs.
8. RTL 3 — The Phase Tracker
// ---------------------------------------------------------------------
// phase_tracker -- compute the beat phase the generator cannot control,
// so a sequence can steer toward it and coverage can record it.
// Sections 7, 8 and 9.
//
// The phase is a running sum modulo 64 over every preceding frame's
// length and gap. The generator cannot constrain it; what it CAN do is
// observe it and choose the next frame's length to move it -- which
// turns an unreachable target into a two-step search.
//
// This block is the observation half. Section 10's injector is the
// steering half.
// ---------------------------------------------------------------------
module phase_tracker
import pktgen_pkg::*;
(
input logic clk,
input logic rst_n,
input logic frame_start,
input logic [15:0] frame_length,
input logic [4:0] gap_octets,
output logic [5:0] phase, // 0..63, this frame's start
output logic [5:0] next_phase, // where the next one lands
output logic [63:0] phases_seen,
output logic [6:0] phases_count,
// Observability. Sections 15 and 16.
output logic [31:0] c_frames,
output logic phase_is_lane_aligned,
output logic unaligned_phase_seen
);
logic [5:0] phase_q;
// preamble and SFD are 8 octets and are part of the wire period, so
// they enter the accumulation. Chapter 5.2.
localparam int PREAMBLE_OCTETS = 8;
assign next_phase = 6'((32'(phase_q) + 32'(PREAMBLE_OCTETS) +
32'(frame_length) + 32'(gap_octets)) % 32'd64);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= '0; phases_seen <= '0; c_frames <= '0;
unaligned_phase_seen <= 1'b0;
end else if (frame_start) begin
c_frames <= c_frames + 1;
phases_seen[phase_q] <= 1'b1;
phase_q <= next_phase;
// Section 9: a transmit path that obeys Chapter 19.3 Section 6's
// lane alignment produces only phases that are multiples of four.
// Seeing one that is not means the stimulus is being injected on
// the RECEIVE side, which is the only place the other 48 exist.
if (phase_q[1:0] != 2'b00) unaligned_phase_seen <= 1'b1;
end
end
assign phase = phase_q;
assign phase_is_lane_aligned = (phase_q[1:0] == 2'b00);
always_comb begin
phases_count = '0;
for (int i = 0; i < 64; i++) if (phases_seen[i]) phases_count = phases_count + 7'd1;
end
endmoduleClassification: an observer of a quantity the generator produces and cannot name.
What it teaches: that observing an accumulator converts it from unreachable to searchable. The generator cannot constrain the phase; it can read next_phase before randomising the next frame and choose a length that lands where it wants. That is a one-step lookahead — the phase is a linear function of the length modulo 64, so the required length is a subtraction — and it turns a class B target into a class A one at the cost of making the generator stateful.
And it teaches that unaligned_phase_seen is a statement about the test bench's topology rather than about the design. A run that never sees an odd phase is injecting stimulus through the transmit path, and 48 of the 64 offsets are structurally absent — Section 9. The bit costs nothing and answers a question that otherwise takes an afternoon of waveform reading.
Deliberately simplified: next_phase uses a modulo by 64 written as %, which synthesises to a bit-slice and is written this way to match the equation in Section 7. gap_octets arrives as an input rather than being derived, so the tracker trusts whoever is measuring the gap — Chapter 19.3 §7's enforcer on transmit, or a wire monitor on receive. And phases_count is a 64-bit population count computed combinationally, needed on read.
Production implication: phases_count against 64 is the coverage number that distinguishes the two injection topologies in one read. Sixteen means transmit-side injection and complete coverage of what that topology can reach; sixty-four means receive-side injection. Anything between means the run was too short — and the three cases have completely different responses: accept it, keep going, or change the testbench. Chapter 19.4 §16's residue_classes_untested could not distinguish them; this can, because it counts a quantity whose ceiling depends on the topology.
9. Sixteen of Sixty-Four
Chapter 19.2 §3 built a barrel shifter for 64 possible start offsets. A generator driving the transmit path can produce sixteen of them, and the reason is a mechanism the design was required to have.
Chapter 19.3 §6's gap is chosen so that the next frame begins on a four-octet lane boundary. That is not an optimisation: a frame placed off-lane is misaligned by the PHY or has its gap silently stretched, and the deficit accumulator exists to make the lane alignment hold while the mean gap stays at twelve.
So every frame this MAC transmits starts at an offset that is a multiple of four.
| Offsets | |
|---|---|
| the parser's input space | 0 … 63 — 64 values |
| reachable from the transmit path | 0, 4, 8, … 60 — 16 values |
| unreachable from the transmit path | 48 values — 75% |
Three quarters of the parser's alignment space does not exist on the transmit side, at any weight, in any sequence, for any frame length. The mechanism that removes it is in the design under test.
Which means a loopback testbench — transmit into receive, the most convenient topology there is — covers a quarter of Chapter 19.2's barrel shifter.
| Topology | Offsets reachable | What it tests |
|---|---|---|
| transmit, looped back | 16 | the barrel shift's lane-aligned cases |
| a wire-level receive driver | 64 | all of it |
| transmit plus a skewing element | 64 | if the skew is octet-granular |
Row three is the compromise most environments reach without deciding to. Inserting anything between the transmit output and the receive input that changes the octet count — a PHY model, a media converter, a repeater — breaks the lane alignment and restores the other 48 offsets, and whether it does depends on a component chosen for a different reason.
And the cost of the missing 48 is Chapter 19.2 §3's own arithmetic.
| Value | |
|---|---|
| the barrel shifter | 1 024 byte-muxes, two stages |
| stage 1 shifts by | 0, 8, 16 … 56 octets |
| stage 2 shifts by | 0 … 7 octets |
| lane-aligned offsets exercise | stage 2 values 0 and 4 only |
Row four is the finding. A lane-aligned offset is a multiple of four, so the second stage — which shifts by 0 to 7 — only ever sees 0 and 4. Six of its eight cases are untested in a loopback environment, and they are exactly the sub-lane shifts that a hand-written barrel shifter is most likely to get wrong.
So the joint coverage space is smaller than it looks and its shape is decided by the topology.
| Cells | Coupon collection | |
|---|---|---|
| 64 residues × 64 offsets | 4 096 | 36 434 frames |
| 64 residues × 16 offsets | 1 024 | 7 689 frames |
| what a loopback run can close | 1 024 of 4 096 — 25% | and then stop |
Row three is a coverage report that reaches 25% and stays there through every weight change anybody makes, because the missing 75% is not a weighting problem. The response is a testbench change, and the evidence that it is needed is Section 8's phases_count reading exactly 16.
10. RTL 4 — The Alignment Injector
// ---------------------------------------------------------------------
// alignment_injector -- put the other 48 offsets back. Sections 9 and 10.
//
// The transmit path lane-aligns every frame -- Chapter 19.3 Section 6 --
// so a loopback topology reaches 16 of the parser's 64 offsets. This
// block sits between the transmit output and the receive input and
// inserts 0 to 3 extra idle octets, which shifts the phase by an
// arbitrary amount and restores the full space.
//
// It is a TESTBENCH component and it deliberately produces gaps that
// Chapter 5.9 permits but this MAC's transmitter would not choose --
// which is the point: the receive path must handle a partner that does
// not lane-align, and no partner is obliged to.
// ---------------------------------------------------------------------
module alignment_injector
import pktgen_pkg::*;
(
input logic clk,
input logic rst_n,
input logic in_valid,
input logic [7:0] in_octet,
input logic in_gap,
input logic [1:0] cfg_skew_mode, // 0 none, 1 fixed, 2 random, 3 sweep
input logic [1:0] cfg_fixed_skew,
input logic [5:0] target_phase,
output logic out_valid,
output logic [7:0] out_octet,
output logic [1:0] skew_applied,
// Observability. Sections 15 and 16.
output logic [31:0] c_frames_skewed,
output logic [3:0] skews_used,
output logic gap_below_min
);
logic [1:0] skew_q;
logic [4:0] gap_count;
logic [1:0] sweep_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
skew_q <= '0; gap_count <= '0; sweep_q <= '0;
c_frames_skewed <= '0; skews_used <= '0; gap_below_min <= 1'b0;
end else begin
if (in_gap) begin
gap_count <= gap_count + 1;
end else if (gap_count != '0) begin
// A frame has started. Choose this frame's skew and record it.
unique case (cfg_skew_mode)
2'd0: skew_q <= 2'd0;
2'd1: skew_q <= cfg_fixed_skew;
2'd2: skew_q <= 2'($urandom_range(3, 0));
2'd3: begin skew_q <= sweep_q; sweep_q <= sweep_q + 1; end
endcase
skews_used[skew_q] <= 1'b1;
if (cfg_skew_mode != 2'd0) c_frames_skewed <= c_frames_skewed + 1;
// Chapter 5.9's floor is 9 octets. Adding idle never violates it;
// REMOVING idle would, and this block only adds -- which is why
// it can shift the phase without producing an illegal gap.
if (gap_count < 5'd9) gap_below_min <= 1'b1;
gap_count <= '0;
end
end
end
assign out_valid = in_valid;
assign out_octet = in_octet;
assign skew_applied = skew_q;
endmoduleClassification: a testbench element that exists to remove a property of the design under test from the stimulus path.
What it teaches: that the fix for a class B unreachability is usually topological. No weight and no sequence restores the missing 48 offsets, because the transmit path removes them after the generator has finished. Inserting 0 to 3 idle octets between the transmitter and the receiver shifts the phase by an arbitrary amount and costs a two-bit counter — and the whole difficulty was recognising that the problem was where the stimulus was injected rather than what it contained.
And it teaches that the injector only ever ADDS idle. Chapter 5.9's floor is nine octets; removing idle could violate it and adding never can. So the block shifts the phase without producing a gap the receiver is entitled to reject, which keeps the stimulus legal — and a skewing element that deletes octets instead is testing error handling rather than alignment.
Deliberately simplified: the skew is applied by counting gap octets rather than by actually inserting them, so the listing shows the decision and not the datapath — a real injector holds a small elastic buffer, which is Chapter 4.4's structure in a testbench. $urandom_range in an always_ff is simulation-only, which is correct for this block and would be flagged by any lint. And cfg_skew_mode = 3 sweeps 0, 1, 2, 3 in order, which correlates the skew with the frame index and therefore with everything else the generator is sweeping.
Production implication: skews_used reaching 4'b1111 is the bit that says the other 48 offsets are being produced, and it should be a gate on the regression rather than a line in a coverage report. A run with cfg_skew_mode at zero — the default, and the mode a loopback environment starts in — reports 25% of the alignment cross and no error. The four-bit register costs nothing and distinguishes "we have not covered it yet" from "this testbench cannot cover it", which is the distinction Section 2's three classes exist to make.
11. Foreign Cases: What No Frame Weight Reaches
Class C cases are not properties of the stimulus at all, and the reason they get treated as generator problems is that they show up in the same coverage report.
Chapter 19.6 §21's case is the clearest. The reorder sink holds responses that arrived before the one the design is waiting for; filling a 7 KiB buffer needs four of the seven non-expected responses to arrive early at once. Whether they do is decided by the interconnect model.
Model it. Let q be the probability that a given outstanding response arrives before the expected one — a parameter of the bus functional model, not of the frames.
q | P(4 or more buffered) | Frames per occurrence |
|---|---|---|
| 0.05 | 0.00019 | 1 in 5 166 |
| 0.10 | 0.00273 | 1 in 367 |
| 0.20 | 0.03334 | 1 in 30 |
| 0.30 | 0.12604 | 1 in 8 |
| 0.50 | 0.50000 | 1 in 2 |
Every row is a different bus functional model and none of them is a different frame stream. A generator can produce a hundred million frames at q = 0 and reach the case never; it can produce a hundred at q = 0.5 and reach it fifty times.
Which is uncomfortable because q is usually not a knob. A clean BFM reorders nothing — Chapter 19.6 §9's reorder_never_seen — and reordering is added, if at all, as an afterthought with a default of zero. So the case is not merely hard to reach; it is absent from the environment, and the coverage hole is in a component nobody was tuning.
The other class C cases in Module 19 have the same shape and different owners.
| Case | Owner | Knob |
|---|---|---|
| four simultaneous reorderings | the bus functional model | q, usually absent |
| Chapter 19.5 §21's 200 ppm drift | the clock generator | two clock periods that differ |
| Chapter 19.5 §18's reset order | the reset sequencer | release order |
| Chapter 19.6 §8's 2 µs memory stall | the memory model | a stall distribution |
| Chapter 14.2's PAUSE arriving | the link partner model | a PAUSE generator |
Every row is a parameter of an agent the frame generator does not talk to, and every one produces a coverage hole that looks identical to an unweighted marginal. Chapter 19.5 §21's row 47 is the sharpest instance: a testbench whose two clocks come from one source has a drift of zero, so the case is not unreachable by chance but by construction.
So the generator's responsibility for class C is not to produce the case. It is to make the absence visible.
| What the generator can do | |
|---|---|
| produce the case | no |
| report that the environment cannot | yes — one bit per agent |
| fail the run when a required agent is inert | yes, and it should |
Row three is the position this chapter takes. A regression whose interconnect model has q = 0 should not report a coverage percentage at all for the reordering bins; it should report that the agent producing them is disabled. A percentage invites tuning; a disabled-agent report invites a configuration change, and the two responses differ by weeks.
12. RTL 5 — The Interconnect Stressor
// ---------------------------------------------------------------------
// interconnect_stressor -- the class C agent for Chapter 19.6's reorder
// cases, and the reporting that says whether it is doing anything.
// Sections 11 and 12.
//
// No property of a frame reaches these cases. What reaches them is a
// probability parameter on a bus functional model, and the whole value
// of this block is that the parameter is VISIBLE -- a run with q at
// zero reports "agent inert" rather than "coverage 0%".
// ---------------------------------------------------------------------
module interconnect_stressor
import pktgen_pkg::*;
#(
parameter int MAX_OUTSTANDING = 8,
parameter int REQUIRED_DEPTH = 4 // Chapter 19.6 Section 8's fourth
) (
input logic clk,
input logic rst_n,
input logic [7:0] cfg_reorder_q_pct, // 0 = inert
input logic response_valid,
input logic [3:0] response_id,
input logic [3:0] expected_id,
output logic hold_response,
output logic [3:0] held_count,
// Observability. Sections 15 and 16.
output logic [31:0] c_responses,
output logic [31:0] c_reordered,
output logic [3:0] peak_held,
output logic depth_reached,
output logic agent_inert
);
logic [3:0] held_q;
// THE reporting this block exists for. An inert agent is a
// configuration fact and must not be reported as a coverage
// percentage -- Section 11.
assign agent_inert = (cfg_reorder_q_pct == 8'd0);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
held_q <= '0; c_responses <= '0; c_reordered <= '0;
peak_held <= '0; depth_reached <= 1'b0;
end else if (response_valid) begin
c_responses <= c_responses + 1;
if ((response_id != expected_id) &&
(8'($urandom_range(99, 0)) < cfg_reorder_q_pct)) begin
held_q <= held_q + 1;
c_reordered <= c_reordered + 1;
end else if (response_id == expected_id && held_q != '0) begin
held_q <= '0; // the wait resolves; the slots drain
end
if (held_q > peak_held) peak_held <= held_q;
// Chapter 19.6 Section 8: a 7 KiB buffer holds 3.5 of this
// datapath's 2 KiB bursts, so the FOURTH simultaneous reordering
// is the one that overflows an undersized buffer. Reaching four
// is the coverage goal and reaching seven is the bound.
if (held_q >= 4'(REQUIRED_DEPTH)) depth_reached <= 1'b1;
end
end
assign hold_response = (held_q != '0);
assign held_count = held_q;
endmoduleClassification: a stimulus agent for a dimension the frame generator does not have, and a reporter for its own inertness.
What it teaches: that agent_inert is the most valuable output in the block and it is one comparison against zero. A coverage report showing the reorder bins empty has two causes with opposite responses: the run was too short, or the agent was disabled. A percentage cannot distinguish them and a boolean can, and the cost is one bit. Chapter 19.6 §16's reorder_untested is the design-side statement of the same fact; this is the stimulus side, and having both says which end to fix.
And it teaches that q has a meaningful floor as well as a ceiling. At q = 0.05 the four-deep case appears once in 5 166 responses, which a long regression reaches; at q = 0.5 it appears once in two, which is not a model of any real interconnect. The parameter has to be chosen to reach the case rather than to be realistic, and saying so in the configuration is better than choosing a realistic value and never covering anything.
Deliberately simplified: the reordering decision is independent per response, so the model has no memory and cannot reproduce the correlated reordering a DRAM refresh actually produces — which is the mechanism Chapter 19.6 §8 cited. held_q clears completely when the expected response arrives, where a real sink drains slot by slot. And $urandom_range inside always_ff makes the block simulation-only, which is correct for a stressor.
Production implication: peak_held against REQUIRED_DEPTH is the number a verification lead should read before trusting any reorder coverage. Reaching four proves the undersized-buffer case was exercised; reaching seven proves the bound was. A run reporting peak_held = 1 with c_reordered in the thousands has an agent that reorders often and never deeply, which is the independent-per-response model showing its limitation — and it is the case a correlated model would reach and this one will not, however long it runs.
13. What the Generator Must Never Do
Six prohibitions, and three of them are about honesty rather than correctness.
| # | Must never | Because | Symptom |
|---|---|---|---|
| 1 | constrain a quantity it does not control | Section 7 — the phase is decided upstream | a solved value the path discards |
| 2 | report a coverage percentage for an inert agent | Section 11 | weeks of weight tuning |
| 3 | drop a soft constraint silently | the solver does it for you | Section 5's collapsed distribution |
| 4 | generate only legal frames by default | Chapter 19.7 §7's runts | a counter path never exercised |
| 5 | assert on its own generated values | Section 20's class 88 | a property that cannot fail |
| 6 | correlate a sweep with the frame index | Section 10's sweep mode | two dimensions that move together |
Row one is the chapter's structural prohibition and it is easy to violate by accident. A rand bit [5:0] start_offset in the item class looks like every other field, solves fine, and is then overwritten by the accumulated phase. The generator reports that it produced offset 37; the design saw offset 12; and the coverage database records whichever one the sampling code happened to read. The two disagree and nothing says so — which is why Section 8's tracker samples the phase from the wire rather than from the item.
Row five is the rejected property and it is worth previewing here because the prohibition is broader than the assertion. Any check whose subject is a value the generator chose — an assertion, a coverage bin's guard, a scoreboard's expectation — is checking the solver. The solver works. Section 20 is the assertion form; the coverage form is a bin that can never be empty, and the scoreboard form is Chapter 20.3's.
Row six is a small, common and under-diagnosed error. Section 10's cfg_skew_mode = 3 sweeps the skew 0, 1, 2, 3 in frame order, so frame n's skew is n mod 4. If anything else in the generator also cycles with a period that shares a factor with four — a size sweep, a tag pattern, an address rotation — the two dimensions are locked and their cross is never covered. The fix is to randomise rather than sweep, or to use a sweep whose period is coprime with everything else's, and the diagnosis is a coverage cross that is exactly 25% full in a diagonal pattern.
And the two prohibitions that look like advice and are not:
| Why it is a prohibition | |
|---|---|
| row two | a percentage invites tuning; the fix is a configuration change |
| row four | a default that excludes a case makes its coverage permanently zero |
Both produce a coverage hole that responds to nothing, which is the single most expensive failure mode in a verification plan — not because the hole is large but because the effort spent on it is unbounded.
14. RTL 6 and 7 — The Directed Sequence and the Driver
// ---------------------------------------------------------------------
// directed_sequence -- the class B mechanism: set a running quantity by
// emitting a scripted run, then return to the random stream.
// Sections 7, 13 and 14.
//
// Chapter 19.3 Section 20's run D needs the deficit at 3, which needs four
// consecutive frames whose lengths are 3 modulo 4. That is a sequence
// and not a distribution -- no weight arrives at an accumulator's value.
//
// The block also implements Section 8's one-step lookahead: given the
// tracked phase and a target, it computes the length that lands there,
// which converts a class B target into a class A one at the cost of
// making the generator stateful.
// ---------------------------------------------------------------------
module directed_sequence
import pktgen_pkg::*;
(
input logic clk,
input logic rst_n,
input logic req,
input logic [2:0] seq_id,
input logic [5:0] current_phase,
input logic [5:0] target_phase,
input logic [4:0] expected_gap,
output logic seq_active,
output logic length_valid,
output logic [15:0] length_out,
output logic seq_done,
// Observability. Sections 15 and 16.
output logic [31:0] c_sequences [8],
output logic [31:0] c_phase_hits,
output logic [31:0] c_phase_misses,
output logic target_unreachable
);
localparam int PREAMBLE_OCTETS = 8;
logic [2:0] step_q;
logic [2:0] active_id;
logic [15:0] needed_len;
logic signed [7:0] delta;
// Section 8's lookahead. The next phase is
// (phase + 8 + length + gap) mod 64
// so the length that lands on target_phase is a subtraction. The
// result must then be legal AND reachable, which is why the block
// reports target_unreachable rather than emitting an illegal length.
assign delta = 8'signed'({2'b0, target_phase} -
{2'b0, current_phase});
assign needed_len = 16'(((32'signed'(delta) - 32'(PREAMBLE_OCTETS) -
32'(expected_gap)) % 32'sd64 + 32'sd64) % 32'sd64);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
step_q <= '0; active_id <= '0; seq_active <= 1'b0; seq_done <= 1'b0;
for (int i = 0; i < 8; i++) c_sequences[i] <= '0;
c_phase_hits <= '0; c_phase_misses <= '0; target_unreachable <= 1'b0;
end else begin
seq_done <= 1'b0;
if (req && !seq_active) begin
seq_active <= 1'b1;
active_id <= seq_id;
step_q <= '0;
c_sequences[seq_id] <= c_sequences[seq_id] + 1;
end else if (seq_active) begin
step_q <= step_q + 1;
// Sequence 0: Chapter 19.3 Section 20's run D. Four frames of a
// size that is 3 modulo 4, which drives the deficit to its bound
// and forces the long gap on the fourth.
// Sequence 1: the phase lookahead, one frame.
if ((active_id == 3'd0 && step_q == 3'd3) ||
(active_id == 3'd1)) begin
seq_active <= 1'b0;
seq_done <= 1'b1;
end
end
// A target phase whose required length is outside the legal range
// cannot be reached in one step. It is reachable in two -- any
// phase is, because the step is a full residue system modulo 64 --
// and this block does not implement the two-step search.
if (req && (needed_len < 16'(MIN_FRAME)) && (seq_id == 3'd1))
target_unreachable <= 1'b1;
end
end
always_comb begin
length_valid = seq_active;
unique case (active_id)
3'd0: length_out = 16'd1519; // 1519 mod 4 == 3
3'd1: length_out = needed_len;
default: length_out = 16'(MIN_FRAME);
endcase
end
endmoduleClassification: the class B mechanism, and the block that makes a running quantity addressable.
What it teaches: that an accumulator is addressable in one step whenever its update is invertible, and the phase's is. phase(n+1) = (phase(n) + 8 + length + gap) mod 64 is linear in length, so the length that reaches a target phase is a subtraction modulo 64. The generator does not have to search; it has to compute — and the only thing standing between it and any of the 64 phases is whether the required length is legal.
And it teaches that target_unreachable is a one-step statement rather than a permanent one. Adding a constant modulo 64 is a full residue system, so every phase is reachable in at most two frames whatever the length constraints — the block reports that it did not implement the search, not that the phase cannot be reached. Saying which is the difference between a testbench limitation and a design property, and Section 9's 48 missing offsets are the latter.
Deliberately simplified: sequence 0 emits a fixed 1 519 rather than randomising within the length ≡ 3 (mod 4) set, so run D exercises the deficit and one size; a complete version randomises within the residue class. The two-step phase search is not implemented, which is the honest reading of target_unreachable. And c_sequences is an unpacked array of counters, needing flattening for a register interface.
Production implication: c_phase_hits against c_phase_misses is the measurement that says whether the lookahead is working, and a miss is not always a bug. The phase is computed from expected_gap, and Chapter 19.3 §6's deficit sometimes chooses a different gap than the one predicted — so a lookahead that assumes 12 and gets 9 lands three octets off. A miss rate near 25% means the gap prediction is ignoring the deficit, which is a testbench that has modelled the design's average rather than its mechanism, and it is the same error Chapter 19.3 §22's complaint 1 describes from the other side.
And the sequence layer is only half of the class B mechanism. The other half is the block that puts an item on the wire, because that is where the generator's intent and the design's behaviour first disagree.
// ---------------------------------------------------------------------
// pktgen_driver -- put an item on the wire and record what the wire
// actually carried. Sections 13, 14 and 18.
//
// The driver is where Section 13's first prohibition is enforced. The
// item says what was asked for; this block reports what was delivered,
// and the two differ legitimately -- Chapter 19.3 Section 2 pads a short
// frame, Chapter 19.4 appends a check value, Chapter 13.2's tags may be
// inserted by the design. A scoreboard that compares them octet for
// octet fails on all three, which is Chapter 20.3's problem.
//
// What this block owns is the OBSERVED side: every property in Section
// 20's group 1 samples these outputs and not the item's fields.
// ---------------------------------------------------------------------
module pktgen_driver
import pktgen_pkg::*;
(
input logic clk,
input logic rst_n,
// From the sequence layer.
input logic item_valid,
input logic [15:0] item_length,
input logic [1:0] item_tags,
input addr_kind_e item_addr_kind,
input err_kind_e item_err_kind,
output logic item_ready,
// To the wire.
output logic tx_valid,
output logic [7:0] tx_octet,
output logic tx_sof,
output logic tx_eof,
// What the wire actually carried, for Section 20's group 1.
output logic frame_end,
output logic [15:0] observed_length,
output logic [5:0] observed_resid,
output logic [31:0] observed_seq,
// Observability. Sections 15 and 16.
output logic [31:0] c_items_accepted,
output logic [31:0] c_frames_driven,
output logic [31:0] c_length_changed,
output logic padding_observed,
output logic item_dropped
);
logic [15:0] octets_sent;
logic [15:0] requested_q;
logic driving;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
octets_sent <= '0; requested_q <= '0; driving <= 1'b0;
observed_length <= '0; observed_seq <= '0;
c_items_accepted <= '0; c_frames_driven <= '0; c_length_changed <= '0;
padding_observed <= 1'b0; item_dropped <= 1'b0;
frame_end <= 1'b0;
end else begin
frame_end <= 1'b0;
if (item_valid && item_ready) begin
requested_q <= item_length;
octets_sent <= '0;
driving <= 1'b1;
c_items_accepted <= c_items_accepted + 1;
end else if (driving && tx_valid) begin
octets_sent <= octets_sent + 1;
if (tx_eof) begin
driving <= 1'b0;
frame_end <= 1'b1;
observed_length <= octets_sent + 1;
observed_seq <= observed_seq + 1;
c_frames_driven <= c_frames_driven + 1;
// THE measurement this block exists for. Chapter 19.3 Section 2
// pads a frame below the 60-octet floor, so a 20-octet request
// arrives as 64 octets and the difference is correct. A
// testbench that asserts on the item never sees it.
if ((octets_sent + 1) != requested_q) begin
c_length_changed <= c_length_changed + 1;
if ((octets_sent + 1) > requested_q) padding_observed <= 1'b1;
end
end
end
// An item accepted and never driven is a dropped frame, and it is
// the one failure that makes every coverage number in Section 15
// optimistic without making anything fail.
if (item_valid && item_ready && driving) item_dropped <= 1'b1;
end
end
assign item_ready = !driving;
assign observed_resid = 6'((observed_length - 16'(FCS_OCTETS)) % 16'd64);
endmoduleClassification: the boundary between what the generator asked for and what the design saw, and the source of every honest assertion in Section 20.
What it teaches: that c_length_changed is a counter of legitimate differences and it should not be zero. Chapter 19.3 §2 pads every frame below the 60-octet floor, so a run with allow_illegal set must show a non-zero count — and a run that shows zero has either generated no short frames or is reading the item rather than the wire. The counter distinguishes those without any assertion firing.
And it teaches why Section 20's group 1 samples this block's outputs. observed_length is produced by counting octets on the wire; item_length is produced by a constraint solver. An assertion on the second checks the solver — the rejected class — and an assertion on the first can fail, which is the whole difference. The driver is not an interesting block and it is where the testbench stops being self-referential.
Deliberately simplified: the octet-level datapath is elided — tx_octet is declared and never driven — because the frame's contents are Chapter 20.3's subject and this block's purpose is the length accounting. item_dropped cannot assert as written, since item_ready is !driving; it is there as a documented impossibility for a version with a deeper pipeline. And observed_resid divides by 64 as a modulo on a 16-bit value, correct because 64 is a power of two.
Production implication: c_frames_driven against c_items_accepted is the pair that catches the most embarrassing testbench failure there is. A generator that produces a million items and a driver that emits nine hundred thousand frames has lost 10% of the stimulus, and every coverage number computed from the item stream is optimistic by that much. Neither counter is interesting alone; their difference is the only evidence that the two ends of the testbench agree, and it is the stimulus-side twin of Chapter 19.7 §13's requested-against-completed capture pair.
15. RTL 8 — Generator Telemetry
// ---------------------------------------------------------------------
// pktgen_telemetry -- what the generator produced, against what it was
// asked to produce. Section 15.
//
// Three groups, and the split is by WHO acts on the number.
// Plan view: coverage against the five lists Module 19 left.
// Mechanism view: which of Section 2's three classes each hole is in.
// Derived: the ceiling, so a weight that is finished can be left alone.
// ---------------------------------------------------------------------
module pktgen_telemetry
import pktgen_pkg::*;
(
input logic clk,
input logic rst_n,
input logic [31:0] c_frames,
input logic [6:0] residues_count, // of 64
input logic [6:0] phases_count, // of 64
input logic [3:0] skews_used,
input logic [31:0] c_spill_sizes,
input logic [31:0] c_runts,
input logic [3:0] peak_held,
input logic agent_inert,
input logic unaligned_phase_seen,
// Plan view.
output logic [15:0] list_coverage_pct,
output logic [4:0] lists_complete,
// Mechanism view.
output logic [2:0] hole_class, // 0 none, 1 A, 2 B, 3 C
output logic topology_limited,
// Derived.
output logic [31:0] frames_to_ceiling,
output logic weight_is_finished
);
logic l_residues, l_spill, l_runts, l_phases, l_reorder;
// The five lists. Chapter 19.4 Section 4, Chapter 19.3 Section 20,
// Chapter 19.7 Section 7, Chapter 19.4 Section 7 and Chapter 19.6
// Section 21 -- one bit each.
assign l_residues = (residues_count == 7'd64);
assign l_spill = (c_spill_sizes > 32'd23);
assign l_runts = (c_runts > 32'd43);
assign l_phases = (phases_count == 7'd64);
assign l_reorder = (peak_held >= 4'd4);
assign lists_complete = {l_reorder, l_phases, l_runts, l_spill, l_residues};
always_comb begin
list_coverage_pct = 16'd0;
for (int i = 0; i < 5; i++) if (lists_complete[i]) list_coverage_pct += 16'd20;
end
// Section 9: sixteen phases means the stimulus is injected through the
// transmit path, which lane-aligns. The remaining 48 are not a
// coverage hole -- they are absent from this topology.
assign topology_limited = (phases_count == 7'd16) && !unaligned_phase_seen;
// Which class the FIRST incomplete list is in, so the response is the
// right one. Section 2's whole taxonomy in three bits.
always_comb begin
if (!l_residues || !l_spill || !l_runts) hole_class = 3'd1; // A
else if (!l_phases) hole_class = 3'd2; // B
else if (!l_reorder) hole_class = 3'd3; // C
else hole_class = 3'd0;
end
// Section 6: the coupon-collection bound at a 100% weight. A run
// already near it has a finished weight, and further tuning buys the
// difference between 172 and 86 frames.
localparam int CEILING_FRAMES = 86;
assign frames_to_ceiling = (c_frames > 32'(CEILING_FRAMES))
? 32'd0 : (32'(CEILING_FRAMES) - c_frames);
assign weight_is_finished = (c_spill_sizes > 32'd23) &&
(c_frames < 32'd400);
endmoduleClassification: a plan-level reporter, and the only block in this chapter whose output is an instruction rather than a measurement.
What it teaches: that hole_class turns a coverage hole into a routed action. A percentage says how much is missing; three bits say whether to tune a weight, change a sequence, or configure another agent — and those responses take an hour, a day and a week respectively, so choosing the wrong one is the expensive mistake. Section 2's taxonomy exists to be computed, and this is where it is computed.
And it teaches that topology_limited is the flag that stops a team tuning forever. Sixteen phases with no unaligned phase ever seen is a complete result for a loopback topology, not a 25% one. Reporting it as 25% coverage of a 64-bin cross invites exactly the work that cannot succeed — Section 13's row two, arriving as a signal.
Deliberately simplified: list_coverage_pct weights the five lists equally, which is a presentation choice and not a measurement — the 23 spill sizes and the 64 residue classes are not equally important and the block cannot know which is. weight_is_finished uses a hard-coded ceiling of 86 frames from Section 6, valid only for the 23-size list. And l_spill tests a count rather than a set, so 24 frames all of the same spill size satisfies it — the complete check needs a 23-bit seen-vector, which Section 5's block has for residues and this does not have for sizes.
Production implication: lists_complete as a five-bit value is the right thing to put on a regression's summary line, and its bits should be read right to left in the order the responses get harder. Bits 0 to 2 are class A and close with weights; bit 3 is class B and closes with a topology change; bit 4 is class C and closes with another agent's parameter. A run reporting 5'b00111 is not 60% done — it is done with everything that tuning reaches, and the remaining two bits are two different conversations with two different people.
16. RTL 9 — The Generator Conformance Monitor
// ---------------------------------------------------------------------
// pktgen_conformance_monitor -- verdicts about the STIMULUS. Section 16.
//
// The fourteenth verdict generator in Modules 18 to 20 and the first
// whose subject is the testbench. Every previous one asked whether the
// design was correct; this one asks whether the run meant anything.
// ---------------------------------------------------------------------
module pktgen_conformance_monitor
import pktgen_pkg::*;
#(
parameter int MIN_FRAMES = 10000
) (
input logic clk,
input logic rst_n,
input logic [31:0] c_frames,
input logic distribution_collapsed,
input logic residues_incomplete,
input logic agent_inert,
input logic topology_limited,
input logic target_unreachable,
input logic [31:0] c_phase_misses,
input logic [31:0] c_phase_hits,
input logic allow_illegal_cfg,
output logic weights_not_realised,
output logic stimulus_incomplete,
output logic agent_disabled,
output logic topology_insufficient,
output logic lookahead_broken,
output logic illegal_sizes_disabled,
output logic none_of_the_above
);
always_comb begin
// The generator did not produce what its configuration asked for.
// Section 5: a dropped soft constraint is silent.
weights_not_realised = distribution_collapsed;
// The run was too short, or the weights are wrong. Class A.
stimulus_incomplete = (c_frames > 32'(MIN_FRAMES)) && residues_incomplete;
// Class C: another agent is off. Section 11 -- this must NOT be
// reported as a coverage percentage.
agent_disabled = agent_inert;
// Class B: the topology removes 48 of 64 offsets. Section 9.
topology_insufficient = topology_limited;
// Section 14: a lookahead that ignores the deficit lands off target
// about a quarter of the time.
lookahead_broken = (c_phase_hits + c_phase_misses > 32'd1000) &&
(c_phase_misses * 32'd4 > (c_phase_hits + c_phase_misses));
// Section 4: runts are behind a boolean, not a weight.
illegal_sizes_disabled = !allow_illegal_cfg;
none_of_the_above = !weights_not_realised && !stimulus_incomplete &&
!agent_disabled && !topology_insufficient &&
!lookahead_broken && !illegal_sizes_disabled;
end
endmoduleClassification: a verdict generator whose subject is the environment, and the first of its kind in the track.
What it teaches: that every verdict here has a different owner and none of them is the design team. weights_not_realised belongs to whoever wrote the constraints; agent_disabled to whoever configured the bus model; topology_insufficient to whoever built the testbench; illegal_sizes_disabled to whoever set the run's options. Thirteen previous monitors in this track reported on a design; this one reports on four different people's configuration files.
And it teaches that none_of_the_above here means something weaker than in every previous chapter. A design-side verdict clearing means the design behaved; this one clearing means the run was capable of finding a bug, which is a precondition rather than a result. The two must be read together — a clean design-side verdict from a run whose stimulus monitor was asserting is worth nothing — and that pairing is the honest use of both.
Deliberately simplified: illegal_sizes_disabled fires on every run with legal-only stimulus, which is most runs, and it should be a per-regression waiver rather than a verdict — it is here to make the point that a default excludes a case. lookahead_broken's threshold of 25% is Section 14's predicted miss rate and is a literal. And there is no verdict about the frame contents at all, because this chapter does not model payload — Chapter 20.3 does.
Production implication: none_of_the_above for the fourteenth time, and the first time a team should expect it to be false on a run that is working as intended. illegal_sizes_disabled is set on every legal-traffic regression and agent_disabled on every run with a clean bus model. The verdict's value is that those are choices somebody made, recorded where the coverage report is read — and a regression suite whose stimulus monitor asserts three verdicts on every run has three configuration decisions nobody has revisited since the environment was written.
17. Composing the Five Lists
Each list is reachable. The question this section answers is whether they are reachable together, and two of the five are in tension.
The five, with the mechanism each needs:
| List | Class | Mechanism | Frames |
|---|---|---|---|
| 64 residue classes | A | uniform lengths | 304 |
| 23 spill sizes | A | w_spill at 50% | 172 |
| 43 runt sizes | A | allow_illegal, w_runt 5% | 3 741 |
| 64 beat phases | B | Section 10's injector | 7 689 with 16 offsets |
| 4 simultaneous reorderings | C | q at 0.05 | 5 166 responses |
The tension is between rows one and two. w_spill at 50% puts half the frames on 23 sizes, all of which have the same residue: (length − 4) mod 64 where length ≡ 3 (mod 64) gives residue 63. So the spill weight concentrates half the stimulus on one of the 64 residue classes.
| Uniform | w_spill = 50% | |
|---|---|---|
| residue 63's share | 1.6% | 50.8% |
| the other 63 classes, each | 1.56% | 0.78% |
| coupon collection over 64 | 304 frames | 608 frames |
Row three is the cost and it is a factor of two, which is the same factor Section 6 found for the general population and for the same reason: the weight's probability comes from everywhere else. Two hundred extra frames is nothing, and the shape matters more than the size — two class A goals can conflict, and the conflict is invisible unless somebody notices that the target sets overlap in a derived quantity.
Rows three and four are in a different kind of tension and it is worse, because it is arithmetic nobody computes.
A fixed-size stream has a fixed wire period, and Chapter 19.3 §6's deficit makes that period a multiple of four.
gap = 12 − (L mod 4), or 12 when L is a multiple of 4
period = L + 8 + gap = L + 20 − (L mod 4)So the period is always divisible by four, which is the lane alignment showing up as a number, and the phases a fixed-size stream visits are 64 / gcd(period, 64) — at most sixteen, which is Section 9's ceiling arriving from the other direction.
| Frame size | Gap | Period | gcd with 64 | Phases visited |
|---|---|---|---|---|
| 47 — a runt | 9 | 64 | 64 | 1 |
| 48 | 12 | 68 | 4 | 16 |
| 64 | 12 | 84 | 4 | 16 |
| 1 517 | 9 | 1 536 | 64 | 1 |
| 1 518 | 10 | 1 536 | 64 | 1 |
| 9 000 — jumbo | 12 | 9 020 | 4 | 16 |
Rows four and five are the finding and they are the two sizes every stress test uses. A 1 518-octet fixed-size stream has a wire period of 1 536 octets — exactly twenty-four beats — so every frame starts at the same offset in a beat, forever. The most common maximum-size stress test in Ethernet verification visits one of the parser's sixteen reachable phases and fifteen of them never.
And it is not a rare property.
| Phases visited | How many of the 1 455 legal sizes | Share |
|---|---|---|
| 16 | 728 | 50.0% |
| 8 | 364 | 25.0% |
| 4 | 180 | 12.4% |
| 2 | 92 | 6.3% |
| 1 | 91 | 6.3% |
Half of all sizes are fine and 6.3% are degenerate, and the degenerate ones include 1 517, 1 518 and 1 519 — a block of sizes right at the maximum, which is exactly where a fixed-size test is aimed. A stress test at 1 518 covers one phase; the same test at 1 520 covers sixteen, and nothing in any coverage report explains the difference.
So the composition rule is short and is not obvious from any single list.
A fixed-size stream visits
64 / gcd(L + 20 − (L mod 4), 64)phases. Check it before choosing a stress size, and prefer a size whose period is4 mod 8.
Which leaves the five lists composable, with two caveats that are both about derived quantities.
| Caveat | Cost | Fix |
|---|---|---|
w_spill concentrates residue 63 | coupon over 64 classes doubles — 607 frames | accept it; 300 frames is nothing |
| fixed-size streams collapse the phase | 1 phase instead of 16 | check the gcd, or randomise the size |
Neither is a conflict between the lists as stated. Both are conflicts between a list and a quantity derived from the same field, which is the shape a composition problem takes when every goal is expressed in terms of the frame's length.
18. What the Generator Assumes
Eight assumptions, and the first two are about the design rather than about the stimulus.
| # | Assumption | Owner | If wrong |
|---|---|---|---|
| 1 | the transmit path lane-aligns | Chapter 19.3 §6 | Section 9's 16 becomes 64 and the injector is unnecessary |
| 2 | the gap is 9 to 12 with a bounded deficit | Chapter 19.3 §6 | Section 14's lookahead mispredicts |
| 3 | the datapath is 64 octets per beat | Chapter 19.1 §4 | every modulo in this chapter changes |
| 4 | the solver honours dist over soft | the simulator | Section 5's collapsed distribution |
| 5 | another agent produces reordering | the bus model | Section 11 — class C is empty |
| 6 | the clocks differ by up to 200 ppm | the clock generator | Chapter 19.5 §21's row 47 |
| 7 | illegal sizes are enabled | the run's options | Chapter 19.7 §7's cases absent |
| 8 | the phase is sampled from the wire | this chapter — Section 8 | Section 13's row 1: two disagreeing values |
Row one is unusual: the generator's architecture depends on a property of the design under test. Section 10's injector exists only because Chapter 19.3 §6 lane-aligns. A different MAC that did not would need no injector and would reach all 64 offsets from a loopback, so this chapter's most distinctive block is a consequence of a design decision three chapters earlier. That dependency should be written down, because it is the kind that survives into an environment reused on a different design.
Row four is the assumption most likely to be silently false. soft constraint resolution is implementation-defined in its details, and a solver that drops c_spill_set under a different dist produces a legal, uniform, useless distribution. Section 5's block exists because this assumption cannot be checked any other way.
And two deliberately not assumed:
| Not assumed | Why not |
|---|---|
| that the design is correct | the generator's job is to produce stimulus, not to judge it |
| that coverage bins exist for any of this | Chapter 20.4's problem; this chapter counts its own reach |
Row two is the boundary with Chapter 20.4 and it is worth being firm about. This chapter's telemetry counts what the stimulus produced; a coverage model counts what the design experienced. They disagree whenever the injection path changes something — Section 9's alignment is the standing example — and a chapter that conflates them cannot report that disagreement, which is the most informative thing either measurement produces.
19. The Cost, Accounted
This chapter's cost is measured in simulation time rather than in flops, and the accounting is unfamiliar enough to be worth laying out the same way.
| Block | Flops | Simulation cost |
|---|---|---|
| the item class | — | one solver call per frame |
size_distribution | ~180 | negligible |
phase_tracker | ~120 | negligible |
alignment_injector | ~40 | negligible |
interconnect_stressor | ~70 | one random call per response |
directed_sequence | ~330 | negligible |
pktgen_driver | ~200 | the wire-level drive |
pktgen_telemetry | ~90 | negligible |
pktgen_conformance_monitor | ~20 | negligible |
| total | ~1 050 flops | dominated by the solver |
The flop count is irrelevant and is here for the comparison. This chapter's blocks are 1 050 flops against Module 19's 14 166 — 7.4% — and none of them ships. What costs something is the solver call per frame, and the frame counts in Section 17's table are the real budget.
| Goal | Frames | At 148.81 M/s, wire time |
|---|---|---|
| 64 residue classes | 304 | 2.0 µs |
| 23 spill sizes, weighted | 172 | 1.2 µs |
| 43 runt sizes | 3 741 | 25.1 µs |
| 64 × 16 joint | 7 689 | 51.7 µs |
| all five lists | about 12 000 | 80.6 µs |
Eighty microseconds of wire time is the whole of Module 19's stimulus requirement, which at any RTL simulation rate is minutes. That is the encouraging half.
The discouraging half is Chapter 19.5 §21's run B, which is not in the table.
| Frames | Wire time | |
|---|---|---|
| all five lists | 12 000 | 80.6 µs |
| Chapter 19.5's 200 ppm drift to overflow | — | 13.107 ms |
| ratio | — | 163× |
One class C case costs 163 times the entire rest of the plan, and it cannot be shortened by stimulus because it is not about frames at all. Chapter 19.5 §5's rate tracker is the answer — measure the drift in 65 536 beats rather than wait 2.56 million — and it is the only place in this batch where the correct response to a simulation cost is to build hardware.
Which is the accounting's real result. The stimulus for six chapters of MAC datapath is eighty microseconds; the environment around it — a clock generator with two frequencies, a bus model with a reorder probability, a reset sequencer with an order, an injector with four skews — is where both the cost and the coverage actually live.
20. Properties Worth Asserting, and One Worth Refusing
Thirty-two properties in six groups, and the refused one is the first assertion anybody writes in a testbench.
Group 1 — the item, where the properties must be about what the design SAW.
// Every property in this group samples the interface, not the item.
// Section 20's rejected class is why.
// The length on the wire matches the length the generator asked for.
// This CAN fail -- the driver, the DMA path and Chapter 19.3's padding
// all sit between the two.
a_length_delivered: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |-> (observed_length == expected_length));
// Chapter 19.3 Section 2: a frame below the 60-octet floor is PADDED, so
// a 20-octet request arrives as 64 octets. The generator's value and the
// wire's differ legitimately, and the property has to know that.
a_short_frame_padded: assert property (@(posedge clk) disable iff (!rst_n)
(frame_end && expected_length < 16'd64) |-> (observed_length == 16'd64));
// The observed residue is what Chapter 19.4 Section 4 branches on.
a_residue_observed: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |-> (observed_resid == ((observed_length - 4) % 64)));
// Illegal sizes reach the wire only when the knob is set.
a_illegal_gated: assert property (@(posedge clk) disable iff (!rst_n)
(frame_end && observed_length < 16'd64) |-> allow_illegal_cfg);
// Chapter 13.2's tags move every field after them, so the observed
// EtherType offset must match the tag count the generator asked for.
a_tags_delivered: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |-> (observed_ethertype_offset == 16'd12 + 16'd4 * expected_tags));
// A frame the generator marked for FCS corruption arrives with a bad
// check value, and one it did not does not.
a_fcs_error_delivered: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |-> (observed_crc_bad == (expected_err == ERR_FCS)));
// The item and the wire agree on the address class, which the DMA path
// does not change.
a_addr_kind_delivered: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |-> (observed_addr_kind == expected_addr_kind));
// And a frame is never delivered twice, which a retrying driver does.
a_no_duplicate: assert property (@(posedge clk) disable iff (!rst_n)
frame_end |-> (observed_seq != $past(observed_seq)));Group 2 — the distribution, which is a property of a run rather than of a frame.
// The realised share of the 23 spill sizes is within tolerance of the
// weight. Section 5: a dropped soft constraint is otherwise silent.
a_weight_realised: assert property (@(posedge clk) disable iff (!rst_n)
(c_frames > 32'd1000) |-> !distribution_collapsed);
// Every residue class appears, eventually, under a uniform weight.
a_residues_reached: assert property (@(posedge clk) disable iff (!rst_n)
(c_frames > 32'd2000) |-> (residues_count == 7'd64));
// The generator never produces a length outside its configured range.
a_range_respected: assert property (@(posedge clk) disable iff (!rst_n)
frame_valid |-> (frame_length inside {[5:JUMBO_MAX]}));
// And the run is long enough for any of the above to mean anything.
a_run_long_enough: assert property (@(posedge clk) disable iff (!rst_n)
$fell(run_active) |-> (c_frames > 32'd10000));Group 3 — the phase, which the generator observes and does not control.
// The tracked phase matches the wire. If these disagree the tracker's
// model of the gap is wrong -- Section 14's lookahead failure.
a_phase_matches_wire: assert property (@(posedge clk) disable iff (!rst_n)
frame_start |-> (phase == wire_octet_index[5:0]));
// The phase advances by the wire period, modulo 64.
a_phase_advances: assert property (@(posedge clk) disable iff (!rst_n)
frame_start |=> (phase == 6'((32'($past(phase)) + 32'd8 +
32'($past(frame_length)) +
32'($past(gap_octets))) % 32'd64)));
// Section 9: a transmit-injected run only ever sees lane-aligned phases.
a_tx_phases_aligned: assert property (@(posedge clk) disable iff (!rst_n)
(frame_start && !injector_enabled) |-> (phase[1:0] == 2'b00));
// And with the injector on, it does not.
a_injector_unaligns: assert property (@(posedge clk) disable iff (!rst_n)
(frame_start && injector_enabled && skew_applied != 2'd0)
|-> ##[0:4] unaligned_phase_seen);
// The injector only ADDS idle, so the gap never goes below Chapter 5.9's
// floor. Section 10.
a_injector_adds_only: assert property (@(posedge clk) disable iff (!rst_n)
injector_enabled |-> !gap_below_min);
// A lookahead target that is reported reachable is reached.
a_lookahead_lands: assert property (@(posedge clk) disable iff (!rst_n)
(seq_done && active_id == 3'd1 && !target_unreachable)
|-> ##1 (phase == target_phase));Group 4 — the sequences, where the properties are about state the generator drove.
// Run D drives the deficit to its bound. Chapter 19.3 Section 20.
a_seq0_fills_deficit: assert property (@(posedge clk) disable iff (!rst_n)
(seq_done && active_id == 3'd0) |-> (observed_deficit == 2'd3));
// A sequence emits exactly the number of frames it claims.
a_seq_length: assert property (@(posedge clk) disable iff (!rst_n)
(seq_active && active_id == 3'd0) |-> (step_q <= 3'd3));
// A sequence never overlaps another.
a_seq_exclusive: assert property (@(posedge clk) disable iff (!rst_n)
(req && seq_active) |-> !$rose(seq_active));
// Control returns to the random stream.
a_seq_returns: assert property (@(posedge clk) disable iff (!rst_n)
seq_done |=> !seq_active);
// Section 13 row 6: a sweep must not share a period with anything else.
a_sweep_coprime: assert property (@(posedge clk)
(cfg_skew_mode != 2'd3) || (SIZE_SWEEP_PERIOD % 4 != 0));Group 5 — the foreign agents, where every property is about whether the agent is doing anything.
// A run that requires reorder coverage must have a non-zero q.
a_stressor_configured: assert property (@(posedge clk)
!require_reorder_coverage || (cfg_reorder_q_pct != 8'd0));
// The stressor reaches the depth Chapter 19.6 Section 8 needs.
a_depth_reached: assert property (@(posedge clk) disable iff (!rst_n)
(c_responses > 32'd100000 && !agent_inert) |-> depth_reached);
// An inert agent is REPORTED, not silently zero.
a_inert_reported: assert property (@(posedge clk) disable iff (!rst_n)
agent_inert |-> agent_disabled);
// The clocks actually differ. Chapter 19.5 Section 21's row 47: a
// testbench with one clock source has a drift of zero.
a_clocks_differ: assert property (@(posedge clk) disable iff (!rst_n)
(c_windows > 32'd16) |-> (drift_ppm != '0));
// The reset order is exercised in both directions.
a_reset_order_swept: assert property (@(posedge clk)
reset_order_seen == 2'b11);Group 6 — coverage.
c_all_residues: cover property (@(posedge clk) residues_count == 7'd64);
c_all_phases: cover property (@(posedge clk) phases_count == 7'd64);
c_sixteen_phases: cover property (@(posedge clk) phases_count == 7'd16);
c_all_skews: cover property (@(posedge clk) skews_used == 4'b1111);
c_spill_and_runt: cover property (@(posedge clk)
(c_spill_sizes > 32'd23) && (c_runts > 32'd43));
c_deficit_full: cover property (@(posedge clk) observed_deficit == 2'd3);
c_reorder_four: cover property (@(posedge clk) peak_held >= 4'd4);21. Verification Scenarios
Fifty-seven scenarios for a block whose subject is verification, plus a five-run directed test whose content is a topology rather than a stimulus.
The item and its constraints — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 1 | w_spill = 0, 10 000 frames | spill share 1.58% |
| 2 | w_spill = 50, 10 000 frames | share 50% ± 5 |
| 3 | w_spill = 50 with a conflicting hard constraint | distribution_collapsed |
| 4 | allow_illegal = 0 | no frame below 64, ever |
| 5 | allow_illegal = 1, w_runt = 5 | 43 runt sizes in 3 741 frames |
| 6 | cfg_mtu = 9 000 | lengths up to 9 000 |
| 7 | cfg_mtu = 1 518 with a 9 000 request | the constraint wins |
| 8 | tags = 2 on a 64-octet frame | c_room_for_tags forces a longer frame |
| 9 | 100 000 frames, uniform | all 64 residues in ~304 |
| 10 | the 23 spill sizes, individually | all reachable |
| 11 | a length of 5 octets | legal only with allow_illegal |
| 12 | describe() on a 1 475-octet frame | resid=63 |
Row three is the scenario that justifies Section 5's existence, and it is the one most environments never run: a soft constraint that loses is silent, so the only way to test the weight machinery is to break it deliberately and check that something notices.
The phase — 13 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 13 | a 64-octet fixed-size stream | 16 phases, all lane-aligned |
| 14 | a 1 518-octet fixed-size stream | 1 phase — period 1 536 |
| 15 | a 1 520-octet fixed-size stream | 16 phases |
| 16 | a 47-octet runt stream | 1 phase — period 64 |
| 17 | uniform lengths, injector off | 16 phases |
| 18 | uniform lengths, injector on | 64 phases |
| 19 | the tracker against the wire | a_phase_matches_wire |
| 20 | a lookahead to phase 37, injector off | target_unreachable or a miss |
| 21 | a lookahead to phase 36, injector off | hits |
| 22 | a lookahead assuming gap 12 on a size ≡ 3 mod 4 | misses by 3 |
| 23 | the same, deficit-aware | hits |
| 24 | 1 000 lookaheads, deficit ignored | miss rate near 25% |
| 25 | a phase sweep over all 16 | phases_count = 16 and stops |
Rows fourteen and fifteen are two octets apart and differ by a factor of sixteen in phase coverage, and row twenty-two is Section 14's predicted failure: a lookahead that models the gap as a constant lands three octets off on the 75% of sizes whose length is not a multiple of four.
The injector and topology — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 26 | cfg_skew_mode = 0 | skews_used = 4'b0001 |
| 27 | cfg_skew_mode = 2, 1 000 frames | all four skews |
| 28 | cfg_skew_mode = 3 with a size sweep of period 4 | skew and size locked |
| 29 | the same with a size sweep of period 5 | the cross fills |
| 30 | the injector inserting idle | gap_below_min clear |
| 31 | an injector that DELETES idle | gap_below_min — an illegal gap |
| 32 | injector on, unaligned_phase_seen | set within four frames |
| 33 | injector off, 1 000 000 frames | unaligned_phase_seen never |
| 34 | topology_limited with 16 phases | set |
| 35 | topology_limited with 17 phases | clear — the injector is working |
Row twenty-eight is Section 13's row 6 as a test and its signature is a coverage cross exactly 25% full in a diagonal pattern, which is the most recognisable coverage-hole shape there is and is almost always a shared sweep period.
The foreign agents — 12 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 36 | cfg_reorder_q_pct = 0 | agent_inert; no coverage percentage |
| 37 | q = 5, 100 000 responses | depth_reached |
| 38 | q = 5, 1 000 responses | usually not — 1 in 5 166 |
| 39 | q = 50 | 1 in 2 — unrealistic and useful |
| 40 | q = 5, peak_held over a long run | reaches 4, rarely 7 |
| 41 | an independent-per-response model | deep reordering is rare by construction |
| 42 | a correlated model | reaches 7 routinely |
| 43 | two clocks from one source | drift 0 — Chapter 19.5 §21's row 47 |
| 44 | two clocks 200 ppm apart | drift measurable in 65 536 beats |
| 45 | a single global reset | one release order only |
| 46 | staggered reset, both orders | reset_order_seen = 2'b11 |
| 47 | a memory model with no stall | Chapter 19.6 §8's cases absent |
Rows forty-one and forty-two are the stressor's limitation stated as a pair. An independent model reaches depth four at q = 0.05 once in 5 166 and reaches depth seven essentially never; a correlated model — one DRAM refresh delaying several responses together — reaches seven routinely. The second is what the hardware sees and the first is what testbenches contain.
The generator's own reporting — 10 scenarios.
| # | Scenario | Expected |
|---|---|---|
| 48 | a complete run | lists_complete = 5'b11111 |
| 49 | a loopback run | 5'b01111 — bit 3 clear |
| 50 | a run with a clean bus model | bit 4 clear |
| 51 | a legal-only run | bit 2 clear |
| 52 | hole_class with residues missing | 1 — class A |
| 53 | hole_class with only phases missing | 2 — class B |
| 54 | hole_class with only reorder missing | 3 — class C |
| 55 | weight_is_finished after 300 frames | set — Section 6's ceiling |
| 56 | none_of_the_above on a default regression | FALSE, correctly |
| 57 | an assertion on a generated value | passes on a broken generator |
Row fifty-six is the scenario a verification lead should be shown first, because the monitor asserting on a working regression is the intended behaviour and looks like a bug. Row fifty-seven is Section 20's rejected class as a test: break the generator, run the assertion, watch it pass.
The directed test — five runs a stimulus change cannot produce.
This chapter's own hard cases are topologies rather than frames, which is the argument of Sections 9 and 11 arriving in a test plan.
| Case | Needs | A default environment provides |
|---|---|---|
| 48 unaligned phases | an injector between TX and RX | a direct loopback |
| deep reordering | a correlated bus model | an in-order BFM |
| the drift overflow | two clock sources | one |
| the reset-order fill | a staggered release | one global reset |
| the collapsed distribution | a deliberately conflicting constraint | a working one |
Row five is the only one that is a stimulus change, and it is a deliberate break rather than a case — which is the shape of every test of a testbench.
Construct it. Five runs.
| Run | Topology | Stimulus | Exercises |
|---|---|---|---|
| A | direct loopback | uniform lengths | 16 phases; the class A lists |
| B | injector, sweep mode | uniform lengths | all 64 phases |
| C | direct loopback | fixed 1 518 | ONE phase — Section 17 |
| D | injector, plus q = 5 | uniform, illegal enabled | all five lists |
| E | direct loopback | w_spill with a conflicting hard constraint | distribution_collapsed |
Run C is one line of configuration and it is the run that proves Section 17's arithmetic. A 1 518-octet fixed-size stream has a wire period of 1 536 octets, exactly twenty-four beats, so every frame starts at the same phase. phases_count reads 1 after a million frames, and the same run at 1 520 reads 16.
Run D is the only run that completes the plan, and it needs three configuration changes from the default: the injector on, illegal sizes enabled, and a non-zero reorder probability. None of them is a weight.
The oracle, in four parts:
| Check | A | B | C | D | E |
|---|---|---|---|---|---|
phases_count | 16 | 64 | 1 | 64 | 16 |
lists_complete | 5'b00011 | 5'b01011 | 5'b00011 | 5'b11111 | 5'b00010 |
hole_class | 1 | 1 | 1 | 0 | 1 |
none_of_the_above | false | false | false | TRUE | false |
Row one is the topology's fingerprint and row four is the only place in this chapter where a clean verdict appears: run D is the only configuration that can find every bug Module 19 identified, and four of the five runs cannot — not because they are short but because of how they are wired.
Row three's constant 1 across A, B, C and E is the most useful entry in the table. Four different failures — a short run, a missing weight, a degenerate size, a collapsed distribution — all report class A, because hole_class reports the first incomplete list and the class A lists come first. That is a deliberate ordering and it has a cost: the class B and C holes are invisible until the class A ones close, which is the right priority and should be said out loud.
22. Debugging a Generator
Four complaints. Three of them present as coverage holes and have three different owners.
Complaint 1 — "coverage of the alignment cross is stuck at 25%."
| Check | If yes | Meaning |
|---|---|---|
phases_count exactly 16? | only lane-aligned phases | Section 9 — the topology |
unaligned_phase_seen ever? | the injector is off | confirms |
topology_limited set? | the monitor already said so | read it before tuning |
| does a weight change move it? | it will not | class B |
Row four is the diagnostic and the answer is always the same: no. The missing 48 offsets are removed by Chapter 19.3 §6's lane alignment after the generator has finished, so nothing in the item, the constraints or the weights reaches them. topology_limited exists so that this conversation takes one read instead of one sprint.
Complaint 2 — "the spill sizes are at 1.6% and the weight says 50."
| Check | If yes | Meaning |
|---|---|---|
distribution_collapsed set? | the realised share is far below target | Section 5 |
is c_spill_set a soft constraint? | it lost to a hard one | silently |
| does the solver report anything? | it will not | dropped softs are not diagnostics |
| does making it hard cause a solver failure? | then the constraints genuinely conflict | and the conflict is the bug |
Row four is the test that separates the two cases and it is worth running before rewriting anything. A soft that loses and a soft that conflicts look identical from the outside; promoting it to hard turns the silent loss into a solver error naming the constraint it fights with, which is the diagnosis.
Complaint 3 — "reorder coverage is zero after a week of regressions."
| Check | If yes | Meaning |
|---|---|---|
agent_inert set? | q is zero | class C — nothing to tune |
c_reordered at zero? | confirms | the bus model is in-order |
peak_held at 1 with c_reordered high? | the model reorders shallowly | independent-per-response |
| does a longer run help? | not at q = 0; not much at depth | the model's structure, not its rate |
Row three is the subtler failure and it survives a fix to row one. An independent-per-response model at q = 0.05 reaches depth four once in 5 166 and depth seven essentially never; the hardware's reordering is correlated — one DRAM refresh delays several responses together — so the model reproduces the rate and not the shape. peak_held is the number that shows it.
Complaint 4 — "a fixed-size stress test finds nothing."
| Check | If yes | Meaning |
|---|---|---|
| what is the size? | compute L + 20 − (L mod 4) | Section 17 |
| is the period a multiple of 64? | one phase, forever | 1 516 to 1 519 all are |
phases_count = 1? | confirms | 15 of 16 phases unvisited |
| does the same test at size + 2 differ? | 16 phases | definitively |
Row four is a two-octet change that multiplies the phase coverage by sixteen, and row two is the reason the classic maximum-size stress test is one of the worst: 1 518 gives a period of 1 536, which is exactly twenty-four beats. Ninety-one of the 1 455 legal sizes are degenerate this way, and four of them are clustered at the maximum where everybody aims.
Complaint 5 — "the item stream and the coverage numbers disagree."
| Check | If yes | Meaning |
|---|---|---|
c_items_accepted above c_frames_driven? | items accepted and never driven | Section 14's item_dropped |
c_length_changed at zero with runts enabled? | nothing is reading the wire | the coverage samples the item |
padding_observed clear on a run with short frames? | Chapter 19.3 §2's pad is invisible | confirms |
| does the coverage sample the driver's outputs? | it should | and usually does not |
Row two is the most common form of a testbench that measures itself. A coverage model sampling item_length records what the generator asked for; the design saw what the driver delivered, and Chapter 19.3 §2's padding makes those differ on every short frame. c_length_changed reading zero on a run that generated runts is proof that the two ends have never been compared — and it costs one counter to know.
And the three symptoms this chapter is systematically blamed for:
| Symptom | Blamed on | Usually is |
|---|---|---|
| a coverage hole that will not close | weights | a topology or another agent |
| "the random seeds are bad" | the seed | a constraint that excludes the case |
| a stress test that finds nothing | the design | a period that divides the beat |
| coverage that disagrees with the log | the sampler | the item and the wire, compared to each other |
| "the weight is set and nothing changed" | the solver | a soft constraint that lost, silently |
23. Misconceptions
Misconception 1 — "a weight can reach any case."
The wrong model: constrained random plus enough tuning reaches everything.
What it costs: unbounded effort on cases that do not respond. A weight is a marginal distribution over one frame's own fields, and two of Module 19's five lists are not that: the beat phase is a running sum over every preceding frame, and the interconnect's reordering belongs to a different agent.
The corrected model: three classes. Class A responds to weights — 31.6× on the spill sizes and then a ceiling. Class B responds to a sequence or a topology change. Class C responds to another agent's parameter and to nothing else. The first question about a coverage hole is which class it is in, and the answer routes an hour, a day or a week of work. Sections 2, 6, 9, 11.
Misconception 2 — "a loopback testbench exercises the receive path."
The wrong model: transmit into receive covers both directions.
What it costs: 75% of Chapter 19.2's barrel shifter. Chapter 19.3 §6's deficit lane-aligns every transmitted frame, so only 16 of the parser's 64 start offsets ever occur — and the second barrel stage, which shifts by 0 to 7 octets, sees only 0 and 4. Six of its eight cases are untested.
The corrected model: the loopback's coverage ceiling is a property of the design under test, not of the run's length. An injector that adds 0 to 3 idle octets restores the other 48, costs a two-bit counter, and is the only thing that does. phases_count reading exactly 16 is the evidence. Sections 9, 10.
Misconception 3 — "an empty coverage bin means run longer."
The wrong model: coverage is a function of simulation time.
What it costs: weeks. A bin can be empty because the run was short, because a constraint excludes it, because the topology removes it, or because the agent that produces it is disabled — and only the first responds to running longer. Chapter 19.7 §7's runts are behind a boolean; Chapter 19.6 §21's reorderings are behind another model's parameter.
The corrected model: distinguish low from zero. A bin at 2% is a weighting question; a bin at exactly zero after a long run is almost always a gate, and Section 4's first diagnostic question is to look for the excluding constraint before touching a weight. Sections 4, 11, 13.
Misconception 4 — "assert that the generated frames are legal."
The wrong model: the testbench should check its own stimulus.
What it costs: an assertion that cannot fail. The constraint solver produced the value to satisfy the same expression the property tests, so the property checks the solver — which works — and is blind to the constraint being wrong, the configuration defaulting, the driver changing the frame, and the generator producing nothing at all.
The corrected model: move the subject somewhere the solver did not construct. Assert on what the design saw — the wire, where Chapter 19.3 §2's padding is visible — assert against a bound transcribed independently from the standard, cover that the antecedent fires, and check the realised distribution. Section 20.
Misconception 5 — "a fixed-size stress test is the harshest case."
The wrong model: minimum size for rate, maximum size for throughput, and those are the corners.
What it costs: phase coverage, silently. A fixed-size stream has a fixed wire period, and Chapter 19.3 §6 makes that period a multiple of four, so the phases visited are 64 / gcd(period, 64). At 1 518 octets the period is 1 536 — exactly twenty-four beats — and the test visits one phase.
The corrected model: compute the gcd before choosing a stress size. Half of all legal sizes visit all sixteen reachable phases and 6.3% visit one, and the degenerate block includes 1 516 through 1 519. The same test at 1 520 covers sixteen, which is a two-octet change and a factor of sixteen. Section 17.
Misconception 6 — "the environment is done when coverage is at 100%."
The wrong model: the coverage model defines completeness.
What it costs: the cases the coverage model does not contain. A loopback environment reaching 100% of a 16-bin phase model has covered 25% of the design's alignment space, and the model was written to match the environment because that is where the bins came from. Chapter 19.6 §16's reorder_untested and this chapter's agent_inert are the same warning from the two ends.
The corrected model: the generator reports what it reached; the coverage model reports what the design experienced; and the two disagree whenever the injection path changes something. That disagreement is the most informative measurement either produces, and an environment that conflates them cannot report it. Section 18.
24. Interview Questions
Question 1 — "Your coverage on one bin is stuck at zero. What do you check first?"
What the answer should establish: whether it is zero or low, because the two have different causes. Low is a weighting question; exactly zero after a long run is almost always a gate — a constraint that excludes the case, a boolean default, or an agent that is disabled. A strong answer gives the taxonomy: a case is a property of one item, of an item and its predecessors, or of a different agent, and only the first responds to a weight. It also names the ceiling: the best a weight can do is put all the probability on the target set, so computing the coupon bound at 100% says whether tuning is finished.
Question 2 — "You loop the transmit path back into the receive path. What does that fail to test?"
What the answer should establish: 48 of the 64 start offsets the receive parser handles. Chapter 19.3 §6's deficit lane-aligns every transmitted frame, so the loopback produces only offsets that are multiples of four. A strong answer goes one level deeper: Chapter 19.2 §3's barrel shifter is two stages, and the second shifts by 0 to 7 octets — a lane-aligned offset exercises only 0 and 4, so six of eight sub-lane cases are untested. The fix is an element between the two paths that adds 0 to 3 idle octets, and it costs a two-bit counter.
Question 3 — "How many beat phases does a stream of 1 518-octet frames visit?"
What the answer should establish: one. The gap is chosen so the next frame lands on a lane boundary — 1 518 mod 4 is 2, so the gap is 10 — and the wire period is 1 518 + 8 + 10 = 1 536 octets, exactly twenty-four 64-octet beats. Every frame therefore starts at the same offset. A strong answer gives the general form: a fixed-size stream visits 64 / gcd(L + 20 − (L mod 4), 64) phases, half of all legal sizes visit all sixteen, and 1 516 through 1 519 all visit one — which is a cluster at exactly the size a maximum-size stress test uses.
Question 4 — "Can a constrained-random generator produce four simultaneous read reorderings?"
What the answer should establish: no, at any weight. Whether four of eight outstanding responses arrive before the expected one is a parameter of the bus functional model, and no field of a frame item touches it. A strong answer quantifies the parameter: at an independent per-response probability of 0.05 the case appears once in 5 166 responses and at 0.5 once in two, and a clean model has it at zero — so the coverage hole is in a component nobody was tuning. It also names the model's limitation: independent reordering reaches depth four rarely and depth seven essentially never, while the hardware's reordering is correlated.
Question 5 — "You assert that every generated frame has a legal length. What have you checked?"
What the answer should establish: the constraint solver, which works. The item's constraint and the property are the same expression written twice, so the property cannot fail — and it is blind to the constraint's bound being wrong, to a configuration default, to the driver changing the frame, and to no frames being generated at all. A strong answer gives the replacements: assert on what the design saw, where Chapter 19.3 §2's padding is visible; assert against a bound transcribed independently from the standard; cover that the antecedent fires; and check the realised distribution, which is the only thing that catches a dropped soft constraint.
Question 6 — "How much simulation does Module 19's whole stimulus plan need?"
What the answer should establish: about twelve thousand frames — eighty microseconds of wire time — for all five lists, which at any RTL rate is minutes. A strong answer immediately names the exception: Chapter 19.5's 200 ppm drift takes 13.107 milliseconds to overflow a 512-beat FIFO, 163 times the entire rest of the plan, and it cannot be shortened by stimulus because it is not about frames. The answer there is hardware — that chapter's rate tracker measures the drift in 65 536 beats — which is the only case in this batch where a simulation cost is solved by adding logic.
25. Questions and Answers
26. What's Next
This chapter produces stimulus and measures only whether it produced what it intended. The next three chapters are the three things that look at the result.
Chapter 20.2 turns twenty chapters of assertions into a library. Every flagship chapter in this track has ended with thirty-odd properties and exactly one rejected class, and the series is now eighty-eight classes long — this chapter's is the latest. What has never been asked is what taxonomy those classes fall into, which of them a parameterised package can express once rather than eighty-eight times, and where the line between a simulation assertion and a formal one falls on a block like Chapter 19.4's 8 512-term network.
Chapter 20.3 builds the scoreboards, and inherits this chapter's rejected class at frame granularity. Chapter 19.4 §14 established that an equivalence reference must be independently authored; a scoreboard is that problem for a whole frame, and it has an extra difficulty this chapter does not: the design is allowed to change the frame. Chapter 19.3 §2's padding, Chapter 19.4's appended check value and Chapter 13.2's tag insertion are all legitimate modifications, and a scoreboard that compares octet for octet fails on every one of them.
And Chapter 20.4 builds the coverage model, which is where this chapter's three classes have to become bins. Chapter 19.4 §21 showed that a model tracking residue classes alone cannot distinguish a run covering one class and six barrel stages from one covering one and one — so the cross is the model — and Section 9's arithmetic says the cross's reachable size depends on the topology. A coverage goal of 100% against a 1 024-cell cross is a different statement from 100% against a 4 096-cell one, and only one of them is a statement about the design.
Continue learning
Related tutorials
- Related topic
Coverage
A six-dimension cross over this MAC declares 860 160 cells; 54.3% of them are reachable and a loopback topology reaches 13.6% — so 100% means three different things.
- Related topic
Scoreboards
A scoreboard that compares octet for octet fails on padding, on an appended check value and on a tag — and on a minimum-size frame only fifteen octets are invariant.
- Related topic
Error Injection
A runt is 0.08% of the coverage cross and three of fourteen design paths; and no sequence of frames can overflow a FIFO whose drain rate exceeds the line rate.
- Related topic
A Reusable UVM Ethernet Agent
Module 20's four components share no key, so the agent is where one lives; and every modulus in the coverage model is the beat width, so the cross moves 128x between 10 and 100 Gb/s.
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.
